diff --git a/Cargo.lock b/Cargo.lock index 324504f08..a319778d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -303,14 +303,13 @@ checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" [[package]] name = "auto_impl" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fee3da8ef1276b0bee5dd1c7258010d8fffd31801447323115a25560e1327b89" +checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" dependencies = [ - "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.50", ] [[package]] @@ -3606,30 +3605,6 @@ dependencies = [ "toml_edit 0.18.1", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.107", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" version = "1.0.78" @@ -4147,7 +4122,6 @@ dependencies = [ "prost", "reqwest", "rslock", - "rundler-pool", "rundler-provider", "rundler-sim", "rundler-task", @@ -4216,11 +4190,13 @@ version = "0.1.0-rc0" dependencies = [ "anyhow", "async-trait", + "auto_impl", "ethers", "metrics 0.22.1", "mockall", "parse-display", "reqwest", + "rundler-provider", "rundler-types", "rundler-utils", "serde", @@ -4240,8 +4216,6 @@ dependencies = [ "jsonrpsee", "metrics 0.22.1", "mockall", - "rundler-builder", - "rundler-pool", "rundler-provider", "rundler-sim", "rundler-task", @@ -4327,14 +4301,20 @@ name = "rundler-types" version = "0.1.0-rc0" dependencies = [ "anyhow", + "async-trait", "chrono", "constcat", "ethers", + "futures-util", + "mockall", "parse-display", + "rand", + "rundler-types", "rundler-utils", "serde", "serde_json", "strum 0.26.1", + "thiserror", ] [[package]] diff --git a/bin/rundler/src/cli/builder.rs b/bin/rundler/src/cli/builder.rs index 1b9ebe260..a8c4f7377 100644 --- a/bin/rundler/src/cli/builder.rs +++ b/bin/rundler/src/cli/builder.rs @@ -17,8 +17,8 @@ use anyhow::Context; use clap::Args; use ethers::types::H256; use rundler_builder::{ - self, BuilderEvent, BuilderEventKind, BuilderTask, BuilderTaskArgs, LocalBuilderBuilder, - TransactionSenderType, + self, BuilderEvent, BuilderEventKind, BuilderTask, BuilderTaskArgs, EntryPointBuilderSettings, + LocalBuilderBuilder, TransactionSenderType, }; use rundler_pool::RemotePoolClient; use rundler_sim::{MempoolConfig, PriorityFeeMode}; @@ -26,7 +26,7 @@ use rundler_task::{ server::{connect_with_retries_shutdown, format_socket_addr}, spawn_tasks_with_shutdown, }; -use rundler_types::chain::ChainSpec; +use rundler_types::{chain::ChainSpec, EntryPointVersion}; use rundler_utils::emit::{self, WithEntryPoint, EVENT_CHANNEL_CAPACITY}; use tokio::sync::broadcast; @@ -110,7 +110,7 @@ pub struct BuilderArgs { pub submit_url: Option, /// Choice of what sender type to to use for transaction submission. - /// Defaults to the value of `raw`. Other options inclue `flashbots`, + /// Defaults to the value of `raw`. Other options include `flashbots`, /// `conditional` and `polygon_bloxroute` #[arg( long = "builder.sender", @@ -198,8 +198,39 @@ impl BuilderArgs { None => HashMap::from([(H256::zero(), MempoolConfig::default())]), }; + 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_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 + .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, private_key: self.private_key.clone(), aws_kms_key_ids: self.aws_kms_key_ids.clone(), @@ -217,14 +248,11 @@ impl BuilderArgs { sender_type: self.sender_type, eth_poll_interval: Duration::from_millis(common.eth_poll_interval_millis), sim_settings: common.into(), - mempool_configs, max_blocks_to_wait_for_mine: self.max_blocks_to_wait_for_mine, replacement_fee_percent_increase: self.replacement_fee_percent_increase, max_fee_increases: self.max_fee_increases, remote_address, bloxroute_auth_header: self.bloxroute_auth_header.clone(), - num_bundle_builders: common.num_builders, - bundle_builder_index_offset: self.builder_index_offset, }) } } diff --git a/bin/rundler/src/cli/mod.rs b/bin/rundler/src/cli/mod.rs index ae5816926..b52ef3101 100644 --- a/bin/rundler/src/cli/mod.rs +++ b/bin/rundler/src/cli/mod.rs @@ -123,6 +123,10 @@ pub struct CommonArgs { )] node_http: Option, + /// Flag for turning unsafe bundling mode on + #[arg(long = "unsafe", env = "UNSAFE", global = true)] + unsafe_mode: bool, + #[arg( long = "max_verification_gas", name = "max_verification_gas", @@ -254,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 66df53cf9..40ec31f34 100644 --- a/bin/rundler/src/cli/pool.rs +++ b/bin/rundler/src/cli/pool.rs @@ -15,11 +15,11 @@ 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; -use rundler_types::chain::ChainSpec; +use rundler_types::{chain::ChainSpec, EntryPointVersion}; use rundler_utils::emit::{self, EVENT_CHANNEL_CAPACITY}; use tokio::sync::broadcast; @@ -181,11 +181,14 @@ impl PoolArgs { tracing::info!("Mempool channel configs: {:?}", mempool_channel_configs); let chain_id = chain_spec.id; - let pool_config = PoolConfig { - entry_point: chain_spec.entry_point_address, + 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, @@ -193,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, @@ -202,14 +204,44 @@ 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, http_url: common .node_http .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 475c6bc2d..65d994855 100644 --- a/bin/rundler/src/cli/rpc.rs +++ b/bin/rundler/src/cli/rpc.rs @@ -97,6 +97,7 @@ impl RpcArgs { Ok(RpcTaskArgs { chain_spec, + unsafe_mode: common.unsafe_mode, port: self.port, host: self.host.clone(), rpc_url: common @@ -110,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/bin/tools/src/bin/get_example_ops.rs b/bin/tools/src/bin/get_example_ops.rs deleted file mode 100644 index 0c967a826..000000000 --- a/bin/tools/src/bin/get_example_ops.rs +++ /dev/null @@ -1,38 +0,0 @@ -// 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 dotenv::dotenv; -use rundler_dev::DevClients; -use rundler_rpc::RpcUserOperation; - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - dotenv()?; - let clients = DevClients::new_from_env()?; - // We'll make operations that call the entry point's addStake. - let op = clients - .new_wallet_op(clients.entry_point.add_stake(1), 1.into()) - .await?; - println!("User operation to make wallet call EntryPoint#addStake():"); - println!( - "{}", - serde_json::to_string(&RpcUserOperation::from(op.clone()))? - ); - let op = clients - .new_wallet_op_with_paymaster(clients.entry_point.add_stake(1), 1.into()) - .await?; - println!(); - println!("User operation to make wallet call EntryPoint#addStake() with paymaster:"); - println!("{}", serde_json::to_string(&RpcUserOperation::from(op))?); - Ok(()) -} diff --git a/crates/builder/Cargo.toml b/crates/builder/Cargo.toml index d2d809106..52d0cc27d 100644 --- a/crates/builder/Cargo.toml +++ b/crates/builder/Cargo.toml @@ -7,7 +7,6 @@ license.workspace = true repository.workspace = true [dependencies] -rundler-pool = { path = "../pool" } rundler-provider = { path = "../provider" } rundler-sim = { path = "../sim" } rundler-task = { path = "../task" } @@ -46,7 +45,7 @@ mockall = {workspace = true, optional = true } [dev-dependencies] mockall.workspace = true -rundler-pool = { path = "../pool", features = ["test-utils"] } +rundler-types = { path = "../types", features = ["test-utils"] } rundler-provider = { path = "../provider", features = ["test-utils"] } rundler-sim = { path = "../sim", features = ["test-utils"] } diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index d2780e3a5..14c1a9eca 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -12,9 +12,9 @@ // If not, see https://www.gnu.org/licenses/. use std::{ - cmp, collections::{BTreeMap, HashMap, HashSet}, future::Future, + marker::PhantomData, mem, pin::Pin, sync::Arc, @@ -29,16 +29,18 @@ use futures_util::TryFutureExt; use linked_hash_map::LinkedHashMap; #[cfg(test)] use mockall::automock; -use rundler_pool::{PoolOperation, PoolServer}; -use rundler_provider::{EntryPoint, HandleOpsOut, Provider}; +use rundler_provider::{ + BundleHandler, EntryPoint, HandleOpsOut, L1GasProvider, Provider, SignatureAggregator, +}; use rundler_sim::{ - gas::{self, GasOverheads}, - EntityInfo, EntityInfos, ExpectedStorage, FeeEstimator, PriorityFeeMode, SimulationError, - SimulationResult, SimulationViolation, Simulator, ViolationError, + gas, ExpectedStorage, FeeEstimator, PriorityFeeMode, SimulationError, SimulationResult, + Simulator, ViolationError, }; use rundler_types::{ - chain::ChainSpec, Entity, EntityType, EntityUpdate, EntityUpdateType, GasFees, Timestamp, - UserOperation, UserOpsPerAggregator, + chain::ChainSpec, + pool::{Pool, PoolOperation, SimulationViolation}, + Entity, EntityInfo, EntityInfos, EntityType, EntityUpdate, EntityUpdateType, GasFees, + GasOverheads, Timestamp, UserOperation, UserOperationVariant, UserOpsPerAggregator, }; use rundler_utils::{emit::WithEntryPoint, math}; use tokio::{sync::broadcast, try_join}; @@ -51,17 +53,30 @@ const TIME_RANGE_BUFFER: Duration = Duration::from_secs(60); /// Extra buffer percent to add on the bundle transaction gas estimate to be sure it will be enough const BUNDLE_TRANSACTION_GAS_OVERHEAD_PERCENT: u64 = 5; -#[derive(Debug, Default)] -pub(crate) struct Bundle { - pub(crate) ops_per_aggregator: Vec, +#[derive(Debug)] +pub(crate) struct Bundle { + pub(crate) ops_per_aggregator: Vec>, pub(crate) gas_estimate: U256, pub(crate) gas_fees: GasFees, pub(crate) expected_storage: ExpectedStorage, - pub(crate) rejected_ops: Vec, + pub(crate) rejected_ops: Vec, pub(crate) entity_updates: Vec, } -impl Bundle { +impl Default for Bundle { + fn default() -> Self { + Self { + ops_per_aggregator: Vec::new(), + gas_estimate: U256::zero(), + gas_fees: GasFees::default(), + expected_storage: ExpectedStorage::default(), + rejected_ops: Vec::new(), + entity_updates: Vec::new(), + } + } +} + +impl Bundle { pub(crate) fn len(&self) -> usize { self.ops_per_aggregator .iter() @@ -73,37 +88,34 @@ impl Bundle { self.ops_per_aggregator.is_empty() } - pub(crate) fn iter_ops(&self) -> impl Iterator + '_ { + pub(crate) fn iter_ops(&self) -> impl Iterator + '_ { self.ops_per_aggregator.iter().flat_map(|ops| &ops.user_ops) } } -#[cfg_attr(test, automock)] +#[cfg_attr(test, automock(type UO = rundler_types::v0_6::UserOperation;))] #[async_trait] pub(crate) trait BundleProposer: Send + Sync + 'static { + type UO: UserOperation; + async fn make_bundle( &self, required_fees: Option, is_replacement: bool, - ) -> anyhow::Result; + ) -> anyhow::Result>; } #[derive(Debug)] -pub(crate) struct BundleProposerImpl -where - S: Simulator, - E: EntryPoint, - P: Provider, - C: PoolServer, -{ +pub(crate) struct BundleProposerImpl { builder_index: u64, - pool: C, + pool: M, simulator: S, entry_point: E, provider: Arc

, settings: Settings, fee_estimator: FeeEstimator

, event_sender: broadcast::Sender>, + _uo_type: PhantomData, } #[derive(Debug)] @@ -117,18 +129,22 @@ pub(crate) struct Settings { } #[async_trait] -impl BundleProposer for BundleProposerImpl +impl BundleProposer for BundleProposerImpl where - S: Simulator, - E: EntryPoint, + UO: UserOperation + From, + UserOperationVariant: AsRef, + S: Simulator, + E: EntryPoint + SignatureAggregator + BundleHandler + L1GasProvider, P: Provider, - C: PoolServer, + M: Pool, { + type UO = UO; + async fn make_bundle( &self, required_fees: Option, is_replacement: bool, - ) -> anyhow::Result { + ) -> anyhow::Result> { let (ops, (block_hash, _), (bundle_fees, base_fee)) = try_join!( self.get_ops_from_pool(), self.provider @@ -219,16 +235,18 @@ where } } -impl BundleProposerImpl +impl BundleProposerImpl where - S: Simulator, - E: EntryPoint, + UO: UserOperation + From, + UserOperationVariant: AsRef, + S: Simulator, + E: EntryPoint + SignatureAggregator + BundleHandler + L1GasProvider, P: Provider, - C: PoolServer, + M: Pool, { pub(crate) fn new( builder_index: u64, - pool: C, + pool: M, simulator: S, entry_point: E, provider: Arc

, @@ -249,6 +267,7 @@ where ), settings, event_sender, + _uo_type: PhantomData, } } @@ -265,9 +284,11 @@ where base_fee: U256, required_op_fees: GasFees, ) -> Option<(PoolOperation, Result)> { + let op_hash = self.op_hash(&op.uo); + // filter by fees - if op.uo.max_fee_per_gas < required_op_fees.max_fee_per_gas - || op.uo.max_priority_fee_per_gas < required_op_fees.max_priority_fee_per_gas + if op.uo.max_fee_per_gas() < required_op_fees.max_fee_per_gas + || op.uo.max_priority_fee_per_gas() < required_op_fees.max_priority_fee_per_gas { self.emit(BuilderEvent::skipped_op( self.builder_index, @@ -275,8 +296,8 @@ where SkipReason::InsufficientFees { required_fees: required_op_fees, actual_fees: GasFees { - max_fee_per_gas: op.uo.max_fee_per_gas, - max_priority_fee_per_gas: op.uo.max_priority_fee_per_gas, + max_fee_per_gas: op.uo.max_fee_per_gas(), + max_priority_fee_per_gas: op.uo.max_priority_fee_per_gas(), }, }, )); @@ -286,15 +307,15 @@ where // Check if the pvg is enough let required_pvg = gas::calc_required_pre_verification_gas( &self.settings.chain_spec, - self.provider.clone(), - &op.uo, + &self.entry_point, + op.uo.as_ref(), base_fee, ) .await .map_err(|e| { self.emit(BuilderEvent::skipped_op( self.builder_index, - self.op_hash(&op.uo), + op_hash, SkipReason::Other { reason: Arc::new(format!( "Failed to calculate required pre-verification gas for op: {e:?}, skipping" @@ -305,18 +326,18 @@ where }) .ok()?; - if op.uo.pre_verification_gas < required_pvg { + if op.uo.pre_verification_gas() < required_pvg { self.emit(BuilderEvent::skipped_op( self.builder_index, - self.op_hash(&op.uo), + op_hash, SkipReason::InsufficientPreVerificationGas { base_fee, op_fees: GasFees { - max_fee_per_gas: op.uo.max_fee_per_gas, - max_priority_fee_per_gas: op.uo.max_priority_fee_per_gas, + max_fee_per_gas: op.uo.max_fee_per_gas(), + max_priority_fee_per_gas: op.uo.max_priority_fee_per_gas(), }, required_pvg, - actual_pvg: op.uo.pre_verification_gas, + actual_pvg: op.uo.pre_verification_gas(), }, )); return None; @@ -325,7 +346,11 @@ where // Simulate let result = self .simulator - .simulate_validation(op.uo.clone(), Some(block_hash), Some(op.expected_code_hash)) + .simulate_validation( + op.uo.clone().into(), + Some(block_hash), + Some(op.expected_code_hash), + ) .await; let result = match result { Ok(success) => (op, Ok(success)), @@ -340,7 +365,7 @@ where } => { self.emit(BuilderEvent::skipped_op( self.builder_index, - self.op_hash(&op.uo), + op_hash, SkipReason::Other { reason: Arc::new(format!("Failed to simulate op: {error:?}, skipping")), }, @@ -357,12 +382,12 @@ where &self, ops_with_simulations: Vec<(PoolOperation, Result)>, mut balances_by_paymaster: HashMap, - ) -> ProposalContext { + ) -> ProposalContext { let all_sender_addresses: HashSet

= ops_with_simulations .iter() - .map(|(op, _)| op.uo.sender) + .map(|(op, _)| op.uo.sender()) .collect(); - let mut context = ProposalContext::new(); + let mut context = ProposalContext::::new(); let mut paymasters_to_reject = Vec::::new(); let ov = GasOverheads::default(); @@ -387,7 +412,7 @@ where // try to use EntityInfos from the latest simulation, but if it doesn't exist use the EntityInfos from the previous simulation let infos = entity_infos.map_or(po.entity_infos, |e| e); context.process_simulation_violations(violations, infos); - context.rejected_ops.push((op, po.entity_infos)); + context.rejected_ops.push((op.into(), po.entity_infos)); } continue; } @@ -405,18 +430,13 @@ where valid_range: simulation.valid_time_range, }, )); - context.rejected_ops.push((op, po.entity_infos)); + context.rejected_ops.push((op.into(), po.entity_infos)); continue; } // Skip this op if the bundle does not have enough remaining gas to execute it. - let required_gas = get_gas_required_for_op( - &self.settings.chain_spec, - gas_spent, - ov, - &op, - simulation.requires_post_op, - ); + let required_gas = gas_spent + + gas::user_operation_execution_gas_limit(&self.settings.chain_spec, &op, false); if required_gas > self.settings.max_bundle_gas.into() { continue; } @@ -424,11 +444,11 @@ where if let Some(&other_sender) = simulation .accessed_addresses .iter() - .find(|&address| *address != op.sender && all_sender_addresses.contains(address)) + .find(|&address| *address != op.sender() && all_sender_addresses.contains(address)) { // Exclude ops that access the sender of another op in the // batch, but don't reject them (remove them from pool). - info!("Excluding op from {:?} because it accessed the address of another sender in the bundle.", op.sender); + info!("Excluding op from {:?} because it accessed the address of another sender in the bundle.", op.sender()); self.emit(BuilderEvent::skipped_op( self.builder_index, self.op_hash(&op), @@ -452,19 +472,18 @@ where } // Update the running gas that would need to be be spent to execute the bundle so far. - gas_spent += gas::user_operation_execution_gas_limit( - &self.settings.chain_spec, - &op, - false, - simulation.requires_post_op, - ); + gas_spent += + gas::user_operation_execution_gas_limit(&self.settings.chain_spec, &op, false); context .groups_by_aggregator .entry(simulation.aggregator_address()) .or_default() .ops_with_simulations - .push(OpWithSimulation { op, simulation }); + .push(OpWithSimulation { + op: op.into(), + simulation, + }); } for paymaster in paymasters_to_reject { // No need to update aggregator signatures because we haven't computed them yet. @@ -475,19 +494,24 @@ where context } - async fn reject_index(&self, context: &mut ProposalContext, i: usize) { + async fn reject_index(&self, context: &mut ProposalContext, i: usize) { let changed_aggregator = context.reject_index(i); self.compute_aggregator_signatures(context, &changed_aggregator) .await; } - async fn reject_entity(&self, context: &mut ProposalContext, entity: Entity, is_staked: bool) { + async fn reject_entity( + &self, + context: &mut ProposalContext, + entity: Entity, + is_staked: bool, + ) { let changed_aggregators = context.reject_entity(entity, is_staked); self.compute_aggregator_signatures(context, &changed_aggregators) .await; } - async fn compute_all_aggregator_signatures(&self, context: &mut ProposalContext) { + async fn compute_all_aggregator_signatures(&self, context: &mut ProposalContext) { let aggregators: Vec<_> = context .groups_by_aggregator .keys() @@ -500,7 +524,7 @@ where async fn compute_aggregator_signatures<'a>( &self, - context: &mut ProposalContext, + context: &mut ProposalContext, aggregators: impl IntoIterator, ) { let signature_futures = aggregators.into_iter().filter_map(|&aggregator| { @@ -520,7 +544,7 @@ where /// op(s) caused the failure. async fn estimate_gas_rejecting_failed_ops( &self, - context: &mut ProposalContext, + context: &mut ProposalContext, ) -> anyhow::Result> { // sum up the gas needed for all the ops in the bundle // and apply an overhead multiplier @@ -572,14 +596,17 @@ where // // NOTE: this assumes that the pool server has as many shards as there // are builders. - self.pool + Ok(self + .pool .get_ops( self.entry_point.address(), self.settings.max_bundle_size, self.builder_index, ) .await - .context("should get ops from pool") + .context("should get ops from pool")? + .into_iter() + .collect()) } async fn get_balances_by_paymaster( @@ -603,14 +630,15 @@ where async fn aggregate_signatures( &self, aggregator: Address, - group: &AggregatorGroup, + group: &AggregatorGroup, ) -> (Address, anyhow::Result>) { let ops = group .ops_with_simulations .iter() .map(|op_with_simulation| op_with_simulation.op.clone()) .collect(); - let result = Arc::clone(&self.provider) + let result = self + .entry_point .aggregate_signatures(aggregator, ops) .await .map_err(anyhow::Error::from); @@ -619,7 +647,7 @@ where async fn process_failed_op( &self, - context: &mut ProposalContext, + context: &mut ProposalContext, index: usize, message: String, ) -> anyhow::Result<()> { @@ -679,7 +707,7 @@ where // from the bundle and from the pool. async fn process_post_op_revert( &self, - context: &mut ProposalContext, + context: &mut ProposalContext, gas: U256, ) -> anyhow::Result<()> { let agg_groups = context.to_ops_per_aggregator(); @@ -732,7 +760,7 @@ where async fn check_for_post_op_revert_single_op( &self, - op: UserOperation, + op: UO, gas: U256, op_index: usize, ) -> Vec { @@ -768,7 +796,7 @@ where async fn check_for_post_op_revert_agg_ops( &self, - group: UserOpsPerAggregator, + group: UserOpsPerAggregator, gas: U256, start_index: usize, ) -> Vec { @@ -809,12 +837,8 @@ where for op in ops { // Here we use optimistic gas limits for the UOs by assuming none of the paymaster UOs use postOp calls. // This way after simulation once we have determined if each UO actually uses a postOp call or not we can still pack a full bundle - let gas = gas::user_operation_execution_gas_limit( - &self.settings.chain_spec, - &op.uo, - false, - false, - ); + let gas = + gas::user_operation_execution_gas_limit(&self.settings.chain_spec, &op.uo, false); if gas_left < gas { self.emit(BuilderEvent::skipped_op( self.builder_index, @@ -841,22 +865,26 @@ where }); } - fn op_hash(&self, op: &UserOperation) -> H256 { - op.op_hash(self.entry_point.address(), self.settings.chain_spec.id) + fn op_hash(&self, op: &T) -> H256 + where + T: UserOperation, + { + op.hash(self.entry_point.address(), self.settings.chain_spec.id) } } #[derive(Debug)] -struct OpWithSimulation { - op: UserOperation, +struct OpWithSimulation { + op: UO, simulation: SimulationResult, } -impl OpWithSimulation { - fn op_with_replaced_sig(&self) -> UserOperation { +impl OpWithSimulation { + fn op_with_replaced_sig(&self) -> UO { let mut op = self.op.clone(); - if let Some(aggregator) = &self.simulation.aggregator { - op.signature = aggregator.signature.clone(); + if self.simulation.aggregator.is_some() { + // if using an aggregator, clear out the user op signature + op.clear_signature(); } op } @@ -867,24 +895,33 @@ impl OpWithSimulation { /// `Vec` that will eventually be passed to the entry /// point, but contains extra context needed for the computation. #[derive(Debug)] -struct ProposalContext { - groups_by_aggregator: LinkedHashMap, AggregatorGroup>, - rejected_ops: Vec<(UserOperation, EntityInfos)>, +struct ProposalContext { + groups_by_aggregator: LinkedHashMap, AggregatorGroup>, + rejected_ops: Vec<(UO, EntityInfos)>, // This is a BTreeMap so that the conversion to a Vec is deterministic, mainly for tests entity_updates: BTreeMap, } -#[derive(Debug, Default)] -struct AggregatorGroup { - ops_with_simulations: Vec, +#[derive(Debug)] +struct AggregatorGroup { + ops_with_simulations: Vec>, signature: Bytes, } -impl ProposalContext { +impl Default for AggregatorGroup { + fn default() -> Self { + Self { + ops_with_simulations: Vec::new(), + signature: Bytes::new(), + } + } +} + +impl ProposalContext { fn new() -> Self { Self { - groups_by_aggregator: LinkedHashMap::, AggregatorGroup>::new(), - rejected_ops: Vec::<(UserOperation, EntityInfos)>::new(), + groups_by_aggregator: LinkedHashMap::, AggregatorGroup>::new(), + rejected_ops: Vec::<(UO, EntityInfos)>::new(), entity_updates: BTreeMap::new(), } } @@ -908,7 +945,7 @@ impl ProposalContext { } } - fn get_op_at(&self, index: usize) -> anyhow::Result<&OpWithSimulation> { + fn get_op_at(&self, index: usize) -> anyhow::Result<&OpWithSimulation> { let mut remaining_i = index; for group in self.groups_by_aggregator.values() { if remaining_i < group.ops_with_simulations.len() { @@ -992,7 +1029,7 @@ impl ProposalContext { /// Reject all ops that match the filter, and return the addresses of any aggregators /// whose signature may need to be recomputed. - fn filter_reject(&mut self, filter: impl Fn(&UserOperation) -> bool) -> Vec
{ + fn filter_reject(&mut self, filter: impl Fn(&UO) -> bool) -> Vec
{ let mut changed_aggregators: Vec
= vec![]; let mut aggregators_to_remove: Vec> = vec![]; for (&aggregator, group) in &mut self.groups_by_aggregator { @@ -1018,7 +1055,7 @@ impl ProposalContext { changed_aggregators } - fn to_ops_per_aggregator(&self) -> Vec { + fn to_ops_per_aggregator(&self) -> Vec> { self.groups_by_aggregator .iter() .map(|(&aggregator, group)| UserOpsPerAggregator { @@ -1034,36 +1071,27 @@ impl ProposalContext { } fn get_bundle_gas_limit(&self, chain_spec: &ChainSpec) -> U256 { - let ov = GasOverheads::default(); - let mut gas_spent = ov.transaction_gas_overhead; - let mut max_gas = U256::zero(); - for op_with_sim in self.iter_ops_with_simulations() { - let op = &op_with_sim.op; - let required_gas = get_gas_required_for_op( - chain_spec, - gas_spent, - ov, - op, - op_with_sim.simulation.requires_post_op, - ); - max_gas = cmp::max(max_gas, required_gas); - gas_spent += gas::user_operation_gas_limit( - chain_spec, - op, - false, - op_with_sim.simulation.requires_post_op, - ); - } - max_gas + // TODO(danc): in the 0.7 entrypoint we could optimize this by removing the need for + // the 10K gas and 63/64 gas overheads for each op in the bundle and instead calculate exactly + // the limit needed to include that overhead for each op. + // + // In the 0.6 entrypoint we're assuming that we need 1 verification gas buffer for each op in the bundle + // regardless of if it uses a post op or not. We can optimize to calculate the exact gas overhead + // needed to have the buffer for each op. + + self.iter_ops_with_simulations() + .map(|sim_op| gas::user_operation_gas_limit(chain_spec, &sim_op.op, false)) + .fold(U256::zero(), |acc, i| acc + i) + + GasOverheads::default().transaction_gas_overhead } - fn iter_ops_with_simulations(&self) -> impl Iterator + '_ { + fn iter_ops_with_simulations(&self) -> impl Iterator> + '_ { self.groups_by_aggregator .values() .flat_map(|group| &group.ops_with_simulations) } - fn iter_ops(&self) -> impl Iterator + '_ { + fn iter_ops(&self) -> impl Iterator + '_ { self.iter_ops_with_simulations().map(|op| &op.op) } @@ -1169,7 +1197,7 @@ impl ProposalContext { fn add_entity_update(&mut self, entity: Entity, entity_infos: EntityInfos) { let entity_update = EntityUpdate { entity, - update_type: ProposalContext::get_entity_update_type(entity.kind, entity_infos), + update_type: ProposalContext::::get_entity_update_type(entity.kind, entity_infos), }; self.entity_updates.insert(entity.address, entity_update); } @@ -1226,26 +1254,6 @@ impl ProposalContext { } } -fn get_gas_required_for_op( - chain_spec: &ChainSpec, - gas_spent: U256, - ov: GasOverheads, - op: &UserOperation, - requires_post_op: bool, -) -> U256 { - let post_exec_req_gas = if requires_post_op { - cmp::max(op.verification_gas_limit, ov.bundle_transaction_gas_buffer) - } else { - ov.bundle_transaction_gas_buffer - }; - - gas_spent - + gas::user_operation_pre_verification_gas_limit(chain_spec, op, false) - + op.verification_gas_limit * 2 - + op.call_gas_limit - + post_exec_req_gas -} - #[cfg(test)] mod tests { use anyhow::anyhow; @@ -1253,10 +1261,13 @@ mod tests { types::{H160, U64}, utils::parse_units, }; - use rundler_pool::MockPoolServer; - use rundler_provider::{AggregatorSimOut, MockEntryPoint, MockProvider}; - use rundler_sim::{MockSimulator, SimulationViolation, ViolationError}; - use rundler_types::ValidTimeRange; + use rundler_provider::{AggregatorSimOut, MockEntryPointV0_6, MockProvider}; + use rundler_sim::MockSimulator; + use rundler_types::{ + pool::{MockPool, SimulationViolation}, + v0_6::UserOperation, + UserOperation as UserOperationTrait, ValidTimeRange, + }; use super::*; @@ -1541,7 +1552,7 @@ mod tests { let op_b_aggregated_sig = 21; let aggregator_a_signature = 101; let aggregator_b_signature = 102; - let bundle = mock_make_bundle( + let mut bundle = mock_make_bundle( vec![ MockOp { op: unaggregated_op.clone(), @@ -1601,11 +1612,15 @@ mod tests { ) .await; // Ops should be grouped by aggregator. Further, the `signature` field - // of each op with an aggregator should be replaced with what was - // returned from simulation. + // of each op with an aggregator should be empty. + + bundle + .ops_per_aggregator + .sort_by(|a, b| a.aggregator.cmp(&b.aggregator)); + assert_eq!( - HashSet::from_iter(bundle.ops_per_aggregator), - HashSet::from([ + bundle.ops_per_aggregator, + vec![ UserOpsPerAggregator { user_ops: vec![unaggregated_op], ..Default::default() @@ -1613,11 +1628,11 @@ mod tests { UserOpsPerAggregator { user_ops: vec![ UserOperation { - signature: bytes(op_a1_aggregated_sig), + signature: Bytes::new(), ..aggregated_op_a1 }, UserOperation { - signature: bytes(op_a2_aggregated_sig), + signature: Bytes::new(), ..aggregated_op_a2 } ], @@ -1626,13 +1641,13 @@ mod tests { }, UserOpsPerAggregator { user_ops: vec![UserOperation { - signature: bytes(op_b_aggregated_sig), + signature: Bytes::new(), ..aggregated_op_b }], aggregator: aggregator_b_address, signature: bytes(aggregator_b_signature) }, - ]), + ], ); } @@ -1758,7 +1773,7 @@ mod tests { assert_eq!( bundle.gas_estimate, U256::from(math::increase_by_percent( - 9_000_000 + 5_000 + 21_000, + 9_000_000 + 2 * 5_000 + 21_000, BUNDLE_TRANSACTION_GAS_OVERHEAD_PERCENT )) ); @@ -1798,13 +1813,14 @@ mod tests { entity_updates: BTreeMap::new(), }; - // The gas requirement from the execution of the first UO is: g >= p_1 + 2v_1 + c_1 + 5000 - // The gas requirement from the execution of the second UO is: g >= p_1 + v_1 + c_1 + p_2 + 2v_2 + c_2 + 5000 - // The first condition dominates and determines the expected gas limit let expected_gas_limit = op1.pre_verification_gas + op1.verification_gas_limit * 2 + op1.call_gas_limit + 5_000 + + op2.pre_verification_gas + + op2.verification_gas_limit * 2 + + op2.call_gas_limit + + 5_000 + 21_000; assert_eq!(context.get_bundle_gas_limit(&cs), expected_gas_limit); @@ -1845,17 +1861,15 @@ mod tests { }; let gas_limit = context.get_bundle_gas_limit(&cs); - // The gas requirement from the execution of the first UO is: g >= p_1 + 3v_1 + c_1 - // The gas requirement from the execution of the second UO is: g >= p_1 + 3v_1 + c_1 + p_2 + 2v_2 + c_2 + 5000 - // The first condition dominates and determines the expected gas limit let expected_gas_limit = op1.pre_verification_gas + op1.verification_gas_limit * 3 + op1.call_gas_limit + + 5_000 + op2.pre_verification_gas + op2.verification_gas_limit * 2 + op2.call_gas_limit - + 21_000 - + 5_000; + + 5_000 + + 21_000; assert_eq!(gas_limit, expected_gas_limit); } @@ -1997,7 +2011,7 @@ mod tests { signature: Box anyhow::Result> + Send + Sync>, } - async fn simple_make_bundle(mock_ops: Vec) -> Bundle { + async fn simple_make_bundle(mock_ops: Vec) -> Bundle { mock_make_bundle( mock_ops, vec![], @@ -2016,7 +2030,7 @@ mod tests { mock_paymaster_deposits: Vec, base_fee: U256, max_priority_fee_per_gas: U256, - ) -> Bundle { + ) -> Bundle { let entry_point_address = address(123); let beneficiary = address(124); let current_block_hash = hash(125); @@ -2025,20 +2039,27 @@ mod tests { let ops: Vec<_> = mock_ops .iter() .map(|MockOp { op, .. }| PoolOperation { - uo: op.clone(), + uo: op.clone().into(), expected_code_hash, - ..Default::default() + entry_point: entry_point_address, + sim_block_hash: current_block_hash, + sim_block_number: 0, + entities_needing_stake: vec![], + account_is_staked: false, + valid_time_range: ValidTimeRange::default(), + entity_infos: EntityInfos::default(), + aggregator: None, }) .collect(); - let mut pool_client = MockPoolServer::new(); + let mut pool_client = MockPool::new(); pool_client .expect_get_ops() .returning(move |_, _, _| Ok(ops.clone())); let simulations_by_op: HashMap<_, _> = mock_ops .into_iter() - .map(|op| (op.op.op_hash(entry_point_address, 0), op.simulation_result)) + .map(|op| (op.op.hash(entry_point_address, 0), op.simulation_result)) .collect(); let mut simulator = MockSimulator::new(); simulator @@ -2046,8 +2067,8 @@ mod tests { .withf(move |_, &block_hash, &code_hash| { block_hash == Some(current_block_hash) && code_hash == Some(expected_code_hash) }) - .returning(move |op, _, _| simulations_by_op[&op.op_hash(entry_point_address, 0)]()); - let mut entry_point = MockEntryPoint::new(); + .returning(move |op, _, _| simulations_by_op[&op.hash(entry_point_address, 0)]()); + let mut entry_point = MockEntryPointV0_6::new(); entry_point .expect_address() .return_const(entry_point_address); @@ -2079,9 +2100,9 @@ mod tests { provider .expect_get_max_priority_fee() .returning(move || Ok(max_priority_fee_per_gas)); - provider + entry_point .expect_aggregate_signatures() - .returning(move |address, _| Ok(signatures_by_aggregator[&address]()?)); + .returning(move |address, _| Ok(signatures_by_aggregator[&address]().unwrap())); let (event_sender, _) = broadcast::channel(16); let proposer = BundleProposerImpl::new( 0, diff --git a/crates/builder/src/bundle_sender.rs b/crates/builder/src/bundle_sender.rs index db95f1bf8..3fa54e665 100644 --- a/crates/builder/src/bundle_sender.rs +++ b/crates/builder/src/bundle_sender.rs @@ -11,16 +11,17 @@ // 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::{sync::Arc, time::Duration}; +use std::{marker::PhantomData, sync::Arc, time::Duration}; use anyhow::{bail, Context}; use async_trait::async_trait; use ethers::types::{transaction::eip2718::TypedTransaction, Address, H256, U256}; use futures_util::StreamExt; -use rundler_pool::PoolServer; -use rundler_provider::EntryPoint; +use rundler_provider::{BundleHandler, EntryPoint}; use rundler_sim::ExpectedStorage; -use rundler_types::{chain::ChainSpec, EntityUpdate, GasFees, UserOperation}; +use rundler_types::{ + builder::BundlingMode, chain::ChainSpec, pool::Pool, EntityUpdate, GasFees, UserOperation, +}; use rundler_utils::emit::WithEntryPoint; use tokio::{ join, @@ -32,7 +33,6 @@ use crate::{ bundle_proposer::BundleProposer, emit::{BuilderEvent, BundleTxDetails}, transaction_tracker::{SendResult, TrackerUpdate, TransactionTracker}, - BundlingMode, }; #[async_trait] @@ -47,13 +47,7 @@ pub(crate) struct Settings { } #[derive(Debug)] -pub(crate) struct BundleSenderImpl -where - P: BundleProposer, - E: EntryPoint, - T: TransactionTracker, - C: PoolServer, -{ +pub(crate) struct BundleSenderImpl { builder_index: u64, bundle_action_receiver: mpsc::Receiver, chain_spec: ChainSpec, @@ -64,6 +58,7 @@ where pool: C, settings: Settings, event_sender: broadcast::Sender>, + _uo_type: PhantomData, } #[derive(Debug)] @@ -99,12 +94,13 @@ pub enum SendBundleResult { } #[async_trait] -impl BundleSender for BundleSenderImpl +impl BundleSender for BundleSenderImpl where - P: BundleProposer, - E: EntryPoint, + UO: UserOperation, + P: BundleProposer, + E: EntryPoint + BundleHandler, T: TransactionTracker, - C: PoolServer, + C: Pool, { /// Loops forever, attempting to form and send a bundle on each new block, /// then waiting for one bundle to be mined or dropped before forming the @@ -247,12 +243,13 @@ where } } -impl BundleSenderImpl +impl BundleSenderImpl where - P: BundleProposer, - E: EntryPoint, + UO: UserOperation, + P: BundleProposer, + E: EntryPoint + BundleHandler, T: TransactionTracker, - C: PoolServer, + C: Pool, { #[allow(clippy::too_many_arguments)] pub(crate) fn new( @@ -278,6 +275,7 @@ where pool, settings, event_sender, + _uo_type: PhantomData, } } @@ -538,12 +536,12 @@ where })) } - async fn remove_ops_from_pool(&self, ops: &[UserOperation]) -> anyhow::Result<()> { + async fn remove_ops_from_pool(&self, ops: &[UO]) -> anyhow::Result<()> { self.pool .remove_ops( self.entry_point.address(), ops.iter() - .map(|op| op.op_hash(self.entry_point.address(), self.chain_spec.id)) + .map(|op| op.hash(self.entry_point.address(), self.chain_spec.id)) .collect(), ) .await @@ -564,8 +562,8 @@ where }); } - fn op_hash(&self, op: &UserOperation) -> H256 { - op.op_hash(self.entry_point.address(), self.chain_spec.id) + fn op_hash(&self, op: &UO) -> H256 { + op.hash(self.entry_point.address(), self.chain_spec.id) } } diff --git a/crates/builder/src/lib.rs b/crates/builder/src/lib.rs index 8e7d9ccb4..3c4173f08 100644 --- a/crates/builder/src/lib.rs +++ b/crates/builder/src/lib.rs @@ -29,14 +29,11 @@ mod sender; pub use sender::TransactionSenderType; mod server; -pub use server::{ - BuilderResult, BuilderServer, BuilderServerError, BundlingMode, LocalBuilderBuilder, - LocalBuilderHandle, RemoteBuilderClient, -}; +pub use server::{LocalBuilderBuilder, LocalBuilderHandle, RemoteBuilderClient}; mod signer; mod task; -pub use task::{Args as BuilderTaskArgs, BuilderTask}; +pub use task::{Args as BuilderTaskArgs, BuilderTask, EntryPointBuilderSettings}; mod transaction_tracker; diff --git a/crates/builder/src/server/local.rs b/crates/builder/src/server/local.rs index d057fc82e..76bc0cf87 100644 --- a/crates/builder/src/server/local.rs +++ b/crates/builder/src/server/local.rs @@ -14,16 +14,14 @@ use async_trait::async_trait; use ethers::types::{Address, H256}; use rundler_task::server::{HealthCheck, ServerStatus}; +use rundler_types::builder::{Builder, BuilderError, BuilderResult, BundlingMode}; use tokio::{ sync::{mpsc, oneshot}, task::JoinHandle, }; use tokio_util::sync::CancellationToken; -use crate::{ - bundle_sender::{BundleSenderAction, SendBundleRequest, SendBundleResult}, - server::{BuilderResult, BuilderServer, BuilderServerError, BundlingMode}, -}; +use crate::bundle_sender::{BundleSenderAction, SendBundleRequest, SendBundleResult}; /// Local builder server builder #[derive(Debug)] @@ -92,13 +90,13 @@ impl LocalBuilderHandle { } #[async_trait] -impl BuilderServer for LocalBuilderHandle { +impl Builder for LocalBuilderHandle { async fn get_supported_entry_points(&self) -> BuilderResult> { let req = ServerRequestKind::GetSupportedEntryPoints; let resp = self.send(req).await?; match resp { ServerResponse::GetSupportedEntryPoints { entry_points } => Ok(entry_points), - _ => Err(BuilderServerError::UnexpectedResponse), + _ => Err(BuilderError::UnexpectedResponse), } } @@ -107,7 +105,7 @@ impl BuilderServer for LocalBuilderHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugSendBundleNow { hash, block_number } => Ok((hash, block_number)), - _ => Err(BuilderServerError::UnexpectedResponse), + _ => Err(BuilderError::UnexpectedResponse), } } @@ -116,7 +114,7 @@ impl BuilderServer for LocalBuilderHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugSetBundlingMode => Ok(()), - _ => Err(BuilderServerError::UnexpectedResponse), + _ => Err(BuilderError::UnexpectedResponse), } } } diff --git a/crates/builder/src/server/mod.rs b/crates/builder/src/server/mod.rs index 4193e3ad3..adf8664be 100644 --- a/crates/builder/src/server/mod.rs +++ b/crates/builder/src/server/mod.rs @@ -12,59 +12,8 @@ // If not, see https://www.gnu.org/licenses/. mod local; -mod remote; - -use async_trait::async_trait; -use ethers::types::{Address, H256}; pub use local::{LocalBuilderBuilder, LocalBuilderHandle}; -#[cfg(feature = "test-utils")] -use mockall::automock; -use parse_display::Display; + +mod remote; pub(crate) use remote::spawn_remote_builder_server; pub use remote::RemoteBuilderClient; -use serde::{Deserialize, Serialize}; - -/// Builder server errors -#[derive(Debug, thiserror::Error)] -pub enum BuilderServerError { - /// Builder returned an unexpected response type for the given request - #[error("Unexpected response from BuilderServer")] - UnexpectedResponse, - /// Internal errors - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -/// Builder server result -pub type BuilderResult = std::result::Result; - -/// Builder server -#[cfg_attr(feature = "test-utils", automock)] -#[async_trait] -pub trait BuilderServer: Send + Sync + 'static { - /// Get the supported entry points of this builder - async fn get_supported_entry_points(&self) -> BuilderResult>; - - /// Trigger the builder to send a bundle now, used for debugging. - /// - /// Bundling mode must be set to `Manual`, or this will error - async fn debug_send_bundle_now(&self) -> BuilderResult<(H256, u64)>; - - /// Set the bundling mode - async fn debug_set_bundling_mode(&self, mode: BundlingMode) -> BuilderResult<()>; -} - -/// Builder bundling mode -#[derive(Display, Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] -#[display(style = "lowercase")] -#[serde(rename_all = "lowercase")] -pub enum BundlingMode { - /// Manual bundling mode for debugging. - /// - /// Bundles will only be sent when `debug_send_bundle_now` is called. - Manual, - /// Auto bundling mode for normal operation. - /// - /// Bundles will be sent automatically. - Auto, -} diff --git a/crates/builder/src/server/remote/client.rs b/crates/builder/src/server/remote/client.rs index 34d23e372..494ae0a78 100644 --- a/crates/builder/src/server/remote/client.rs +++ b/crates/builder/src/server/remote/client.rs @@ -18,6 +18,7 @@ use rundler_task::{ grpc::protos::{from_bytes, ConversionError}, server::{HealthCheck, ServerStatus}, }; +use rundler_types::builder::{Builder, BuilderError, BuilderResult, BundlingMode}; use tonic::{ async_trait, transport::{Channel, Uri}, @@ -32,7 +33,6 @@ use super::protos::{ debug_set_bundling_mode_response, BundlingMode as ProtoBundlingMode, DebugSendBundleNowRequest, DebugSetBundlingModeRequest, GetSupportedEntryPointsRequest, }; -use crate::server::{BuilderResult, BuilderServer, BuilderServerError, BundlingMode}; /// Remote builder client, used for communicating with a remote builder server #[derive(Debug, Clone)] @@ -55,18 +55,20 @@ impl RemoteBuilderClient { } #[async_trait] -impl BuilderServer for RemoteBuilderClient { +impl Builder for RemoteBuilderClient { async fn get_supported_entry_points(&self) -> BuilderResult> { Ok(self .grpc_client .clone() .get_supported_entry_points(GetSupportedEntryPointsRequest {}) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .entry_points .into_iter() .map(|ep| from_bytes(ep.as_slice())) - .collect::>()?) + .collect::>() + .map_err(anyhow::Error::from)?) } async fn debug_send_bundle_now(&self) -> BuilderResult<(H256, u64)> { @@ -74,7 +76,8 @@ impl BuilderServer for RemoteBuilderClient { .grpc_client .clone() .debug_send_bundle_now(DebugSendBundleNowRequest {}) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -83,7 +86,7 @@ impl BuilderServer for RemoteBuilderClient { Ok((H256::from_slice(&s.transaction_hash), s.block_number)) } Some(debug_send_bundle_now_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(BuilderServerError::Other(anyhow::anyhow!( + None => Err(BuilderError::Other(anyhow::anyhow!( "should have received result from builder" )))?, } @@ -96,14 +99,15 @@ impl BuilderServer for RemoteBuilderClient { .debug_set_bundling_mode(DebugSetBundlingModeRequest { mode: ProtoBundlingMode::from(mode) as i32, }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(debug_set_bundling_mode_response::Result::Success(_)) => Ok(()), Some(debug_set_bundling_mode_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(BuilderServerError::Other(anyhow::anyhow!( + None => Err(BuilderError::Other(anyhow::anyhow!( "should have received result from builder" )))?, } diff --git a/crates/builder/src/server/remote/error.rs b/crates/builder/src/server/remote/error.rs index 0b49ea6aa..d2264fcb1 100644 --- a/crates/builder/src/server/remote/error.rs +++ b/crates/builder/src/server/remote/error.rs @@ -11,45 +11,30 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use rundler_task::grpc::protos::ConversionError; +use rundler_types::builder::BuilderError; use super::protos::{builder_error, BuilderError as ProtoBuilderError}; -use crate::server::BuilderServerError; -impl From for BuilderServerError { - fn from(value: tonic::Status) -> Self { - BuilderServerError::Other(anyhow::anyhow!(value.to_string())) - } -} - -impl From for BuilderServerError { - fn from(value: ConversionError) -> Self { - BuilderServerError::Other(anyhow::anyhow!(value.to_string())) - } -} - -impl TryFrom for BuilderServerError { +impl TryFrom for BuilderError { type Error = anyhow::Error; fn try_from(value: ProtoBuilderError) -> Result { match value.error { - Some(builder_error::Error::Internal(e)) => { - Ok(BuilderServerError::Other(anyhow::anyhow!(e))) - } - None => Ok(BuilderServerError::Other(anyhow::anyhow!("Unknown error"))), + Some(builder_error::Error::Internal(e)) => Ok(BuilderError::Other(anyhow::anyhow!(e))), + None => Ok(BuilderError::Other(anyhow::anyhow!("Unknown error"))), } } } -impl From for ProtoBuilderError { - fn from(value: BuilderServerError) -> Self { +impl From for ProtoBuilderError { + fn from(value: BuilderError) -> Self { match value { - BuilderServerError::UnexpectedResponse => ProtoBuilderError { + BuilderError::UnexpectedResponse => ProtoBuilderError { error: Some(builder_error::Error::Internal( "Unexpected response".to_string(), )), }, - BuilderServerError::Other(e) => ProtoBuilderError { + BuilderError::Other(e) => ProtoBuilderError { error: Some(builder_error::Error::Internal(e.to_string())), }, } diff --git a/crates/builder/src/server/remote/protos.rs b/crates/builder/src/server/remote/protos.rs index e3f4ae66a..35c100cb4 100644 --- a/crates/builder/src/server/remote/protos.rs +++ b/crates/builder/src/server/remote/protos.rs @@ -12,8 +12,7 @@ // If not, see https://www.gnu.org/licenses/. use rundler_task::grpc::protos::ConversionError; - -use crate::server::BundlingMode as RpcBundlingMode; +use rundler_types::builder::BundlingMode as RpcBundlingMode; tonic::include_proto!("builder"); @@ -40,15 +39,3 @@ impl TryFrom for RpcBundlingMode { } } } - -impl TryFrom for RpcBundlingMode { - type Error = ConversionError; - - fn try_from(status: i32) -> Result { - match status { - x if x == BundlingMode::Auto as i32 => Ok(Self::Auto), - x if x == BundlingMode::Manual as i32 => Ok(Self::Manual), - _ => Err(ConversionError::InvalidEnumValue(status)), - } - } -} diff --git a/crates/builder/src/server/remote/server.rs b/crates/builder/src/server/remote/server.rs index 6c25be149..a94259747 100644 --- a/crates/builder/src/server/remote/server.rs +++ b/crates/builder/src/server/remote/server.rs @@ -13,20 +13,19 @@ use std::net::SocketAddr; +use rundler_types::builder::Builder; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; use tonic::{async_trait, transport::Server, Request, Response, Status}; use super::protos::{ builder_server::{Builder as GrpcBuilder, BuilderServer as GrpcBuilderServer}, - debug_send_bundle_now_response, debug_set_bundling_mode_response, DebugSendBundleNowRequest, - DebugSendBundleNowResponse, DebugSetBundlingModeRequest, DebugSetBundlingModeResponse, - DebugSetBundlingModeSuccess, GetSupportedEntryPointsRequest, GetSupportedEntryPointsResponse, - BUILDER_FILE_DESCRIPTOR_SET, -}; -use crate::server::{ - local::LocalBuilderHandle, remote::protos::DebugSendBundleNowSuccess, BuilderServer, + debug_send_bundle_now_response, debug_set_bundling_mode_response, BundlingMode, + DebugSendBundleNowRequest, DebugSendBundleNowResponse, DebugSetBundlingModeRequest, + DebugSetBundlingModeResponse, DebugSetBundlingModeSuccess, GetSupportedEntryPointsRequest, + GetSupportedEntryPointsResponse, BUILDER_FILE_DESCRIPTOR_SET, }; +use crate::server::{local::LocalBuilderHandle, remote::protos::DebugSendBundleNowSuccess}; /// Spawn a remote builder server pub(crate) async fn spawn_remote_builder_server( @@ -122,13 +121,14 @@ impl GrpcBuilder for GrpcBuilderServerImpl { &self, request: Request, ) -> tonic::Result> { - let resp = match self - .local_builder - .debug_set_bundling_mode(request.into_inner().mode.try_into().map_err(|e| { - Status::internal(format!("Failed to convert from proto reputation {e}")) - })?) - .await - { + let mode = BundlingMode::try_from(request.into_inner().mode).map_err(|e| { + Status::internal(format!("Failed to convert from proto reputation {e}")) + })?; + let mode = mode.try_into().map_err(|e| { + Status::internal(format!("Failed to convert from proto reputation {e}")) + })?; + + let resp = match self.local_builder.debug_set_bundling_mode(mode).await { Ok(()) => DebugSetBundlingModeResponse { result: Some(debug_set_bundling_mode_response::Result::Success( DebugSetBundlingModeSuccess {}, diff --git a/crates/builder/src/task.rs b/crates/builder/src/task.rs index a69dc3b73..26c9edce6 100644 --- a/crates/builder/src/task.rs +++ b/crates/builder/src/task.rs @@ -16,19 +16,28 @@ use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; use anyhow::{bail, Context}; use async_trait::async_trait; use ethers::{ - providers::{JsonRpcClient, Provider}, - types::H256, + providers::{JsonRpcClient, Provider as EthersProvider}, + types::{Address, H256}, }; use ethers_signers::Signer; use futures::future; use futures_util::TryFutureExt; -use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, EthersEntryPoint}; +use rundler_provider::{EntryPointProvider, EthersEntryPointV0_6, EthersEntryPointV0_7, Provider}; use rundler_sim::{ - MempoolConfig, PriorityFeeMode, SimulateValidationTracerImpl, SimulationSettings, SimulatorImpl, + 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; +use rundler_types::{ + chain::ChainSpec, pool::Pool, v0_6, v0_7, EntryPointVersion, UserOperation, + UserOperationVariant, +}; use rundler_utils::{emit::WithEntryPoint, handle}; use rusoto_core::Region; use tokio::{ @@ -56,6 +65,8 @@ pub struct Args { pub chain_spec: ChainSpec, /// Full node RPC url pub rpc_url: String, + /// True if using unsafe mode + pub unsafe_mode: bool, /// Private key to use for signing transactions /// If not provided, AWS KMS will be used pub private_key: Option, @@ -84,8 +95,6 @@ pub struct Args { pub eth_poll_interval: Duration, /// Operation simulation settings pub sim_settings: SimulationSettings, - /// Alt-mempool configs - pub mempool_configs: HashMap, /// Maximum number of blocks to wait for a transaction to be mined pub max_blocks_to_wait_for_mine: u64, /// Percentage to increase the fees by when replacing a bundle transaction @@ -100,10 +109,23 @@ pub struct Args { /// /// Checked ~after~ checking for conditional sender or Flashbots sender. pub bloxroute_auth_header: Option, + /// Entry points to start builders for + pub entry_points: Vec, +} + +/// Builder settings for an entrypoint +#[derive(Debug)] +pub struct EntryPointBuilderSettings { + /// Entry point address + pub address: Address, + /// Entry point version + pub version: EntryPointVersion, /// Number of bundle builders to start pub num_bundle_builders: u64, /// Index offset for bundle builders pub bundle_builder_index_offset: u64, + /// Mempool configs + pub mempool_configs: HashMap, } /// Builder task @@ -118,26 +140,46 @@ pub struct BuilderTask

{ #[async_trait] impl

Task for BuilderTask

where - P: PoolServer + Clone, + P: Pool + Clone, { async fn run(mut self: Box, shutdown_token: CancellationToken) -> anyhow::Result<()> { - info!("Mempool config: {:?}", self.args.mempool_configs); - let provider = 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_v0_6, + Arc::clone(&provider), + ); + let ep_v0_7 = EthersEntryPointV0_7::new( + self.args.chain_spec.entry_point_address_v0_7, + Arc::clone(&provider), + ); + let mut sender_handles = vec![]; let mut bundle_sender_actions = vec![]; - for i in 0..self.args.num_bundle_builders { - let (spawn_guard, bundle_sender_action) = self - .create_bundle_builder( - i + self.args.bundle_builder_index_offset, - Arc::clone(&provider), - ) - .await?; - sender_handles.push(spawn_guard); - bundle_sender_actions.push(bundle_sender_action); + + for ep in &self.args.entry_points { + 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") + } + } } + // flatten the senders handles to one handle, short-circuit on errors let sender_handle = tokio::spawn( future::try_join_all(sender_handles) @@ -148,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(), ); @@ -186,7 +228,7 @@ where impl

BuilderTask

where - P: PoolServer + Clone, + P: Pool + Clone, { /// Create a new builder task pub fn new( @@ -208,14 +250,110 @@ where Box::new(self) } - async fn create_bundle_builder( + // 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, - provider: Arc>, + provider: Arc>, + entry_point: E, + simulator: S, ) -> anyhow::Result<( JoinHandle>, mpsc::Sender, - )> { + )> + where + UO: UserOperation + From, + UserOperationVariant: AsRef, + E: EntryPointProvider + Clone, + S: Simulator, + C: JsonRpcClient + 'static, + { let (send_bundle_tx, send_bundle_rx) = mpsc::channel(1); let signer = if let Some(pk) = &self.args.private_key { @@ -262,20 +400,6 @@ where bundle_priority_fee_overhead_percent: self.args.bundle_priority_fee_overhead_percent, }; - let ep = EthersEntryPoint::new( - self.args.chain_spec.entry_point_address, - Arc::clone(&provider), - ); - let simulate_validation_tracer = - SimulateValidationTracerImpl::new(Arc::clone(&provider), ep.clone()); - let simulator = SimulatorImpl::new( - Arc::clone(&provider), - ep.address(), - simulate_validation_tracer, - self.args.sim_settings, - self.args.mempool_configs.clone(), - ); - let submit_provider = rundler_provider::new_provider( &self.args.submit_url, Some(self.args.eth_poll_interval), @@ -311,7 +435,7 @@ where index, self.pool.clone(), simulator, - ep.clone(), + entry_point.clone(), Arc::clone(&provider), proposer_settings, self.event_sender.clone(), @@ -322,7 +446,7 @@ where self.args.chain_spec.clone(), beneficiary, proposer, - ep, + entry_point, transaction_tracker, self.pool.clone(), builder_settings, @@ -332,4 +456,25 @@ where // Spawn each sender as its own independent task Ok((tokio::spawn(builder.send_bundles_in_loop()), send_bundle_tx)) } + + fn create_simulator_v0_6( + &self, + provider: Arc, + ep: E, + mempool_configs: HashMap, + ) -> SimulatorV0_6> + where + C: Provider, + E: EntryPointProvider + Clone, + { + let simulate_validation_tracer = + SimulateValidationTracerImplV0_6::new(Arc::clone(&provider), ep.clone()); + SimulatorV0_6::new( + Arc::clone(&provider), + ep, + simulate_validation_tracer, + self.args.sim_settings, + mempool_configs, + ) + } } diff --git a/crates/dev/src/lib.rs b/crates/dev/src/lib.rs index 7fe999e55..0d70c9724 100644 --- a/crates/dev/src/lib.rs +++ b/crates/dev/src/lib.rs @@ -43,10 +43,10 @@ use ethers::{ }; use rundler_types::{ contracts::v0_6::{ - entry_point::EntryPoint, simple_account::SimpleAccount, + i_entry_point::IEntryPoint, simple_account::SimpleAccount, simple_account_factory::SimpleAccountFactory, verifying_paymaster::VerifyingPaymaster, }, - UserOperation, + v0_6, UserOperation, }; /// Chain ID used by Geth in --dev mode. @@ -185,14 +185,14 @@ pub fn test_signing_key_bytes(test_account_id: u8) -> [u8; 32] { } /// An alternative to the default user op with gas values prefilled. -pub fn base_user_op() -> UserOperation { - UserOperation { +pub fn base_user_op() -> v0_6::UserOperation { + v0_6::UserOperation { call_gas_limit: 1_000_000.into(), verification_gas_limit: 1_000_000.into(), pre_verification_gas: 1_000_000.into(), max_fee_per_gas: 100.into(), max_priority_fee_per_gas: 5.into(), - ..UserOperation::default() + ..v0_6::UserOperation::default() } } @@ -281,7 +281,7 @@ pub async fn deploy_dev_contracts(entry_point_bytecode: &str) -> anyhow::Result< let entry_point_address = deterministic_deploy .deploy_bytecode(entry_point_bytecode, 0) .await?; - let entry_point = EntryPoint::new(entry_point_address, Arc::clone(&deployer_client)); + let entry_point = IEntryPoint::new(entry_point_address, Arc::clone(&deployer_client)); // TODO use deterministic deployment // account factory @@ -315,12 +315,12 @@ pub async fn deploy_dev_contracts(entry_point_bytecode: &str) -> anyhow::Result< factory.create_account(wallet_owner_eoa.address(), salt), ); - let mut op = UserOperation { + let mut op = v0_6::UserOperation { sender: wallet_address, init_code, ..base_user_op() }; - let op_hash = op.op_hash(entry_point.address(), DEV_CHAIN_ID); + let op_hash = op.hash(entry_point.address(), DEV_CHAIN_ID); let signature = wallet_owner_eoa .sign_message(op_hash) .await @@ -348,7 +348,7 @@ pub struct DevClients { /// The client used by the bundler. pub bundler_client: Arc, /// The entry point contract. - pub entry_point: EntryPoint, + pub entry_point: IEntryPoint, /// The account factory contract. pub factory: SimpleAccountFactory>, /// The wallet contract. @@ -373,7 +373,7 @@ impl DevClients { let provider = new_local_provider(); let bundler_client = new_test_client(Arc::clone(&provider), BUNDLER_ACCOUNT_ID); let wallet_owner_client = new_test_client(Arc::clone(&provider), WALLET_OWNER_ACCOUNT_ID); - let entry_point = EntryPoint::new(entry_point_address, Arc::clone(&bundler_client)); + let entry_point = IEntryPoint::new(entry_point_address, Arc::clone(&bundler_client)); let factory = SimpleAccountFactory::new(factory_address, Arc::clone(&provider)); let wallet = SimpleAccount::new(wallet_address, Arc::clone(&provider)); let paymaster = VerifyingPaymaster::new(paymaster_address, Arc::clone(&provider)); @@ -400,7 +400,7 @@ impl DevClients { /// Adds a signature to a user operation. pub async fn add_signature( &self, - op: &mut UserOperation, + op: &mut v0_6::UserOperation, use_paymaster: bool, ) -> anyhow::Result<()> { if use_paymaster { @@ -426,7 +426,7 @@ impl DevClients { paymaster_and_data.extend(paymaster_signature.to_vec()); op.paymaster_and_data = paymaster_and_data.into() } - let op_hash = op.op_hash(self.entry_point.address(), DEV_CHAIN_ID); + let op_hash = op.hash(self.entry_point.address(), DEV_CHAIN_ID); let signature = self .wallet_owner_signer .sign_message(op_hash) @@ -441,7 +441,7 @@ impl DevClients { &self, call: ContractCall, value: U256, - ) -> anyhow::Result { + ) -> anyhow::Result { self.new_wallet_op_internal(call, value, false).await } @@ -450,7 +450,7 @@ impl DevClients { &self, call: ContractCall, value: U256, - ) -> anyhow::Result { + ) -> anyhow::Result { self.new_wallet_op_internal(call, value, true).await } @@ -459,7 +459,7 @@ impl DevClients { call: ContractCall, value: U256, use_paymaster: bool, - ) -> anyhow::Result { + ) -> anyhow::Result { let tx = &call.tx; let inner_call_data = Bytes::clone( tx.data() @@ -480,7 +480,7 @@ impl DevClients { .data() .context("wallet execute should have call data")?, ); - let mut op = UserOperation { + let mut op = v0_6::UserOperation { sender: self.wallet.address(), call_data, nonce, diff --git a/crates/pool/Cargo.toml b/crates/pool/Cargo.toml index 192976810..85ee659ab 100644 --- a/crates/pool/Cargo.toml +++ b/crates/pool/Cargo.toml @@ -44,6 +44,3 @@ rundler-provider = { path = "../provider", features = ["test-utils"] } [build-dependencies] tonic-build.workspace = true - -[features] -test-utils = [ "mockall" ] diff --git a/crates/pool/proto/op_pool/op_pool.proto b/crates/pool/proto/op_pool/op_pool.proto index b6152bfb6..4ab24383c 100644 --- a/crates/pool/proto/op_pool/op_pool.proto +++ b/crates/pool/proto/op_pool/op_pool.proto @@ -17,9 +17,16 @@ syntax = "proto3"; package op_pool; +message UserOperation { + oneof uo { + UserOperationV06 v06 = 1; + UserOperationV07 v07 = 2; + } +} + // Protocol Buffer representation of an ERC-4337 UserOperation. See the official // specification at https://eips.ethereum.org/EIPS/eip-4337#definitions -message UserOperation { +message UserOperationV06 { // The account making the operation bytes sender = 1; // Anti-replay parameter (see “Semi-abstracted Nonce Support” ) @@ -43,11 +50,48 @@ message UserOperation { // 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; @@ -502,20 +546,18 @@ message OperationDropTooSoon { // PRECHECK VIOLATIONS message PrecheckViolationError { oneof violation { - InitCodeTooShort init_code_too_short = 1; - SenderIsNotContractAndNoInitCode sender_is_not_contract_and_no_init_code = 2; - ExistingSenderWithInitCode existing_sender_with_init_code = 3; - FactoryIsNotContract factory_is_not_contract = 4; - TotalGasLimitTooHigh total_gas_limit_too_high = 5; - VerificationGasLimitTooHigh verification_gas_limit_too_high = 6; - PreVerificationGasTooLow pre_verification_gas_too_low = 7; - PaymasterTooShort paymaster_too_short = 8; - PaymasterIsNotContract paymaster_is_not_contract = 9; - PaymasterDepositTooLow paymaster_deposit_too_low = 10; - SenderFundsTooLow sender_funds_too_low = 11; - MaxFeePerGasTooLow max_fee_per_gas_too_low = 12; - MaxPriorityFeePerGasTooLow max_priority_fee_per_gas_too_low = 13; - CallGasLimitTooLow call_gas_limit_too_low = 14; + SenderIsNotContractAndNoInitCode sender_is_not_contract_and_no_init_code = 1; + ExistingSenderWithInitCode existing_sender_with_init_code = 2; + FactoryIsNotContract factory_is_not_contract = 3; + TotalGasLimitTooHigh total_gas_limit_too_high = 4; + VerificationGasLimitTooHigh verification_gas_limit_too_high = 5; + PreVerificationGasTooLow pre_verification_gas_too_low = 6; + PaymasterIsNotContract paymaster_is_not_contract = 7; + PaymasterDepositTooLow paymaster_deposit_too_low = 8; + SenderFundsTooLow sender_funds_too_low = 9; + MaxFeePerGasTooLow max_fee_per_gas_too_low = 10; + MaxPriorityFeePerGasTooLow max_priority_fee_per_gas_too_low = 11; + CallGasLimitTooLow call_gas_limit_too_low = 12; } } @@ -605,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; } } @@ -686,3 +729,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/chain.rs b/crates/pool/src/chain.rs index b35bfcb3c..4709046f4 100644 --- a/crates/pool/src/chain.rs +++ b/crates/pool/src/chain.rs @@ -12,14 +12,14 @@ // If not, see https://www.gnu.org/licenses/. use std::{ - collections::{HashSet, VecDeque}, + collections::{HashMap, HashSet, VecDeque}, sync::Arc, time::Duration, }; use anyhow::{ensure, Context}; use ethers::{ - contract, + contract::EthLogDecode, prelude::EthEvent, types::{Address, Block, Filter, Log, H256, U256}, }; @@ -27,11 +27,8 @@ use futures::future; use rundler_provider::Provider; use rundler_task::block_watcher; use rundler_types::{ - contracts::v0_6::{ - entry_point::{DepositedFilter, WithdrawnFilter}, - i_entry_point::UserOperationEventFilter, - }, - Timestamp, UserOperationId, + contracts::{v0_6::i_entry_point as entry_point_v0_6, v0_7::i_entry_point as entry_point_v0_7}, + EntryPointVersion, Timestamp, UserOperationId, }; use tokio::{ select, @@ -110,7 +107,7 @@ impl MinedOp { pub(crate) struct Settings { pub(crate) history_size: u64, pub(crate) poll_interval: Duration, - pub(crate) entry_point_addresses: Vec

, + pub(crate) entry_point_addresses: HashMap, } #[derive(Debug)] @@ -402,12 +399,33 @@ impl Chain

{ .await .expect("semaphore should not be closed"); - let deposit = DepositedFilter::abi_signature(); - let uo_filter = UserOperationEventFilter::abi_signature(); - let events: Vec<&str> = vec![&deposit, &uo_filter]; + // Load events for both entry point versions. + // v0.6 + let uo_filter_v0_6 = entry_point_v0_6::UserOperationEventFilter::abi_signature(); + let deposit_v0_6 = entry_point_v0_6::DepositedFilter::abi_signature(); + let withdrawn_v0_6 = entry_point_v0_6::WithdrawnFilter::abi_signature(); + // v0.7 + let uo_filter_v0_7 = entry_point_v0_7::UserOperationEventFilter::abi_signature(); + let deposit_v0_7 = entry_point_v0_7::DepositedFilter::abi_signature(); + let withdrawn_v0_7 = entry_point_v0_7::WithdrawnFilter::abi_signature(); + + let events: Vec<&str> = vec![ + &uo_filter_v0_6, + &deposit_v0_6, + &withdrawn_v0_6, + &uo_filter_v0_7, + &deposit_v0_7, + &withdrawn_v0_7, + ]; let filter = Filter::new() - .address(self.settings.entry_point_addresses.clone()) + .address( + self.settings + .entry_point_addresses + .keys() + .cloned() + .collect::>(), + ) .events(events) .at_block_hash(block_hash); let logs = self @@ -416,68 +434,112 @@ impl Chain

{ .await .context("chain state should load user operation events")?; - let mined_ops = self.load_mined_ops(&logs); - let entity_balance_updates = self.load_entity_balance_updates(&logs); - - Ok((mined_ops, entity_balance_updates)) - } - - fn load_mined_ops(&self, logs: &Vec) -> Vec { let mut mined_ops = vec![]; + let mut entity_balance_updates = vec![]; for log in logs { - let entry_point = log.address; - if let Ok(event) = contract::parse_log::(log.clone()) { - let paymaster = if event.paymaster.is_zero() { - None - } else { - Some(event.paymaster) - }; - - let mined = MinedOp { - hash: event.user_op_hash.into(), - entry_point, - sender: event.sender, - nonce: event.nonce, - actual_gas_cost: event.actual_gas_cost, - paymaster, - }; - - mined_ops.push(mined); + match self.settings.entry_point_addresses.get(&log.address) { + Some(EntryPointVersion::V0_6) => { + Self::load_v0_6(log, &mut mined_ops, &mut entity_balance_updates) + } + Some(EntryPointVersion::V0_7) => { + Self::load_v0_7(log, &mut mined_ops, &mut entity_balance_updates) + } + Some(EntryPointVersion::Unspecified) | None => { + warn!( + "Log with unknown entry point address: {:?}. Ignoring.", + log.address + ); + } } } - mined_ops + Ok((mined_ops, entity_balance_updates)) } - fn load_entity_balance_updates(&self, logs: &Vec) -> Vec { - let mut balance_updates = vec![]; - - for log in logs { - let entrypoint = log.address; - if let Ok(event) = contract::parse_log::(log.clone()) { - let info = BalanceUpdate { - entrypoint, - address: event.account, - amount: event.total_deposit, - is_addition: true, - }; - - balance_updates.push(info); + fn load_v0_6(log: Log, mined_ops: &mut Vec, balance_updates: &mut Vec) { + let address = log.address; + if let Ok(event) = entry_point_v0_6::IEntryPointEvents::decode_log(&log.into()) { + match event { + entry_point_v0_6::IEntryPointEvents::UserOperationEventFilter(event) => { + let paymaster = if event.paymaster.is_zero() { + None + } else { + Some(event.paymaster) + }; + let mined = MinedOp { + hash: event.user_op_hash.into(), + entry_point: address, + sender: event.sender, + nonce: event.nonce, + actual_gas_cost: event.actual_gas_cost, + paymaster, + }; + mined_ops.push(mined); + } + entry_point_v0_6::IEntryPointEvents::DepositedFilter(event) => { + let info = BalanceUpdate { + entrypoint: address, + address: event.account, + amount: event.total_deposit, + is_addition: true, + }; + balance_updates.push(info); + } + entry_point_v0_6::IEntryPointEvents::WithdrawnFilter(event) => { + let info = BalanceUpdate { + entrypoint: address, + address: event.account, + amount: event.amount, + is_addition: false, + }; + balance_updates.push(info); + } + _ => {} } + } + } - if let Ok(event) = contract::parse_log::(log.clone()) { - let info = BalanceUpdate { - entrypoint, - address: event.account, - amount: event.amount, - is_addition: false, - }; - - balance_updates.push(info); + fn load_v0_7(log: Log, mined_ops: &mut Vec, balance_updates: &mut Vec) { + let address = log.address; + if let Ok(event) = entry_point_v0_7::IEntryPointEvents::decode_log(&log.into()) { + match event { + entry_point_v0_7::IEntryPointEvents::UserOperationEventFilter(event) => { + let paymaster = if event.paymaster.is_zero() { + None + } else { + Some(event.paymaster) + }; + let mined = MinedOp { + hash: event.user_op_hash.into(), + entry_point: address, + sender: event.sender, + nonce: event.nonce, + actual_gas_cost: event.actual_gas_cost, + paymaster, + }; + mined_ops.push(mined); + } + entry_point_v0_7::IEntryPointEvents::DepositedFilter(event) => { + let info = BalanceUpdate { + entrypoint: address, + address: event.account, + amount: event.total_deposit, + is_addition: true, + }; + balance_updates.push(info); + } + entry_point_v0_7::IEntryPointEvents::WithdrawnFilter(event) => { + let info = BalanceUpdate { + entrypoint: address, + address: event.account, + amount: event.amount, + is_addition: false, + }; + balance_updates.push(info); + } + _ => {} } } - - balance_updates } fn block_with_number(&self, number: u64) -> Option<&BlockSummary> { @@ -614,29 +676,45 @@ mod tests { use super::*; const HISTORY_SIZE: u64 = 3; - const ENTRY_POINT_ADDRESS: Address = H160(*b"01234567890123456789"); + const ENTRY_POINT_ADDRESS_V0_6: Address = H160(*b"01234567890123456789"); + const ENTRY_POINT_ADDRESS_V0_7: Address = H160(*b"98765432109876543210"); #[derive(Clone, Debug)] struct MockBlock { hash: H256, + events: Vec, + } + + #[derive(Clone, Debug, Default)] + struct MockEntryPointEvents { + address: Address, op_hashes: Vec, deposit_addresses: Vec

, withdrawal_addresses: Vec
, } impl MockBlock { - fn new( - hash: H256, + fn new(hash: H256) -> Self { + Self { + hash, + events: vec![], + } + } + + fn add_ep( + mut self, + address: Address, op_hashes: Vec, deposit_addresses: Vec
, withdrawal_addresses: Vec
, ) -> Self { - Self { - hash, + self.events.push(MockEntryPointEvents { + address, op_hashes, deposit_addresses, withdrawal_addresses, - } + }); + self } } @@ -683,21 +761,44 @@ mod tests { }; let mut joined_logs: Vec = Vec::new(); - joined_logs.extend(block.op_hashes.iter().copied().map(fake_log)); - joined_logs.extend( - block - .deposit_addresses - .iter() - .copied() - .map(fake_deposit_log), - ); - joined_logs.extend( - block - .withdrawal_addresses - .iter() - .copied() - .map(fake_withdrawal_log), - ); + + for events in &block.events { + if events.address == ENTRY_POINT_ADDRESS_V0_6 { + joined_logs.extend(events.op_hashes.iter().copied().map(fake_mined_log_v0_6)); + joined_logs.extend( + events + .deposit_addresses + .iter() + .copied() + .map(fake_deposit_log_v0_6), + ); + joined_logs.extend( + events + .withdrawal_addresses + .iter() + .copied() + .map(fake_withdrawal_log_v0_6), + ); + } else if events.address == ENTRY_POINT_ADDRESS_V0_7 { + joined_logs.extend(events.op_hashes.iter().copied().map(fake_mined_log_v0_7)); + joined_logs.extend( + events + .deposit_addresses + .iter() + .copied() + .map(fake_deposit_log_v0_7), + ); + joined_logs.extend( + events + .withdrawal_addresses + .iter() + .copied() + .map(fake_withdrawal_log_v0_7), + ); + } else { + panic!("Unknown entry point address: {:?}", events.address); + } + } joined_logs } @@ -707,10 +808,25 @@ mod tests { async fn test_initial_load() { let (mut chain, controller) = new_chain(); controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(101), hash(102)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(103)], vec![], vec![]), - MockBlock::new(hash(2), vec![], vec![], vec![]), - MockBlock::new(hash(3), vec![hash(104), hash(105)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101), hash(102)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(103)], + vec![], + vec![], + ), + MockBlock::new(hash(2)).add_ep(ENTRY_POINT_ADDRESS_V0_6, vec![], vec![], vec![]), + MockBlock::new(hash(3)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(104), hash(105)], + vec![], + vec![], + ), ]); let update = chain.sync_to_block(controller.get_head()).await.unwrap(); // With a history size of 3, we should get updates from all blocks except the first one. @@ -722,7 +838,11 @@ mod tests { latest_block_timestamp: 0.into(), earliest_remembered_block_number: 1, reorg_depth: 0, - mined_ops: vec![fake_mined_op(103), fake_mined_op(104), fake_mined_op(105),], + mined_ops: vec![ + fake_mined_op(103, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(104, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(105, ENTRY_POINT_ADDRESS_V0_6), + ], unmined_ops: vec![], entity_balance_updates: vec![], unmined_entity_balance_updates: vec![], @@ -735,15 +855,35 @@ mod tests { async fn test_simple_advance() { let (mut chain, controller) = new_chain(); controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(101), hash(102)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(103)], vec![], vec![]), - MockBlock::new(hash(2), vec![], vec![], vec![]), - MockBlock::new(hash(3), vec![hash(104), hash(105)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101), hash(102)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(103)], + vec![], + vec![], + ), + MockBlock::new(hash(2)).add_ep(ENTRY_POINT_ADDRESS_V0_6, vec![], vec![], vec![]), + MockBlock::new(hash(3)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(104), hash(105)], + vec![], + vec![], + ), ]); chain.sync_to_block(controller.get_head()).await.unwrap(); controller .get_blocks_mut() - .push(MockBlock::new(hash(4), vec![hash(106)], vec![], vec![])); + .push(MockBlock::new(hash(4)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(106)], + vec![], + vec![], + )); let update = chain.sync_to_block(controller.get_head()).await.unwrap(); assert_eq!( update, @@ -753,7 +893,7 @@ mod tests { latest_block_timestamp: 0.into(), earliest_remembered_block_number: 2, reorg_depth: 0, - mined_ops: vec![fake_mined_op(106)], + mined_ops: vec![fake_mined_op(106, ENTRY_POINT_ADDRESS_V0_6)], unmined_ops: vec![], entity_balance_updates: vec![], unmined_entity_balance_updates: vec![], @@ -766,10 +906,20 @@ mod tests { async fn test_forward_reorg() { let (mut chain, controller) = new_chain(); controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(100)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(101)], vec![], vec![]), - MockBlock::new( - hash(2), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(100)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101)], + vec![], + vec![], + ), + MockBlock::new(hash(2)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, vec![hash(102)], vec![Address::zero()], vec![addr(1)], @@ -781,9 +931,24 @@ mod tests { let mut blocks = controller.get_blocks_mut(); blocks.pop(); blocks.extend([ - MockBlock::new(hash(12), vec![hash(112)], vec![], vec![]), - MockBlock::new(hash(13), vec![hash(113)], vec![], vec![]), - MockBlock::new(hash(14), vec![hash(114)], vec![], vec![addr(3)]), + MockBlock::new(hash(12)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(112)], + vec![], + vec![], + ), + MockBlock::new(hash(13)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(113)], + vec![], + vec![], + ), + MockBlock::new(hash(14)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(114)], + vec![], + vec![addr(3)], + ), ]); } let update = chain.sync_to_block(controller.get_head()).await.unwrap(); @@ -795,12 +960,21 @@ mod tests { latest_block_timestamp: 0.into(), earliest_remembered_block_number: 2, reorg_depth: 1, - mined_ops: vec![fake_mined_op(112), fake_mined_op(113), fake_mined_op(114)], - unmined_ops: vec![fake_mined_op(102)], - entity_balance_updates: vec![fake_mined_balance_update(addr(3), 0.into(), false)], + mined_ops: vec![ + fake_mined_op(112, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(113, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(114, ENTRY_POINT_ADDRESS_V0_6) + ], + unmined_ops: vec![fake_mined_op(102, ENTRY_POINT_ADDRESS_V0_6)], + entity_balance_updates: vec![fake_mined_balance_update( + addr(3), + 0.into(), + false, + ENTRY_POINT_ADDRESS_V0_6 + )], unmined_entity_balance_updates: vec![ - fake_mined_balance_update(addr(0), 0.into(), true), - fake_mined_balance_update(addr(1), 0.into(), false), + fake_mined_balance_update(addr(0), 0.into(), true, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_balance_update(addr(1), 0.into(), false, ENTRY_POINT_ADDRESS_V0_6), ], reorg_larger_than_history: false, } @@ -811,9 +985,24 @@ mod tests { async fn test_sideways_reorg() { let (mut chain, controller) = new_chain(); controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(100)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(101)], vec![addr(1)], vec![addr(9)]), - MockBlock::new(hash(2), vec![hash(102)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(100)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101)], + vec![addr(1)], + vec![addr(9)], + ), + MockBlock::new(hash(2)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(102)], + vec![], + vec![], + ), ]); chain.sync_to_block(controller.get_head()).await.unwrap(); { @@ -822,25 +1011,46 @@ mod tests { blocks.pop(); blocks.pop(); blocks.extend([ - MockBlock::new(hash(11), vec![hash(111)], vec![addr(2)], vec![]), - MockBlock::new(hash(12), vec![hash(112)], vec![], vec![]), + MockBlock::new(hash(11)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(111)], + vec![addr(2)], + vec![], + ), + MockBlock::new(hash(12)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(112)], + vec![], + vec![], + ), ]); } let update = chain.sync_to_block(controller.get_head()).await.unwrap(); assert_eq!( update, ChainUpdate { - entity_balance_updates: vec![fake_mined_balance_update(addr(2), 0.into(), true)], + entity_balance_updates: vec![fake_mined_balance_update( + addr(2), + 0.into(), + true, + ENTRY_POINT_ADDRESS_V0_6 + )], latest_block_number: 2, latest_block_hash: hash(12), latest_block_timestamp: 0.into(), earliest_remembered_block_number: 0, reorg_depth: 2, - mined_ops: vec![fake_mined_op(111), fake_mined_op(112)], - unmined_ops: vec![fake_mined_op(101), fake_mined_op(102)], + mined_ops: vec![ + fake_mined_op(111, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(112, ENTRY_POINT_ADDRESS_V0_6) + ], + unmined_ops: vec![ + fake_mined_op(101, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(102, ENTRY_POINT_ADDRESS_V0_6) + ], unmined_entity_balance_updates: vec![ - fake_mined_balance_update(addr(1), 0.into(), true), - fake_mined_balance_update(addr(9), 0.into(), false), + fake_mined_balance_update(addr(1), 0.into(), true, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_balance_update(addr(9), 0.into(), false, ENTRY_POINT_ADDRESS_V0_6), ], reorg_larger_than_history: false, } @@ -851,9 +1061,24 @@ mod tests { async fn test_backwards_reorg() { let (mut chain, controller) = new_chain(); controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(100)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(101)], vec![], vec![]), - MockBlock::new(hash(2), vec![hash(102)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(100)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101)], + vec![], + vec![], + ), + MockBlock::new(hash(2)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(102)], + vec![], + vec![], + ), ]); chain.sync_to_block(controller.get_head()).await.unwrap(); { @@ -861,8 +1086,8 @@ mod tests { let mut blocks = controller.get_blocks_mut(); blocks.pop(); blocks.pop(); - blocks.push(MockBlock::new( - hash(11), + blocks.push(MockBlock::new(hash(11)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, vec![hash(111)], vec![addr(1)], vec![], @@ -873,13 +1098,21 @@ mod tests { update, ChainUpdate { latest_block_number: 1, - entity_balance_updates: vec![fake_mined_balance_update(addr(1), 0.into(), true)], + entity_balance_updates: vec![fake_mined_balance_update( + addr(1), + 0.into(), + true, + ENTRY_POINT_ADDRESS_V0_6 + )], latest_block_hash: hash(11), latest_block_timestamp: 0.into(), earliest_remembered_block_number: 0, reorg_depth: 2, - mined_ops: vec![fake_mined_op(111)], - unmined_ops: vec![fake_mined_op(101), fake_mined_op(102)], + mined_ops: vec![fake_mined_op(111, ENTRY_POINT_ADDRESS_V0_6)], + unmined_ops: vec![ + fake_mined_op(101, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(102, ENTRY_POINT_ADDRESS_V0_6) + ], unmined_entity_balance_updates: vec![], reorg_larger_than_history: false, } @@ -890,18 +1123,58 @@ mod tests { async fn test_reorg_longer_than_history() { let (mut chain, controller) = new_chain(); controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(100)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(101)], vec![], vec![]), - MockBlock::new(hash(2), vec![hash(102)], vec![], vec![]), - MockBlock::new(hash(3), vec![hash(103)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(100)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101)], + vec![], + vec![], + ), + MockBlock::new(hash(2)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(102)], + vec![], + vec![], + ), + MockBlock::new(hash(3)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(103)], + vec![], + vec![], + ), ]); chain.sync_to_block(controller.get_head()).await.unwrap(); // The history has size 3, so after this update it's completely unrecognizable. controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(100)], vec![], vec![]), - MockBlock::new(hash(11), vec![hash(111)], vec![], vec![]), - MockBlock::new(hash(12), vec![hash(112)], vec![], vec![]), - MockBlock::new(hash(13), vec![hash(113)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(100)], + vec![], + vec![], + ), + MockBlock::new(hash(11)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(111)], + vec![], + vec![], + ), + MockBlock::new(hash(12)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(112)], + vec![], + vec![], + ), + MockBlock::new(hash(13)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(113)], + vec![], + vec![], + ), ]); let update = chain.sync_to_block(controller.get_head()).await.unwrap(); assert_eq!( @@ -912,8 +1185,16 @@ mod tests { latest_block_timestamp: 0.into(), earliest_remembered_block_number: 1, reorg_depth: 3, - mined_ops: vec![fake_mined_op(111), fake_mined_op(112), fake_mined_op(113)], - unmined_ops: vec![fake_mined_op(101), fake_mined_op(102), fake_mined_op(103)], + mined_ops: vec![ + fake_mined_op(111, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(112, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(113, ENTRY_POINT_ADDRESS_V0_6) + ], + unmined_ops: vec![ + fake_mined_op(101, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(102, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(103, ENTRY_POINT_ADDRESS_V0_6) + ], entity_balance_updates: vec![], unmined_entity_balance_updates: vec![], reorg_larger_than_history: true, @@ -925,16 +1206,31 @@ mod tests { async fn test_advance_larger_than_history_size() { let (mut chain, controller) = new_chain(); controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(100)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(101)], vec![], vec![]), - MockBlock::new(hash(2), vec![hash(102)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(100)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101)], + vec![], + vec![], + ), + MockBlock::new(hash(2)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(102)], + vec![], + vec![], + ), ]); chain.sync_to_block(controller.get_head()).await.unwrap(); { let mut blocks = controller.get_blocks_mut(); for i in 3..7 { - blocks.push(MockBlock::new( - hash(10 + i), + blocks.push(MockBlock::new(hash(10 + i)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, vec![hash(100 + i)], vec![], vec![], @@ -952,7 +1248,11 @@ mod tests { reorg_depth: 0, entity_balance_updates: vec![], unmined_entity_balance_updates: vec![], - mined_ops: vec![fake_mined_op(104), fake_mined_op(105), fake_mined_op(106)], + mined_ops: vec![ + fake_mined_op(104, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(105, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(106, ENTRY_POINT_ADDRESS_V0_6) + ], unmined_ops: vec![], reorg_larger_than_history: false, } @@ -964,8 +1264,18 @@ mod tests { async fn test_latest_block_number_smaller_than_history_size() { let (mut chain, controller) = new_chain(); let blocks = vec![ - MockBlock::new(hash(0), vec![hash(101), hash(102)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(103)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101), hash(102)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(103)], + vec![], + vec![], + ), ]; controller.set_blocks(blocks); let update = chain.sync_to_block(controller.get_head()).await.unwrap(); @@ -977,7 +1287,11 @@ mod tests { latest_block_timestamp: 0.into(), earliest_remembered_block_number: 0, reorg_depth: 0, - mined_ops: vec![fake_mined_op(101), fake_mined_op(102), fake_mined_op(103),], + mined_ops: vec![ + fake_mined_op(101, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(102, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(103, ENTRY_POINT_ADDRESS_V0_6), + ], unmined_ops: vec![], entity_balance_updates: vec![], unmined_entity_balance_updates: vec![], @@ -986,6 +1300,54 @@ mod tests { ); } + #[tokio::test] + async fn test_mixed_event_types() { + let (mut chain, controller) = new_chain(); + controller.set_blocks(vec![MockBlock::new(hash(0)) + .add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101), hash(102)], + vec![addr(1), addr(2)], + vec![addr(3), addr(4)], + ) + .add_ep( + ENTRY_POINT_ADDRESS_V0_7, + vec![hash(201), hash(202)], + vec![addr(5), addr(6)], + vec![addr(7), addr(8)], + )]); + let update = chain.sync_to_block(controller.get_head()).await.unwrap(); + assert_eq!( + update, + ChainUpdate { + latest_block_number: 0, + latest_block_hash: hash(0), + latest_block_timestamp: 0.into(), + earliest_remembered_block_number: 0, + reorg_depth: 0, + mined_ops: vec![ + fake_mined_op(101, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(102, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(201, ENTRY_POINT_ADDRESS_V0_7), + fake_mined_op(202, ENTRY_POINT_ADDRESS_V0_7), + ], + unmined_ops: vec![], + entity_balance_updates: vec![ + fake_mined_balance_update(addr(1), 0.into(), true, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_balance_update(addr(2), 0.into(), true, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_balance_update(addr(3), 0.into(), false, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_balance_update(addr(4), 0.into(), false, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_balance_update(addr(5), 0.into(), true, ENTRY_POINT_ADDRESS_V0_7), + fake_mined_balance_update(addr(6), 0.into(), true, ENTRY_POINT_ADDRESS_V0_7), + fake_mined_balance_update(addr(7), 0.into(), false, ENTRY_POINT_ADDRESS_V0_7), + fake_mined_balance_update(addr(8), 0.into(), false, ENTRY_POINT_ADDRESS_V0_7), + ], + unmined_entity_balance_updates: vec![], + reorg_larger_than_history: false, + } + ); + } + fn new_chain() -> (Chain, ProviderController) { let (provider, controller) = new_mock_provider(); let chain = Chain::new( @@ -993,7 +1355,10 @@ mod tests { Settings { history_size: HISTORY_SIZE, poll_interval: Duration::from_secs(250), // Not used in tests. - entry_point_addresses: vec![ENTRY_POINT_ADDRESS], + entry_point_addresses: HashMap::from([ + (ENTRY_POINT_ADDRESS_V0_6, EntryPointVersion::V0_6), + (ENTRY_POINT_ADDRESS_V0_7, EntryPointVersion::V0_7), + ]), }, ); (chain, controller) @@ -1023,12 +1388,69 @@ mod tests { (provider, controller) } - fn fake_log(op_hash: H256) -> Log { + fn fake_mined_log_v0_6(op_hash: H256) -> Log { + Log { + address: ENTRY_POINT_ADDRESS_V0_6, + topics: vec![ + H256::from(utils::keccak256( + entry_point_v0_6::UserOperationEventFilter::abi_signature().as_bytes(), + )), + op_hash, + H256::zero(), // sender + H256::zero(), // paymaster + ], + data: AbiEncode::encode(( + U256::zero(), // nonce + true, // success + U256::zero(), // actual_gas_cost + U256::zero(), // actual_gas_used + )) + .into(), + ..Default::default() + } + } + + fn fake_deposit_log_v0_6(deposit_address: Address) -> Log { + Log { + address: ENTRY_POINT_ADDRESS_V0_6, + topics: vec![ + H256::from(utils::keccak256( + entry_point_v0_6::DepositedFilter::abi_signature().as_bytes(), + )), + H256::from(deposit_address), + ], + data: AbiEncode::encode(( + U256::zero(), // totalDeposits + )) + .into(), + ..Default::default() + } + } + + fn fake_withdrawal_log_v0_6(withdrawal_address: Address) -> Log { + Log { + address: ENTRY_POINT_ADDRESS_V0_6, + topics: vec![ + H256::from(utils::keccak256( + entry_point_v0_6::WithdrawnFilter::abi_signature().as_bytes(), + )), + H256::from(withdrawal_address), + ], + data: AbiEncode::encode(( + Address::zero(), // withdrawAddress + U256::zero(), // amount + )) + .into(), + ..Default::default() + } + } + + fn fake_mined_log_v0_7(op_hash: H256) -> Log { Log { - address: ENTRY_POINT_ADDRESS, + address: ENTRY_POINT_ADDRESS_V0_7, topics: vec![ H256::from(utils::keccak256( - UserOperationEventFilter::abi_signature().as_bytes(), + entry_point_v0_7::UserOperationEventFilter::abi_signature().as_bytes(), )), op_hash, H256::zero(), // sender @@ -1045,12 +1467,12 @@ mod tests { } } - fn fake_deposit_log(deposit_address: Address) -> Log { + fn fake_deposit_log_v0_7(deposit_address: Address) -> Log { Log { - address: ENTRY_POINT_ADDRESS, + address: ENTRY_POINT_ADDRESS_V0_7, topics: vec![ H256::from(utils::keccak256( - DepositedFilter::abi_signature().as_bytes(), + entry_point_v0_7::DepositedFilter::abi_signature().as_bytes(), )), H256::from(deposit_address), ], @@ -1062,12 +1484,12 @@ mod tests { } } - fn fake_withdrawal_log(withdrawal_address: Address) -> Log { + fn fake_withdrawal_log_v0_7(withdrawal_address: Address) -> Log { Log { - address: ENTRY_POINT_ADDRESS, + address: ENTRY_POINT_ADDRESS_V0_7, topics: vec![ H256::from(utils::keccak256( - WithdrawnFilter::abi_signature().as_bytes(), + entry_point_v0_7::WithdrawnFilter::abi_signature().as_bytes(), )), H256::from(withdrawal_address), ], @@ -1080,10 +1502,10 @@ mod tests { } } - fn fake_mined_op(n: u8) -> MinedOp { + fn fake_mined_op(n: u8, ep: Address) -> MinedOp { MinedOp { hash: hash(n), - entry_point: ENTRY_POINT_ADDRESS, + entry_point: ep, sender: Address::zero(), nonce: U256::zero(), actual_gas_cost: U256::zero(), @@ -1095,10 +1517,11 @@ mod tests { address: Address, amount: U256, is_addition: bool, + ep: Address, ) -> BalanceUpdate { BalanceUpdate { address, - entrypoint: ENTRY_POINT_ADDRESS, + entrypoint: ep, amount, is_addition, } diff --git a/crates/pool/src/emit.rs b/crates/pool/src/emit.rs index 520a1d4b3..3d6d2ce7e 100644 --- a/crates/pool/src/emit.rs +++ b/crates/pool/src/emit.rs @@ -14,7 +14,7 @@ use std::fmt::Display; use ethers::types::{Address, H256}; -use rundler_types::{Entity, EntityType, Timestamp, UserOperation}; +use rundler_types::{Entity, EntityType, Timestamp, UserOperation, UserOperationVariant}; use rundler_utils::strs; use crate::mempool::OperationOrigin; @@ -27,7 +27,7 @@ pub enum OpPoolEvent { /// Operation hash op_hash: H256, /// The full operation - op: UserOperation, + op: UserOperationVariant, /// Block number the operation was added to the pool block_number: u64, /// Operation origin @@ -157,8 +157,8 @@ impl Display for OpPoolEvent { format_entity_status("Factory", entities.factory.as_ref()), format_entity_status("Paymaster", entities.paymaster.as_ref()), format_entity_status("Aggregator", entities.aggregator.as_ref()), - op.max_fee_per_gas, - op.max_priority_fee_per_gas, + op.max_fee_per_gas(), + op.max_priority_fee_per_gas(), ) } OpPoolEvent::RemovedOp { op_hash, reason } => { diff --git a/crates/pool/src/lib.rs b/crates/pool/src/lib.rs index aab69baf5..8dc63e071 100644 --- a/crates/pool/src/lib.rs +++ b/crates/pool/src/lib.rs @@ -25,16 +25,12 @@ mod emit; pub use emit::OpPoolEvent as PoolEvent; mod mempool; -pub use mempool::{ - MempoolError, PoolConfig, PoolOperation, Reputation, ReputationStatus, StakeStatus, -}; +pub use mempool::PoolConfig; mod server; #[cfg(feature = "test-utils")] pub use server::MockPoolServer; -pub use server::{ - LocalPoolBuilder, LocalPoolHandle, PoolResult, PoolServer, PoolServerError, RemotePoolClient, -}; +pub use server::{LocalPoolBuilder, LocalPoolHandle, RemotePoolClient}; mod task; pub use task::{Args as PoolTaskArgs, PoolTask}; diff --git a/crates/pool/src/mempool/error.rs b/crates/pool/src/mempool/error.rs deleted file mode 100644 index 3d3370bbc..000000000 --- a/crates/pool/src/mempool/error.rs +++ /dev/null @@ -1,114 +0,0 @@ -// 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::mem; - -use ethers::{abi::Address, types::U256}; -use rundler_sim::{ - PrecheckError, PrecheckViolation, SimulationError, SimulationViolation, ViolationError, -}; -use rundler_types::Entity; - -/// Mempool result type. -pub(crate) type MempoolResult = std::result::Result; - -/// Mempool error type. -#[derive(Debug, thiserror::Error)] -pub enum MempoolError { - /// Some other error occurred - #[error(transparent)] - Other(#[from] anyhow::Error), - /// Operation with the same hash already in pool - #[error("Operation already known")] - OperationAlreadyKnown, - /// Operation with same sender/nonce already in pool - /// and the replacement operation has lower gas price. - #[error("Replacement operation underpriced. Existing priority fee: {0}. Existing fee: {1}")] - ReplacementUnderpriced(U256, U256), - /// Max operations reached for unstaked sender [UREP-010] or unstaked non-sender entity [UREP-020] - #[error("Max operations ({0}) reached for entity {1}")] - MaxOperationsReached(usize, Address), - /// Multiple roles violation - /// Spec rule: STO-040 - #[error("A {} at {} in this UserOperation is used as a sender entity in another UserOperation currently in mempool.", .0.kind, .0.address)] - MultipleRolesViolation(Entity), - /// An associated storage slot that is accessed in the UserOperation is being used as a sender by another UserOperation in the mempool. - /// Spec rule: STO-041 - #[error("An associated storage slot that is accessed in the UserOperation is being used as a sender by another UserOperation in the mempool")] - AssociatedStorageIsAlternateSender, - /// Sender address used as different entity in another UserOperation currently in the mempool. - /// Spec rule: STO-040 - #[error("The sender address {0} is used as a different entity in another UserOperation currently in mempool")] - SenderAddressUsedAsAlternateEntity(Address), - /// An entity associated with the operation is throttled/banned. - #[error("Entity {0} is throttled/banned")] - EntityThrottled(Entity), - /// Operation was discarded on inserting due to size limit - #[error("Operation was discarded on inserting")] - DiscardedOnInsert, - /// Paymaster balance too low - /// Spec rule: EREP-010 - #[error("Paymaster balance too low. Required balance: {0}. Current balance {1}")] - PaymasterBalanceTooLow(U256, U256), - /// Operation was rejected due to a precheck violation - #[error("Operation violation during precheck {0}")] - PrecheckViolation(PrecheckViolation), - /// Operation was rejected due to a simulation violation - #[error("Operation violation during simulation {0}")] - SimulationViolation(SimulationViolation), - /// Operation was rejected because it used an unsupported aggregator - #[error("Unsupported aggregator {0}")] - UnsupportedAggregator(Address), - /// An unknown entry point was specified - #[error("Unknown entry point {0}")] - UnknownEntryPoint(Address), - /// The operation drop attempt too soon after being added to the pool - #[error("Operation drop attempt too soon after being added to the pool. Added at {0}, attempted to drop at {1}, must wait {2} blocks.")] - OperationDropTooSoon(u64, u64, u64), -} - -impl From for MempoolError { - fn from(mut error: SimulationError) -> Self { - let SimulationError { - violation_error, .. - } = &mut error; - let ViolationError::Violations(violations) = violation_error else { - return Self::Other((*violation_error).clone().into()); - }; - - let Some(violation) = violations.iter_mut().min() else { - return Self::Other((*violation_error).clone().into()); - }; - - // extract violation and replace with dummy - Self::SimulationViolation(mem::replace(violation, SimulationViolation::DidNotRevert)) - } -} - -impl From for MempoolError { - fn from(mut error: PrecheckError) -> Self { - let PrecheckError::Violations(violations) = &mut error else { - return Self::Other(error.into()); - }; - - let Some(violation) = violations.iter_mut().min() else { - return Self::Other(error.into()); - }; - - // extract violation and replace with dummy - Self::PrecheckViolation(mem::replace( - violation, - PrecheckViolation::InitCodeTooShort(0), - )) - } -} diff --git a/crates/pool/src/mempool/mod.rs b/crates/pool/src/mempool/mod.rs index c9c22fd78..d034d2d96 100644 --- a/crates/pool/src/mempool/mod.rs +++ b/crates/pool/src/mempool/mod.rs @@ -11,15 +11,11 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -mod error; -pub use error::MempoolError; - mod entity_tracker; mod pool; mod reputation; pub(crate) use reputation::{AddressReputation, ReputationParams}; -pub use reputation::{Reputation, ReputationStatus}; mod size; @@ -32,19 +28,23 @@ use std::{ sync::Arc, }; -use ethers::types::{Address, H256, U256}; +use ethers::types::{Address, H256}; #[cfg(test)] use mockall::automock; -use rundler_sim::{EntityInfos, MempoolConfig, PrecheckSettings, SimulationSettings}; +use rundler_sim::{MempoolConfig, PrecheckSettings, SimulationSettings}; use rundler_types::{ - Entity, EntityType, EntityUpdate, UserOperation, UserOperationId, ValidTimeRange, + pool::{ + MempoolError, PaymasterMetadata, PoolOperation, Reputation, ReputationStatus, StakeStatus, + }, + EntityUpdate, EntryPointVersion, UserOperationId, UserOperationVariant, }; use tonic::async_trait; pub(crate) use uo_pool::UoPool; -use self::error::MempoolResult; use super::chain::ChainUpdate; +pub(crate) type MempoolResult = std::result::Result; + #[cfg_attr(test, automock)] #[async_trait] /// In-memory operation pool @@ -55,11 +55,14 @@ pub trait Mempool: Send + Sync + 'static { /// Returns the entry point address this pool targets. fn entry_point(&self) -> Address; + /// Returns the entry point version this pool targets. + fn entry_point_version(&self) -> EntryPointVersion; + /// Adds a user operation to the pool async fn add_operation( &self, origin: OperationOrigin, - op: UserOperation, + op: UserOperationVariant, ) -> MempoolResult; /// Removes a set of operations from the pool. @@ -123,6 +126,8 @@ pub trait Mempool: Send + Sync + 'static { pub struct PoolConfig { /// Address of the entry point this pool targets pub entry_point: Address, + /// Version of the entry point this pool targets + pub entry_point_version: EntryPointVersion, /// Chain ID this pool targets pub chain_id: u64, /// The maximum number of operations an unstaked sender can have in the mempool @@ -161,23 +166,6 @@ pub struct PoolConfig { pub drop_min_num_blocks: u64, } -/// Stake status structure -#[derive(Debug, Clone, Copy)] -pub struct StakeStatus { - /// Address is staked - pub is_staked: bool, - /// Stake information about address - pub stake_info: StakeInfo, -} - -#[derive(Debug, Clone, Copy)] -pub struct StakeInfo { - /// Stake amount - pub stake: u128, - /// Unstake delay in seconds - pub unstake_delay_sec: u32, -} - /// Origin of an operation. #[derive(Debug, Clone, Copy)] #[allow(dead_code)] // TODO(danc): remove once implemented @@ -191,109 +179,9 @@ pub enum OperationOrigin { ReturnedAfterReorg, } -/// A user operation with additional metadata from validation. -#[derive(Debug, Default, Clone, Eq, PartialEq)] -pub struct PoolOperation { - /// The user operation stored in the pool - pub uo: UserOperation, - /// The entry point address for this operation - pub entry_point: Address, - /// The aggregator address for this operation, if any. - pub aggregator: Option
, - /// The valid time range for this operation. - pub valid_time_range: ValidTimeRange, - /// The expected code hash for all contracts accessed during validation for this operation. - pub expected_code_hash: H256, - /// The block hash simulation was completed at - pub sim_block_hash: H256, - /// The block number simulation was completed at - pub sim_block_number: u64, - /// List of entities that need to stake for this operation. - pub entities_needing_stake: Vec, - /// Whether the account is staked. - pub account_is_staked: bool, - /// Staking information about all the entities. - pub entity_infos: EntityInfos, -} - -#[derive(Debug, Default, Clone, Eq, PartialEq, Copy)] -pub struct PaymasterMetadata { - /// Paymaster address - pub address: Address, - /// The on-chain balance of the paymaster - pub confirmed_balance: U256, - /// The pending balance is the confirm balance subtracted by - /// the max cost of all the pending user operations that use the paymaster - pub pending_balance: U256, -} - -impl PoolOperation { - /// Returns true if the operation contains the given entity. - pub fn contains_entity(&self, entity: &Entity) -> bool { - if let Some(e) = self.entity_infos.get(entity.kind) { - e.address == entity.address - } else { - false - } - } - - /// Returns true if the operation requires the given entity to stake. - /// - /// For non-accounts, its possible that the entity is staked, but doesn't - /// _need_ to stake for this operation. For example, if the operation does not - /// access any storage slots that require staking. In that case this function - /// will return false. - /// - /// For staked accounts, this function will always return true. Staked accounts - /// are able to circumvent the mempool operation limits always need their reputation - /// checked to prevent them from filling the pool. - pub fn requires_stake(&self, entity: EntityType) -> bool { - match entity { - EntityType::Account => self.account_is_staked, - _ => self.entities_needing_stake.contains(&entity), - } - } - - /// Returns an iterator over all entities that are included in this operation. - pub fn entities(&'_ self) -> impl Iterator + '_ { - self.entity_infos - .entities() - .map(|(t, entity)| Entity::new(t, entity.address)) - } - - /// Returns an iterator over all entities that need stake in this operation. This can be a subset of entities that are staked in the operation. - pub fn entities_requiring_stake(&'_ self) -> impl Iterator + '_ { - self.entity_infos.entities().filter_map(|(t, entity)| { - if self.requires_stake(t) { - Entity::new(t, entity.address).into() - } else { - None - } - }) - } - - /// Return all the unstaked entities that are used in this operation. - pub fn unstaked_entities(&'_ self) -> impl Iterator + '_ { - self.entity_infos.entities().filter_map(|(t, entity)| { - if entity.is_staked { - None - } else { - Entity::new(t, entity.address).into() - } - }) - } - - /// Compute the amount of heap memory the PoolOperation takes up. - pub fn mem_size(&self) -> usize { - std::mem::size_of::() - + self.uo.heap_size() - + self.entities_needing_stake.len() * std::mem::size_of::() - } -} - #[cfg(test)] mod tests { - use rundler_sim::EntityInfo; + use rundler_types::{v0_6::UserOperation, EntityInfo, EntityInfos, EntityType, ValidTimeRange}; use super::*; @@ -310,7 +198,8 @@ mod tests { paymaster_and_data: paymaster.as_fixed_bytes().into(), init_code: factory.as_fixed_bytes().into(), ..Default::default() - }, + } + .into(), entry_point: Address::random(), aggregator: Some(aggregator), valid_time_range: ValidTimeRange::all_time(), diff --git a/crates/pool/src/mempool/paymaster.rs b/crates/pool/src/mempool/paymaster.rs index 5b77c718f..28599459d 100644 --- a/crates/pool/src/mempool/paymaster.rs +++ b/crates/pool/src/mempool/paymaster.rs @@ -19,14 +19,14 @@ use anyhow::Context; use ethers::{abi::Address, types::U256}; use parking_lot::RwLock; use rundler_provider::EntryPoint; -use rundler_types::{UserOperation, UserOperationId}; +use rundler_types::{ + pool::{MempoolError, PaymasterMetadata, PoolOperation, StakeStatus}, + StakeInfo, UserOperation, UserOperationId, UserOperationVariant, +}; use rundler_utils::cache::LruMap; -use super::{error::MempoolResult, PaymasterMetadata, StakeInfo}; -use crate::{ - chain::{BalanceUpdate, MinedOp}, - MempoolError, PoolOperation, StakeStatus, -}; +use super::MempoolResult; +use crate::chain::{BalanceUpdate, MinedOp}; /// Keeps track of current and pending paymaster balances #[derive(Debug)] @@ -85,8 +85,8 @@ where let stake_status = StakeStatus { stake_info: StakeInfo { - stake: deposit_info.stake, - unstake_delay_sec: deposit_info.unstake_delay_sec, + stake: deposit_info.stake.into(), + unstake_delay_sec: deposit_info.unstake_delay_sec.into(), }, is_staked, }; @@ -150,7 +150,10 @@ where Ok(paymaster_meta) } - pub(crate) async fn check_operation_cost(&self, op: &UserOperation) -> MempoolResult<()> { + pub(crate) async fn check_operation_cost( + &self, + op: &UserOperationVariant, + ) -> MempoolResult<()> { if let Some(paymaster) = op.paymaster() { let balance = self.paymaster_balance(paymaster).await?; self.state.read().check_operation_cost(op, &balance)? @@ -218,14 +221,14 @@ where } } -/// Keeps track of current and pending paymaster balances +// Keeps track of current and pending paymaster balances #[derive(Debug)] struct PaymasterTrackerInner { - /// map for userop based on id + // map for userop based on id user_op_fees: HashMap, - /// map for paymaster balance status + // map for paymaster balance status paymaster_balances: LruMap, - /// boolean for operation of tracker + // boolean for operation of tracker tracker_enabled: bool, } @@ -248,7 +251,7 @@ impl PaymasterTrackerInner { fn check_operation_cost( &self, - op: &UserOperation, + op: &UserOperationVariant, paymaster_metadata: &PaymasterMetadata, ) -> MempoolResult<()> { let max_op_cost = op.max_gas_cost(); @@ -520,20 +523,19 @@ impl PaymasterBalance { #[cfg(test)] mod tests { use ethers::types::{Address, H256, U256}; - use rundler_provider::MockEntryPoint; - use rundler_sim::EntityInfos; - use rundler_types::{DepositInfo, UserOperation, UserOperationId, ValidTimeRange}; + use rundler_provider::{DepositInfo, MockEntryPointV0_6}; + use rundler_types::{ + pool::{PaymasterMetadata, PoolOperation}, + v0_6::UserOperation, + EntityInfos, UserOperation as UserOperationTrait, UserOperationId, ValidTimeRange, + }; use super::*; - use crate::{ - chain::BalanceUpdate, - mempool::{paymaster::PaymasterTracker, PaymasterMetadata}, - PoolOperation, - }; + use crate::{chain::BalanceUpdate, mempool::paymaster::PaymasterTracker}; fn demo_pool_op(uo: UserOperation) -> PoolOperation { PoolOperation { - uo, + uo: uo.into(), entry_point: Address::random(), aggregator: None, valid_time_range: ValidTimeRange::all_time(), @@ -963,12 +965,12 @@ mod tests { assert!(inner.paymaster_exists(paymaster_2)); } - fn new_paymaster_tracker() -> PaymasterTracker { - let mut entrypoint = MockEntryPoint::new(); + fn new_paymaster_tracker() -> PaymasterTracker { + let mut entrypoint = MockEntryPointV0_6::new(); entrypoint.expect_get_deposit_info().returning(|_| { Ok(DepositInfo { - deposit: 1000, + deposit: 1000.into(), staked: true, stake: 10000, unstake_delay_sec: 100, @@ -989,7 +991,7 @@ mod tests { PaymasterTracker::new(entrypoint, config) } - impl PaymasterTracker { + impl PaymasterTracker { fn add_new_user_op( &self, id: &UserOperationId, diff --git a/crates/pool/src/mempool/pool.rs b/crates/pool/src/mempool/pool.rs index d836eb446..7471b890f 100644 --- a/crates/pool/src/mempool/pool.rs +++ b/crates/pool/src/mempool/pool.rs @@ -22,16 +22,14 @@ use ethers::{ abi::Address, types::{H256, U256}, }; -use rundler_types::{Entity, EntityType, Timestamp, UserOperation, UserOperationId}; +use rundler_types::{ + pool::{MempoolError, PoolOperation}, + Entity, EntityType, Timestamp, UserOperation, UserOperationId, UserOperationVariant, +}; use rundler_utils::math; use tracing::info; -use super::{ - entity_tracker::EntityCounter, - error::{MempoolError, MempoolResult}, - size::SizeTracker, - PoolConfig, PoolOperation, -}; +use super::{entity_tracker::EntityCounter, size::SizeTracker, MempoolResult, PoolConfig}; use crate::chain::MinedOp; #[derive(Debug, Clone)] @@ -102,11 +100,14 @@ impl PoolInner { } /// Returns hash of operation to replace if operation is a replacement - pub(crate) fn check_replacement(&self, op: &UserOperation) -> MempoolResult> { + pub(crate) fn check_replacement( + &self, + op: &UserOperationVariant, + ) -> MempoolResult> { // Check if operation already known if self .by_hash - .contains_key(&op.op_hash(self.config.entry_point, self.config.chain_id)) + .contains_key(&op.hash(self.config.entry_point, self.config.chain_id)) { return Err(MempoolError::OperationAlreadyKnown); } @@ -115,19 +116,19 @@ impl PoolInner { let (replacement_priority_fee, replacement_fee) = self.get_min_replacement_fees(pool_op.uo()); - if op.max_priority_fee_per_gas < replacement_priority_fee - || op.max_fee_per_gas < replacement_fee + if op.max_priority_fee_per_gas() < replacement_priority_fee + || op.max_fee_per_gas() < replacement_fee { return Err(MempoolError::ReplacementUnderpriced( - pool_op.uo().max_priority_fee_per_gas, - pool_op.uo().max_fee_per_gas, + pool_op.uo().max_priority_fee_per_gas(), + pool_op.uo().max_fee_per_gas(), )); } Ok(Some( pool_op .uo() - .op_hash(self.config.entry_point, self.config.chain_id), + .hash(self.config.entry_point, self.config.chain_id), )) } else { Ok(None) @@ -186,10 +187,15 @@ impl PoolInner { } // STO-040 - pub(crate) fn check_multiple_roles_violation(&self, uo: &UserOperation) -> MempoolResult<()> { - if let Some(ec) = self.count_by_address.get(&uo.sender) { + pub(crate) fn check_multiple_roles_violation( + &self, + uo: &UserOperationVariant, + ) -> MempoolResult<()> { + if let Some(ec) = self.count_by_address.get(&uo.sender()) { if ec.includes_non_sender() { - return Err(MempoolError::SenderAddressUsedAsAlternateEntity(uo.sender)); + return Err(MempoolError::SenderAddressUsedAsAlternateEntity( + uo.sender(), + )); } } @@ -213,11 +219,11 @@ impl PoolInner { pub(crate) fn check_associated_storage( &self, accessed_storage: &HashSet
, - uo: &UserOperation, + uo: &UserOperationVariant, ) -> MempoolResult<()> { for storage_address in accessed_storage { if let Some(ec) = self.count_by_address.get(storage_address) { - if ec.sender().gt(&0) && storage_address.ne(&uo.sender) { + if ec.sender().gt(&0) && storage_address.ne(&uo.sender()) { // Reject UO if the sender is also an entity in another UO in the mempool for entity in uo.entities() { if storage_address.eq(&entity.address) { @@ -240,7 +246,7 @@ impl PoolInner { let hash = tx_in_pool .uo() - .op_hash(mined_op.entry_point, self.config.chain_id); + .hash(mined_op.entry_point, self.config.chain_id); let ret = self.remove_operation_internal(hash, Some(block_number)); @@ -285,10 +291,7 @@ impl PoolInner { } false }) - .map(|o| { - o.po.uo - .op_hash(self.config.entry_point, self.config.chain_id) - }) + .map(|o| o.po.uo.hash(self.config.entry_point, self.config.chain_id)) .collect::>(); for &hash in &to_remove { self.remove_operation_internal(hash, None); @@ -346,7 +349,7 @@ impl PoolInner { if let Some(worst) = self.best.pop_last() { let hash = worst .uo() - .op_hash(self.config.entry_point, self.config.chain_id); + .hash(self.config.entry_point, self.config.chain_id); let _ = self .remove_operation_internal(hash, None) @@ -390,7 +393,7 @@ impl PoolInner { // create and insert ordered operation let hash = pool_op .uo() - .op_hash(self.config.entry_point, self.config.chain_id); + .hash(self.config.entry_point, self.config.chain_id); self.pool_size += pool_op.mem_size(); self.by_hash.insert(hash, pool_op.clone()); self.by_id.insert(pool_op.uo().id(), pool_op.clone()); @@ -449,13 +452,13 @@ impl PoolInner { id } - fn get_min_replacement_fees(&self, op: &UserOperation) -> (U256, U256) { + fn get_min_replacement_fees(&self, op: &UserOperationVariant) -> (U256, U256) { let replacement_priority_fee = math::increase_by_percent( - op.max_priority_fee_per_gas, + op.max_priority_fee_per_gas(), self.config.min_replacement_fee_increase_percentage, ); let replacement_fee = math::increase_by_percent( - op.max_fee_per_gas, + op.max_fee_per_gas(), self.config.min_replacement_fee_increase_percentage, ); (replacement_priority_fee, replacement_fee) @@ -484,12 +487,12 @@ struct OrderedPoolOperation { } impl OrderedPoolOperation { - fn uo(&self) -> &UserOperation { + fn uo(&self) -> &UserOperationVariant { &self.po.uo } fn mem_size(&self) -> usize { - std::mem::size_of::() + self.po.mem_size() + std::mem::size_of::() + self.po.mem_size() } } @@ -500,8 +503,8 @@ impl Ord for OrderedPoolOperation { // Sort by gas price descending then by id ascending other .uo() - .max_fee_per_gas - .cmp(&self.uo().max_fee_per_gas) + .max_fee_per_gas() + .cmp(&self.uo().max_fee_per_gas()) .then_with(|| self.submission_id.cmp(&other.submission_id)) } } @@ -537,7 +540,10 @@ impl PoolMetrics { #[cfg(test)] mod tests { - use rundler_sim::{EntityInfo, EntityInfos}; + use rundler_types::{ + v0_6::UserOperation, EntityInfo, EntityInfos, UserOperation as UserOperationTrait, + ValidTimeRange, + }; use super::*; @@ -689,7 +695,7 @@ mod tests { let op = create_op(sender, nonce, 1); - let hash = op.uo.op_hash(pool.config.entry_point, pool.config.chain_id); + let hash = op.uo.hash(pool.config.entry_point, pool.config.chain_id); pool.add_operation(op).unwrap(); @@ -718,9 +724,7 @@ mod tests { let op = create_op(sender, nonce, 1); let op_2 = create_op(sender, nonce, 2); - let hash = op_2 - .uo - .op_hash(pool.config.entry_point, pool.config.chain_id); + let hash = op_2.uo.hash(pool.config.entry_point, pool.config.chain_id); pool.add_operation(op).unwrap(); pool.add_operation(op_2).unwrap(); @@ -776,7 +780,9 @@ mod tests { create_op(Address::random(), 0, 1), ]; for mut op in ops.into_iter() { - op.uo.paymaster_and_data = paymaster.as_bytes().to_vec().into(); + let uo: &mut UserOperation = op.uo.as_mut(); + + uo.paymaster_and_data = paymaster.as_bytes().to_vec().into(); op.entity_infos.paymaster = Some(EntityInfo { address: op.uo.paymaster().unwrap(), is_staked: false, @@ -800,12 +806,13 @@ mod tests { let aggregator = Address::random(); let mut op = create_op(sender, 0, 1); - op.uo.paymaster_and_data = paymaster.as_bytes().to_vec().into(); + let uo: &mut UserOperation = op.uo.as_mut(); + uo.paymaster_and_data = paymaster.as_bytes().to_vec().into(); op.entity_infos.paymaster = Some(EntityInfo { - address: op.uo.paymaster().unwrap(), + address: uo.paymaster().unwrap(), is_staked: false, }); - op.uo.init_code = factory.as_bytes().to_vec().into(); + uo.init_code = factory.as_bytes().to_vec().into(); op.entity_infos.factory = Some(EntityInfo { address: op.uo.factory().unwrap(), is_staked: false, @@ -820,7 +827,8 @@ mod tests { let mut hashes = vec![]; for i in 0..count { let mut op = op.clone(); - op.uo.nonce = i.into(); + let uo: &mut UserOperation = op.uo.as_mut(); + uo.nonce = i.into(); hashes.push(pool.add_operation(op).unwrap()); } @@ -877,11 +885,13 @@ mod tests { let mut pool = PoolInner::new(conf()); let sender = Address::random(); let mut po1 = create_op(sender, 0, 100); - po1.uo.max_priority_fee_per_gas = 100.into(); + let uo1: &mut UserOperation = po1.uo.as_mut(); + uo1.max_priority_fee_per_gas = 100.into(); let _ = pool.add_operation(po1.clone()).unwrap(); let mut po2 = create_op(sender, 0, 101); - po2.uo.max_priority_fee_per_gas = 101.into(); + let uo2: &mut UserOperation = po2.uo.as_mut(); + uo2.max_priority_fee_per_gas = 101.into(); let res = pool.add_operation(po2); assert!(res.is_err()); match res.err().unwrap() { @@ -909,8 +919,9 @@ mod tests { let sender = Address::random(); let paymaster1 = Address::random(); let mut po1 = create_op(sender, 0, 10); - po1.uo.max_priority_fee_per_gas = 10.into(); - po1.uo.paymaster_and_data = paymaster1.as_bytes().to_vec().into(); + let uo1: &mut UserOperation = po1.uo.as_mut(); + uo1.max_priority_fee_per_gas = 10.into(); + uo1.paymaster_and_data = paymaster1.as_bytes().to_vec().into(); po1.entity_infos.paymaster = Some(EntityInfo { address: po1.uo.paymaster().unwrap(), is_staked: false, @@ -920,8 +931,9 @@ mod tests { let paymaster2 = Address::random(); let mut po2 = create_op(sender, 0, 11); - po2.uo.max_priority_fee_per_gas = 11.into(); - po2.uo.paymaster_and_data = paymaster2.as_bytes().to_vec().into(); + let uo2: &mut UserOperation = po2.uo.as_mut(); + uo2.max_priority_fee_per_gas = 11.into(); + uo2.paymaster_and_data = paymaster2.as_bytes().to_vec().into(); po2.entity_infos.paymaster = Some(EntityInfo { address: po2.uo.paymaster().unwrap(), is_staked: false, @@ -946,7 +958,8 @@ mod tests { let mut pool = PoolInner::new(conf()); let sender = Address::random(); let mut po1 = create_op(sender, 0, 10); - po1.uo.max_priority_fee_per_gas = 10.into(); + let uo1: &mut UserOperation = po1.uo.as_mut(); + uo1.max_priority_fee_per_gas = 10.into(); let _ = pool.add_operation(po1.clone()).unwrap(); let res = pool.add_operation(po1); @@ -968,7 +981,7 @@ mod tests { let res = pool.remove_expired(Timestamp::from(2)); assert_eq!(res.len(), 1); - assert_eq!(res[0].0, po1.uo.op_hash(conf.entry_point, conf.chain_id)); + assert_eq!(res[0].0, po1.uo.hash(conf.entry_point, conf.chain_id)); assert_eq!(res[0].1, Timestamp::from(1)); } @@ -990,8 +1003,8 @@ mod tests { let res = pool.remove_expired(10.into()); assert_eq!(res.len(), 2); - assert!(res.contains(&(po1.uo.op_hash(conf.entry_point, conf.chain_id), 5.into()))); - assert!(res.contains(&(po3.uo.op_hash(conf.entry_point, conf.chain_id), 9.into()))); + assert!(res.contains(&(po1.uo.hash(conf.entry_point, conf.chain_id), 5.into()))); + assert!(res.contains(&(po3.uo.hash(conf.entry_point, conf.chain_id), 9.into()))); } fn conf() -> PoolInnerConfig { @@ -1019,9 +1032,9 @@ mod tests { sender, nonce: nonce.into(), max_fee_per_gas: max_fee_per_gas.into(), - ..UserOperation::default() - }, + } + .into(), entity_infos: EntityInfos { factory: None, sender: EntityInfo { @@ -1031,7 +1044,14 @@ mod tests { paymaster: None, aggregator: None, }, - ..PoolOperation::default() + entry_point: Address::random(), + valid_time_range: ValidTimeRange::default(), + aggregator: None, + expected_code_hash: H256::random(), + sim_block_hash: H256::random(), + sim_block_number: 0, + entities_needing_stake: vec![], + account_is_staked: false, } } diff --git a/crates/pool/src/mempool/reputation.rs b/crates/pool/src/mempool/reputation.rs index 87e79ab60..aa81a0d28 100644 --- a/crates/pool/src/mempool/reputation.rs +++ b/crates/pool/src/mempool/reputation.rs @@ -18,59 +18,9 @@ use std::{ use ethers::types::Address; use parking_lot::RwLock; -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use rundler_types::pool::{Reputation, ReputationStatus}; use tokio::time::interval; -/// Reputation status for an entity -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum ReputationStatus { - /// Entity is not throttled or banned - Ok, - /// Entity is throttled - Throttled, - /// Entity is banned - Banned, -} - -impl Serialize for ReputationStatus { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - ReputationStatus::Ok => serializer.serialize_str("ok"), - ReputationStatus::Throttled => serializer.serialize_str("throttled"), - ReputationStatus::Banned => serializer.serialize_str("banned"), - } - } -} - -impl<'de> Deserialize<'de> for ReputationStatus { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - match s.as_str() { - "ok" => Ok(ReputationStatus::Ok), - "throttled" => Ok(ReputationStatus::Throttled), - "banned" => Ok(ReputationStatus::Banned), - _ => Err(de::Error::custom(format!("Invalid reputation status {s}"))), - } - } -} - -/// The reputation of an entity -#[derive(Debug, Clone)] -pub struct Reputation { - /// The entity's address - pub address: Address, - /// Number of ops seen in the current interval - pub ops_seen: u64, - /// Number of ops included in the current interval - pub ops_included: u64, -} - #[derive(Debug, Clone, Copy)] pub(crate) struct ReputationParams { bundle_invalidation_ops_seen_staked_penalty: u64, diff --git a/crates/pool/src/mempool/uo_pool.rs b/crates/pool/src/mempool/uo_pool.rs index 6e9e4054c..f1cac317a 100644 --- a/crates/pool/src/mempool/uo_pool.rs +++ b/crates/pool/src/mempool/uo_pool.rs @@ -11,7 +11,7 @@ // 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::HashSet, sync::Arc}; +use std::{collections::HashSet, marker::PhantomData, sync::Arc}; use ethers::{ types::{Address, H256, U256}, @@ -21,23 +21,25 @@ use itertools::Itertools; use parking_lot::RwLock; use rundler_provider::EntryPoint; use rundler_sim::{Prechecker, Simulator}; -use rundler_types::{Entity, EntityUpdate, EntityUpdateType, UserOperation, UserOperationId}; +use rundler_types::{ + pool::{ + MempoolError, PaymasterMetadata, PoolOperation, Reputation, ReputationStatus, StakeStatus, + }, + Entity, EntityUpdate, EntityUpdateType, EntryPointVersion, UserOperation, UserOperationId, + UserOperationVariant, +}; use rundler_utils::emit::WithEntryPoint; use tokio::sync::broadcast; use tonic::async_trait; use tracing::info; use super::{ - error::{MempoolError, MempoolResult}, - paymaster::PaymasterTracker, - pool::PoolInner, - reputation::{AddressReputation, Reputation, ReputationStatus}, - Mempool, OperationOrigin, PaymasterMetadata, PoolConfig, PoolOperation, + paymaster::PaymasterTracker, pool::PoolInner, reputation::AddressReputation, Mempool, + MempoolResult, OperationOrigin, PoolConfig, }; use crate::{ chain::ChainUpdate, emit::{EntityReputation, EntityStatus, EntitySummary, OpPoolEvent, OpRemovalReason}, - StakeStatus, }; /// User Operation Mempool @@ -45,7 +47,7 @@ use crate::{ /// Wrapper around a pool object that implements thread-safety /// via a RwLock. Safe to call from multiple threads. Methods /// block on write locks. -pub(crate) struct UoPool { +pub(crate) struct UoPool { config: PoolConfig, state: RwLock, paymaster: PaymasterTracker, @@ -53,6 +55,7 @@ pub(crate) struct UoPool { event_sender: broadcast::Sender>, prechecker: P, simulator: S, + _uo_type: PhantomData, } struct UoPoolState { @@ -61,10 +64,11 @@ struct UoPoolState { block_number: u64, } -impl UoPool +impl UoPool where - P: Prechecker, - S: Simulator, + UO: UserOperation, + P: Prechecker, + S: Simulator, E: EntryPoint, { pub(crate) fn new( @@ -87,6 +91,7 @@ where prechecker, simulator, config, + _uo_type: PhantomData, } } @@ -131,10 +136,11 @@ where } #[async_trait] -impl Mempool for UoPool +impl Mempool for UoPool where - P: Prechecker, - S: Simulator, + UO: UserOperation + From + Into, + P: Prechecker, + S: Simulator, E: EntryPoint, { async fn on_chain_update(&self, update: &ChainUpdate) { @@ -307,6 +313,10 @@ where self.config.entry_point } + fn entry_point_version(&self) -> EntryPointVersion { + self.config.entry_point_version + } + fn set_tracking(&self, paymaster: bool, reputation: bool) { self.paymaster.set_tracking(paymaster); self.reputation.set_tracking(reputation); @@ -323,7 +333,7 @@ where async fn add_operation( &self, origin: OperationOrigin, - op: UserOperation, + op: UserOperationVariant, ) -> MempoolResult { // TODO(danc) aggregator reputation is not implemented // TODO(danc) catch ops with aggregators prior to simulation and reject @@ -375,12 +385,13 @@ where self.paymaster.check_operation_cost(&op).await?; // Prechecks - self.prechecker.check(&op).await?; + let versioned_op = op.clone().into(); + self.prechecker.check(&versioned_op).await?; // Only let ops with successful simulations through let sim_result = self .simulator - .simulate_validation(op.clone(), None, None) + .simulate_validation(versioned_op, None, None) .await?; // No aggregators supported for now @@ -412,12 +423,12 @@ where { let state = self.state.read(); if !pool_op.account_is_staked - && state.pool.address_count(&pool_op.uo.sender) + && state.pool.address_count(&pool_op.uo.sender()) >= self.config.same_sender_mempool_count { return Err(MempoolError::MaxOperationsReached( self.config.same_sender_mempool_count, - pool_op.uo.sender, + pool_op.uo.sender(), )); } @@ -464,7 +475,7 @@ where } let op_hash = pool_op .uo - .op_hash(self.config.entry_point, self.config.chain_id); + .hash(self.config.entry_point, self.config.chain_id); let valid_after = pool_op.valid_time_range.valid_after; let valid_until = pool_op.valid_time_range.valid_until; self.emit(OpPoolEvent::ReceivedOp { @@ -522,7 +533,7 @@ where } }; - let hash = po.uo.op_hash(self.config.entry_point, self.config.chain_id); + let hash = po.uo.hash(self.config.entry_point, self.config.chain_id); // This can return none if the operation was removed by another thread if self @@ -577,14 +588,15 @@ where .filter(|op| { // short-circuit the mod if there is only 1 shard ((self.config.num_shards == 1) || - (U256::from_little_endian(op.uo.sender.as_bytes()) + (U256::from_little_endian(op.uo.sender().as_bytes()) .div_mod(self.config.num_shards.into()) .1 == shard_index.into())) && // filter out ops from senders we've already seen - senders.insert(op.uo.sender) + senders.insert(op.uo.sender()) }) .take(max) + .map(Into::into) .collect()) } @@ -669,13 +681,17 @@ mod tests { use std::collections::HashMap; use ethers::types::{Bytes, H160}; - use rundler_provider::MockEntryPoint; + use rundler_provider::{DepositInfo, MockEntryPointV0_6}; use rundler_sim::{ - EntityInfo, EntityInfos, MockPrechecker, MockSimulator, PrecheckError, PrecheckSettings, - PrecheckViolation, SimulationError, SimulationResult, SimulationSettings, - SimulationViolation, ViolationError, + MockPrechecker, MockSimulator, PrecheckError, PrecheckSettings, SimulationError, + SimulationResult, SimulationSettings, ViolationError, + }; + use rundler_types::{ + pool::{PrecheckViolation, SimulationViolation}, + v0_6::UserOperation, + EntityInfo, EntityInfos, EntityType, EntryPointVersion, GasFees, + UserOperation as UserOperationTrait, ValidTimeRange, }; - use rundler_types::{DepositInfo, EntityType, GasFees, ValidTimeRange}; use super::*; use crate::{ @@ -765,9 +781,9 @@ mod tests { reorg_depth: 0, mined_ops: vec![MinedOp { entry_point: pool.config.entry_point, - hash: uos[0].op_hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + hash: uos[0].hash(pool.config.entry_point, 1), + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: U256::zero(), paymaster: None, }], @@ -806,10 +822,11 @@ mod tests { // add pending max cost of 30 for each uo for op in &mut ops { - op.op.call_gas_limit = 10.into(); - op.op.verification_gas_limit = 10.into(); - op.op.pre_verification_gas = 10.into(); - op.op.max_fee_per_gas = 1.into(); + let uo: &mut UserOperation = op.op.as_mut(); + uo.call_gas_limit = 10.into(); + uo.verification_gas_limit = 10.into(); + uo.pre_verification_gas = 10.into(); + uo.max_fee_per_gas = 1.into(); } let (pool, uos) = create_pool_insert_ops(ops).await; @@ -827,9 +844,9 @@ mod tests { reorg_depth: 0, mined_ops: vec![MinedOp { entry_point: pool.config.entry_point, - hash: uos[0].op_hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + hash: uos[0].hash(pool.config.entry_point, 1), + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: 10.into(), paymaster: Some(paymaster), }], @@ -867,9 +884,9 @@ mod tests { mined_ops: vec![], unmined_ops: vec![MinedOp { entry_point: pool.config.entry_point, - hash: uos[0].op_hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + hash: uos[0].hash(pool.config.entry_point, 1), + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: 10.into(), paymaster: None, }], @@ -908,9 +925,9 @@ mod tests { reorg_depth: 0, mined_ops: vec![MinedOp { entry_point: Address::random(), - hash: uos[0].op_hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + hash: uos[0].hash(pool.config.entry_point, 1), + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: U256::zero(), paymaster: None, }], @@ -950,9 +967,9 @@ mod tests { reorg_depth: 0, mined_ops: vec![MinedOp { entry_point: pool.config.entry_point, - hash: uos[0].op_hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + hash: uos[0].hash(pool.config.entry_point, 1), + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: U256::zero(), paymaster: None, }], @@ -1026,9 +1043,9 @@ mod tests { reorg_depth: 0, mined_ops: vec![MinedOp { entry_point: pool.config.entry_point, - hash: uos[0].op_hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + hash: uos[0].hash(pool.config.entry_point, 1), + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: U256::zero(), paymaster: None, }], @@ -1083,10 +1100,11 @@ mod tests { async fn test_paymaster_balance_insufficient() { let paymaster = Address::random(); let mut op = create_op(Address::random(), 0, 0, Some(paymaster)); - op.op.call_gas_limit = 1000.into(); - op.op.verification_gas_limit = 1000.into(); - op.op.pre_verification_gas = 1000.into(); - op.op.max_fee_per_gas = 1.into(); + let uo: &mut UserOperation = op.op.as_mut(); + uo.call_gas_limit = 1000.into(); + uo.verification_gas_limit = 1000.into(); + uo.pre_verification_gas = 1000.into(); + uo.max_fee_per_gas = 1.into(); let uo = op.op.clone(); let pool = create_pool(vec![op]); @@ -1101,11 +1119,12 @@ mod tests { #[tokio::test] async fn precheck_error() { + let sender = Address::random(); let op = create_op_with_errors( - Address::random(), + sender, 0, 0, - Some(PrecheckViolation::InitCodeTooShort(0)), + Some(PrecheckViolation::SenderIsNotContractAndNoInitCode(sender)), None, false, ); @@ -1113,7 +1132,9 @@ mod tests { let pool = create_pool(ops); match pool.add_operation(OperationOrigin::Local, op.op).await { - Err(MempoolError::PrecheckViolation(PrecheckViolation::InitCodeTooShort(_))) => {} + Err(MempoolError::PrecheckViolation( + PrecheckViolation::SenderIsNotContractAndNoInitCode(_), + )) => {} _ => panic!("Expected InitCodeTooShort error"), } assert_eq!(pool.best_operations(1, 0).unwrap(), vec![]); @@ -1169,7 +1190,8 @@ mod tests { .unwrap(); let mut replacement = op.op.clone(); - replacement.max_fee_per_gas = replacement.max_fee_per_gas + 1; + let r: &mut UserOperation = replacement.as_mut(); + r.max_fee_per_gas = r.max_fee_per_gas + 1; let err = pool .add_operation(OperationOrigin::Local, replacement) @@ -1198,10 +1220,11 @@ mod tests { let paymaster = Address::random(); let mut op = create_op(Address::random(), 0, 5, Some(paymaster)); - op.op.call_gas_limit = 10.into(); - op.op.verification_gas_limit = 10.into(); - op.op.pre_verification_gas = 10.into(); - op.op.max_fee_per_gas = 1.into(); + let uo: &mut UserOperation = op.op.as_mut(); + uo.call_gas_limit = 10.into(); + uo.verification_gas_limit = 10.into(); + uo.pre_verification_gas = 10.into(); + uo.max_fee_per_gas = 1.into(); let pool = create_pool(vec![op.clone()]); @@ -1211,7 +1234,8 @@ mod tests { .unwrap(); let mut replacement = op.op.clone(); - replacement.max_fee_per_gas = replacement.max_fee_per_gas + 1; + let r: &mut UserOperation = replacement.as_mut(); + r.max_fee_per_gas = r.max_fee_per_gas + 1; let _ = pool .add_operation(OperationOrigin::Local, replacement.clone()) @@ -1224,7 +1248,7 @@ mod tests { assert_eq!(paymaster_balance.pending_balance, U256::from(900)); let rep = pool.dump_reputation(); assert_eq!(rep.len(), 1); - assert_eq!(rep[0].address, op.op.sender); + assert_eq!(rep[0].address, op.op.sender()); assert_eq!(rep[0].ops_seen, 1); assert_eq!(rep[0].ops_included, 0); } @@ -1314,7 +1338,7 @@ mod tests { .add_operation(OperationOrigin::Local, op.op.clone()) .await .unwrap(); - let hash = op.op.op_hash(pool.config.entry_point, 1); + let hash = op.op.hash(pool.config.entry_point, 1); pool.on_chain_update(&ChainUpdate { latest_block_number: 11, @@ -1362,7 +1386,7 @@ mod tests { #[derive(Clone, Debug)] struct OpWithErrors { - op: UserOperation, + op: UserOperationVariant, valid_time_range: ValidTimeRange, precheck_error: Option, simulation_error: Option, @@ -1371,9 +1395,15 @@ mod tests { fn create_pool( ops: Vec, - ) -> UoPool { + ) -> UoPool< + UserOperation, + impl Prechecker, + impl Simulator, + impl EntryPoint, + > { let args = PoolConfig { entry_point: Address::random(), + entry_point_version: EntryPointVersion::V0_6, chain_id: 1, min_replacement_fee_increase_percentage: 10, max_size_of_pool_bytes: 10000, @@ -1394,10 +1424,10 @@ mod tests { let mut simulator = MockSimulator::new(); let mut prechecker = MockPrechecker::new(); - let mut entrypoint = MockEntryPoint::new(); + let mut entrypoint = MockEntryPointV0_6::new(); entrypoint.expect_get_deposit_info().returning(|_| { Ok(DepositInfo { - deposit: 1000, + deposit: 1000.into(), staked: true, stake: 10000, unstake_delay_sec: 100, @@ -1457,7 +1487,7 @@ mod tests { valid_time_range: op.valid_time_range, entity_infos: EntityInfos { sender: EntityInfo { - address: op.op.sender, + address: op.op.sender(), is_staked: false, }, ..EntityInfos::default() @@ -1483,8 +1513,13 @@ mod tests { async fn create_pool_insert_ops( ops: Vec, ) -> ( - UoPool, - Vec, + UoPool< + UserOperation, + impl Prechecker, + impl Simulator, + impl EntryPoint, + >, + Vec, ) { let uos = ops.iter().map(|op| op.op.clone()).collect::>(); let pool = create_pool(ops); @@ -1513,7 +1548,8 @@ mod tests { max_fee_per_gas: max_fee_per_gas.into(), paymaster_and_data, ..UserOperation::default() - }, + } + .into(), valid_time_range: ValidTimeRange::default(), precheck_error: None, simulation_error: None, @@ -1535,7 +1571,8 @@ mod tests { nonce: nonce.into(), max_fee_per_gas: max_fee_per_gas.into(), ..UserOperation::default() - }, + } + .into(), valid_time_range: ValidTimeRange::default(), precheck_error, simulation_error, @@ -1543,7 +1580,7 @@ mod tests { } } - fn check_ops(ops: Vec>, expected: Vec) { + fn check_ops(ops: Vec>, expected: Vec) { assert_eq!(ops.len(), expected.len()); for (actual, expected) in ops.into_iter().zip(expected) { assert_eq!(actual.uo, expected); diff --git a/crates/pool/src/server/local.rs b/crates/pool/src/server/local.rs index 873f99341..0733f9a78 100644 --- a/crates/pool/src/server/local.rs +++ b/crates/pool/src/server/local.rs @@ -19,7 +19,13 @@ use ethers::types::{Address, H256}; use futures::future; use futures_util::Stream; use rundler_task::server::{HealthCheck, ServerStatus}; -use rundler_types::{EntityUpdate, UserOperation, UserOperationId}; +use rundler_types::{ + pool::{ + MempoolError, NewHead, PaymasterMetadata, Pool, PoolError, PoolOperation, PoolResult, + Reputation, ReputationStatus, StakeStatus, + }, + EntityUpdate, EntryPointVersion, UserOperationId, UserOperationVariant, +}; use tokio::{ sync::{broadcast, mpsc, oneshot}, task::JoinHandle, @@ -27,14 +33,9 @@ use tokio::{ use tokio_util::sync::CancellationToken; use tracing::error; -use super::{PoolResult, PoolServerError}; use crate::{ chain::ChainUpdate, - mempool::{ - Mempool, MempoolError, OperationOrigin, PaymasterMetadata, PoolOperation, StakeStatus, - }, - server::{NewHead, PoolServer, Reputation}, - ReputationStatus, + mempool::{Mempool, OperationOrigin}, }; /// Local pool server builder @@ -65,9 +66,9 @@ impl LocalPoolBuilder { } /// Run the local pool server, consumes the builder - pub fn run( + pub fn run( self, - mempools: HashMap>, + mempools: HashMap>, chain_updates: broadcast::Receiver>, shutdown_token: CancellationToken, ) -> JoinHandle> { @@ -89,10 +90,10 @@ pub struct LocalPoolHandle { req_sender: mpsc::Sender, } -struct LocalPoolServerRunner { +struct LocalPoolServerRunner { req_receiver: mpsc::Receiver, block_sender: broadcast::Sender, - mempools: HashMap>, + mempools: HashMap>, chain_updates: broadcast::Receiver>, } @@ -112,17 +113,17 @@ impl LocalPoolHandle { } #[async_trait] -impl PoolServer for LocalPoolHandle { +impl Pool for LocalPoolHandle { async fn get_supported_entry_points(&self) -> PoolResult> { let req = ServerRequestKind::GetSupportedEntryPoints; let resp = self.send(req).await?; match resp { ServerResponse::GetSupportedEntryPoints { entry_points } => Ok(entry_points), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } - async fn add_op(&self, entry_point: Address, op: UserOperation) -> PoolResult { + async fn add_op(&self, entry_point: Address, op: UserOperationVariant) -> PoolResult { let req = ServerRequestKind::AddOp { entry_point, op, @@ -131,7 +132,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::AddOp { hash } => Ok(hash), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -149,7 +150,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::GetOps { ops } => Ok(ops), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -158,7 +159,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::GetOpByHash { op } => Ok(op), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -167,7 +168,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::RemoveOps => Ok(()), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -180,7 +181,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::RemoveOpById { hash } => Ok(hash), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -196,7 +197,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::UpdateEntities => Ok(()), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -205,7 +206,7 @@ impl PoolServer for LocalPoolHandle { clear_mempool: bool, clear_paymaster: bool, clear_reputation: bool, - ) -> Result<(), PoolServerError> { + ) -> Result<(), PoolError> { let req = ServerRequestKind::DebugClearState { clear_mempool, clear_reputation, @@ -214,7 +215,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugClearState => Ok(()), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -223,7 +224,7 @@ impl PoolServer for LocalPoolHandle { entry_point: Address, paymaster: bool, reputation: bool, - ) -> Result<(), PoolServerError> { + ) -> Result<(), PoolError> { let req = ServerRequestKind::AdminSetTracking { entry_point, paymaster, @@ -232,7 +233,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::AdminSetTracking => Ok(()), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -241,7 +242,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugDumpMempool { ops } => Ok(ops), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -257,7 +258,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugSetReputations => Ok(()), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -266,7 +267,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugDumpReputation { reputations } => Ok(reputations), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -278,7 +279,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugDumpPaymasterBalances { balances } => Ok(balances), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -294,7 +295,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::GetStakeStatus { status } => Ok(status), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -310,7 +311,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::GetReputationStatus { status } => Ok(status), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -332,7 +333,7 @@ impl PoolServer for LocalPoolHandle { } } })), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } } @@ -352,14 +353,11 @@ impl HealthCheck for LocalPoolHandle { } } -impl LocalPoolServerRunner -where - M: Mempool, -{ +impl LocalPoolServerRunner { fn new( req_receiver: mpsc::Receiver, block_sender: broadcast::Sender, - mempools: HashMap>, + mempools: HashMap>, chain_updates: broadcast::Receiver>, ) -> Self { Self { @@ -370,10 +368,10 @@ where } } - fn get_pool(&self, entry_point: Address) -> PoolResult<&Arc> { - self.mempools.get(&entry_point).ok_or_else(|| { - PoolServerError::MempoolError(MempoolError::UnknownEntryPoint(entry_point)) - }) + fn get_pool(&self, entry_point: Address) -> PoolResult<&Arc> { + self.mempools + .get(&entry_point) + .ok_or_else(|| PoolError::MempoolError(MempoolError::UnknownEntryPoint(entry_point))) } fn get_ops( @@ -495,10 +493,10 @@ where fn get_pool_and_spawn( &self, entry_point: Address, - response: oneshot::Sender>, + response: oneshot::Sender>, f: F, ) where - F: FnOnce(Arc, oneshot::Sender>) -> Fut, + F: FnOnce(Arc, oneshot::Sender>) -> Fut, Fut: Future + Send + 'static, { match self.get_pool(entry_point) { @@ -548,11 +546,30 @@ where // Async methods // Responses are sent in the spawned task ServerRequestKind::AddOp { entry_point, op, origin } => { - let fut = |mempool: Arc, response: oneshot::Sender>| async move { - let resp = match mempool.add_operation(origin, op).await { - Ok(hash) => Ok(ServerResponse::AddOp { hash }), - Err(e) => Err(e.into()), + let fut = |mempool: Arc, response: oneshot::Sender>| async move { + let resp = 'resp: { + match mempool.entry_point_version() { + EntryPointVersion::V0_6 => { + if !matches!(&op, UserOperationVariant::V0_6(_)){ + break 'resp Err(anyhow::anyhow!("Invalid user operation version for mempool v0.6 {:?}", op.uo_type()).into()); + } + } + EntryPointVersion::V0_7 => { + if !matches!(&op, UserOperationVariant::V0_7(_)){ + break 'resp Err(anyhow::anyhow!("Invalid user operation version for mempool v0.7 {:?}", op.uo_type()).into()); + } + } + EntryPointVersion::Unspecified => { + panic!("Found mempool with unspecified entry point version") + } + } + + match mempool.add_operation(origin, op).await { + Ok(hash) => Ok(ServerResponse::AddOp { hash }), + Err(e) => Err(e.into()), + } }; + if let Err(e) = response.send(resp) { tracing::error!("Failed to send response: {:?}", e); } @@ -562,7 +579,7 @@ where continue; }, ServerRequestKind::GetStakeStatus { entry_point, address }=> { - let fut = |mempool: Arc, response: oneshot::Sender>| async move { + let fut = |mempool: Arc, response: oneshot::Sender>| async move { let resp = match mempool.get_stake_status(address).await { Ok(status) => Ok(ServerResponse::GetStakeStatus { status }), Err(e) => Err(e.into()), @@ -680,7 +697,7 @@ enum ServerRequestKind { GetSupportedEntryPoints, AddOp { entry_point: Address, - op: UserOperation, + op: UserOperationVariant, origin: OperationOrigin, }, GetOps { @@ -784,6 +801,7 @@ mod tests { use std::{iter::zip, sync::Arc}; use futures_util::StreamExt; + use rundler_types::v0_6::UserOperation; use super::*; use crate::{chain::ChainUpdate, mempool::MockMempool}; @@ -792,18 +810,18 @@ mod tests { async fn test_add_op() { let mut mock_pool = MockMempool::new(); let hash0 = H256::random(); + mock_pool + .expect_entry_point_version() + .returning(|| EntryPointVersion::V0_6); mock_pool .expect_add_operation() .returning(move |_, _| Ok(hash0)); let ep = Address::random(); - let state = setup(HashMap::from([(ep, Arc::new(mock_pool))])); + let pool: Arc = Arc::new(mock_pool); + let state = setup(HashMap::from([(ep, pool)])); - let hash1 = state - .handle - .add_op(ep, UserOperation::default()) - .await - .unwrap(); + let hash1 = state.handle.add_op(ep, mock_op()).await.unwrap(); assert_eq!(hash0, hash1); } @@ -813,7 +831,8 @@ mod tests { mock_pool.expect_on_chain_update().returning(|_| ()); let ep = Address::random(); - let state = setup(HashMap::from([(ep, Arc::new(mock_pool))])); + let pool: Arc = Arc::new(mock_pool); + let state = setup(HashMap::from([(ep, pool)])); let mut sub = state.handle.subscribe_new_heads().await.unwrap(); @@ -839,7 +858,10 @@ mod tests { let state = setup( eps0.iter() - .map(|ep| (*ep, Arc::new(MockMempool::new()))) + .map(|ep| { + let pool: Arc = Arc::new(MockMempool::new()); + (*ep, pool) + }) .collect(), ); @@ -858,31 +880,36 @@ mod tests { let h1 = H256::random(); let h2 = H256::random(); let hashes = [h0, h1, h2]; + pools[0] + .expect_entry_point_version() + .returning(|| EntryPointVersion::V0_6); pools[0] .expect_add_operation() .returning(move |_, _| Ok(h0)); + pools[1] + .expect_entry_point_version() + .returning(|| EntryPointVersion::V0_6); pools[1] .expect_add_operation() .returning(move |_, _| Ok(h1)); + pools[2] + .expect_entry_point_version() + .returning(|| EntryPointVersion::V0_6); pools[2] .expect_add_operation() .returning(move |_, _| Ok(h2)); let state = setup( zip(eps.iter(), pools.into_iter()) - .map(|(ep, pool)| (*ep, Arc::new(pool))) + .map(|(ep, pool)| { + let pool: Arc = Arc::new(pool); + (*ep, pool) + }) .collect(), ); for (ep, hash) in zip(eps.iter(), hashes.iter()) { - assert_eq!( - *hash, - state - .handle - .add_op(*ep, UserOperation::default()) - .await - .unwrap() - ); + assert_eq!(*hash, state.handle.add_op(*ep, mock_op()).await.unwrap()); } } @@ -892,7 +919,7 @@ mod tests { _run_handle: JoinHandle>, } - fn setup(pools: HashMap>) -> State { + fn setup(pools: HashMap>) -> State { let builder = LocalPoolBuilder::new(10, 10); let handle = builder.get_handle(); let (tx, rx) = broadcast::channel(10); @@ -903,4 +930,8 @@ mod tests { _run_handle: run_handle, } } + + fn mock_op() -> UserOperationVariant { + UserOperationVariant::V0_6(UserOperation::default()) + } } diff --git a/crates/pool/src/server/mod.rs b/crates/pool/src/server/mod.rs index 311317b49..2cf32bef2 100644 --- a/crates/pool/src/server/mod.rs +++ b/crates/pool/src/server/mod.rs @@ -11,138 +11,9 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -mod error; mod local; -mod remote; - -use std::pin::Pin; - -use async_trait::async_trait; -pub use error::PoolServerError; -use ethers::types::{Address, H256}; -use futures_util::Stream; pub use local::{LocalPoolBuilder, LocalPoolHandle}; -#[cfg(feature = "test-utils")] -use mockall::automock; + +mod remote; pub(crate) use remote::spawn_remote_mempool_server; pub use remote::RemotePoolClient; -use rundler_types::{EntityUpdate, UserOperation, UserOperationId}; - -use crate::{ - mempool::{PaymasterMetadata, PoolOperation, Reputation, StakeStatus}, - ReputationStatus, -}; - -/// Result type for pool server operations. -pub type PoolResult = std::result::Result; - -#[derive(Clone, Debug)] -pub struct NewHead { - pub block_hash: H256, - pub block_number: u64, -} - -impl Default for NewHead { - fn default() -> NewHead { - NewHead { - block_hash: H256::zero(), - block_number: 0, - } - } -} - -/// Pool server trait -#[cfg_attr(feature = "test-utils", automock)] -#[async_trait] -pub trait PoolServer: Send + Sync + 'static { - /// Get the supported entry points of the pool - async fn get_supported_entry_points(&self) -> PoolResult>; - - /// Add an operation to the pool - async fn add_op(&self, entry_point: Address, op: UserOperation) -> PoolResult; - - /// Get operations from the pool - async fn get_ops( - &self, - entry_point: Address, - max_ops: u64, - shard_index: u64, - ) -> PoolResult>; - - /// Get an operation from the pool by hash - /// Checks each entry point in order until the operation is found - /// Returns None if the operation is not found - async fn get_op_by_hash(&self, hash: H256) -> PoolResult>; - - /// Remove operations from the pool by hash - async fn remove_ops(&self, entry_point: Address, ops: Vec) -> PoolResult<()>; - - /// Remove an operation from the pool by id - async fn remove_op_by_id( - &self, - entry_point: Address, - id: UserOperationId, - ) -> PoolResult>; - - /// Update operations associated with entities from the pool - async fn update_entities( - &self, - entry_point: Address, - entities: Vec, - ) -> PoolResult<()>; - - /// Subscribe to new chain heads from the pool. - /// - /// The pool will notify the subscriber when a new chain head is received, and the pool - /// has processed all operations up to that head. - async fn subscribe_new_heads(&self) -> PoolResult + Send>>>; - - /// Get reputation status given entrypoint and address - async fn get_reputation_status( - &self, - entry_point: Address, - address: Address, - ) -> PoolResult; - - /// Get stake status given entrypoint and address - async fn get_stake_status( - &self, - entry_point: Address, - address: Address, - ) -> PoolResult; - - /// Clear the pool state, used for debug methods - async fn debug_clear_state( - &self, - clear_mempool: bool, - clear_paymaster: bool, - clear_reputation: bool, - ) -> PoolResult<()>; - - /// Dump all operations in the pool, used for debug methods - async fn debug_dump_mempool(&self, entry_point: Address) -> PoolResult>; - - /// Set reputations for entities, used for debug methods - async fn debug_set_reputations( - &self, - entry_point: Address, - reputations: Vec, - ) -> PoolResult<()>; - - /// Dump reputations for entities, used for debug methods - async fn debug_dump_reputation(&self, entry_point: Address) -> PoolResult>; - - /// Dump paymaster balances, used for debug methods - async fn debug_dump_paymaster_balances( - &self, - entry_point: Address, - ) -> PoolResult>; - - /// Controls whether or not the certain tracking data structures are used to block user operations - async fn admin_set_tracking( - &self, - entry_point: Address, - paymaster: bool, - reputation: bool, - ) -> PoolResult<()>; -} diff --git a/crates/pool/src/server/remote/client.rs b/crates/pool/src/server/remote/client.rs index 91a045597..3d64e4ca7 100644 --- a/crates/pool/src/server/remote/client.rs +++ b/crates/pool/src/server/remote/client.rs @@ -16,10 +16,16 @@ 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::{EntityUpdate, UserOperation, UserOperationId}; +use rundler_types::{ + pool::{ + NewHead, PaymasterMetadata, Pool, PoolError, PoolOperation, PoolResult, Reputation, + ReputationStatus, StakeStatus, + }, + EntityUpdate, UserOperationId, UserOperationVariant, +}; use rundler_utils::retry::{self, UnlimitedRetryOpts}; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -41,12 +47,8 @@ use super::protos::{ update_entities_response, AddOpRequest, AdminSetTrackingRequest, DebugClearStateRequest, DebugDumpMempoolRequest, DebugDumpPaymasterBalancesRequest, DebugDumpReputationRequest, DebugSetReputationRequest, GetOpsRequest, GetReputationStatusRequest, GetStakeStatusRequest, - RemoveOpsRequest, SubscribeNewHeadsRequest, SubscribeNewHeadsResponse, UpdateEntitiesRequest, -}; -use crate::{ - mempool::{PaymasterMetadata, PoolOperation, Reputation, StakeStatus}, - server::{error::PoolServerError, NewHead, PoolResult, PoolServer}, - ReputationStatus, + RemoveOpsRequest, ReputationStatus as ProtoReputationStatus, SubscribeNewHeadsRequest, + SubscribeNewHeadsResponse, UpdateEntitiesRequest, }; /// Remote pool client @@ -123,21 +125,23 @@ impl RemotePoolClient { } #[async_trait] -impl PoolServer for RemotePoolClient { +impl Pool for RemotePoolClient { async fn get_supported_entry_points(&self) -> PoolResult> { Ok(self .op_pool_client .clone() .get_supported_entry_points(protos::GetSupportedEntryPointsRequest {}) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .entry_points .into_iter() .map(|ep| from_bytes(ep.as_slice())) - .collect::>()?) + .collect::>() + .map_err(anyhow::Error::from)?) } - async fn add_op(&self, entry_point: Address, op: UserOperation) -> PoolResult { + async fn add_op(&self, entry_point: Address, op: UserOperationVariant) -> PoolResult { let res = self .op_pool_client .clone() @@ -145,14 +149,15 @@ impl PoolServer for RemotePoolClient { entry_point: entry_point.as_bytes().to_vec(), op: Some(protos::UserOperation::from(&op)), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(add_op_response::Result::Success(s)) => Ok(H256::from_slice(&s.hash)), Some(add_op_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -172,7 +177,8 @@ impl PoolServer for RemotePoolClient { max_ops, shard_index, }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -181,10 +187,10 @@ impl PoolServer for RemotePoolClient { .ops .into_iter() .map(PoolOperation::try_from) - .map(|res| res.map_err(PoolServerError::from)) + .map(|res| res.map_err(PoolError::from)) .collect(), Some(get_ops_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -197,7 +203,8 @@ impl PoolServer for RemotePoolClient { .get_op_by_hash(protos::GetOpByHashRequest { hash: hash.as_bytes().to_vec(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -207,11 +214,11 @@ impl PoolServer for RemotePoolClient { } Some(get_op_by_hash_response::Result::Failure(e)) => match e.error { Some(_) => Err(e.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received error from op pool" )))?, }, - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -225,14 +232,15 @@ impl PoolServer for RemotePoolClient { entry_point: entry_point.as_bytes().to_vec(), hashes: ops.into_iter().map(|h| h.as_bytes().to_vec()).collect(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(remove_ops_response::Result::Success(_)) => Ok(()), Some(remove_ops_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -247,11 +255,12 @@ impl PoolServer 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? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -264,7 +273,7 @@ impl PoolServer for RemotePoolClient { } } Some(remove_op_by_id_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -285,14 +294,15 @@ impl PoolServer for RemotePoolClient { .map(protos::EntityUpdate::from) .collect(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(update_entities_response::Result::Success(_)) => Ok(()), Some(update_entities_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -312,14 +322,15 @@ impl PoolServer for RemotePoolClient { clear_paymaster, clear_reputation, }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(debug_clear_state_response::Result::Success(_)) => Ok(()), Some(debug_clear_state_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -339,14 +350,15 @@ impl PoolServer for RemotePoolClient { reputation, paymaster, }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(admin_set_tracking_response::Result::Success(_)) => Ok(()), Some(admin_set_tracking_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -359,7 +371,8 @@ impl PoolServer for RemotePoolClient { .debug_dump_mempool(DebugDumpMempoolRequest { entry_point: entry_point.as_bytes().to_vec(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -368,10 +381,10 @@ impl PoolServer for RemotePoolClient { .ops .into_iter() .map(PoolOperation::try_from) - .map(|res| res.map_err(PoolServerError::from)) + .map(|res| res.map_err(PoolError::from)) .collect(), Some(debug_dump_mempool_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -392,14 +405,15 @@ impl PoolServer for RemotePoolClient { .map(protos::Reputation::from) .collect(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(debug_set_reputation_response::Result::Success(_)) => Ok(()), Some(debug_set_reputation_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -412,7 +426,8 @@ impl PoolServer for RemotePoolClient { .debug_dump_reputation(DebugDumpReputationRequest { entry_point: entry_point.as_bytes().to_vec(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -421,10 +436,10 @@ impl PoolServer for RemotePoolClient { .reputations .into_iter() .map(Reputation::try_from) - .map(|res| res.map_err(PoolServerError::from)) + .map(|res| res.map_err(anyhow::Error::from).map_err(PoolError::from)) .collect(), Some(debug_dump_reputation_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -440,7 +455,8 @@ impl PoolServer for RemotePoolClient { .debug_dump_paymaster_balances(DebugDumpPaymasterBalancesRequest { entry_point: entry_point.as_bytes().to_vec(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -449,10 +465,10 @@ impl PoolServer for RemotePoolClient { .balances .into_iter() .map(PaymasterMetadata::try_from) - .map(|res| res.map_err(PoolServerError::from)) + .map(|res| res.map_err(anyhow::Error::from).map_err(PoolError::from)) .collect(), Some(debug_dump_paymaster_balances_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -470,16 +486,20 @@ impl PoolServer for RemotePoolClient { entry_point: entry_point.as_bytes().to_vec(), address: address.as_bytes().to_vec(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(get_reputation_status_response::Result::Success(s)) => { - Ok(ReputationStatus::try_from(s.status)?) + Ok(ProtoReputationStatus::try_from(s.status) + .map_err(anyhow::Error::from)? + .try_into() + .map_err(anyhow::Error::from)?) } Some(get_reputation_status_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -497,7 +517,8 @@ impl PoolServer for RemotePoolClient { entry_point: entry_point.as_bytes().to_vec(), address: address.as_bytes().to_vec(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -506,7 +527,7 @@ impl PoolServer for RemotePoolClient { Ok(s.status.unwrap_or_default().try_into()?) } Some(get_stake_status_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } diff --git a/crates/pool/src/server/remote/error.rs b/crates/pool/src/server/remote/error.rs index d155724ff..0d860f4d7 100644 --- a/crates/pool/src/server/remote/error.rs +++ b/crates/pool/src/server/remote/error.rs @@ -13,60 +13,51 @@ use anyhow::{bail, Context}; use ethers::types::Opcode; -use rundler_sim::{NeedsStakeInformation, PrecheckViolation, SimulationViolation, ViolationOpCode}; -use rundler_task::grpc::protos::{from_bytes, to_le_bytes, ConversionError}; -use rundler_types::StorageSlot; +use rundler_task::grpc::protos::{from_bytes, ToProtoBytes}; +use rundler_types::{ + pool::{ + MempoolError, NeedsStakeInformation, PoolError, PrecheckViolation, SimulationViolation, + }, + 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, InitCodeTooShort, InvalidSignature, + DiscardedOnInsertError, Entity, EntityThrottledError, EntityType, EntryPointRevert, + ExistingSenderWithInitCode, FactoryCalledCreate2Twice, FactoryIsNotContract, InvalidSignature, InvalidStorageAccess, MaxFeePerGasTooLow, MaxOperationsReachedError, MaxPriorityFeePerGasTooLow, MempoolError as ProtoMempoolError, MultipleRolesViolation, - NotStaked, OperationAlreadyKnownError, OperationDropTooSoon, OutOfGas, PaymasterBalanceTooLow, - PaymasterDepositTooLow, PaymasterIsNotContract, PaymasterTooShort, PreVerificationGasTooLow, - PrecheckViolationError as ProtoPrecheckViolationError, ReplacementUnderpricedError, - SenderAddressUsedAsAlternateEntity, SenderFundsTooLow, SenderIsNotContractAndNoInitCode, - SimulationViolationError as ProtoSimulationViolationError, TotalGasLimitTooHigh, - UnintendedRevert, UnintendedRevertWithMessage, UnknownEntryPointError, UnstakedAggregator, - UnstakedPaymasterContext, UnsupportedAggregatorError, UsedForbiddenOpcode, - UsedForbiddenPrecompile, VerificationGasLimitBufferTooLow, VerificationGasLimitTooHigh, - WrongNumberOfPhases, + 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, }; -use crate::{mempool::MempoolError, server::error::PoolServerError}; -impl From for PoolServerError { - fn from(value: tonic::Status) -> Self { - PoolServerError::Other(anyhow::anyhow!(value.to_string())) - } -} - -impl From for PoolServerError { - fn from(value: ConversionError) -> Self { - PoolServerError::Other(anyhow::anyhow!(value.to_string())) - } -} - -impl TryFrom for PoolServerError { +impl TryFrom for PoolError { type Error = anyhow::Error; fn try_from(value: ProtoMempoolError) -> Result { - Ok(PoolServerError::MempoolError(value.try_into()?)) + Ok(PoolError::MempoolError(value.try_into()?)) } } -impl From for ProtoMempoolError { - fn from(value: PoolServerError) -> Self { +impl From for ProtoMempoolError { + fn from(value: PoolError) -> Self { match value { - PoolServerError::MempoolError(e) => e.into(), - PoolServerError::UnexpectedResponse => ProtoMempoolError { + PoolError::MempoolError(e) => e.into(), + PoolError::UnexpectedResponse => ProtoMempoolError { error: Some(mempool_error::Error::Internal( "unexpected response from pool server".to_string(), )), }, - PoolServerError::Other(e) => ProtoMempoolError { + PoolError::Other(e) => ProtoMempoolError { error: Some(mempool_error::Error::Internal(e.to_string())), }, } @@ -164,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(), }, )), }, @@ -180,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(), }, )), }, @@ -200,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(), }, )), } @@ -215,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(), }, )), }, @@ -244,19 +235,12 @@ impl From for ProtoMempoolError { impl From for ProtoPrecheckViolationError { fn from(value: PrecheckViolation) -> Self { match value { - PrecheckViolation::InitCodeTooShort(length) => ProtoPrecheckViolationError { - violation: Some(precheck_violation_error::Violation::InitCodeTooShort( - InitCodeTooShort { - length: length as u64, - }, - )), - }, PrecheckViolation::SenderIsNotContractAndNoInitCode(addr) => { ProtoPrecheckViolationError { violation: Some( precheck_violation_error::Violation::SenderIsNotContractAndNoInitCode( SenderIsNotContractAndNoInitCode { - sender_address: addr.as_bytes().to_vec(), + sender_address: addr.to_proto_bytes(), }, ), ), @@ -266,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(), }, ), ), @@ -274,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(), }, )), }, @@ -291,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(), }, ), ), @@ -303,48 +287,41 @@ 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(), }, ), ), } } - PrecheckViolation::PaymasterTooShort(length) => ProtoPrecheckViolationError { - violation: Some(precheck_violation_error::Violation::PaymasterTooShort( - PaymasterTooShort { - length: length as u64, - }, - )), - }, 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(), }, )), }, @@ -353,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(), }, ), ), @@ -363,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(), }, )), }, @@ -377,9 +354,6 @@ impl TryFrom for PrecheckViolation { fn try_from(value: ProtoPrecheckViolationError) -> Result { Ok(match value.violation { - Some(precheck_violation_error::Violation::InitCodeTooShort(e)) => { - PrecheckViolation::InitCodeTooShort(e.length as usize) - } Some(precheck_violation_error::Violation::SenderIsNotContractAndNoInitCode(e)) => { PrecheckViolation::SenderIsNotContractAndNoInitCode(from_bytes(&e.sender_address)?) } @@ -407,9 +381,6 @@ impl TryFrom for PrecheckViolation { from_bytes(&e.min_gas)?, ) } - Some(precheck_violation_error::Violation::PaymasterTooShort(e)) => { - PrecheckViolation::PaymasterTooShort(e.length as usize) - } Some(precheck_violation_error::Violation::PaymasterIsNotContract(e)) => { PrecheckViolation::PaymasterIsNotContract(from_bytes(&e.paymaster_address)?) } @@ -473,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, }, @@ -486,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, }, )), @@ -501,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(), }, ), ), @@ -511,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(), }, ), ), @@ -521,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(), }, )), } @@ -532,11 +503,11 @@ impl From for ProtoSimulationViolationError { NotStaked { needs_stake: Some((&stake_data.needs_stake).into()), accessing_entity: EntityType::from(stake_data.accessing_entity) as i32, - 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(), }, )), }, @@ -546,13 +517,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 {}, @@ -586,7 +561,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(), }, ), ), @@ -620,8 +595,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(), }, ), ), @@ -721,6 +696,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 } @@ -770,6 +748,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::*; @@ -787,12 +810,16 @@ mod tests { #[test] fn test_precheck_error() { - let error = MempoolError::PrecheckViolation(PrecheckViolation::InitCodeTooShort(0)); + let error = MempoolError::PrecheckViolation(PrecheckViolation::SenderFundsTooLow( + 0.into(), + 0.into(), + )); let proto_error: ProtoMempoolError = error.into(); let error2 = proto_error.try_into().unwrap(); match error2 { - MempoolError::PrecheckViolation(PrecheckViolation::InitCodeTooShort(v)) => { - assert_eq!(v, 0) + MempoolError::PrecheckViolation(PrecheckViolation::SenderFundsTooLow(x, y)) => { + assert_eq!(x, 0.into()); + assert_eq!(y, 0.into()); } _ => panic!("wrong error type"), } diff --git a/crates/pool/src/server/remote/protos.rs b/crates/pool/src/server/remote/protos.rs index a19ad9f73..90b0948fc 100644 --- a/crates/pool/src/server/remote/protos.rs +++ b/crates/pool/src/server/remote/protos.rs @@ -13,20 +13,16 @@ 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::{ - Entity as RundlerEntity, EntityType as RundlerEntityType, EntityUpdate as RundlerEntityUpdate, - EntityUpdateType as RundlerEntityUpdateType, UserOperation as RundlerUserOperation, - ValidTimeRange, -}; - -use crate::{ - mempool::{ - PaymasterMetadata as PoolPaymasterMetadata, PoolOperation, Reputation as PoolReputation, - ReputationStatus as PoolReputationStatus, StakeInfo as RundlerStakeInfo, + pool::{ + NewHead as PoolNewHead, PaymasterMetadata as PoolPaymasterMetadata, PoolOperation, + Reputation as PoolReputation, ReputationStatus as PoolReputationStatus, StakeStatus as RundlerStakeStatus, }, - server::NewHead as PoolNewHead, + v0_6, v0_7, Entity as RundlerEntity, EntityInfos, EntityType as RundlerEntityType, + EntityUpdate as RundlerEntityUpdate, EntityUpdateType as RundlerEntityUpdateType, + StakeInfo as RundlerStakeInfo, UserOperationVariant, ValidTimeRange, }; tonic::include_proto!("op_pool"); @@ -34,29 +30,41 @@ tonic::include_proto!("op_pool"); pub const OP_POOL_FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("op_pool_descriptor"); -impl From<&RundlerUserOperation> for UserOperation { - fn from(op: &RundlerUserOperation) -> Self { +impl From<&UserOperationVariant> for UserOperation { + fn from(op: &UserOperationVariant) -> Self { + match op { + UserOperationVariant::V0_6(op) => op.into(), + UserOperationVariant::V0_7(op) => op.into(), + } + } +} + +impl From<&v0_6::UserOperation> for UserOperation { + fn from(op: &v0_6::UserOperation) -> Self { + let op = UserOperationV06 { + 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 { - 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(), + uo: Some(user_operation::Uo::V06(op)), } } } -impl TryFrom for RundlerUserOperation { +impl TryFrom for v0_6::UserOperation { type Error = ConversionError; - fn try_from(op: UserOperation) -> Result { - Ok(RundlerUserOperation { + fn try_from(op: UserOperationV06) -> Result { + Ok(v0_6::UserOperation { sender: from_bytes(&op.sender)?, nonce: from_bytes(&op.nonce)?, init_code: op.init_code.into(), @@ -72,6 +80,85 @@ impl TryFrom for RundlerUserOperation { } } +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; + + fn try_from(op: UserOperation) -> Result { + let op = op + .uo + .expect("User operation should contain user operation oneof"); + + 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()?)), + } + } +} + impl TryFrom for RundlerEntityType { type Error = ConversionError; @@ -148,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(), } } } @@ -181,29 +268,29 @@ impl From for ReputationStatus { } } +impl TryFrom for PoolReputationStatus { + type Error = ConversionError; + + fn try_from(status: ReputationStatus) -> Result { + match status { + ReputationStatus::Ok => Ok(PoolReputationStatus::Ok), + ReputationStatus::Throttled => Ok(PoolReputationStatus::Throttled), + ReputationStatus::Banned => Ok(PoolReputationStatus::Banned), + ReputationStatus::Unspecified => Err(ConversionError::InvalidEnumValue(status as i32)), + } + } +} + 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, } } } -impl TryFrom for PoolReputationStatus { - type Error = ConversionError; - - fn try_from(status: i32) -> Result { - match status { - x if x == ReputationStatus::Ok as i32 => Ok(Self::Ok), - x if x == ReputationStatus::Throttled as i32 => Ok(Self::Throttled), - x if x == ReputationStatus::Banned as i32 => Ok(Self::Banned), - _ => Err(ConversionError::InvalidEnumValue(status)), - } - } -} - impl TryFrom for PoolReputation { type Error = ConversionError; @@ -225,7 +312,7 @@ impl TryFrom for RundlerStakeStatus { is_staked: stake_status.is_staked, stake_info: RundlerStakeInfo { stake: stake_info.stake.into(), - unstake_delay_sec: stake_info.unstake_delay_sec, + unstake_delay_sec: stake_info.unstake_delay_sec.into(), }, }); } @@ -239,8 +326,8 @@ impl From for StakeStatus { StakeStatus { is_staked: stake_status.is_staked, stake_info: Some(StakeInfo { - stake: stake_status.stake_info.stake as u64, - unstake_delay_sec: stake_status.stake_info.unstake_delay_sec, + stake: stake_status.stake_info.stake.as_u64(), + unstake_delay_sec: stake_status.stake_info.unstake_delay_sec.as_u32(), }), } } @@ -250,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() @@ -305,7 +392,7 @@ impl TryFrom for PoolOperation { sim_block_hash, sim_block_number: 0, account_is_staked: op.account_is_staked, - entity_infos: rundler_sim::EntityInfos::default(), + entity_infos: EntityInfos::default(), }) } } @@ -324,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, } } @@ -346,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/server/remote/server.rs b/crates/pool/src/server/remote/server.rs index d8dd3d1c2..6ebc87002 100644 --- a/crates/pool/src/server/remote/server.rs +++ b/crates/pool/src/server/remote/server.rs @@ -23,7 +23,10 @@ use async_trait::async_trait; use ethers::types::{Address, H256}; use futures_util::StreamExt; use rundler_task::grpc::{metrics::GrpcMetricsLayer, protos::from_bytes}; -use rundler_types::{EntityUpdate, UserOperationId}; +use rundler_types::{ + pool::{Pool, Reputation}, + EntityUpdate, UserOperationId, +}; use tokio::{sync::mpsc, task::JoinHandle}; use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_util::sync::CancellationToken; @@ -51,10 +54,7 @@ use super::protos::{ SubscribeNewHeadsRequest, SubscribeNewHeadsResponse, UpdateEntitiesRequest, UpdateEntitiesResponse, UpdateEntitiesSuccess, OP_POOL_FILE_DESCRIPTOR_SET, }; -use crate::{ - mempool::Reputation, - server::{local::LocalPoolHandle, PoolServer}, -}; +use crate::server::local::LocalPoolHandle; const MAX_REMOTE_BLOCK_SUBSCRIPTIONS: usize = 32; diff --git a/crates/pool/src/task.rs b/crates/pool/src/task.rs index 7a556d13c..d6cea391f 100644 --- a/crates/pool/src/task.rs +++ b/crates/pool/src/task.rs @@ -16,12 +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::{EntryPoint, EthersEntryPoint, Provider}; +use rundler_provider::{EntryPointProvider, EthersEntryPointV0_6, EthersEntryPointV0_7, Provider}; use rundler_sim::{ - Prechecker, PrecheckerImpl, SimulateValidationTracerImpl, Simulator, SimulatorImpl, + simulation::{v0_6 as sim_v0_6, UnsafeSimulator}, + PrecheckerImpl, Simulator, }; use rundler_task::Task; -use rundler_types::chain::ChainSpec; +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; @@ -30,7 +31,9 @@ use super::mempool::PoolConfig; use crate::{ chain::{self, Chain}, emit::OpPoolEvent, - mempool::{AddressReputation, PaymasterConfig, PaymasterTracker, ReputationParams, UoPool}, + mempool::{ + AddressReputation, Mempool, PaymasterConfig, PaymasterTracker, ReputationParams, UoPool, + }, server::{spawn_remote_mempool_server, LocalPoolBuilder}, }; @@ -39,6 +42,8 @@ use crate::{ pub struct Args { /// Chain specification. pub chain_spec: ChainSpec, + /// True if using unsafe mode. + pub unsafe_mode: bool, /// HTTP URL for the full node. pub http_url: String, /// Poll interval for full node requests. @@ -75,7 +80,7 @@ impl Task for PoolTask { .args .pool_configs .iter() - .map(|config| config.entry_point) + .map(|config| (config.entry_point, config.entry_point_version)) .collect(), }; let provider = rundler_provider::new_provider( @@ -89,16 +94,35 @@ impl Task for PoolTask { // create mempools let mut mempools = HashMap::new(); for pool_config in &self.args.pool_configs { - let pool = PoolTask::create_mempool( - self.args.chain_spec.clone(), - pool_config, - self.event_sender.clone(), - provider.clone(), - ) - .await - .context("should have created mempool")?; + match pool_config.entry_point_version { + EntryPointVersion::V0_6 => { + let pool = PoolTask::create_mempool_v0_6( + self.args.chain_spec.clone(), + pool_config, + self.args.unsafe_mode, + self.event_sender.clone(), + provider.clone(), + ) + .context("should have created mempool")?; - mempools.insert(pool_config.entry_point, Arc::new(pool)); + mempools.insert(pool_config.entry_point, pool); + } + EntryPointVersion::V0_7 => { + let pool = PoolTask::create_mempool_v0_7( + self.args.chain_spec.clone(), + pool_config, + self.args.unsafe_mode, + self.event_sender.clone(), + provider.clone(), + ) + .context("should have created mempool")?; + + mempools.insert(pool_config.entry_point, pool); + } + EntryPointVersion::Unspecified => { + bail!("Unsupported entry point version"); + } + } } let pool_handle = self.pool_builder.get_handle(); @@ -157,14 +181,88 @@ impl PoolTask { Box::new(self) } - async fn create_mempool( + // 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> { + 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, + ) + } + } + + fn create_mempool_v0_7( chain_spec: ChainSpec, pool_config: &PoolConfig, + unsafe_mode: bool, event_sender: broadcast::Sender>, provider: Arc

, - ) -> anyhow::Result> { - let ep = EthersEntryPoint::new(pool_config.entry_point, Arc::clone(&provider)); + ) -> anyhow::Result> { + 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), @@ -172,16 +270,6 @@ impl PoolTask { pool_config.precheck_settings, ); - let simulate_validation_tracer = - SimulateValidationTracerImpl::new(Arc::clone(&provider), ep.clone()); - let simulator = SimulatorImpl::new( - Arc::clone(&provider), - ep.address(), - simulate_validation_tracer, - pool_config.sim_settings, - pool_config.mempool_channel_configs.clone(), - ); - let reputation = Arc::new(AddressReputation::new( ReputationParams::new(pool_config.reputation_tracking_enabled), pool_config.blocklist.clone().unwrap_or_default(), @@ -211,6 +299,6 @@ impl PoolTask { reputation, ); - Ok(uo_pool) + Ok(Arc::new(uo_pool)) } } diff --git a/crates/provider/Cargo.toml b/crates/provider/Cargo.toml index efcf5032b..263945689 100644 --- a/crates/provider/Cargo.toml +++ b/crates/provider/Cargo.toml @@ -12,6 +12,7 @@ rundler-utils = { path = "../utils" } anyhow.workspace = true async-trait.workspace = true +auto_impl = "1.2.0" ethers.workspace = true metrics.workspace = true reqwest.workspace = true @@ -25,3 +26,6 @@ mockall = {workspace = true, optional = true } [features] test-utils = [ "mockall" ] + +[dev-dependencies] +rundler-provider = { path = ".", features = ["test-utils"] } diff --git a/crates/provider/src/ethers/entry_point.rs b/crates/provider/src/ethers/entry_point.rs deleted file mode 100644 index f7b8ef158..000000000 --- a/crates/provider/src/ethers/entry_point.rs +++ /dev/null @@ -1,258 +0,0 @@ -// 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::sync::Arc; - -use anyhow::Context; -use ethers::{ - abi::AbiDecode, - contract::{ContractError, FunctionCall}, - providers::{spoof, Middleware, RawCall}, - types::{ - transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, Eip1559TransactionRequest, - H256, U256, - }, - utils::hex, -}; -use rundler_types::{ - contracts::v0_6::{ - get_balances::{GetBalancesResult, GETBALANCES_BYTECODE}, - i_entry_point::{ExecutionResult, FailedOp, IEntryPoint, SignatureValidationFailed}, - shared_types::UserOpsPerAggregator, - }, - DepositInfo, GasFees, UserOperation, ValidationOutput, -}; -use rundler_utils::eth::{self, ContractRevertError}; - -use crate::{ - traits::{EntryPoint, HandleOpsOut}, - Provider, -}; - -const REVERT_REASON_MAX_LEN: usize = 2048; - -/// Implementation of the `EntryPoint` trait for the v0.6 version of the entry point contract using ethers -#[derive(Debug)] -pub struct EntryPointImpl { - i_entry_point: IEntryPoint

, - provider: Arc

, -} - -impl

Clone for EntryPointImpl

-where - P: Provider + Middleware, -{ - fn clone(&self) -> Self { - Self { - i_entry_point: self.i_entry_point.clone(), - provider: self.provider.clone(), - } - } -} - -impl

EntryPointImpl

-where - P: Provider + Middleware, -{ - /// Create a new `EntryPointImpl` instance - pub fn new(entry_point_address: Address, provider: Arc

) -> Self { - Self { - i_entry_point: IEntryPoint::new(entry_point_address, Arc::clone(&provider)), - provider, - } - } -} - -#[async_trait::async_trait] -impl

EntryPoint for EntryPointImpl

-where - P: Provider + Middleware + Send + Sync + 'static, -{ - fn address(&self) -> Address { - self.i_entry_point.address() - } - - async fn get_simulate_validation_call( - &self, - user_op: UserOperation, - max_validation_gas: u64, - ) -> anyhow::Result { - let pvg = user_op.pre_verification_gas; - let tx = self - .i_entry_point - .simulate_validation(user_op) - .gas(U256::from(max_validation_gas) + pvg) - .tx; - Ok(tx) - } - - async fn call_simulate_validation( - &self, - user_op: UserOperation, - max_validation_gas: u64, - ) -> anyhow::Result { - let pvg = user_op.pre_verification_gas; - match self - .i_entry_point - .simulate_validation(user_op) - .gas(U256::from(max_validation_gas) + pvg) - .call() - .await - { - Ok(()) => anyhow::bail!("simulateValidation should always revert"), - Err(ContractError::Revert(revert_data)) => ValidationOutput::decode(revert_data) - .context("entry point should return validation output"), - Err(error) => Err(error).context("call simulation RPC failed")?, - } - } - - async fn call_handle_ops( - &self, - ops_per_aggregator: Vec, - beneficiary: Address, - gas: U256, - ) -> anyhow::Result { - let result = get_handle_ops_call(&self.i_entry_point, ops_per_aggregator, beneficiary, gas) - .call() - .await; - let error = match result { - Ok(()) => return Ok(HandleOpsOut::Success), - Err(error) => error, - }; - if let ContractError::Revert(revert_data) = &error { - if let Ok(FailedOp { op_index, reason }) = FailedOp::decode(revert_data) { - match &reason[..4] { - "AA95" => anyhow::bail!("Handle ops called with insufficient gas"), - _ => return Ok(HandleOpsOut::FailedOp(op_index.as_usize(), reason)), - } - } - if let Ok(failure) = SignatureValidationFailed::decode(revert_data) { - return Ok(HandleOpsOut::SignatureValidationFailed(failure.aggregator)); - } - // Special handling for a bug in the 0.6 entry point contract to detect the bug where - // the `returndatacopy` opcode reverts due to a postOp revert and the revert data is too short. - // See https://github.com/eth-infinitism/account-abstraction/pull/325 for more details. - // NOTE: this error message is copied directly from Geth and assumes it will not change. - if error.to_string().contains("return data out of bounds") { - return Ok(HandleOpsOut::PostOpRevert); - } - } - Err(error)? - } - - async fn balance_of( - &self, - address: Address, - block_id: Option, - ) -> anyhow::Result { - block_id - .map_or(self.i_entry_point.balance_of(address), |bid| { - self.i_entry_point.balance_of(address).block(bid) - }) - .call() - .await - .context("entry point should return balance") - } - - async fn call_spoofed_simulate_op( - &self, - op: UserOperation, - target: Address, - target_call_data: Bytes, - block_hash: H256, - gas: U256, - spoofed_state: &spoof::State, - ) -> anyhow::Result> { - let contract_error = self - .i_entry_point - .simulate_handle_op(op, target, target_call_data) - .block(block_hash) - .gas(gas) - .call_raw() - .state(spoofed_state) - .await - .err() - .context("simulateHandleOp succeeded, but should always revert")?; - let revert_data = eth::get_revert_bytes(contract_error) - .context("simulateHandleOps should return revert data")?; - return Ok(self.decode_simulate_handle_ops_revert(revert_data)); - } - - fn get_send_bundle_transaction( - &self, - ops_per_aggregator: Vec, - beneficiary: Address, - gas: U256, - gas_fees: GasFees, - ) -> TypedTransaction { - let tx: Eip1559TransactionRequest = - get_handle_ops_call(&self.i_entry_point, ops_per_aggregator, beneficiary, gas) - .tx - .into(); - tx.max_fee_per_gas(gas_fees.max_fee_per_gas) - .max_priority_fee_per_gas(gas_fees.max_priority_fee_per_gas) - .into() - } - - fn decode_simulate_handle_ops_revert( - &self, - revert_data: Bytes, - ) -> Result { - if let Ok(result) = ExecutionResult::decode(&revert_data) { - Ok(result) - } else if let Ok(failed_op) = FailedOp::decode(&revert_data) { - Err(failed_op.reason) - } else if let Ok(err) = ContractRevertError::decode(&revert_data) { - Err(err.reason) - } else { - Err(hex::encode(&revert_data[..REVERT_REASON_MAX_LEN])) - } - } - - async fn get_deposit_info(&self, address: Address) -> anyhow::Result { - Ok(self - .i_entry_point - .get_deposit_info(address) - .await - .context("should get deposit info")?) - } - - async fn get_balances(&self, addresses: Vec

) -> anyhow::Result> { - let out: GetBalancesResult = self - .provider - .call_constructor( - &GETBALANCES_BYTECODE, - (self.address(), addresses), - None, - &spoof::state(), - ) - .await - .context("should compute balances")?; - Ok(out.balances) - } -} - -fn get_handle_ops_call( - entry_point: &IEntryPoint, - mut ops_per_aggregator: Vec, - beneficiary: Address, - gas: U256, -) -> FunctionCall, M, ()> { - let call = - if ops_per_aggregator.len() == 1 && ops_per_aggregator[0].aggregator == Address::zero() { - entry_point.handle_ops(ops_per_aggregator.swap_remove(0).user_ops, beneficiary) - } else { - entry_point.handle_aggregated_ops(ops_per_aggregator, beneficiary) - }; - call.gas(gas) -} diff --git a/crates/provider/src/ethers/entry_point/mod.rs b/crates/provider/src/ethers/entry_point/mod.rs new file mode 100644 index 000000000..d90460131 --- /dev/null +++ b/crates/provider/src/ethers/entry_point/mod.rs @@ -0,0 +1,58 @@ +// 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 ethers::{ + providers::Middleware, + types::{Address, Bytes, Eip1559TransactionRequest, U256, U64}, +}; +use rundler_types::contracts::{ + arbitrum::node_interface::NodeInterface, optimism::gas_price_oracle::GasPriceOracle, +}; + +pub(crate) mod v0_6; +pub(crate) mod v0_7; + +async fn estimate_arbitrum_l1_gas( + arb_node: &NodeInterface

, + address: Address, + data: Bytes, +) -> anyhow::Result { + let gas = arb_node + .gas_estimate_l1_component(address, false, data) + .call() + .await?; + Ok(U256::from(gas.0)) +} + +async fn estimate_optimism_l1_gas( + opt_oracle: &GasPriceOracle

, + address: Address, + data: Bytes, + gas_price: U256, +) -> anyhow::Result { + // construct an unsigned transaction with default values just for L1 gas estimation + let tx = Eip1559TransactionRequest::new() + .from(Address::random()) + .to(address) + .gas(U256::from(1_000_000)) + .max_priority_fee_per_gas(U256::from(100_000_000)) + .max_fee_per_gas(U256::from(100_000_000)) + .value(U256::from(0)) + .data(data) + .nonce(U256::from(100_000)) + .chain_id(U64::from(100_000)) + .rlp(); + + let l1_fee = opt_oracle.get_l1_fee(tx).call().await?; + Ok(l1_fee.checked_div(gas_price).unwrap_or(U256::MAX)) +} diff --git a/crates/provider/src/ethers/entry_point/v0_6.rs b/crates/provider/src/ethers/entry_point/v0_6.rs new file mode 100644 index 000000000..b4dc549a4 --- /dev/null +++ b/crates/provider/src/ethers/entry_point/v0_6.rs @@ -0,0 +1,438 @@ +// 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::sync::Arc; + +use anyhow::Context; +use ethers::{ + abi::AbiDecode, + contract::{ContractError, FunctionCall}, + providers::{spoof, Middleware, RawCall}, + types::{ + transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, Eip1559TransactionRequest, + H160, H256, U256, + }, + utils::hex, +}; +use rundler_types::{ + contracts::{ + arbitrum::node_interface::NodeInterface, + optimism::gas_price_oracle::GasPriceOracle, + v0_6::{ + get_balances::{GetBalancesResult, GETBALANCES_BYTECODE}, + i_aggregator::IAggregator, + i_entry_point::{ + DepositInfo as DepositInfoV0_6, ExecutionResult as ExecutionResultV0_6, FailedOp, + IEntryPoint, SignatureValidationFailed, + UserOpsPerAggregator as UserOpsPerAggregatorV0_6, + }, + }, + }, + v0_6::UserOperation, + GasFees, UserOpsPerAggregator, ValidationError, ValidationOutput, ValidationRevert, +}; +use rundler_utils::eth::{self, ContractRevertError}; + +use crate::{ + traits::HandleOpsOut, AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, + EntryPoint as EntryPointTrait, EntryPointProvider, ExecutionResult, L1GasProvider, Provider, + SignatureAggregator, SimulationProvider, +}; + +const ARBITRUM_NITRO_NODE_INTERFACE_ADDRESS: Address = H160([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xc8, +]); + +const OPTIMISM_BEDROCK_GAS_ORACLE_ADDRESS: Address = H160([ + 0x42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0F, +]); + +const REVERT_REASON_MAX_LEN: usize = 2048; + +/// Implementation of the `EntryPoint` trait for the v0.6 version of the entry point contract using ethers +#[derive(Debug)] +pub struct EntryPoint { + i_entry_point: IEntryPoint

, + provider: Arc

, + arb_node: NodeInterface

, + opt_gas_oracle: GasPriceOracle

, +} + +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(), + } + } +} + +impl

EntryPoint

+where + P: Provider + Middleware, +{ + /// Create a new `EntryPointV0_6` instance + pub fn new(entry_point_address: Address, provider: Arc

) -> Self { + Self { + i_entry_point: IEntryPoint::new(entry_point_address, Arc::clone(&provider)), + provider: Arc::clone(&provider), + arb_node: NodeInterface::new( + ARBITRUM_NITRO_NODE_INTERFACE_ADDRESS, + Arc::clone(&provider), + ), + opt_gas_oracle: GasPriceOracle::new(OPTIMISM_BEDROCK_GAS_ORACLE_ADDRESS, provider), + } + } +} + +#[async_trait::async_trait] +impl

EntryPointTrait for EntryPoint

+where + P: Provider + Middleware + Send + Sync + 'static, +{ + fn address(&self) -> Address { + self.i_entry_point.address() + } + + async fn balance_of( + &self, + address: Address, + block_id: Option, + ) -> anyhow::Result { + block_id + .map_or(self.i_entry_point.balance_of(address), |bid| { + self.i_entry_point.balance_of(address).block(bid) + }) + .call() + .await + .context("entry point should return balance") + } + + async fn get_deposit_info(&self, address: Address) -> anyhow::Result { + Ok(self + .i_entry_point + .get_deposit_info(address) + .await + .context("should get deposit info")? + .into()) + } + + async fn get_balances(&self, addresses: Vec

) -> anyhow::Result> { + let out: GetBalancesResult = self + .provider + .call_constructor( + &GETBALANCES_BYTECODE, + (self.address(), addresses), + None, + &spoof::state(), + ) + .await + .context("should compute balances")?; + Ok(out.balances) + } +} + +#[async_trait::async_trait] +impl

SignatureAggregator for EntryPoint

+where + P: Provider + Middleware + Send + Sync + 'static, +{ + type UO = UserOperation; + + async fn aggregate_signatures( + &self, + aggregator_address: Address, + ops: Vec, + ) -> anyhow::Result> { + let aggregator = IAggregator::new(aggregator_address, Arc::clone(&self.provider)); + // TODO: Cap the gas here. + let result = aggregator.aggregate_signatures(ops).call().await; + match result { + Ok(bytes) => Ok(Some(bytes)), + Err(ContractError::Revert(_)) => Ok(None), + Err(error) => Err(error).context("aggregator contract should aggregate signatures")?, + } + } + + async fn validate_user_op_signature( + &self, + aggregator_address: Address, + user_op: UserOperation, + gas_cap: u64, + ) -> anyhow::Result { + let aggregator = IAggregator::new(aggregator_address, Arc::clone(&self.provider)); + + let result = aggregator + .validate_user_op_signature(user_op) + .gas(gas_cap) + .call() + .await; + + match result { + Ok(sig) => Ok(AggregatorOut::SuccessWithInfo(AggregatorSimOut { + address: aggregator_address, + signature: sig, + })), + Err(ContractError::Revert(_)) => Ok(AggregatorOut::ValidationReverted), + Err(error) => Err(error).context("should call aggregator to validate signature")?, + } + } +} + +#[async_trait::async_trait] +impl

BundleHandler for EntryPoint

+where + P: Provider + Middleware + Send + Sync + 'static, +{ + type UO = UserOperation; + + async fn call_handle_ops( + &self, + ops_per_aggregator: Vec>, + beneficiary: Address, + gas: U256, + ) -> anyhow::Result { + let result = get_handle_ops_call(&self.i_entry_point, ops_per_aggregator, beneficiary, gas) + .call() + .await; + let error = match result { + Ok(()) => return Ok(HandleOpsOut::Success), + Err(error) => error, + }; + if let ContractError::Revert(revert_data) = &error { + if let Ok(FailedOp { op_index, reason }) = FailedOp::decode(revert_data) { + match &reason[..4] { + "AA95" => anyhow::bail!("Handle ops called with insufficient gas"), + _ => return Ok(HandleOpsOut::FailedOp(op_index.as_usize(), reason)), + } + } + if let Ok(failure) = SignatureValidationFailed::decode(revert_data) { + return Ok(HandleOpsOut::SignatureValidationFailed(failure.aggregator)); + } + // Special handling for a bug in the 0.6 entry point contract to detect the bug where + // the `returndatacopy` opcode reverts due to a postOp revert and the revert data is too short. + // See https://github.com/eth-infinitism/account-abstraction/pull/325 for more details. + // NOTE: this error message is copied directly from Geth and assumes it will not change. + if error.to_string().contains("return data out of bounds") { + return Ok(HandleOpsOut::PostOpRevert); + } + } + Err(error)? + } + + fn get_send_bundle_transaction( + &self, + ops_per_aggregator: Vec>, + beneficiary: Address, + gas: U256, + gas_fees: GasFees, + ) -> TypedTransaction { + let tx: Eip1559TransactionRequest = + get_handle_ops_call(&self.i_entry_point, ops_per_aggregator, beneficiary, gas) + .tx + .into(); + tx.max_fee_per_gas(gas_fees.max_fee_per_gas) + .max_priority_fee_per_gas(gas_fees.max_priority_fee_per_gas) + .into() + } +} + +#[async_trait::async_trait] +impl

L1GasProvider for EntryPoint

+where + P: Provider + Middleware + Send + Sync + 'static, +{ + type UO = UserOperation; + + async fn calc_arbitrum_l1_gas( + &self, + entry_point_address: Address, + user_op: UserOperation, + ) -> anyhow::Result { + let data = self + .i_entry_point + .handle_ops(vec![user_op], Address::random()) + .calldata() + .context("should get calldata for entry point handle ops")?; + + super::estimate_arbitrum_l1_gas(&self.arb_node, entry_point_address, data).await + } + + async fn calc_optimism_l1_gas( + &self, + entry_point_address: Address, + user_op: UserOperation, + gas_price: U256, + ) -> anyhow::Result { + let data = self + .i_entry_point + .handle_ops(vec![user_op], Address::random()) + .calldata() + .context("should get calldata for entry point handle ops")?; + + super::estimate_optimism_l1_gas(&self.opt_gas_oracle, entry_point_address, data, gas_price) + .await + } +} + +#[async_trait::async_trait] +impl

SimulationProvider for EntryPoint

+where + P: Provider + Middleware + Send + Sync + 'static, +{ + type UO = UserOperation; + + fn get_tracer_simulate_validation_call( + &self, + user_op: UserOperation, + max_validation_gas: u64, + ) -> (TypedTransaction, spoof::State) { + let pvg = user_op.pre_verification_gas; + let call = self + .i_entry_point + .simulate_validation(user_op) + .gas(U256::from(max_validation_gas) + pvg) + .tx; + (call, spoof::State::default()) + } + + async fn call_simulate_validation( + &self, + user_op: UserOperation, + max_validation_gas: u64, + block_hash: Option, + ) -> Result { + let pvg = user_op.pre_verification_gas; + let blockless = self + .i_entry_point + .simulate_validation(user_op) + .gas(U256::from(max_validation_gas) + pvg); + let call = match block_hash { + Some(block_hash) => blockless.block(block_hash), + None => blockless, + }; + + match call.call().await { + 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")?, + } + } + + fn decode_simulate_handle_ops_revert( + &self, + revert_data: Bytes, + ) -> Result { + if let Ok(result) = ExecutionResultV0_6::decode(&revert_data) { + Ok(result.into()) + } else if let Ok(failed_op) = FailedOp::decode(&revert_data) { + Err(failed_op.reason) + } else if let Ok(err) = ContractRevertError::decode(&revert_data) { + Err(err.reason) + } else { + Err(hex::encode(&revert_data[..REVERT_REASON_MAX_LEN])) + } + } + + async fn call_spoofed_simulate_op( + &self, + user_op: UserOperation, + target: Address, + target_call_data: Bytes, + block_hash: H256, + gas: U256, + spoofed_state: &spoof::State, + ) -> anyhow::Result> { + let contract_error = self + .i_entry_point + .simulate_handle_op(user_op, target, target_call_data) + .block(block_hash) + .gas(gas) + .call_raw() + .state(spoofed_state) + .await + .err() + .context("simulateHandleOp succeeded, but should always revert")?; + let revert_data = eth::get_revert_bytes(contract_error) + .context("simulateHandleOps should return revert data")?; + return Ok(self.decode_simulate_handle_ops_revert(revert_data)); + } +} + +impl

EntryPointProvider for EntryPoint

where + P: Provider + Middleware + Send + Sync + 'static +{ +} + +fn get_handle_ops_call( + entry_point: &IEntryPoint, + ops_per_aggregator: Vec>, + beneficiary: Address, + gas: U256, +) -> FunctionCall, M, ()> { + let mut ops_per_aggregator: Vec = ops_per_aggregator + .into_iter() + .map(|uoa| UserOpsPerAggregatorV0_6 { + user_ops: uoa.user_ops, + aggregator: uoa.aggregator, + signature: uoa.signature, + }) + .collect(); + let call = + if ops_per_aggregator.len() == 1 && ops_per_aggregator[0].aggregator == Address::zero() { + entry_point.handle_ops(ops_per_aggregator.swap_remove(0).user_ops, beneficiary) + } else { + entry_point.handle_aggregated_ops(ops_per_aggregator, beneficiary) + }; + call.gas(gas) +} + +impl From for ExecutionResult { + fn from(result: ExecutionResultV0_6) -> Self { + ExecutionResult { + pre_op_gas: result.pre_op_gas, + paid: result.paid, + valid_after: result.valid_after.into(), + valid_until: result.valid_until.into(), + target_success: result.target_success, + target_result: result.target_result, + } + } +} + +impl From for DepositInfo { + fn from(info: DepositInfoV0_6) -> Self { + DepositInfo { + deposit: info.deposit.into(), + staked: info.staked, + stake: info.stake, + unstake_delay_sec: info.unstake_delay_sec, + withdraw_time: info.withdraw_time, + } + } +} diff --git a/crates/provider/src/ethers/entry_point/v0_7.rs b/crates/provider/src/ethers/entry_point/v0_7.rs new file mode 100644 index 000000000..3e9be7000 --- /dev/null +++ b/crates/provider/src/ethers/entry_point/v0_7.rs @@ -0,0 +1,478 @@ +// 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::sync::Arc; + +use anyhow::Context; +use ethers::{ + abi::AbiDecode, + contract::{ContractError, FunctionCall}, + providers::{Middleware, RawCall}, + types::{ + spoof, transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, + Eip1559TransactionRequest, H160, H256, U256, + }, + utils::hex, +}; +use rundler_types::{ + contracts::{ + arbitrum::node_interface::NodeInterface, + optimism::gas_price_oracle::GasPriceOracle, + v0_7::{ + entry_point_simulations::{ + EntryPointSimulations, ExecutionResult as ExecutionResultV0_7, + ENTRYPOINTSIMULATIONS_DEPLOYED_BYTECODE, + }, + get_balances::{GetBalancesResult, GETBALANCES_BYTECODE}, + i_aggregator::IAggregator, + i_entry_point::{ + DepositInfo as DepositInfoV0_7, FailedOp, FailedOpWithRevert, IEntryPoint, + SignatureValidationFailed, UserOpsPerAggregator as UserOpsPerAggregatorV0_7, + }, + }, + }, + v0_7::UserOperation, + GasFees, UserOpsPerAggregator, ValidationError, ValidationOutput, ValidationRevert, +}; +use rundler_utils::eth::{self, ContractRevertError}; + +use crate::{ + AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint as EntryPointTrait, + EntryPointProvider, ExecutionResult, HandleOpsOut, L1GasProvider, Provider, + SignatureAggregator, SimulationProvider, +}; + +// From v0.7 EP contract +const REVERT_REASON_MAX_LEN: usize = 2048; + +// TODO(danc): These should be configurable from chain spec +const ARBITRUM_NITRO_NODE_INTERFACE_ADDRESS: Address = H160([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xc8, +]); +const OPTIMISM_BEDROCK_GAS_ORACLE_ADDRESS: Address = H160([ + 0x42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0F, +]); + +/// Entry point for the v0.7 contract. +#[derive(Debug)] +pub struct EntryPoint

{ + i_entry_point: IEntryPoint

, + provider: Arc

, + arb_node: NodeInterface

, + opt_gas_oracle: GasPriceOracle

, +} + +impl

EntryPoint

+where + P: Middleware, +{ + /// Create a new `EntryPoint` instance for v0.7 + pub fn new(entry_point_address: Address, provider: Arc

) -> Self { + Self { + i_entry_point: IEntryPoint::new(entry_point_address, Arc::clone(&provider)), + provider: Arc::clone(&provider), + arb_node: NodeInterface::new( + ARBITRUM_NITRO_NODE_INTERFACE_ADDRESS, + Arc::clone(&provider), + ), + opt_gas_oracle: GasPriceOracle::new(OPTIMISM_BEDROCK_GAS_ORACLE_ADDRESS, provider), + } + } +} + +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

EntryPointTrait for EntryPoint

+where + P: Provider + Middleware + Send + Sync + 'static, +{ + fn address(&self) -> Address { + self.i_entry_point.address() + } + + async fn balance_of( + &self, + address: Address, + block_id: Option, + ) -> anyhow::Result { + block_id + .map_or(self.i_entry_point.balance_of(address), |bid| { + self.i_entry_point.balance_of(address).block(bid) + }) + .call() + .await + .context("entry point should return balance") + } + + async fn get_deposit_info(&self, address: Address) -> anyhow::Result { + Ok(self + .i_entry_point + .get_deposit_info(address) + .await + .context("should get deposit info")? + .into()) + } + + async fn get_balances(&self, addresses: Vec

) -> anyhow::Result> { + let out: GetBalancesResult = self + .provider + .call_constructor( + &GETBALANCES_BYTECODE, + (self.address(), addresses), + None, + &spoof::state(), + ) + .await + .context("should compute balances")?; + Ok(out.balances) + } +} + +#[async_trait::async_trait] +impl

SignatureAggregator for EntryPoint

+where + P: Provider + Middleware + Send + Sync + 'static, +{ + type UO = UserOperation; + + async fn aggregate_signatures( + &self, + aggregator_address: Address, + ops: Vec, + ) -> anyhow::Result> { + let aggregator = IAggregator::new(aggregator_address, Arc::clone(&self.provider)); + + // pack the ops + let packed_ops = ops.into_iter().map(|op| op.pack()).collect(); + + // TODO: Cap the gas here. + let result = aggregator.aggregate_signatures(packed_ops).call().await; + match result { + Ok(bytes) => Ok(Some(bytes)), + Err(ContractError::Revert(_)) => Ok(None), + Err(error) => Err(error).context("aggregator contract should aggregate signatures")?, + } + } + + async fn validate_user_op_signature( + &self, + aggregator_address: Address, + user_op: UserOperation, + gas_cap: u64, + ) -> anyhow::Result { + let aggregator = IAggregator::new(aggregator_address, Arc::clone(&self.provider)); + + let result = aggregator + .validate_user_op_signature(user_op.pack()) + .gas(gas_cap) + .call() + .await; + + match result { + Ok(sig) => Ok(AggregatorOut::SuccessWithInfo(AggregatorSimOut { + address: aggregator_address, + signature: sig, + })), + Err(ContractError::Revert(_)) => Ok(AggregatorOut::ValidationReverted), + Err(error) => Err(error).context("should call aggregator to validate signature")?, + } + } +} + +#[async_trait::async_trait] +impl

BundleHandler for EntryPoint

+where + P: Provider + Middleware + Send + Sync + 'static, +{ + type UO = UserOperation; + + async fn call_handle_ops( + &self, + ops_per_aggregator: Vec>, + beneficiary: Address, + gas: U256, + ) -> anyhow::Result { + let result = get_handle_ops_call(&self.i_entry_point, ops_per_aggregator, beneficiary, gas) + .call() + .await; + let error = match result { + Ok(()) => return Ok(HandleOpsOut::Success), + Err(error) => error, + }; + if let ContractError::Revert(revert_data) = &error { + if let Ok(FailedOp { op_index, reason }) = FailedOp::decode(revert_data) { + match &reason[..4] { + // This revert is a bundler issue, not a user op issue, handle it differently + "AA95" => anyhow::bail!("Handle ops called with insufficient gas"), + _ => return Ok(HandleOpsOut::FailedOp(op_index.as_usize(), reason)), + } + } + if let Ok(failure) = SignatureValidationFailed::decode(revert_data) { + return Ok(HandleOpsOut::SignatureValidationFailed(failure.aggregator)); + } + } + Err(error)? + } + + fn get_send_bundle_transaction( + &self, + ops_per_aggregator: Vec>, + beneficiary: Address, + gas: U256, + gas_fees: GasFees, + ) -> TypedTransaction { + let tx: Eip1559TransactionRequest = + get_handle_ops_call(&self.i_entry_point, ops_per_aggregator, beneficiary, gas) + .tx + .into(); + tx.max_fee_per_gas(gas_fees.max_fee_per_gas) + .max_priority_fee_per_gas(gas_fees.max_priority_fee_per_gas) + .into() + } +} + +#[async_trait::async_trait] +impl

L1GasProvider for EntryPoint

+where + P: Provider + Middleware + Send + Sync + 'static, +{ + type UO = UserOperation; + + async fn calc_arbitrum_l1_gas( + &self, + entry_point_address: Address, + user_op: UserOperation, + ) -> anyhow::Result { + let data = self + .i_entry_point + .handle_ops(vec![user_op.pack()], Address::random()) + .calldata() + .context("should get calldata for entry point handle ops")?; + + super::estimate_arbitrum_l1_gas(&self.arb_node, entry_point_address, data).await + } + + async fn calc_optimism_l1_gas( + &self, + entry_point_address: Address, + user_op: UserOperation, + gas_price: U256, + ) -> anyhow::Result { + let data = self + .i_entry_point + .handle_ops(vec![user_op.pack()], Address::random()) + .calldata() + .context("should get calldata for entry point handle ops")?; + + super::estimate_optimism_l1_gas(&self.opt_gas_oracle, entry_point_address, data, gas_price) + .await + } +} + +#[async_trait::async_trait] +impl

SimulationProvider for EntryPoint

+where + P: Provider + Middleware + Send + Sync + 'static, +{ + type UO = UserOperation; + + fn get_tracer_simulate_validation_call( + &self, + user_op: UserOperation, + max_validation_gas: u64, + ) -> (TypedTransaction, spoof::State) { + 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_DEPLOYED_BYTECODE.clone()); + let ep_simulations = EntryPointSimulations::new(addr, Arc::clone(&self.provider)); + + let call = ep_simulations + .simulate_validation(user_op.pack()) + .gas(U256::from(max_validation_gas) + pvg) + .tx; + + (call, spoof_ep) + } + + async fn call_simulate_validation( + &self, + user_op: UserOperation, + max_validation_gas: u64, + block_hash: Option, + ) -> 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_DEPLOYED_BYTECODE.clone()); + + let ep_simulations = EntryPointSimulations::new(addr, Arc::clone(&self.provider)); + let blockless = ep_simulations + .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, + }; + + 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( + &self, + revert_data: Bytes, + ) -> Result { + if let Ok(result) = ExecutionResultV0_7::decode(&revert_data) { + Ok(result.into()) + } else if let Ok(failed_op) = FailedOp::decode(&revert_data) { + Err(failed_op.reason) + } else if let Ok(err) = ContractRevertError::decode(&revert_data) { + Err(err.reason) + } else { + Err(hex::encode(&revert_data[..REVERT_REASON_MAX_LEN])) + } + } + + // NOTE: A spoof of the entry point code will be ignored by this function. + async fn call_spoofed_simulate_op( + &self, + user_op: UserOperation, + target: Address, + target_call_data: Bytes, + block_hash: H256, + gas: U256, + spoofed_state: &spoof::State, + ) -> anyhow::Result> { + let addr = self.i_entry_point.address(); + let mut spoof_ep = spoofed_state.clone(); + spoof_ep + .account(addr) + .code(ENTRYPOINTSIMULATIONS_DEPLOYED_BYTECODE.clone()); + let ep_simulations = EntryPointSimulations::new(addr, Arc::clone(&self.provider)); + + let contract_error = ep_simulations + .simulate_handle_op(user_op.pack(), target, target_call_data) + .block(block_hash) + .gas(gas) + .call_raw() + .state(spoofed_state) + .await + .err() + .context("simulateHandleOp succeeded, but should always revert")?; + let revert_data = eth::get_revert_bytes(contract_error) + .context("simulateHandleOps should return revert data")?; + return Ok(self.decode_simulate_handle_ops_revert(revert_data)); + } +} + +impl

EntryPointProvider for EntryPoint

where + P: Provider + Middleware + Send + Sync + 'static +{ +} + +// 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>, + beneficiary: Address, + gas: U256, +) -> FunctionCall, M, ()> { + let mut ops_per_aggregator: Vec = ops_per_aggregator + .into_iter() + .map(|uoa| UserOpsPerAggregatorV0_7 { + user_ops: uoa.user_ops.into_iter().map(|op| op.pack()).collect(), + aggregator: uoa.aggregator, + signature: uoa.signature, + }) + .collect(); + let call = + if ops_per_aggregator.len() == 1 && ops_per_aggregator[0].aggregator == Address::zero() { + entry_point.handle_ops(ops_per_aggregator.swap_remove(0).user_ops, beneficiary) + } else { + entry_point.handle_aggregated_ops(ops_per_aggregator, beneficiary) + }; + call.gas(gas) +} + +impl From for ExecutionResult { + fn from(result: ExecutionResultV0_7) -> Self { + let account = rundler_types::parse_validation_data(result.account_validation_data); + let paymaster = rundler_types::parse_validation_data(result.paymaster_validation_data); + let intersect_range = account + .valid_time_range() + .intersect(paymaster.valid_time_range()); + + ExecutionResult { + pre_op_gas: result.pre_op_gas, + paid: result.paid, + valid_after: intersect_range.valid_after, + valid_until: intersect_range.valid_until, + target_success: result.target_success, + target_result: result.target_result, + } + } +} + +impl From for DepositInfo { + fn from(deposit_info: DepositInfoV0_7) -> Self { + Self { + deposit: deposit_info.deposit, + staked: deposit_info.staked, + stake: deposit_info.stake, + unstake_delay_sec: deposit_info.unstake_delay_sec, + withdraw_time: deposit_info.withdraw_time, + } + } +} diff --git a/crates/provider/src/ethers/mod.rs b/crates/provider/src/ethers/mod.rs index 072d4bc8f..ad0f669b0 100644 --- a/crates/provider/src/ethers/mod.rs +++ b/crates/provider/src/ethers/mod.rs @@ -14,6 +14,6 @@ //! Provider implementations using [ethers-rs](https://github.com/gakonst/ethers-rs) mod entry_point; -pub use entry_point::EntryPointImpl as EthersEntryPoint; +pub use entry_point::{v0_6::EntryPoint as EntryPointV0_6, v0_7::EntryPoint as EntryPointV0_7}; mod metrics_middleware; pub(crate) mod provider; diff --git a/crates/provider/src/ethers/provider.rs b/crates/provider/src/ethers/provider.rs index 4fcef0b33..8e700aeab 100644 --- a/crates/provider/src/ethers/provider.rs +++ b/crates/provider/src/ethers/provider.rs @@ -16,7 +16,6 @@ use std::{fmt::Debug, sync::Arc, time::Duration}; use anyhow::Context; use ethers::{ abi::{AbiDecode, AbiEncode}, - contract::ContractError, prelude::ContractError as EthersContractError, providers::{ Http, HttpRateLimitRetryPolicy, JsonRpcClient, Middleware, Provider as EthersProvider, @@ -25,31 +24,15 @@ use ethers::{ types::{ spoof, transaction::eip2718::TypedTransaction, Address, Block, BlockId, BlockNumber, Bytes, Eip1559TransactionRequest, FeeHistory, Filter, GethDebugTracingCallOptions, - GethDebugTracingOptions, GethTrace, Log, Transaction, TransactionReceipt, TxHash, H160, - H256, U256, U64, + GethDebugTracingOptions, GethTrace, Log, Transaction, TransactionReceipt, TxHash, H256, + U256, U64, }, }; use reqwest::Url; -use rundler_types::{ - contracts::{ - arbitrum::node_interface::NodeInterface, - optimism::gas_price_oracle::GasPriceOracle, - v0_6::{i_aggregator::IAggregator, i_entry_point::IEntryPoint}, - }, - UserOperation, -}; use serde::{de::DeserializeOwned, Serialize}; use super::metrics_middleware::MetricsMiddleware; -use crate::{AggregatorOut, AggregatorSimOut, Provider, ProviderError, ProviderResult}; - -const ARBITRUM_NITRO_NODE_INTERFACE_ADDRESS: Address = H160([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xc8, -]); - -const OPTIMISM_BEDROCK_GAS_ORACLE_ADDRESS: Address = H160([ - 0x42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0F, -]); +use crate::{Provider, ProviderError, ProviderResult}; #[async_trait::async_trait] impl Provider for EthersProvider { @@ -192,44 +175,6 @@ impl Provider for EthersProvider { Ok(Middleware::get_logs(self, filter).await?) } - async fn aggregate_signatures( - self: Arc, - aggregator_address: Address, - ops: Vec, - ) -> ProviderResult> { - let aggregator = IAggregator::new(aggregator_address, self); - // TODO: Cap the gas here. - let result = aggregator.aggregate_signatures(ops).call().await; - match result { - Ok(bytes) => Ok(Some(bytes)), - Err(ContractError::Revert(_)) => Ok(None), - Err(error) => Err(error).context("aggregator contract should aggregate signatures")?, - } - } - - async fn validate_user_op_signature( - self: Arc, - aggregator_address: Address, - user_op: UserOperation, - gas_cap: u64, - ) -> ProviderResult { - let aggregator = IAggregator::new(aggregator_address, self); - let result = aggregator - .validate_user_op_signature(user_op) - .gas(gas_cap) - .call() - .await; - - match result { - Ok(sig) => Ok(AggregatorOut::SuccessWithInfo(AggregatorSimOut { - address: aggregator_address, - signature: sig, - })), - Err(ContractError::Revert(_)) => Ok(AggregatorOut::ValidationReverted), - Err(error) => Err(error).context("should call aggregator to validate signature")?, - } - } - async fn get_code(&self, address: Address, block_hash: Option) -> ProviderResult { Ok(Middleware::get_code(self, address, block_hash.map(|b| b.into())).await?) } @@ -237,57 +182,6 @@ impl Provider for EthersProvider { async fn get_transaction_count(&self, address: Address) -> ProviderResult { Ok(Middleware::get_transaction_count(self, address, None).await?) } - - async fn calc_arbitrum_l1_gas( - self: Arc, - entry_point_address: Address, - op: UserOperation, - ) -> ProviderResult { - let entry_point = IEntryPoint::new(entry_point_address, Arc::clone(&self)); - let data = entry_point - .handle_ops(vec![op], Address::random()) - .calldata() - .context("should get calldata for entry point handle ops")?; - - let arb_node = NodeInterface::new(ARBITRUM_NITRO_NODE_INTERFACE_ADDRESS, self); - let gas = arb_node - .gas_estimate_l1_component(entry_point_address, false, data) - .call() - .await?; - Ok(U256::from(gas.0)) - } - - async fn calc_optimism_l1_gas( - self: Arc, - entry_point_address: Address, - op: UserOperation, - gas_price: U256, - ) -> ProviderResult { - let entry_point = IEntryPoint::new(entry_point_address, Arc::clone(&self)); - let data = entry_point - .handle_ops(vec![op], Address::random()) - .calldata() - .context("should get calldata for entry point handle ops")?; - - // construct an unsigned transaction with default values just for L1 gas estimation - let tx = Eip1559TransactionRequest::new() - .from(Address::random()) - .to(entry_point_address) - .gas(U256::from(1_000_000)) - .max_priority_fee_per_gas(U256::from(100_000_000)) - .max_fee_per_gas(U256::from(100_000_000)) - .value(U256::from(0)) - .data(data) - .nonce(U256::from(100_000)) - .chain_id(U64::from(100_000)) - .rlp(); - - let gas_oracle = - GasPriceOracle::new(OPTIMISM_BEDROCK_GAS_ORACLE_ADDRESS, Arc::clone(&self)); - - let l1_fee = gas_oracle.get_l1_fee(tx).call().await?; - Ok(l1_fee.checked_div(gas_price).unwrap_or(U256::MAX)) - } } impl From for ProviderError { diff --git a/crates/provider/src/lib.rs b/crates/provider/src/lib.rs index 1f2415d8f..4b98ddcbd 100644 --- a/crates/provider/src/lib.rs +++ b/crates/provider/src/lib.rs @@ -22,12 +22,14 @@ //! A provider is a type that provides access to blockchain data and functions mod ethers; -pub use ethers::{provider::new_provider, EthersEntryPoint}; +pub use ethers::{ + provider::new_provider, EntryPointV0_6 as EthersEntryPointV0_6, + EntryPointV0_7 as EthersEntryPointV0_7, +}; mod traits; -pub use traits::{ - AggregatorOut, AggregatorSimOut, EntryPoint, HandleOpsOut, Provider, ProviderError, - ProviderResult, -}; #[cfg(any(test, feature = "test-utils"))] -pub use traits::{MockEntryPoint, MockProvider}; +pub use traits::test_utils::*; +#[cfg(any(test, feature = "test-utils"))] +pub use traits::MockProvider; +pub use traits::*; diff --git a/crates/provider/src/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index 6a67c3563..b85be3922 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -14,13 +14,30 @@ use ethers::types::{ spoof, transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, H256, U256, }; -#[cfg(feature = "test-utils")] -use mockall::automock; use rundler_types::{ - contracts::v0_6::{i_entry_point::ExecutionResult, shared_types::UserOpsPerAggregator}, - DepositInfo, GasFees, UserOperation, ValidationOutput, + GasFees, Timestamp, UserOperation, UserOpsPerAggregator, ValidationError, ValidationOutput, }; +/// Output of a successful signature aggregator simulation call +#[derive(Clone, Debug, Default)] +pub struct AggregatorSimOut { + /// Address of the aggregator contract + pub address: Address, + /// Aggregated signature + pub signature: Bytes, +} + +/// Result of a signature aggregator call +#[derive(Debug)] +pub enum AggregatorOut { + /// No aggregator used + NotNeeded, + /// Successful call + SuccessWithInfo(AggregatorSimOut), + /// Aggregator validation function reverted + ValidationReverted, +} + /// Result of an entry point handle ops call #[derive(Clone, Debug)] pub enum HandleOpsOut { @@ -35,46 +52,156 @@ pub enum HandleOpsOut { PostOpRevert, } +/// Deposit info for an address from the entry point contract +#[derive(Clone, Debug, Default)] +pub struct DepositInfo { + /// Amount deposited on the entry point + pub deposit: U256, + /// Whether the address has staked + pub staked: bool, + /// Amount staked on the entry point + pub stake: u128, + /// The amount of time in sections that must pass before the stake can be withdrawn + pub unstake_delay_sec: u32, + /// The time at which the stake can be withdrawn + pub withdraw_time: u64, +} + +/// Result of an execution +#[derive(Clone, Debug, Default)] +pub struct ExecutionResult { + /// Gas used before the operation execution + pub pre_op_gas: U256, + /// Amount paid by the operation + pub paid: U256, + /// Time which the operation is valid after + pub valid_after: Timestamp, + /// Time which the operation is valid until + pub valid_until: Timestamp, + /// True if the operation execution succeeded + pub target_success: bool, + /// Result of the operation execution + pub target_result: Bytes, +} + /// Trait for interacting with an entry point contract. -/// Implemented for the v0.6 version of the entry point contract. -/// [Contracts can be found here](https://github.com/eth-infinitism/account-abstraction/tree/v0.6.0). -#[cfg_attr(feature = "test-utils", automock)] #[async_trait::async_trait] +#[auto_impl::auto_impl(&, Arc)] pub trait EntryPoint: Send + Sync + 'static { /// Get the address of the entry point contract fn address(&self) -> Address; + /// Get the balance of an address + async fn balance_of(&self, address: Address, block_id: Option) + -> anyhow::Result; + + /// Get the deposit info for an address + async fn get_deposit_info(&self, address: Address) -> anyhow::Result; + + /// Get the balances of a list of addresses in order + async fn get_balances(&self, addresses: Vec

) -> anyhow::Result>; +} + +/// Trait for handling signature aggregators +#[async_trait::async_trait] +#[auto_impl::auto_impl(&, Arc)] +pub trait SignatureAggregator: Send + Sync + 'static { + /// The type of user operation used by this entry point + type UO: UserOperation; + + /// Call an aggregator to aggregate signatures for a set of operations + async fn aggregate_signatures( + &self, + aggregator_address: Address, + ops: Vec, + ) -> anyhow::Result>; + + /// Validate a user operation signature using an aggregator + async fn validate_user_op_signature( + &self, + aggregator_address: Address, + user_op: Self::UO, + gas_cap: u64, + ) -> anyhow::Result; +} + +/// Trait for submitting bundles of operations to an entry point contract +#[async_trait::async_trait] +#[auto_impl::auto_impl(&, Arc)] +pub trait BundleHandler: Send + Sync + 'static { + /// The type of user operation used by this entry point + type UO: UserOperation; + /// Call the entry point contract's `handleOps` function async fn call_handle_ops( &self, - ops_per_aggregator: Vec, + ops_per_aggregator: Vec>, beneficiary: Address, gas: U256, ) -> anyhow::Result; - /// Get the balance of an address - async fn balance_of(&self, address: Address, block_id: Option) - -> anyhow::Result; + /// Construct the transaction to send a bundle of operations to the entry point contract + fn get_send_bundle_transaction( + &self, + ops_per_aggregator: Vec>, + beneficiary: Address, + gas: U256, + gas_fees: GasFees, + ) -> TypedTransaction; +} + +/// Trait for calculating L1 gas costs for user operations +/// +/// Used for L2 gas estimation +#[async_trait::async_trait] +#[auto_impl::auto_impl(&, Arc)] +pub trait L1GasProvider: Send + Sync + 'static { + /// The type of user operation used by this entry point + type UO: UserOperation; + + /// Calculate the L1 portion of the gas for a user operation on Arbitrum + async fn calc_arbitrum_l1_gas( + &self, + entry_point_address: Address, + op: Self::UO, + ) -> anyhow::Result; + + /// Calculate the L1 portion of the gas for a user operation on optimism + async fn calc_optimism_l1_gas( + &self, + entry_point_address: Address, + op: Self::UO, + gas_price: U256, + ) -> anyhow::Result; +} + +/// Trait for simulating user operations on an entry point contract +#[async_trait::async_trait] +#[auto_impl::auto_impl(&, Arc)] +pub trait SimulationProvider: Send + Sync + 'static { + /// The type of user operation used by this entry point + type UO: UserOperation; /// Construct a call for the entry point contract's `simulateValidation` function - async fn get_simulate_validation_call( + fn get_tracer_simulate_validation_call( &self, - user_op: UserOperation, + user_op: Self::UO, max_validation_gas: u64, - ) -> anyhow::Result; + ) -> (TypedTransaction, spoof::State); /// Call the entry point contract's `simulateValidation` function. async fn call_simulate_validation( &self, - user_op: UserOperation, + user_op: Self::UO, max_validation_gas: u64, - ) -> anyhow::Result; + block_hash: Option, + ) -> Result; /// Call the entry point contract's `simulateHandleOps` function /// with a spoofed state async fn call_spoofed_simulate_op( &self, - op: UserOperation, + op: Self::UO, target: Address, target_call_data: Bytes, block_hash: H256, @@ -82,24 +209,19 @@ pub trait EntryPoint: Send + Sync + 'static { spoofed_state: &spoof::State, ) -> anyhow::Result>; - /// Construct the transaction to send a bundle of operations to the entry point contract - fn get_send_bundle_transaction( - &self, - ops_per_aggregator: Vec, - beneficiary: Address, - gas: U256, - gas_fees: GasFees, - ) -> TypedTransaction; - /// Decode the revert data from a call to `simulateHandleOps` fn decode_simulate_handle_ops_revert( &self, revert_data: Bytes, ) -> Result; +} - /// Get the deposit info for an address - async fn get_deposit_info(&self, address: Address) -> anyhow::Result; - - /// Get the balances of a list of addresses in order - async fn get_balances(&self, addresses: Vec
) -> anyhow::Result>; +/// Trait for a provider that provides all entry point functionality +pub trait EntryPointProvider: + EntryPoint + + SignatureAggregator + + BundleHandler + + SimulationProvider + + L1GasProvider +{ } diff --git a/crates/provider/src/traits/mod.rs b/crates/provider/src/traits/mod.rs index 87bde2210..73d9a54c0 100644 --- a/crates/provider/src/traits/mod.rs +++ b/crates/provider/src/traits/mod.rs @@ -17,11 +17,11 @@ mod error; pub use error::ProviderError; mod entry_point; -#[cfg(feature = "test-utils")] -pub use entry_point::MockEntryPoint; -pub use entry_point::{EntryPoint, HandleOpsOut}; +pub use entry_point::*; mod provider; #[cfg(feature = "test-utils")] pub use provider::MockProvider; -pub use provider::{AggregatorOut, AggregatorSimOut, Provider, ProviderResult}; +pub use provider::{Provider, ProviderResult}; +#[cfg(feature = "test-utils")] +pub(crate) mod test_utils; diff --git a/crates/provider/src/traits/provider.rs b/crates/provider/src/traits/provider.rs index 0c0fbec76..4578194c4 100644 --- a/crates/provider/src/traits/provider.rs +++ b/crates/provider/src/traits/provider.rs @@ -13,7 +13,7 @@ //! Trait for interacting with chain data and contracts. -use std::{fmt::Debug, sync::Arc}; +use std::fmt::Debug; use ethers::{ abi::{AbiDecode, AbiEncode}, @@ -25,31 +25,10 @@ use ethers::{ }; #[cfg(feature = "test-utils")] use mockall::automock; -use rundler_types::UserOperation; use serde::{de::DeserializeOwned, Serialize}; use super::error::ProviderError; -/// Output of a successful signature aggregator simulation call -#[derive(Clone, Debug, Default)] -pub struct AggregatorSimOut { - /// Address of the aggregator contract - pub address: Address, - /// Aggregated signature - pub signature: Bytes, -} - -/// Result of a signature aggregator call -#[derive(Debug)] -pub enum AggregatorOut { - /// No aggregator used - NotNeeded, - /// Successful call - SuccessWithInfo(AggregatorSimOut), - /// Aggregator validation function reverted - ValidationReverted, -} - /// Result of a provider method call pub type ProviderResult = Result; @@ -148,34 +127,4 @@ pub trait Provider: Send + Sync + Debug + 'static { /// Get the logs matching a filter async fn get_logs(&self, filter: &Filter) -> ProviderResult>; - - /// Call an aggregator to aggregate signatures for a set of operations - async fn aggregate_signatures( - self: Arc, - aggregator_address: Address, - ops: Vec, - ) -> ProviderResult>; - - /// Validate a user operation signature using an aggregator - async fn validate_user_op_signature( - self: Arc, - aggregator_address: Address, - user_op: UserOperation, - gas_cap: u64, - ) -> ProviderResult; - - /// Calculate the L1 portion of the gas for a user operation on Arbitrum - async fn calc_arbitrum_l1_gas( - self: Arc, - entry_point_address: Address, - op: UserOperation, - ) -> ProviderResult; - - /// Calculate the L1 portion of the gas for a user operation on optimism - async fn calc_optimism_l1_gas( - self: Arc, - entry_point_address: Address, - op: UserOperation, - gas_price: U256, - ) -> ProviderResult; } diff --git a/crates/provider/src/traits/test_utils.rs b/crates/provider/src/traits/test_utils.rs new file mode 100644 index 000000000..28913f643 --- /dev/null +++ b/crates/provider/src/traits/test_utils.rs @@ -0,0 +1,114 @@ +// 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 ethers::types::{ + spoof, transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, H256, U256, +}; +use rundler_types::{v0_6, GasFees, UserOpsPerAggregator, ValidationError, ValidationOutput}; + +use crate::{ + AggregatorOut, BundleHandler, DepositInfo, EntryPoint, ExecutionResult, HandleOpsOut, + L1GasProvider, SignatureAggregator, SimulationProvider, +}; + +mockall::mock! { + pub EntryPointV0_6 {} + + #[async_trait::async_trait] + impl EntryPoint for EntryPointV0_6 { + fn address(&self) -> Address; + async fn balance_of(&self, address: Address, block_id: Option) + -> anyhow::Result; + async fn get_deposit_info(&self, address: Address) -> anyhow::Result; + async fn get_balances(&self, addresses: Vec
) -> anyhow::Result>; + } + + #[async_trait::async_trait] + impl SignatureAggregator for EntryPointV0_6 { + type UO = v0_6::UserOperation; + async fn aggregate_signatures( + &self, + aggregator_address: Address, + ops: Vec, + ) -> anyhow::Result>; + async fn validate_user_op_signature( + &self, + aggregator_address: Address, + user_op: v0_6::UserOperation, + gas_cap: u64, + ) -> anyhow::Result; + } + + #[async_trait::async_trait] + impl SimulationProvider for EntryPointV0_6 { + type UO = v0_6::UserOperation; + fn get_tracer_simulate_validation_call( + &self, + user_op: v0_6::UserOperation, + max_validation_gas: u64, + ) -> (TypedTransaction, spoof::State); + async fn call_simulate_validation( + &self, + user_op: v0_6::UserOperation, + max_validation_gas: u64, + block_hash: Option + ) -> Result; + async fn call_spoofed_simulate_op( + &self, + op: v0_6::UserOperation, + target: Address, + target_call_data: Bytes, + block_hash: H256, + gas: U256, + spoofed_state: &spoof::State, + ) -> anyhow::Result>; + fn decode_simulate_handle_ops_revert( + &self, + revert_data: Bytes, + ) -> Result; + } + + #[async_trait::async_trait] + impl L1GasProvider for EntryPointV0_6 { + type UO = v0_6::UserOperation; + async fn calc_arbitrum_l1_gas( + &self, + entry_point_address: Address, + op: v0_6::UserOperation, + ) -> anyhow::Result; + async fn calc_optimism_l1_gas( + &self, + entry_point_address: Address, + op: v0_6::UserOperation, + gas_price: U256, + ) -> anyhow::Result; + } + + #[async_trait::async_trait] + impl BundleHandler for EntryPointV0_6 { + type UO = v0_6::UserOperation; + async fn call_handle_ops( + &self, + ops_per_aggregator: Vec>, + beneficiary: Address, + gas: U256, + ) -> anyhow::Result; + fn get_send_bundle_transaction( + &self, + ops_per_aggregator: Vec>, + beneficiary: Address, + gas: U256, + gas_fees: GasFees, + ) -> ethers::types::transaction::eip2718::TypedTransaction; + } +} diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index b3f8a4adb..6307787ba 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -7,8 +7,6 @@ license.workspace = true repository.workspace = true [dependencies] -rundler-builder = { path = "../builder" } -rundler-pool = { path = "../pool" } rundler-provider = { path = "../provider" } rundler-sim = { path = "../sim" } rundler-task = { path = "../task" } @@ -34,4 +32,4 @@ futures-util.workspace = true [dev-dependencies] mockall.workspace = true rundler-provider = { path = "../provider", features = ["test-utils"]} -rundler-pool = { path = "../pool", features = ["test-utils"] } +rundler-types= { path = "../types", features = ["test-utils"]} diff --git a/crates/rpc/src/admin.rs b/crates/rpc/src/admin.rs index 958ceafc1..12147d683 100644 --- a/crates/rpc/src/admin.rs +++ b/crates/rpc/src/admin.rs @@ -14,7 +14,7 @@ use async_trait::async_trait; use ethers::types::Address; use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::error::INTERNAL_ERROR_CODE}; -use rundler_pool::PoolServer; +use rundler_types::pool::Pool; use crate::{ error::rpc_err, @@ -50,7 +50,7 @@ impl

AdminApi

{ #[async_trait] impl

AdminApiServer for AdminApi

where - P: PoolServer, + P: Pool, { async fn clear_state(&self, clear_params: RpcAdminClearState) -> RpcResult { let _ = self diff --git a/crates/rpc/src/debug.rs b/crates/rpc/src/debug.rs index a7b3b20bd..48899880f 100644 --- a/crates/rpc/src/debug.rs +++ b/crates/rpc/src/debug.rs @@ -15,8 +15,10 @@ use async_trait::async_trait; use ethers::types::{Address, H256}; use futures_util::StreamExt; use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::error::INTERNAL_ERROR_CODE}; -use rundler_builder::{BuilderServer, BundlingMode}; -use rundler_pool::PoolServer; +use rundler_types::{ + builder::{Builder, BundlingMode}, + pool::Pool, +}; use crate::{ error::rpc_err, @@ -96,8 +98,8 @@ impl DebugApi { #[async_trait] impl DebugApiServer for DebugApi where - P: PoolServer, - B: BuilderServer, + P: Pool, + B: Builder, { async fn bundler_clear_state(&self) -> RpcResult { let _ = self @@ -234,8 +236,8 @@ where is_staked: result.is_staked, stake_info: RpcStakeInfo { addr: address, - stake: result.stake_info.stake, - unstake_delay_sec: result.stake_info.unstake_delay_sec, + stake: result.stake_info.stake.as_u128(), + unstake_delay_sec: result.stake_info.unstake_delay_sec.as_u32(), }, }) } diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 15f1ab060..1f12e0e7b 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -11,40 +11,22 @@ // 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::{HashMap, VecDeque}, - sync::Arc, -}; +use std::{future::Future, pin::Pin}; -use anyhow::Context; use ethers::{ - abi::{AbiDecode, RawLog}, - prelude::EthEvent, - types::{ - spoof, Address, Bytes, Filter, GethDebugBuiltInTracerType, GethDebugTracerType, - GethDebugTracingOptions, GethTrace, GethTraceFrame, Log, TransactionReceipt, H256, U256, - U64, - }, + types::{spoof, Address, H256, U64}, utils::to_checksum, }; -use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, Provider}; -use rundler_sim::{ - EstimationSettings, FeeEstimator, GasEstimate, GasEstimationError, GasEstimator, - GasEstimatorImpl, PrecheckSettings, UserOperationOptionalGas, -}; -use rundler_types::{ - chain::ChainSpec, - contracts::v0_6::i_entry_point::{ - IEntryPointCalls, UserOperationEventFilter, UserOperationRevertReasonFilter, - }, - UserOperation, -}; -use rundler_utils::{eth::log_to_raw_log, log::LogOnError}; +use futures_util::future; +use rundler_types::{chain::ChainSpec, pool::Pool, UserOperationOptionalGas, UserOperationVariant}; +use rundler_utils::log::LogOnError; use tracing::Level; -use super::error::{EthResult, EthRpcError, ExecutionRevertedWithBytesData}; -use crate::types::{RichUserOperation, RpcUserOperation, UserOperationReceipt}; +use super::{ + error::{EthResult, EthRpcError}, + router::EntryPointRouter, +}; +use crate::types::{RpcGasEstimate, RpcUserOperationByHash, RpcUserOperationReceipt}; /// Settings for the `eth_` API #[derive(Copy, Clone, Debug)] @@ -62,103 +44,33 @@ impl Settings { } } -#[derive(Debug)] -struct EntryPointContext { - gas_estimator: GasEstimatorImpl, +pub(crate) struct EthApi

{ + pub(crate) chain_spec: ChainSpec, + pool: P, + router: EntryPointRouter, } -impl EntryPointContext +impl

EthApi

where - P: Provider, - E: EntryPoint, + P: Pool, { - fn new( - chain_spec: ChainSpec, - provider: Arc

, - entry_point: E, - estimation_settings: EstimationSettings, - fee_estimator: FeeEstimator

, - ) -> Self { - let gas_estimator = GasEstimatorImpl::new( - chain_spec, - provider, - entry_point, - estimation_settings, - fee_estimator, - ); - Self { gas_estimator } - } -} - -#[derive(Debug)] -pub(crate) struct EthApi { - contexts_by_entry_point: HashMap>, - provider: Arc

, - chain_spec: ChainSpec, - pool: PS, - settings: Settings, -} - -impl EthApi -where - P: Provider, - E: EntryPoint, - PS: PoolServer, -{ - pub(crate) fn new( - chain_spec: ChainSpec, - provider: Arc

, - entry_points: Vec, - pool: PS, - settings: Settings, - estimation_settings: EstimationSettings, - precheck_settings: PrecheckSettings, - ) -> Self - where - E: Clone, - { - let contexts_by_entry_point = entry_points - .into_iter() - .map(|entry_point| { - ( - entry_point.address(), - EntryPointContext::new( - chain_spec.clone(), - Arc::clone(&provider), - entry_point, - estimation_settings, - FeeEstimator::new( - &chain_spec, - Arc::clone(&provider), - precheck_settings.priority_fee_mode, - precheck_settings.bundle_priority_fee_overhead_percent, - ), - ), - ) - }) - .collect(); - + pub(crate) fn new(chain_spec: ChainSpec, router: EntryPointRouter, pool: P) -> Self { Self { - settings, - contexts_by_entry_point, - provider, - chain_spec, + router, pool, + chain_spec, } } pub(crate) async fn send_user_operation( &self, - op: RpcUserOperation, + op: UserOperationVariant, entry_point: Address, ) -> EthResult { - if !self.contexts_by_entry_point.contains_key(&entry_point) { - return Err(EthRpcError::InvalidParams( - "supplied entry point addr is not a known entry point".to_string(), - )); - } + self.router.check_and_get_route(&entry_point, &op)?; + self.pool - .add_op(entry_point, op.into()) + .add_op(entry_point, op) .await .map_err(EthRpcError::from) .log_on_error_level(Level::DEBUG, "failed to add op to the mempool") @@ -166,142 +78,63 @@ where pub(crate) async fn estimate_user_operation_gas( &self, - op: UserOperationOptionalGas, + uo: UserOperationOptionalGas, entry_point: Address, state_override: Option, - ) -> EthResult { - let context = self - .contexts_by_entry_point - .get(&entry_point) - .ok_or_else(|| { - EthRpcError::InvalidParams( - "supplied entry_point address is not a known entry point".to_string(), - ) - })?; - - let result = context - .gas_estimator - .estimate_op_gas(op, state_override.unwrap_or_default()) - .await; - match result { - Ok(estimate) => Ok(estimate), - Err(GasEstimationError::RevertInValidation(message)) => { - Err(EthRpcError::EntryPointValidationRejected(message))? - } - Err(GasEstimationError::RevertInCallWithMessage(message)) => { - Err(EthRpcError::ExecutionReverted(message))? - } - Err(GasEstimationError::RevertInCallWithBytes(b)) => { - Err(EthRpcError::ExecutionRevertedWithBytes( - ExecutionRevertedWithBytesData { revert_data: b }, - ))? - } - Err(GasEstimationError::Other(error)) => Err(error)?, - } + ) -> EthResult { + self.router + .estimate_gas(&entry_point, uo, state_override) + .await } pub(crate) async fn get_user_operation_by_hash( &self, hash: H256, - ) -> EthResult> { + ) -> EthResult> { if hash == H256::zero() { return Err(EthRpcError::InvalidParams( "Missing/invalid userOpHash".to_string(), )); } - // check for the user operation both in the pool and mined on chain - let mined_fut = self.get_mined_user_operation_by_hash(hash); - let pending_fut = self.get_pending_user_operation_by_hash(hash); - let (mined, pending) = tokio::join!(mined_fut, pending_fut); - - // mined takes precedence over pending - if let Ok(Some(mined)) = mined { - Ok(Some(mined)) - } else if let Ok(Some(pending)) = pending { - Ok(Some(pending)) - } else if mined.is_err() || pending.is_err() { - // if either futures errored, and the UO was not found, return the errors - Err(EthRpcError::Internal(anyhow::anyhow!( - "error fetching user operation by hash: mined: {:?}, pending: {:?}", - mined.err().map(|e| e.to_string()).unwrap_or_default(), - pending.err().map(|e| e.to_string()).unwrap_or_default(), - ))) - } else { - // not found in either pool or mined - Ok(None) + // check both entry points and pending for the user operation event + #[allow(clippy::type_complexity)] + let mut futs: Vec< + Pin>> + Send>>, + > = vec![]; + + for ep in self.router.entry_points() { + futs.push(Box::pin(self.router.get_mined_by_hash(ep, hash))); } + futs.push(Box::pin(self.get_pending_user_operation_by_hash(hash))); + + let results = future::try_join_all(futs).await?; + Ok(results.into_iter().find_map(|x| x)) } pub(crate) async fn get_user_operation_receipt( &self, hash: H256, - ) -> EthResult> { + ) -> EthResult> { if hash == H256::zero() { return Err(EthRpcError::InvalidParams( "Missing/invalid userOpHash".to_string(), )); } - // Get event associated with hash (need to check all entry point addresses associated with this API) - let log = self - .get_user_operation_event_by_hash(hash) - .await - .context("should have fetched user ops by hash")?; - - let Some(log) = log else { return Ok(None) }; - let entry_point = log.address; + let futs = self + .router + .entry_points() + .map(|ep| self.router.get_receipt(ep, hash)); - // If the event is found, get the TX receipt - let tx_hash = log.transaction_hash.context("tx_hash should be present")?; - let tx_receipt = self - .provider - .get_transaction_receipt(tx_hash) - .await - .context("should have fetched tx receipt")? - .context("Failed to fetch tx receipt")?; - - // Return null if the tx isn't included in the block yet - if tx_receipt.block_hash.is_none() && tx_receipt.block_number.is_none() { - return Ok(None); - } - - // Filter receipt logs to match just those belonging to the user op - let filtered_logs = - EthApi::::filter_receipt_logs_matching_user_op(&log, &tx_receipt) - .context("should have found receipt logs matching user op")?; - - // Decode log and find failure reason if not success - let uo_event = self - .decode_user_operation_event(log) - .context("should have decoded user operation event")?; - let reason: String = if uo_event.success { - "".to_owned() - } else { - EthApi::::get_user_operation_failure_reason(&tx_receipt.logs, hash) - .context("should have found revert reason if tx wasn't successful")? - .unwrap_or_default() - }; - - Ok(Some(UserOperationReceipt { - 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, - receipt: tx_receipt, - reason, - })) + let results = future::try_join_all(futs).await?; + Ok(results.into_iter().find_map(|x| x)) } pub(crate) async fn supported_entry_points(&self) -> EthResult> { Ok(self - .contexts_by_entry_point - .keys() + .router + .entry_points() .map(|ep| to_checksum(ep, None)) .collect()) } @@ -310,74 +143,17 @@ where Ok(self.chain_spec.id.into()) } - async fn get_mined_user_operation_by_hash( - &self, - hash: H256, - ) -> EthResult> { - // Get event associated with hash (need to check all entry point addresses associated with this API) - let event = self - .get_user_operation_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.contexts_by_entry_point.contains_key(&to) { - self.get_user_operations_from_tx_data(tx.input) - .into_iter() - .find(|op| op.op_hash(to, self.chain_spec.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(RichUserOperation { - user_operation: 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_pending_user_operation_by_hash( &self, hash: H256, - ) -> EthResult> { + ) -> EthResult> { let res = self .pool .get_op_by_hash(hash) .await .map_err(EthRpcError::from)?; - Ok(res.map(|op| RichUserOperation { + + Ok(res.map(|op| RpcUserOperationByHash { user_operation: op.uo.into(), entry_point: op.entry_point.into(), block_number: None, @@ -385,347 +161,51 @@ where transaction_hash: None, })) } - - async fn get_user_operation_event_by_hash(&self, hash: H256) -> EthResult> { - let to_block = self.provider.get_block_number().await?; - - let from_block = match self.settings.user_operation_event_block_distance { - Some(distance) => to_block.saturating_sub(distance), - None => 0, - }; - - let filter = Filter::new() - .address::>( - self.contexts_by_entry_point - .iter() - .map(|ep| *ep.0) - .collect(), - ) - .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 { - 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, - IEntryPointCalls::HandleAggregatedOps(handle_aggregated_ops_call) => { - handle_aggregated_ops_call - .ops_per_aggregator - .into_iter() - .flat_map(|ops| ops.user_ops) - .collect() - } - _ => vec![], - } - } - - fn decode_user_operation_event(&self, log: Log) -> EthResult { - Ok(UserOperationEventFilter::decode_log(&log_to_raw_log(log)) - .context("log should be a user operation event")?) - } - - /// This method takes a user operation event and a transaction receipt and filters out all the logs - /// relevant to the user operation. Since there are potentially many user operations in a transaction, - /// we want to find all the logs (including the user operation event itself) that are sandwiched between - /// ours and the one before it that wasn't ours. - /// eg. reference_log: UserOp(hash_moldy) logs: \[...OtherLogs, UserOp(hash1), ...OtherLogs, UserOp(hash_moldy), ...OtherLogs\] - /// -> logs: logs\[(idx_of_UserOp(hash1) + 1)..=idx_of_UserOp(hash_moldy)\] - /// - /// topic\[0\] == event name - /// topic\[1\] == user operation hash - /// - /// NOTE: we can't convert just decode all the logs as user operations and filter because we still want all the other log types - /// - fn filter_receipt_logs_matching_user_op( - reference_log: &Log, - tx_receipt: &TransactionReceipt, - ) -> EthResult> { - let mut start_idx = 0; - let mut end_idx = tx_receipt.logs.len() - 1; - let logs = &tx_receipt.logs; - - let is_ref_user_op = |log: &Log| { - log.topics[0] == reference_log.topics[0] - && log.topics[1] == reference_log.topics[1] - && log.address == reference_log.address - }; - - let is_user_op_event = |log: &Log| log.topics[0] == reference_log.topics[0]; - - let mut i = 0; - while i < logs.len() { - if i < end_idx && is_user_op_event(&logs[i]) && !is_ref_user_op(&logs[i]) { - start_idx = i; - } else if is_ref_user_op(&logs[i]) { - end_idx = i; - } - - i += 1; - } - - if !is_ref_user_op(&logs[end_idx]) { - return Err(EthRpcError::Internal(anyhow::anyhow!( - "fatal: no user ops found in tx receipt ({start_idx},{end_idx})" - ))); - } - - let start_idx = if start_idx == 0 { 0 } else { start_idx + 1 }; - Ok(logs[start_idx..=end_idx].to_vec()) - } - - fn get_user_operation_failure_reason( - logs: &[Log], - user_op_hash: H256, - ) -> EthResult> { - let revert_reason_evt: Option = logs - .iter() - .filter(|l| l.topics.len() > 1 && l.topics[1] == user_op_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())) - } - - /// 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, - ) -> EthResult> { - // 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| self.contexts_by_entry_point.contains_key(to)) - { - // 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.op_hash(*to, self.chain_spec.id) == user_op_hash) - { - return Ok(Some(uo)); - } - } else if let Some(calls) = call_frame.calls { - frame_queue.extend(calls) - } - } - - Ok(None) - } } #[cfg(test)] mod tests { + use std::sync::Arc; + use ethers::{ abi::AbiEncode, - types::{Log, Transaction, TransactionReceipt}, - utils::keccak256, + types::{Bytes, Log, Transaction}, }; use mockall::predicate::eq; - use rundler_pool::{MockPoolServer, PoolOperation}; - use rundler_provider::{MockEntryPoint, MockProvider}; - use rundler_sim::PriorityFeeMode; - use rundler_types::contracts::v0_6::i_entry_point::HandleOpsCall; + use rundler_provider::{EntryPoint, MockEntryPointV0_6, MockProvider}; + use rundler_sim::{EstimationSettings, FeeEstimator, GasEstimatorV0_6, PriorityFeeMode}; + use rundler_types::{ + contracts::v0_6::i_entry_point::{HandleOpsCall, IEntryPointCalls}, + pool::{MockPool, PoolOperation}, + v0_6::UserOperation, + EntityInfos, UserOperation as UserOperationTrait, ValidTimeRange, + }; use super::*; - - const UO_OP_TOPIC: &str = "user-op-event-topic"; - - #[test] - fn test_filter_receipt_logs_when_at_beginning_of_list() { - let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); - let receipt = given_receipt(vec![ - given_log("other-topic", "some-hash"), - reference_log.clone(), - given_log(UO_OP_TOPIC, "other-hash"), - given_log(UO_OP_TOPIC, "another-hash"), - ]); - - let result = - EthApi::::filter_receipt_logs_matching_user_op( - &reference_log, - &receipt, - ); - - assert!(result.is_ok(), "{}", result.unwrap_err()); - let result = result.unwrap(); - assert_eq!(result, receipt.logs[0..=1]); - } - - #[test] - fn test_filter_receipt_logs_when_in_middle_of_list() { - let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); - let receipt = given_receipt(vec![ - given_log("other-topic", "some-hash"), - given_log(UO_OP_TOPIC, "other-hash"), - given_log("another-topic", "some-hash"), - given_log("another-topic-2", "some-hash"), - reference_log.clone(), - given_log(UO_OP_TOPIC, "another-hash"), - ]); - - let result = - EthApi::::filter_receipt_logs_matching_user_op( - &reference_log, - &receipt, - ); - - assert!(result.is_ok(), "{}", result.unwrap_err()); - let result = result.unwrap(); - assert_eq!(result, receipt.logs[2..=4]); - } - - #[test] - fn test_filter_receipt_logs_when_at_end_of_list() { - let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); - let receipt = given_receipt(vec![ - given_log("other-topic", "some-hash"), - given_log(UO_OP_TOPIC, "other-hash"), - given_log(UO_OP_TOPIC, "another-hash"), - given_log("another-topic", "some-hash"), - given_log("another-topic-2", "some-hash"), - reference_log.clone(), - ]); - - let result = - EthApi::::filter_receipt_logs_matching_user_op( - &reference_log, - &receipt, - ); - - assert!(result.is_ok(), "{}", result.unwrap_err()); - let result = result.unwrap(); - assert_eq!(result, receipt.logs[3..=5]); - } - - #[test] - fn test_filter_receipt_logs_skips_event_from_different_address() { - let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); - let mut reference_log_w_different_address = reference_log.clone(); - reference_log_w_different_address.address = Address::from_low_u64_be(0x1234); - - let receipt = given_receipt(vec![ - given_log("other-topic", "some-hash"), - given_log(UO_OP_TOPIC, "other-hash"), - given_log(UO_OP_TOPIC, "another-hash"), - reference_log_w_different_address, - given_log("another-topic", "some-hash"), - given_log("another-topic-2", "some-hash"), - reference_log.clone(), - ]); - - let result = - EthApi::::filter_receipt_logs_matching_user_op( - &reference_log, - &receipt, - ); - - assert!(result.is_ok(), "{}", result.unwrap_err()); - let result = result.unwrap(); - assert_eq!(result, receipt.logs[4..=6]); - } - - #[test] - fn test_filter_receipt_logs_includes_multiple_sets_of_ref_uo() { - let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); - - let receipt = given_receipt(vec![ - given_log("other-topic", "some-hash"), - given_log(UO_OP_TOPIC, "other-hash"), - given_log("other-topic-2", "another-hash"), - reference_log.clone(), - given_log("another-topic", "some-hash"), - given_log("another-topic-2", "some-hash"), - reference_log.clone(), - given_log(UO_OP_TOPIC, "other-hash"), - ]); - - let result = - EthApi::::filter_receipt_logs_matching_user_op( - &reference_log, - &receipt, - ); - - assert!(result.is_ok(), "{}", result.unwrap_err()); - let result = result.unwrap(); - assert_eq!(result, receipt.logs[2..=6]); - } - - #[test] - fn test_filter_receipt_logs_when_not_found() { - let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); - let receipt = given_receipt(vec![ - given_log("other-topic", "some-hash"), - given_log(UO_OP_TOPIC, "other-hash"), - given_log(UO_OP_TOPIC, "another-hash"), - given_log("another-topic", "some-hash"), - given_log("another-topic-2", "some-hash"), - ]); - - let result = - EthApi::::filter_receipt_logs_matching_user_op( - &reference_log, - &receipt, - ); - - assert!(result.is_err(), "{:?}", result.unwrap()); - } + use crate::eth::{ + EntryPointRouteImpl, EntryPointRouterBuilder, UserOperationEventProviderV0_6, + }; #[tokio::test] async fn test_get_user_op_by_hash_pending() { let ep = Address::random(); let uo = UserOperation::default(); - let hash = uo.op_hash(ep, 1); + let hash = uo.hash(ep, 1); let po = PoolOperation { - uo: uo.clone(), + uo: uo.clone().into(), entry_point: ep, - ..Default::default() + aggregator: None, + valid_time_range: ValidTimeRange::default(), + expected_code_hash: H256::random(), + sim_block_hash: H256::random(), + sim_block_number: 1000, + entities_needing_stake: vec![], + account_is_staked: false, + entity_infos: EntityInfos::default(), }; - let mut pool = MockPoolServer::default(); + let mut pool = MockPool::default(); pool.expect_get_op_by_hash() .with(eq(hash)) .times(1) @@ -735,13 +215,13 @@ mod tests { provider.expect_get_logs().returning(move |_| Ok(vec![])); provider.expect_get_block_number().returning(|| Ok(1000)); - let mut entry_point = MockEntryPoint::default(); + let mut entry_point = MockEntryPointV0_6::default(); entry_point.expect_address().returning(move || ep); let api = create_api(provider, entry_point, pool); let res = api.get_user_operation_by_hash(hash).await.unwrap(); - let ro = RichUserOperation { - user_operation: uo.into(), + let ro = RpcUserOperationByHash { + user_operation: UserOperationVariant::from(uo).into(), entry_point: ep.into(), block_number: None, block_hash: None, @@ -754,11 +234,11 @@ mod tests { async fn test_get_user_op_by_hash_mined() { let ep = Address::random(); let uo = UserOperation::default(); - let hash = uo.op_hash(ep, 1); + let hash = uo.hash(ep, 1); let block_number = 1000; let block_hash = H256::random(); - let mut pool = MockPoolServer::default(); + let mut pool = MockPool::default(); pool.expect_get_op_by_hash() .with(eq(hash)) .returning(move |_| Ok(None)); @@ -794,13 +274,13 @@ mod tests { .with(eq(tx_hash)) .returning(move |_| Ok(Some(tx.clone()))); - let mut entry_point = MockEntryPoint::default(); + let mut entry_point = MockEntryPointV0_6::default(); entry_point.expect_address().returning(move || ep); let api = create_api(provider, entry_point, pool); let res = api.get_user_operation_by_hash(hash).await.unwrap(); - let ro = RichUserOperation { - user_operation: uo.into(), + let ro = RpcUserOperationByHash { + user_operation: UserOperationVariant::from(uo).into(), entry_point: ep.into(), block_number: Some(block_number.into()), block_hash: Some(block_hash), @@ -813,9 +293,9 @@ mod tests { async fn test_get_user_op_by_hash_not_found() { let ep = Address::random(); let uo = UserOperation::default(); - let hash = uo.op_hash(ep, 1); + let hash = uo.hash(ep, 1); - let mut pool = MockPoolServer::default(); + let mut pool = MockPool::default(); pool.expect_get_op_by_hash() .with(eq(hash)) .times(1) @@ -825,7 +305,7 @@ mod tests { provider.expect_get_logs().returning(move |_| Ok(vec![])); provider.expect_get_block_number().returning(|| Ok(1000)); - let mut entry_point = MockEntryPoint::default(); + let mut entry_point = MockEntryPointV0_6::default(); entry_point.expect_address().returning(move || ep); let api = create_api(provider, entry_point, pool); @@ -833,60 +313,53 @@ mod tests { assert_eq!(res, None); } - fn given_log(topic_0: &str, topic_1: &str) -> Log { - Log { - topics: vec![ - keccak256(topic_0.as_bytes()).into(), - keccak256(topic_1.as_bytes()).into(), - ], - ..Default::default() - } - } - - fn given_receipt(logs: Vec) -> TransactionReceipt { - TransactionReceipt { - logs, - ..Default::default() - } - } - fn create_api( provider: MockProvider, - ep: MockEntryPoint, - pool: MockPoolServer, - ) -> EthApi { - let mut contexts_by_entry_point = HashMap::new(); + ep: MockEntryPointV0_6, + pool: MockPool, + ) -> EthApi { + let ep = Arc::new(ep); let provider = Arc::new(provider); let chain_spec = ChainSpec { id: 1, ..Default::default() }; - contexts_by_entry_point.insert( - ep.address(), - EntryPointContext::new( - chain_spec.clone(), + + let gas_estimator = GasEstimatorV0_6::new( + chain_spec.clone(), + provider.clone(), + ep.clone(), + EstimationSettings { + max_verification_gas: 1_000_000, + max_call_gas: 1_000_000, + max_simulate_handle_ops_gas: 1_000_000, + verification_estimation_gas_fee: 1_000_000_000_000, + }, + FeeEstimator::new( + &chain_spec, Arc::clone(&provider), - ep, - EstimationSettings { - max_verification_gas: 1_000_000, - max_call_gas: 1_000_000, - max_simulate_handle_ops_gas: 1_000_000, - verification_estimation_gas_fee: 1_000_000_000_000, - }, - FeeEstimator::new( - &chain_spec, - Arc::clone(&provider), - PriorityFeeMode::BaseFeePercent(0), - 0, - ), + PriorityFeeMode::BaseFeePercent(0), + 0, ), ); + + let router = EntryPointRouterBuilder::default() + .v0_6(EntryPointRouteImpl::new( + ep.clone(), + gas_estimator, + UserOperationEventProviderV0_6::new( + chain_spec.id, + ep.address(), + provider.clone(), + None, + ), + )) + .build(); + EthApi { - contexts_by_entry_point, - provider, + router, chain_spec, pool, - settings: Settings::new(None), } } } diff --git a/crates/rpc/src/eth/error.rs b/crates/rpc/src/eth/error.rs index 25bd4e4a2..2a980f3bb 100644 --- a/crates/rpc/src/eth/error.rs +++ b/crates/rpc/src/eth/error.rs @@ -11,15 +11,19 @@ // 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}, ErrorObjectOwned, }; -use rundler_pool::{MempoolError, PoolServerError}; use rundler_provider::ProviderError; -use rundler_sim::{PrecheckViolation, SimulationViolation}; -use rundler_types::{Entity, EntityType, Timestamp}; +use rundler_sim::GasEstimationError; +use rundler_types::{ + pool::{MempoolError, PoolError, PrecheckViolation, SimulationViolation}, + Entity, EntityType, Timestamp, ValidationRevert, +}; use serde::Serialize; use crate::error::{rpc_err, rpc_err_with_data}; @@ -115,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")] @@ -172,6 +178,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 { @@ -199,14 +224,14 @@ pub struct ExecutionRevertedWithBytesData { pub revert_data: Bytes, } -impl From for EthRpcError { - fn from(value: PoolServerError) -> Self { +impl From for EthRpcError { + fn from(value: PoolError) -> Self { match value { - PoolServerError::MempoolError(e) => e.into(), - PoolServerError::UnexpectedResponse => { + PoolError::MempoolError(e) => e.into(), + PoolError::UnexpectedResponse => { EthRpcError::Internal(anyhow::anyhow!("unexpected response from pool server")) } - PoolServerError::Other(e) => EthRpcError::Internal(e), + PoolError::Other(e) => EthRpcError::Internal(e), } } } @@ -304,6 +329,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), } } @@ -353,6 +395,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), } } @@ -373,3 +416,22 @@ impl From for EthRpcError { EthRpcError::Internal(anyhow::anyhow!("provider error: {e:?}")) } } + +impl From for EthRpcError { + fn from(e: GasEstimationError) -> Self { + match e { + GasEstimationError::RevertInValidation(message) => { + EthRpcError::EntryPointValidationRejected(message) + } + GasEstimationError::RevertInCallWithMessage(message) => { + EthRpcError::ExecutionReverted(message) + } + GasEstimationError::RevertInCallWithBytes(b) => { + EthRpcError::ExecutionRevertedWithBytes(ExecutionRevertedWithBytesData { + revert_data: b, + }) + } + GasEstimationError::Other(error) => EthRpcError::Internal(error), + } + } +} diff --git a/crates/rpc/src/eth/events/common.rs b/crates/rpc/src/eth/events/common.rs new file mode 100644 index 000000000..29c2edc84 --- /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, marker::PhantomData, 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: 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: 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 new file mode 100644 index 000000000..eba1f184f --- /dev/null +++ b/crates/rpc/src/eth/events/mod.rs @@ -0,0 +1,222 @@ +// 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 anyhow::bail; +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 { + async fn get_mined_by_hash(&self, hash: H256) + -> anyhow::Result>; + + async fn get_receipt(&self, hash: H256) -> anyhow::Result>; +} + +// This method takes a user operation event and a transaction receipt and filters out all the logs +// relevant to the user operation. Since there are potentially many user operations in a transaction, +// we want to find all the logs (including the user operation event itself) that are sandwiched between +// ours and the one before it that wasn't ours. +// eg. reference_log: UserOp(hash_moldy) logs: \[...OtherLogs, UserOp(hash1), ...OtherLogs, UserOp(hash_moldy), ...OtherLogs\] +// -> logs: logs\[(idx_of_UserOp(hash1) + 1)..=idx_of_UserOp(hash_moldy)\] +// +// topic\[0\] == event name +// topic\[1\] == user operation hash +// +// NOTE: we can't convert just decode all the logs as user operations and filter because we still want all the other log types +// +fn filter_receipt_logs_matching_user_op( + reference_log: &Log, + tx_receipt: &TransactionReceipt, +) -> anyhow::Result> { + let mut start_idx = 0; + let mut end_idx = tx_receipt.logs.len() - 1; + let logs = &tx_receipt.logs; + + let is_ref_user_op = |log: &Log| { + log.topics[0] == reference_log.topics[0] + && log.topics[1] == reference_log.topics[1] + && log.address == reference_log.address + }; + + let is_user_op_event = |log: &Log| log.topics[0] == reference_log.topics[0]; + + let mut i = 0; + while i < logs.len() { + if i < end_idx && is_user_op_event(&logs[i]) && !is_ref_user_op(&logs[i]) { + start_idx = i; + } else if is_ref_user_op(&logs[i]) { + end_idx = i; + } + + i += 1; + } + + if !is_ref_user_op(&logs[end_idx]) { + bail!("fatal: no user ops found in tx receipt ({start_idx},{end_idx})"); + } + + let start_idx = if start_idx == 0 { 0 } else { start_idx + 1 }; + Ok(logs[start_idx..=end_idx].to_vec()) +} + +#[cfg(test)] +mod tests { + + use ethers::{types::Address, utils::keccak256}; + + use super::*; + + const UO_OP_TOPIC: &str = "user-op-event-topic"; + + #[test] + fn test_filter_receipt_logs_when_at_beginning_of_list() { + let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); + let receipt = given_receipt(vec![ + given_log("other-topic", "some-hash"), + reference_log.clone(), + given_log(UO_OP_TOPIC, "other-hash"), + given_log(UO_OP_TOPIC, "another-hash"), + ]); + + let result = filter_receipt_logs_matching_user_op(&reference_log, &receipt); + + assert!(result.is_ok(), "{}", result.unwrap_err()); + let result = result.unwrap(); + assert_eq!(result, receipt.logs[0..=1]); + } + + #[test] + fn test_filter_receipt_logs_when_in_middle_of_list() { + let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); + let receipt = given_receipt(vec![ + given_log("other-topic", "some-hash"), + given_log(UO_OP_TOPIC, "other-hash"), + given_log("another-topic", "some-hash"), + given_log("another-topic-2", "some-hash"), + reference_log.clone(), + given_log(UO_OP_TOPIC, "another-hash"), + ]); + + let result = filter_receipt_logs_matching_user_op(&reference_log, &receipt); + + assert!(result.is_ok(), "{}", result.unwrap_err()); + let result = result.unwrap(); + assert_eq!(result, receipt.logs[2..=4]); + } + + #[test] + fn test_filter_receipt_logs_when_at_end_of_list() { + let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); + let receipt = given_receipt(vec![ + given_log("other-topic", "some-hash"), + given_log(UO_OP_TOPIC, "other-hash"), + given_log(UO_OP_TOPIC, "another-hash"), + given_log("another-topic", "some-hash"), + given_log("another-topic-2", "some-hash"), + reference_log.clone(), + ]); + + let result = filter_receipt_logs_matching_user_op(&reference_log, &receipt); + + assert!(result.is_ok(), "{}", result.unwrap_err()); + let result = result.unwrap(); + assert_eq!(result, receipt.logs[3..=5]); + } + + #[test] + fn test_filter_receipt_logs_skips_event_from_different_address() { + let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); + let mut reference_log_w_different_address = reference_log.clone(); + reference_log_w_different_address.address = Address::from_low_u64_be(0x1234); + + let receipt = given_receipt(vec![ + given_log("other-topic", "some-hash"), + given_log(UO_OP_TOPIC, "other-hash"), + given_log(UO_OP_TOPIC, "another-hash"), + reference_log_w_different_address, + given_log("another-topic", "some-hash"), + given_log("another-topic-2", "some-hash"), + reference_log.clone(), + ]); + + let result = filter_receipt_logs_matching_user_op(&reference_log, &receipt); + + assert!(result.is_ok(), "{}", result.unwrap_err()); + let result = result.unwrap(); + assert_eq!(result, receipt.logs[4..=6]); + } + + #[test] + fn test_filter_receipt_logs_includes_multiple_sets_of_ref_uo() { + let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); + + let receipt = given_receipt(vec![ + given_log("other-topic", "some-hash"), + given_log(UO_OP_TOPIC, "other-hash"), + given_log("other-topic-2", "another-hash"), + reference_log.clone(), + given_log("another-topic", "some-hash"), + given_log("another-topic-2", "some-hash"), + reference_log.clone(), + given_log(UO_OP_TOPIC, "other-hash"), + ]); + + let result = filter_receipt_logs_matching_user_op(&reference_log, &receipt); + + assert!(result.is_ok(), "{}", result.unwrap_err()); + let result = result.unwrap(); + assert_eq!(result, receipt.logs[2..=6]); + } + + #[test] + fn test_filter_receipt_logs_when_not_found() { + let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); + let receipt = given_receipt(vec![ + given_log("other-topic", "some-hash"), + given_log(UO_OP_TOPIC, "other-hash"), + given_log(UO_OP_TOPIC, "another-hash"), + given_log("another-topic", "some-hash"), + given_log("another-topic-2", "some-hash"), + ]); + + let result = filter_receipt_logs_matching_user_op(&reference_log, &receipt); + + assert!(result.is_err(), "{:?}", result.unwrap()); + } + + fn given_log(topic_0: &str, topic_1: &str) -> Log { + Log { + topics: vec![ + keccak256(topic_0.as_bytes()).into(), + keccak256(topic_1.as_bytes()).into(), + ], + ..Default::default() + } + } + + fn given_receipt(logs: Vec) -> TransactionReceipt { + TransactionReceipt { + logs, + ..Default::default() + } + } +} diff --git a/crates/rpc/src/eth/events/v0_6.rs b/crates/rpc/src/eth/events/v0_6.rs new file mode 100644 index 000000000..803e436c4 --- /dev/null +++ b/crates/rpc/src/eth/events/v0_6.rs @@ -0,0 +1,104 @@ +// 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 ethers::{ + abi::{AbiDecode, RawLog}, + prelude::EthEvent, + types::{Address, Bytes, Log, TransactionReceipt, H256}, +}; +use rundler_types::{ + contracts::v0_6::i_entry_point::{ + IEntryPointCalls, UserOperationEventFilter, UserOperationRevertReasonFilter, + }, + v0_6::UserOperation, +}; + +use super::common::{EntryPointFilters, UserOperationEventProviderImpl}; +use crate::types::RpcUserOperationReceipt; + +pub(crate) type UserOperationEventProviderV0_6

= + UserOperationEventProviderImpl; + +pub(crate) struct EntryPointFiltersV0_6; + +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 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, + } + } + + 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, + IEntryPointCalls::HandleAggregatedOps(handle_aggregated_ops_call) => { + handle_aggregated_ops_call + .ops_per_aggregator + .into_iter() + .flat_map(|ops| ops.user_ops) + .collect() + } + _ => vec![], + } + } +} diff --git a/crates/rpc/src/eth/events/v0_7.rs b/crates/rpc/src/eth/events/v0_7.rs new file mode 100644 index 000000000..ffa1fbeab --- /dev/null +++ b/crates/rpc/src/eth/events/v0_7.rs @@ -0,0 +1,112 @@ +// 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 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::common::{EntryPointFilters, UserOperationEventProviderImpl}; +use crate::types::RpcUserOperationReceipt; + +pub(crate) type UserOperationEventProviderV0_7

= + UserOperationEventProviderImpl; + +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, + } + } + + 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 3cb50e52b..d0751050a 100644 --- a/crates/rpc/src/eth/mod.rs +++ b/crates/rpc/src/eth/mod.rs @@ -15,15 +15,22 @@ mod api; pub(crate) use api::EthApi; pub use api::Settings as EthApiSettings; +mod router; +pub(crate) use router::*; + mod error; pub(crate) use error::EthRpcError; +mod events; +pub(crate) use events::{UserOperationEventProviderV0_6, UserOperationEventProviderV0_7}; mod server; use ethers::types::{spoof, Address, H256, U64}; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; -use rundler_sim::{GasEstimate, UserOperationOptionalGas}; -use crate::types::{RichUserOperation, RpcUserOperation, UserOperationReceipt}; +use crate::types::{ + RpcGasEstimate, RpcUserOperation, RpcUserOperationByHash, RpcUserOperationOptionalGas, + RpcUserOperationReceipt, +}; /// Eth API #[rpc(client, server, namespace = "eth")] @@ -41,21 +48,24 @@ pub trait EthApi { #[method(name = "estimateUserOperationGas")] async fn estimate_user_operation_gas( &self, - op: UserOperationOptionalGas, + op: RpcUserOperationOptionalGas, entry_point: Address, state_override: Option, - ) -> RpcResult; + ) -> RpcResult; /// Returns the user operation with the given hash. #[method(name = "getUserOperationByHash")] - async fn get_user_operation_by_hash(&self, hash: H256) -> RpcResult>; + async fn get_user_operation_by_hash( + &self, + hash: H256, + ) -> RpcResult>; /// Returns the user operation receipt with the given hash. #[method(name = "getUserOperationReceipt")] async fn get_user_operation_receipt( &self, hash: H256, - ) -> RpcResult>; + ) -> RpcResult>; /// Returns the supported entry points addresses #[method(name = "supportedEntryPoints")] diff --git a/crates/rpc/src/eth/router.rs b/crates/rpc/src/eth/router.rs new file mode 100644 index 000000000..b6a223ffc --- /dev/null +++ b/crates/rpc/src/eth/router.rs @@ -0,0 +1,306 @@ +// 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::{fmt::Debug, marker::PhantomData, sync::Arc}; + +use ethers::types::{spoof, Address, H256}; +use rundler_provider::{EntryPoint, SimulationProvider}; +use rundler_sim::{GasEstimationError, GasEstimator}; +use rundler_types::{ + EntryPointVersion, GasEstimate, UserOperation, UserOperationOptionalGas, UserOperationVariant, +}; + +use super::events::UserOperationEventProvider; +use crate::{ + eth::{error::EthResult, EthRpcError}, + types::{RpcGasEstimate, RpcUserOperationByHash, RpcUserOperationReceipt}, +}; + +#[derive(Default)] +pub(crate) struct EntryPointRouterBuilder { + entry_points: Vec

, + v0_6: Option<(Address, Arc)>, + v0_7: Option<(Address, Arc)>, +} + +impl EntryPointRouterBuilder { + pub(crate) fn v0_6(mut self, route: R) -> Self + where + R: EntryPointRoute, + { + if route.version() != EntryPointVersion::V0_6 { + panic!( + "Invalid entry point version for route: {:?}", + route.version() + ); + } + + self.entry_points.push(route.address()); + self.v0_6 = Some((route.address(), Arc::new(route))); + self + } + + pub(crate) fn v0_7(mut self, route: R) -> Self + where + R: EntryPointRoute, + { + if route.version() != EntryPointVersion::V0_7 { + panic!( + "Invalid entry point version for route: {:?}", + route.version() + ); + } + + self.entry_points.push(route.address()); + self.v0_7 = Some((route.address(), Arc::new(route))); + self + } + + pub(crate) fn build(self) -> EntryPointRouter { + EntryPointRouter { + entry_points: self.entry_points, + v0_6: self.v0_6, + v0_7: self.v0_7, + } + } +} + +#[derive(Clone)] +pub(crate) struct EntryPointRouter { + entry_points: Vec
, + v0_6: Option<(Address, Arc)>, + v0_7: Option<(Address, Arc)>, +} + +impl EntryPointRouter { + pub(crate) fn entry_points(&self) -> impl Iterator { + self.entry_points.iter() + } + + pub(crate) fn check_and_get_route( + &self, + entry_point: &Address, + uo: &UserOperationVariant, + ) -> EthResult<&Arc> { + match self.get_ep_version(entry_point)? { + EntryPointVersion::V0_6 => { + if !matches!(uo, UserOperationVariant::V0_6(_)) { + return Err(EthRpcError::InvalidParams(format!( + "Invalid user operation for entry point: {:?}", + entry_point + ))); + } + Ok(&self.v0_6.as_ref().unwrap().1) + } + EntryPointVersion::V0_7 => { + if !matches!(uo, UserOperationVariant::V0_7(_)) { + return Err(EthRpcError::InvalidParams(format!( + "Invalid user operation for entry point: {:?}", + entry_point + ))); + } + Ok(&self.v0_7.as_ref().unwrap().1) + } + EntryPointVersion::Unspecified => unreachable!("unspecified entry point version"), + } + } + + pub(crate) async fn get_mined_by_hash( + &self, + entry_point: &Address, + hash: H256, + ) -> EthResult> { + self.get_route(entry_point)? + .get_mined_by_hash(hash) + .await + .map_err(Into::into) + } + + pub(crate) async fn get_receipt( + &self, + entry_point: &Address, + hash: H256, + ) -> EthResult> { + self.get_route(entry_point)? + .get_receipt(hash) + .await + .map_err(Into::into) + } + + pub(crate) async fn estimate_gas( + &self, + entry_point: &Address, + uo: UserOperationOptionalGas, + state_override: Option, + ) -> EthResult { + let route = match self.get_ep_version(entry_point)? { + EntryPointVersion::V0_6 => { + if !matches!(uo, UserOperationOptionalGas::V0_6(_)) { + return Err(EthRpcError::InvalidParams(format!( + "Invalid user operation for entry point: {:?}", + entry_point + ))); + } + &self.v0_6.as_ref().unwrap().1 + } + EntryPointVersion::V0_7 => { + if !matches!(uo, UserOperationOptionalGas::V0_7(_)) { + return Err(EthRpcError::InvalidParams(format!( + "Invalid user operation for entry point: {:?}", + entry_point + ))); + } + &self.v0_7.as_ref().unwrap().1 + } + EntryPointVersion::Unspecified => unreachable!("unspecified entry point version"), + }; + + let estimate = route.estimate_gas(uo, state_override).await?; + Ok(estimate.into()) + } + + pub(crate) async fn check_signature( + &self, + entry_point: &Address, + uo: UserOperationVariant, + max_verification_gas: u64, + ) -> EthResult { + self.check_and_get_route(entry_point, &uo)? + .check_signature(uo, max_verification_gas) + .await + .map_err(Into::into) + } + + fn get_ep_version(&self, entry_point: &Address) -> EthResult { + if let Some((addr, _)) = self.v0_6 { + if addr == *entry_point { + return Ok(EntryPointVersion::V0_6); + } + } + if let Some((addr, _)) = self.v0_7 { + if addr == *entry_point { + return Ok(EntryPointVersion::V0_7); + } + } + + Err(EthRpcError::InvalidParams(format!( + "No entry point found for address: {:?}", + entry_point + ))) + } + + fn get_route(&self, entry_point: &Address) -> EthResult<&Arc> { + let ep = self.get_ep_version(entry_point)?; + + match ep { + EntryPointVersion::V0_6 => Ok(&self.v0_6.as_ref().unwrap().1), + EntryPointVersion::V0_7 => Ok(&self.v0_7.as_ref().unwrap().1), + EntryPointVersion::Unspecified => unreachable!("unspecified entry point version"), + } + } +} + +#[async_trait::async_trait] +pub(crate) trait EntryPointRoute: Send + Sync + 'static { + fn version(&self) -> EntryPointVersion; + + fn address(&self) -> Address; + + async fn get_mined_by_hash(&self, hash: H256) + -> anyhow::Result>; + + async fn get_receipt(&self, hash: H256) -> anyhow::Result>; + + async fn estimate_gas( + &self, + uo: UserOperationOptionalGas, + state_override: Option, + ) -> Result; + + async fn check_signature( + &self, + uo: UserOperationVariant, + max_verification_gas: u64, + ) -> anyhow::Result; +} + +#[derive(Debug)] +pub(crate) struct EntryPointRouteImpl { + entry_point: E, + gas_estimator: G, + event_provider: EV, + _uo_type: PhantomData, +} + +#[async_trait::async_trait] +impl EntryPointRoute for EntryPointRouteImpl +where + UO: UserOperation + From, + E: EntryPoint + SimulationProvider, + G: GasEstimator, + G::UserOperationOptionalGas: From, + EV: UserOperationEventProvider, +{ + fn version(&self) -> EntryPointVersion { + UO::entry_point_version() + } + + fn address(&self) -> Address { + self.entry_point.address() + } + + async fn get_mined_by_hash( + &self, + hash: H256, + ) -> anyhow::Result> { + self.event_provider.get_mined_by_hash(hash).await + } + + async fn get_receipt(&self, hash: H256) -> anyhow::Result> { + self.event_provider.get_receipt(hash).await + } + + async fn estimate_gas( + &self, + uo: UserOperationOptionalGas, + state_override: Option, + ) -> Result { + self.gas_estimator + .estimate_op_gas(uo.into(), state_override.unwrap_or_default()) + .await + } + + async fn check_signature( + &self, + uo: UserOperationVariant, + max_verification_gas: u64, + ) -> anyhow::Result { + let output = self + .entry_point + .call_simulate_validation(uo.into(), max_verification_gas, None) + .await?; + + Ok(!output.return_info.account_sig_failed) + } +} + +impl EntryPointRouteImpl { + pub(crate) fn new(entry_point: E, gas_estimator: G, event_provider: EP) -> Self { + Self { + entry_point, + gas_estimator, + event_provider, + _uo_type: PhantomData, + } + } +} diff --git a/crates/rpc/src/eth/server.rs b/crates/rpc/src/eth/server.rs index 58a0d290d..6f5576f49 100644 --- a/crates/rpc/src/eth/server.rs +++ b/crates/rpc/src/eth/server.rs @@ -11,48 +11,57 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use async_trait::async_trait; use ethers::types::{spoof, Address, H256, U64}; use jsonrpsee::core::RpcResult; -use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, Provider}; -use rundler_sim::{GasEstimate, UserOperationOptionalGas}; +use rundler_types::{pool::Pool, UserOperationVariant}; use super::{api::EthApi, EthApiServer}; -use crate::types::{RichUserOperation, RpcUserOperation, UserOperationReceipt}; +use crate::types::{ + FromRpc, RpcGasEstimate, RpcUserOperation, RpcUserOperationByHash, RpcUserOperationOptionalGas, + RpcUserOperationReceipt, +}; -#[async_trait] -impl EthApiServer for EthApi +#[async_trait::async_trait] +impl

EthApiServer for EthApi

where - P: Provider, - E: EntryPoint, - PS: PoolServer, + P: Pool, { async fn send_user_operation( &self, op: RpcUserOperation, entry_point: Address, ) -> RpcResult { - Ok(EthApi::send_user_operation(self, op, 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( &self, - op: UserOperationOptionalGas, + op: RpcUserOperationOptionalGas, entry_point: Address, state_override: Option, - ) -> RpcResult { - Ok(EthApi::estimate_user_operation_gas(self, op, entry_point, state_override).await?) + ) -> RpcResult { + Ok( + EthApi::estimate_user_operation_gas(self, op.into(), entry_point, state_override) + .await?, + ) } - async fn get_user_operation_by_hash(&self, hash: H256) -> RpcResult> { + async fn get_user_operation_by_hash( + &self, + hash: H256, + ) -> RpcResult> { Ok(EthApi::get_user_operation_by_hash(self, hash).await?) } async fn get_user_operation_receipt( &self, hash: H256, - ) -> RpcResult> { + ) -> RpcResult> { Ok(EthApi::get_user_operation_receipt(self, hash).await?) } diff --git a/crates/rpc/src/lib.rs b/crates/rpc/src/lib.rs index b44893641..bda4e98ef 100644 --- a/crates/rpc/src/lib.rs +++ b/crates/rpc/src/lib.rs @@ -40,4 +40,3 @@ mod task; pub use task::{Args as RpcTaskArgs, RpcTask}; mod types; -pub use types::{RichUserOperation, RpcUserOperation, UserOperationReceipt}; diff --git a/crates/rpc/src/rundler.rs b/crates/rpc/src/rundler.rs index 5f11815bf..2bf0cd05e 100644 --- a/crates/rpc/src/rundler.rs +++ b/crates/rpc/src/rundler.rs @@ -20,12 +20,15 @@ use jsonrpsee::{ proc_macros::rpc, types::error::{INTERNAL_ERROR_CODE, INVALID_REQUEST_CODE}, }; -use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, Provider}; +use rundler_provider::Provider; use rundler_sim::{gas, FeeEstimator}; -use rundler_types::{chain::ChainSpec, UserOperation, UserOperationId}; +use rundler_types::{chain::ChainSpec, pool::Pool, UserOperation, UserOperationVariant}; -use crate::{error::rpc_err, eth::EthRpcError, RpcUserOperation}; +use crate::{ + error::rpc_err, + eth::{EntryPointRouter, EthRpcError}, + types::{FromRpc, RpcUserOperation}, +}; /// Settings for the `rundler_` API #[derive(Copy, Clone, Debug)] @@ -61,27 +64,28 @@ pub trait RundlerApi { ) -> RpcResult>; } -pub(crate) struct RundlerApi { +pub(crate) struct RundlerApi { + chain_spec: ChainSpec, settings: Settings, fee_estimator: FeeEstimator

, - entry_point: E, - pool_server: PS, + pool_server: PL, + entry_point_router: EntryPointRouter, } -impl RundlerApi +impl RundlerApi where P: Provider, - E: EntryPoint, - PS: PoolServer, + PL: Pool, { pub(crate) fn new( chain_spec: &ChainSpec, provider: Arc

, - entry_point: E, - pool_server: PS, + entry_point_router: EntryPointRouter, + pool_server: PL, settings: Settings, ) -> Self { Self { + chain_spec: chain_spec.clone(), settings, fee_estimator: FeeEstimator::new( chain_spec, @@ -89,18 +93,17 @@ where settings.priority_fee_mode, settings.bundle_priority_fee_overhead_percent, ), - entry_point, + entry_point_router, pool_server, } } } #[async_trait] -impl RundlerApiServer for RundlerApi +impl RundlerApiServer for RundlerApi where P: Provider, - E: EntryPoint, - PS: PoolServer, + PL: Pool, { async fn max_priority_fee_per_gas(&self) -> RpcResult { let (bundle_fees, _) = self @@ -119,23 +122,13 @@ where user_op: RpcUserOperation, entry_point: Address, ) -> RpcResult> { - if entry_point != self.entry_point.address() { - return Err(rpc_err( - INVALID_REQUEST_CODE, - format!("entry point {} not supported", entry_point), - )); - } + let uo = UserOperationVariant::from_rpc(user_op, entry_point, self.chain_spec.id); + let id = uo.id(); - let uo: UserOperation = user_op.into(); - let id = UserOperationId { - sender: uo.sender, - nonce: uo.nonce, - }; - - if uo.pre_verification_gas != U256::zero() - || uo.call_gas_limit != U256::zero() - || uo.call_data.len() != 0 - || uo.max_fee_per_gas != U256::zero() + if uo.pre_verification_gas() != U256::zero() + || uo.call_gas_limit() != U256::zero() + || uo.call_data().len() != 0 + || uo.max_fee_per_gas() != U256::zero() { return Err(rpc_err( INVALID_REQUEST_CODE, @@ -143,16 +136,14 @@ where )); } - let output = self - .entry_point - .call_simulate_validation(uo, self.settings.max_verification_gas) - .await - .map_err(|e| rpc_err(INTERNAL_ERROR_CODE, e.to_string()))?; - - if output.return_info.sig_failed { + let valid = self + .entry_point_router + .check_signature(&entry_point, uo, self.settings.max_verification_gas) + .await?; + if !valid { return Err(rpc_err( INVALID_REQUEST_CODE, - "User operation for drop failed simulateValidation", + "Invalid user operation for drop: invalid signature", )); } diff --git a/crates/rpc/src/task.rs b/crates/rpc/src/task.rs index 54b05e6d4..1ce33b418 100644 --- a/crates/rpc/src/task.rs +++ b/crates/rpc/src/task.rs @@ -20,22 +20,25 @@ use jsonrpsee::{ server::{middleware::ProxyGetRequestLayer, ServerBuilder}, RpcModule, }; -use rundler_builder::BuilderServer; -use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, EthersEntryPoint}; -use rundler_sim::{EstimationSettings, 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, }; -use rundler_types::chain::ChainSpec; +use rundler_types::{builder::Builder, chain::ChainSpec, pool::Pool}; use tokio_util::sync::CancellationToken; use tracing::info; use crate::{ admin::{AdminApi, AdminApiServer}, debug::{DebugApi, DebugApiServer}, - eth::{EthApi, EthApiServer, EthApiSettings}, + eth::{ + EntryPointRouteImpl, EntryPointRouter, EntryPointRouterBuilder, EthApi, EthApiServer, + EthApiSettings, UserOperationEventProviderV0_6, UserOperationEventProviderV0_7, + }, health::{HealthChecker, SystemApiServer}, metrics::RpcMetricsLogger, rundler::{RundlerApi, RundlerApiServer, Settings as RundlerApiSettings}, @@ -47,6 +50,8 @@ use crate::{ pub struct Args { /// Chain spec pub chain_spec: ChainSpec, + /// True if using unsafe mode + pub unsafe_mode: bool, /// Port to listen on. pub port: u16, /// Host to listen on. @@ -67,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. @@ -80,18 +89,84 @@ pub struct RpcTask { #[async_trait] impl Task for RpcTask where - P: PoolServer + HealthCheck + Clone, - B: BuilderServer + HealthCheck + Clone, + P: Pool + HealthCheck + Clone, + B: Builder + HealthCheck + Clone, { async fn run(mut self: Box, shutdown_token: CancellationToken) -> anyhow::Result<()> { let addr: SocketAddr = format_socket_addr(&self.args.host, self.args.port).parse()?; tracing::info!("Starting rpc server on {}", addr); let provider = rundler_provider::new_provider(&self.args.rpc_url, None)?; - let ep = EthersEntryPoint::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(), + ); + + 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_v0_6, + 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_6::new( + self.args.chain_spec.id, + 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, + ), + )); + } + + // create the entry point router + let router = router_builder.build(); let mut module = RpcModule::new(()); - self.attach_namespaces(provider, ep, &mut module)?; + self.attach_namespaces(provider, router, &mut module)?; let servers: Vec> = vec![Box::new(self.pool.clone()), Box::new(self.builder.clone())]; @@ -130,8 +205,8 @@ where impl RpcTask where - P: PoolServer + HealthCheck + Clone, - B: BuilderServer + HealthCheck + Clone, + P: Pool + HealthCheck + Clone, + B: Builder + HealthCheck + Clone, { /// Creates a new RPC server task. pub fn new(args: Args, pool: P, builder: B) -> Self { @@ -147,41 +222,45 @@ where Box::new(self) } - fn attach_namespaces( + fn attach_namespaces( &self, provider: Arc>, - entry_point: E, + entry_point_router: EntryPointRouter, module: &mut RpcModule<()>, - ) -> anyhow::Result<()> { - for api in &self.args.api_namespaces { - match api { - ApiNamespace::Eth => module.merge( - EthApi::new( - self.args.chain_spec.clone(), - provider.clone(), - // TODO: support multiple entry points - vec![entry_point.clone()], - self.pool.clone(), - self.args.eth_api_settings, - self.args.estimation_settings, - self.args.precheck_settings, - ) - .into_rpc(), - )?, - ApiNamespace::Debug => module - .merge(DebugApi::new(self.pool.clone(), self.builder.clone()).into_rpc())?, - ApiNamespace::Admin => module.merge(AdminApi::new(self.pool.clone()).into_rpc())?, - ApiNamespace::Rundler => module.merge( - RundlerApi::new( - &self.args.chain_spec, - provider.clone(), - entry_point.clone(), - self.pool.clone(), - self.args.rundler_api_settings, - ) - .into_rpc(), - )?, - } + ) -> anyhow::Result<()> + where + C: JsonRpcClient + 'static, + { + if self.args.api_namespaces.contains(&ApiNamespace::Eth) { + module.merge( + EthApi::new( + self.args.chain_spec.clone(), + entry_point_router.clone(), + self.pool.clone(), + ) + .into_rpc(), + )? + } + + if self.args.api_namespaces.contains(&ApiNamespace::Debug) { + module.merge(DebugApi::new(self.pool.clone(), self.builder.clone()).into_rpc())?; + } + + if self.args.api_namespaces.contains(&ApiNamespace::Admin) { + module.merge(AdminApi::new(self.pool.clone()).into_rpc())?; + } + + if self.args.api_namespaces.contains(&ApiNamespace::Rundler) { + module.merge( + RundlerApi::new( + &self.args.chain_spec, + provider.clone(), + entry_point_router, + self.pool.clone(), + self.args.rundler_api_settings, + ) + .into_rpc(), + )?; } Ok(()) diff --git a/crates/rpc/src/types.rs b/crates/rpc/src/types/mod.rs similarity index 63% rename from crates/rpc/src/types.rs rename to crates/rpc/src/types/mod.rs index 30c182858..235a0a92e 100644 --- a/crates/rpc/src/types.rs +++ b/crates/rpc/src/types/mod.rs @@ -12,13 +12,28 @@ // If not, see https://www.gnu.org/licenses/. use ethers::{ - types::{Address, Bytes, Log, TransactionReceipt, H160, H256, U256}, + types::{Address, Log, TransactionReceipt, H160, H256, U256}, utils::to_checksum, }; -use rundler_pool::{Reputation, ReputationStatus}; -use rundler_types::UserOperation; +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}; +mod v0_6; +pub(crate) use v0_6::{ + RpcUserOperation as RpcUserOperationV0_6, + RpcUserOperationOptionalGas as RpcUserOperationOptionalGasV0_6, +}; +mod v0_7; +pub(crate) use v0_7::{ + RpcUserOperation as RpcUserOperationV0_7, + RpcUserOperationOptionalGas as RpcUserOperationOptionalGasV0_7, +}; + /// API namespace #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumString)] #[strum(serialize_all = "lowercase", ascii_case_insensitive)] @@ -29,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); @@ -66,92 +86,106 @@ impl From

for RpcAddress { /// Stake info definition for RPC #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct RpcStakeStatus { - pub is_staked: bool, - pub stake_info: RpcStakeInfo, +pub(crate) struct RpcStakeStatus { + pub(crate) is_staked: bool, + pub(crate) stake_info: RpcStakeInfo, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct RpcStakeInfo { - pub addr: Address, - pub stake: u128, - pub unstake_delay_sec: u32, +pub(crate) struct RpcStakeInfo { + pub(crate) addr: Address, + pub(crate) stake: u128, + pub(crate) unstake_delay_sec: u32, } -/// User operation definition for RPC -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct RpcUserOperation { - sender: RpcAddress, - nonce: U256, - init_code: Bytes, - call_data: Bytes, - call_gas_limit: U256, - verification_gas_limit: U256, - pre_verification_gas: U256, - max_fee_per_gas: U256, - max_priority_fee_per_gas: U256, - paymaster_and_data: Bytes, - signature: Bytes, +#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] +#[serde(untagged)] +pub(crate) enum RpcUserOperation { + V0_6(RpcUserOperationV0_6), + V0_7(RpcUserOperationV0_7), } -impl From for RpcUserOperation { - fn from(op: UserOperation) -> Self { - RpcUserOperation { - sender: op.sender.into(), - nonce: op.nonce, - init_code: op.init_code, - 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_fee_per_gas: op.max_fee_per_gas, - max_priority_fee_per_gas: op.max_priority_fee_per_gas, - paymaster_and_data: op.paymaster_and_data, - signature: op.signature, +impl From for RpcUserOperation { + fn from(op: UserOperationVariant) -> Self { + match op { + UserOperationVariant::V0_6(op) => RpcUserOperation::V0_6(op.into()), + UserOperationVariant::V0_7(op) => RpcUserOperation::V0_7(op.into()), } } } -impl From for UserOperation { - fn from(def: RpcUserOperation) -> Self { - UserOperation { - sender: def.sender.into(), - nonce: def.nonce, - init_code: def.init_code, - 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_fee_per_gas: def.max_fee_per_gas, - max_priority_fee_per_gas: def.max_priority_fee_per_gas, - paymaster_and_data: def.paymaster_and_data, - signature: def.signature, +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(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)) + } } } } /// User operation with additional metadata -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] -pub struct RichUserOperation { +pub(crate) struct RpcUserOperationByHash { /// The full user operation - pub user_operation: RpcUserOperation, + pub(crate) user_operation: RpcUserOperation, /// The entry point address this operation was sent to - pub entry_point: RpcAddress, + pub(crate) entry_point: RpcAddress, /// The number of the block this operation was included in - pub block_number: Option, + pub(crate) block_number: Option, /// The hash of the block this operation was included in - pub block_hash: Option, + pub(crate) block_hash: Option, /// The hash of the transaction this operation was included in - pub transaction_hash: Option, + pub(crate) transaction_hash: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub(crate) enum RpcUserOperationOptionalGas { + V0_6(RpcUserOperationOptionalGasV0_6), + V0_7(RpcUserOperationOptionalGasV0_7), +} + +impl From for UserOperationOptionalGas { + fn from(op: RpcUserOperationOptionalGas) -> Self { + match op { + RpcUserOperationOptionalGas::V0_6(op) => UserOperationOptionalGas::V0_6(op.into()), + RpcUserOperationOptionalGas::V0_7(op) => UserOperationOptionalGas::V0_7(op.into()), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RpcGasEstimate { + pre_verification_gas: U256, + call_gas_limit: U256, + verification_gas_limit: U256, + paymaster_verification_gas_limit: Option, + paymaster_post_op_gas_limit: Option, +} + +impl From for RpcGasEstimate { + fn from(estimate: GasEstimate) -> Self { + RpcGasEstimate { + pre_verification_gas: estimate.pre_verification_gas, + call_gas_limit: estimate.call_gas_limit, + verification_gas_limit: estimate.verification_gas_limit, + paymaster_verification_gas_limit: estimate.paymaster_verification_gas_limit, + paymaster_post_op_gas_limit: estimate.paymaster_post_op_gas_limit, + } + } } /// User operation receipt #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct UserOperationReceipt { +pub struct RpcUserOperationReceipt { /// The hash of the user operation pub user_op_hash: H256, /// The entry point address this operation was sent to diff --git a/crates/rpc/src/types/v0_6.rs b/crates/rpc/src/types/v0_6.rs new file mode 100644 index 000000000..3c1958304 --- /dev/null +++ b/crates/rpc/src/types/v0_6.rs @@ -0,0 +1,105 @@ +// 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 ethers::types::{Address, Bytes, U256}; +use rundler_types::v0_6::{UserOperation, UserOperationOptionalGas}; +use serde::{Deserialize, Serialize}; + +use super::{FromRpc, RpcAddress}; + +/// User operation definition for RPC +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RpcUserOperation { + sender: RpcAddress, + nonce: U256, + init_code: Bytes, + call_data: Bytes, + call_gas_limit: U256, + verification_gas_limit: U256, + pre_verification_gas: U256, + max_fee_per_gas: U256, + max_priority_fee_per_gas: U256, + paymaster_and_data: Bytes, + signature: Bytes, +} + +impl From for RpcUserOperation { + fn from(op: UserOperation) -> Self { + RpcUserOperation { + sender: op.sender.into(), + nonce: op.nonce, + init_code: op.init_code, + 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_fee_per_gas: op.max_fee_per_gas, + max_priority_fee_per_gas: op.max_priority_fee_per_gas, + paymaster_and_data: op.paymaster_and_data, + signature: op.signature, + } + } +} + +impl FromRpc for UserOperation { + fn from_rpc(def: RpcUserOperation, _entry_point: Address, _chain_id: u64) -> Self { + UserOperation { + sender: def.sender.into(), + nonce: def.nonce, + init_code: def.init_code, + 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_fee_per_gas: def.max_fee_per_gas, + max_priority_fee_per_gas: def.max_priority_fee_per_gas, + paymaster_and_data: def.paymaster_and_data, + signature: def.signature, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RpcUserOperationOptionalGas { + sender: Address, + nonce: U256, + init_code: Bytes, + call_data: Bytes, + call_gas_limit: Option, + verification_gas_limit: Option, + pre_verification_gas: Option, + max_fee_per_gas: Option, + max_priority_fee_per_gas: Option, + paymaster_and_data: Bytes, + signature: Bytes, +} + +impl From for UserOperationOptionalGas { + fn from(def: RpcUserOperationOptionalGas) -> Self { + UserOperationOptionalGas { + sender: def.sender, + nonce: def.nonce, + init_code: def.init_code, + 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_fee_per_gas: def.max_fee_per_gas, + max_priority_fee_per_gas: def.max_priority_fee_per_gas, + paymaster_and_data: def.paymaster_and_data, + signature: def.signature, + } + } +} diff --git a/crates/rpc/src/types/v0_7.rs b/crates/rpc/src/types/v0_7.rs new file mode 100644 index 000000000..17162347e --- /dev/null +++ b/crates/rpc/src/types/v0_7.rs @@ -0,0 +1,149 @@ +// 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 ethers::types::{Address, Bytes, H256, U128, U256}; +use rundler_types::v0_7::{ + UserOperation, UserOperationBuilder, UserOperationOptionalGas, UserOperationRequiredFields, +}; +use serde::{Deserialize, Serialize}; + +use super::{FromRpc, RpcAddress}; + +/// User operation definition for RPC +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RpcUserOperation { + sender: Address, + nonce: U256, + call_data: Bytes, + call_gas_limit: U128, + verification_gas_limit: U128, + pre_verification_gas: U256, + max_priority_fee_per_gas: U128, + max_fee_per_gas: U128, + factory: Option
, + factory_data: Option, + paymaster: Option
, + paymaster_verification_gas_limit: Option, + paymaster_post_op_gas_limit: Option, + paymaster_data: Option, + signature: Bytes, +} + +impl From for RpcUserOperation { + 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 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() + } +} + +/// User operation with additional metadata +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RpcUserOperationByHash { + user_operation: RpcUserOperation, + entry_point: RpcAddress, + block_number: Option, + block_hash: Option, + transaction_hash: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RpcUserOperationOptionalGas { + sender: Address, + nonce: U256, + call_data: Bytes, + call_gas_limit: Option, + verification_gas_limit: Option, + pre_verification_gas: Option, + max_priority_fee_per_gas: Option, + max_fee_per_gas: Option, + factory: Option
, + factory_data: Option, + paymaster: Option
, + paymaster_verification_gas_limit: Option, + paymaster_post_op_gas_limit: Option, + paymaster_data: Option, + signature: Bytes, +} + +impl From for UserOperationOptionalGas { + 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/mod.rs b/crates/sim/src/estimation/mod.rs index ce3a96c45..7848ff1cf 100644 --- a/crates/sim/src/estimation/mod.rs +++ b/crates/sim/src/estimation/mod.rs @@ -11,9 +11,78 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -#[allow(clippy::module_inception)] -mod estimation; -pub use estimation::*; +use ethers::types::{Bytes, U256}; +#[cfg(feature = "test-utils")] +use mockall::automock; +use rundler_types::GasEstimate; -mod types; -pub use types::{GasEstimate, Settings, UserOperationOptionalGas}; +use crate::precheck::MIN_CALL_GAS_LIMIT; + +/// Gas estimation module for Entry Point v0.6 +mod v0_6; +pub use v0_6::GasEstimator as GasEstimatorV0_6; +mod v0_7; +pub use v0_7::GasEstimator as GasEstimatorV0_7; + +/// Error type for gas estimation +#[derive(Debug, thiserror::Error)] +pub enum GasEstimationError { + /// Validation reverted + #[error("{0}")] + RevertInValidation(String), + /// Call reverted with a string message + #[error("user operation's call reverted: {0}")] + RevertInCallWithMessage(String), + /// Call reverted with bytes + #[error("user operation's call reverted: {0:#x}")] + RevertInCallWithBytes(Bytes), + /// Other error + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +/// Gas estimator trait +#[cfg_attr(feature = "test-utils", automock(type UserOperationOptionalGas = rundler_types::v0_6::UserOperationOptionalGas;))] +#[async_trait::async_trait] +pub trait GasEstimator: Send + Sync + 'static { + /// The user operation type estimated by this gas estimator + type UserOperationOptionalGas; + + /// Returns a gas estimate or a revert message, or an anyhow error on any + /// other error. + async fn estimate_op_gas( + &self, + op: Self::UserOperationOptionalGas, + state_override: ethers::types::spoof::State, + ) -> Result; +} + +/// Settings for gas estimation +#[derive(Clone, Copy, Debug)] +pub struct Settings { + /// The maximum amount of gas that can be used for the verification step of a user operation + pub max_verification_gas: u64, + /// The maximum amount of gas that can be used for the call step of a user operation + pub max_call_gas: u64, + /// The maximum amount of gas that can be used in a call to `simulateHandleOps` + pub max_simulate_handle_ops_gas: u64, + /// The gas fee to use during verification gas estimation, required to be held by the fee-payer + /// during estimation. If using a paymaster, the fee-payer must have 3x this value. + /// As the gas limit is varied during estimation, the fee is held constant by varied the + /// gas price. + /// Clients can use state overrides to set the balance of the fee-payer to at least this value. + pub verification_estimation_gas_fee: u64, +} + +impl Settings { + /// Check if the settings are valid + pub fn validate(&self) -> Option { + if U256::from(self.max_call_gas) + .cmp(&MIN_CALL_GAS_LIMIT) + .is_lt() + { + return Some("max_call_gas field cannot be lower than MIN_CALL_GAS_LIMIT".to_string()); + } + None + } +} diff --git a/crates/sim/src/estimation/estimation.rs b/crates/sim/src/estimation/v0_6.rs similarity index 91% rename from crates/sim/src/estimation/estimation.rs rename to crates/sim/src/estimation/v0_6.rs index a32b61dd8..8c8cc26c0 100644 --- a/crates/sim/src/estimation/estimation.rs +++ b/crates/sim/src/estimation/v0_6.rs @@ -24,10 +24,8 @@ use ethers::{ providers::spoof, types::{Address, Bytes, H256, U256}, }; -#[cfg(feature = "test-utils")] -use mockall::automock; use rand::Rng; -use rundler_provider::{EntryPoint, Provider}; +use rundler_provider::{EntryPoint, L1GasProvider, Provider, SimulationProvider}; use rundler_types::{ chain::ChainSpec, contracts::v0_6::{ @@ -38,13 +36,17 @@ use rundler_types::{ }, i_entry_point, }, - UserOperation, + v0_6::{UserOperation, UserOperationOptionalGas}, + GasEstimate, UserOperation as UserOperationTrait, }; use rundler_utils::{eth, math}; use tokio::join; -use super::types::{GasEstimate, Settings, UserOperationOptionalGas}; -use crate::{gas, precheck::MIN_CALL_GAS_LIMIT, simulation, utils, FeeEstimator}; +use super::{GasEstimationError, Settings}; +use crate::{ + gas, precheck::MIN_CALL_GAS_LIMIT, simulation, utils, FeeEstimator, + GasEstimator as GasEstimatorTrait, +}; /// Gas estimates will be rounded up to the next multiple of this. Increasing /// this value reduces the number of rounds of `eth_call` needed in binary @@ -69,39 +71,9 @@ const CALL_GAS_BUFFER_VALUE: U256 = U256([3000, 0, 0, 0]); /// failure will tell you the new value. const PROXY_TARGET_OFFSET: usize = 137; -/// Error type for gas estimation -#[derive(Debug, thiserror::Error)] -pub enum GasEstimationError { - /// Validation reverted - #[error("{0}")] - RevertInValidation(String), - /// Call reverted with a string message - #[error("user operation's call reverted: {0}")] - RevertInCallWithMessage(String), - /// Call reverted with bytes - #[error("user operation's call reverted: {0:#x}")] - RevertInCallWithBytes(Bytes), - /// Other error - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -/// Gas estimator trait -#[cfg_attr(feature = "test-utils", automock)] -#[async_trait::async_trait] -pub trait GasEstimator: Send + Sync + 'static { - /// Returns a gas estimate or a revert message, or an anyhow error on any - /// other error. - async fn estimate_op_gas( - &self, - op: UserOperationOptionalGas, - state_override: spoof::State, - ) -> Result; -} - /// Gas estimator implementation #[derive(Debug)] -pub struct GasEstimatorImpl { +pub struct GasEstimator { chain_spec: ChainSpec, provider: Arc

, entry_point: E, @@ -110,7 +82,13 @@ pub struct GasEstimatorImpl { } #[async_trait::async_trait] -impl GasEstimator for GasEstimatorImpl { +impl GasEstimatorTrait for GasEstimator +where + P: Provider, + E: EntryPoint + SimulationProvider + L1GasProvider, +{ + type UserOperationOptionalGas = UserOperationOptionalGas; + async fn estimate_op_gas( &self, op: UserOperationOptionalGas, @@ -139,7 +117,10 @@ impl GasEstimator for GasEstimatorImpl { let op = UserOperation { pre_verification_gas, - ..op.into_user_operation(settings) + ..op.into_user_operation( + settings.max_call_gas.into(), + settings.max_verification_gas.into(), + ) }; let verification_future = @@ -163,7 +144,7 @@ impl GasEstimator for GasEstimatorImpl { // to ensure we get at least a 2000 gas buffer. Cap at the max verification gas. let verification_gas_limit = cmp::max( math::increase_by_percent(verification_gas_limit, VERIFICATION_GAS_BUFFER_PERCENT), - verification_gas_limit + simulation::REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER, + verification_gas_limit + simulation::v0_6::REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER, ) .min(settings.max_verification_gas.into()); @@ -176,11 +157,17 @@ impl GasEstimator for GasEstimatorImpl { pre_verification_gas, verification_gas_limit, call_gas_limit, + paymaster_verification_gas_limit: None, + paymaster_post_op_gas_limit: None, }) } } -impl GasEstimatorImpl { +impl GasEstimator +where + P: Provider, + E: EntryPoint + SimulationProvider + L1GasProvider, +{ /// Create a new gas estimator pub fn new( chain_spec: ChainSpec, @@ -452,9 +439,15 @@ impl GasEstimatorImpl { ) -> Result { Ok(gas::estimate_pre_verification_gas( &self.chain_spec, - self.provider.clone(), - &op.max_fill(&self.settings), - &op.random_fill(&self.settings), + &self.entry_point, + &op.max_fill( + self.settings.max_call_gas.into(), + self.settings.max_verification_gas.into(), + ), + &op.random_fill( + self.settings.max_call_gas.into(), + self.settings.max_verification_gas.into(), + ), gas_price, ) .await?) @@ -476,14 +469,19 @@ mod tests { types::U64, utils::hex, }; - use rundler_provider::{MockEntryPoint, MockProvider}; + use rundler_provider::{ExecutionResult, MockEntryPointV0_6, MockProvider}; use rundler_types::{ chain::L1GasOracleContractType, - contracts::{utils::get_gas_used::GasUsedResult, v0_6::i_entry_point::ExecutionResult}, + contracts::utils::get_gas_used::GasUsedResult, + v0_6::{UserOperation, UserOperationOptionalGas}, + UserOperation as UserOperationTrait, }; use super::*; - use crate::PriorityFeeMode; + use crate::{ + estimation::GasEstimator as GasEstimatorTrait, + simulation::v0_6::REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER, PriorityFeeMode, + }; // Gas overhead defaults const FIXED: u32 = 21000; @@ -494,8 +492,8 @@ mod tests { /// Must match the constant in `CallGasEstimationProxy.sol`. const PROXY_TARGET_CONSTANT: &str = "A13dB4eCfbce0586E57D1AeE224FbE64706E8cd3"; - fn create_base_config() -> (MockEntryPoint, MockProvider) { - let entry = MockEntryPoint::new(); + fn create_base_config() -> (MockEntryPointV0_6, MockProvider) { + let entry = MockEntryPointV0_6::new(); let provider = MockProvider::new(); (entry, provider) @@ -511,9 +509,9 @@ mod tests { } fn create_estimator( - entry: MockEntryPoint, + entry: MockEntryPointV0_6, provider: MockProvider, - ) -> (GasEstimatorImpl, Settings) { + ) -> (GasEstimator, Settings) { let settings = Settings { max_verification_gas: 10000000000, max_call_gas: 10000000000, @@ -521,7 +519,7 @@ mod tests { verification_estimation_gas_fee: 1_000_000_000_000, }; let provider = Arc::new(provider); - let estimator: GasEstimatorImpl = GasEstimatorImpl::new( + let estimator: GasEstimator = GasEstimator::new( ChainSpec::default(), provider.clone(), entry, @@ -588,7 +586,10 @@ mod tests { .await .unwrap(); - let u_o = user_op.max_fill(&settings); + let u_o = user_op.max_fill( + settings.max_call_gas.into(), + settings.max_verification_gas.into(), + ); let u_o_encoded = u_o.encode(); let length_in_words = (u_o_encoded.len() + 31) / 32; @@ -610,9 +611,9 @@ mod tests { #[tokio::test] async fn test_calc_pre_verification_input_arbitrum() { - let (mut entry, mut provider) = create_base_config(); + let (mut entry, provider) = create_base_config(); entry.expect_address().return_const(Address::zero()); - provider + entry .expect_calc_arbitrum_l1_gas() .returning(|_a, _b| Ok(U256::from(1000))); @@ -631,7 +632,7 @@ mod tests { ..Default::default() }; let provider = Arc::new(provider); - let estimator: GasEstimatorImpl = GasEstimatorImpl::new( + let estimator: GasEstimator = GasEstimator::new( cs, provider.clone(), entry, @@ -645,7 +646,10 @@ mod tests { .await .unwrap(); - let u_o = user_op.max_fill(&settings); + let u_o = user_op.max_fill( + settings.max_call_gas.into(), + settings.max_verification_gas.into(), + ); let u_o_encoded = u_o.encode(); let length_in_words = (u_o_encoded.len() + 31) / 32; @@ -668,10 +672,10 @@ mod tests { #[tokio::test] async fn test_calc_pre_verification_input_op() { - let (mut entry, mut provider) = create_base_config(); + let (mut entry, provider) = create_base_config(); entry.expect_address().return_const(Address::zero()); - provider + entry .expect_calc_optimism_l1_gas() .returning(|_a, _b, _c| Ok(U256::from(1000))); @@ -690,7 +694,7 @@ mod tests { ..Default::default() }; let provider = Arc::new(provider); - let estimator: GasEstimatorImpl = GasEstimatorImpl::new( + let estimator: GasEstimator = GasEstimator::new( cs, provider.clone(), entry, @@ -704,7 +708,10 @@ mod tests { .await .unwrap(); - let u_o = user_op.max_fill(&settings); + let u_o = user_op.max_fill( + settings.max_call_gas.into(), + settings.max_verification_gas.into(), + ); let u_o_encoded: Bytes = u_o.encode().into(); let length_in_words = (u_o_encoded.len() + 31) / 32; @@ -738,8 +745,8 @@ mod tests { Ok(ExecutionResult { pre_op_gas: U256::from(10000), paid: U256::from(100000), - valid_after: 100000000000, - valid_until: 100000000001, + valid_after: 100000000000.into(), + valid_until: 100000000001.into(), target_success: true, target_result: Bytes::new(), }) @@ -747,7 +754,7 @@ mod tests { entry .expect_call_spoofed_simulate_op() .returning(move |op, _b, _c, _d, _e, _f| { - if op.verification_gas_limit < gas_usage { + if op.total_verification_gas_limit() < gas_usage { return Ok(Err("AA23".to_string())); } @@ -796,8 +803,8 @@ mod tests { Ok(ExecutionResult { pre_op_gas: U256::from(10000), paid: U256::from(100000), - valid_after: 100000000000, - valid_until: 100000000001, + valid_after: 100000000000.into(), + valid_until: 100000000001.into(), target_success: true, target_result: Bytes::new(), }) @@ -853,8 +860,8 @@ mod tests { Ok(ExecutionResult { pre_op_gas: U256::from(10000), paid: U256::from(100000), - valid_after: 100000000000, - valid_until: 100000000001, + valid_after: 100000000000.into(), + valid_until: 100000000001.into(), target_success: true, target_result: Bytes::new(), }) @@ -949,8 +956,8 @@ mod tests { Ok(ExecutionResult { pre_op_gas: U256::from(10000), paid: U256::from(100000), - valid_after: 100000000000, - valid_until: 100000000001, + valid_after: 100000000000.into(), + valid_until: 100000000001.into(), target_success: true, target_result: Bytes::new(), }) @@ -991,8 +998,8 @@ mod tests { Ok(ExecutionResult { pre_op_gas: U256::from(10000), paid: U256::from(100000), - valid_after: 100000000000, - valid_until: 100000000001, + valid_after: 100000000000.into(), + valid_until: 100000000001.into(), target_success: true, target_result: Bytes::new(), }) @@ -1170,7 +1177,7 @@ mod tests { entry .expect_call_spoofed_simulate_op() .returning(move |op, _b, _c, _d, _e, _f| { - if op.verification_gas_limit < gas_usage { + if op.total_verification_gas_limit() < gas_usage { return Ok(Err("AA23".to_string())); } @@ -1191,8 +1198,8 @@ mod tests { Ok(ExecutionResult { pre_op_gas: U256::from(10000), paid: U256::from(100000), - valid_after: 100000000000, - valid_until: 100000000001, + valid_after: 100000000000.into(), + valid_until: 100000000001.into(), target_success: true, target_result: Bytes::new(), }) @@ -1239,7 +1246,7 @@ mod tests { estimation.verification_gas_limit, cmp::max( math::increase_by_percent(expected, 10), - expected + simulation::REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER + expected + REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER ) ); @@ -1275,8 +1282,8 @@ mod tests { Ok(ExecutionResult { pre_op_gas: U256::from(10000), paid: U256::from(100000), - valid_after: 100000000000, - valid_until: 100000000001, + valid_after: 100000000000.into(), + valid_until: 100000000001.into(), target_success: true, target_result: Bytes::new(), }) @@ -1315,7 +1322,8 @@ mod tests { }; let provider = Arc::new(provider); - let estimator: GasEstimatorImpl = GasEstimatorImpl::new( + let entry = entry; + let estimator: GasEstimator = GasEstimator::new( ChainSpec::default(), provider.clone(), entry, diff --git a/crates/sim/src/estimation/v0_7.rs b/crates/sim/src/estimation/v0_7.rs new file mode 100644 index 000000000..41df315c6 --- /dev/null +++ b/crates/sim/src/estimation/v0_7.rs @@ -0,0 +1,109 @@ +// 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::cmp; + +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 { + chain_spec: ChainSpec, + entry_point: E, + _settings: Settings, + fee_estimator: FeeEstimator

, +} + +#[async_trait::async_trait] +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, + _state_override: spoof::State, + ) -> Result { + // 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 002c1a3ce..1051b2d84 100644 --- a/crates/sim/src/gas/gas.rs +++ b/crates/sim/src/gas/gas.rs @@ -14,8 +14,8 @@ use std::{cmp, fmt::Debug, sync::Arc}; use anyhow::Context; -use ethers::{abi::AbiEncode, types::U256}; -use rundler_provider::Provider; +use ethers::types::U256; +use rundler_provider::{EntryPoint, L1GasProvider, Provider}; use rundler_types::{ chain::{self, ChainSpec, L1GasOracleContractType}, GasFees, UserOperation, @@ -27,32 +27,6 @@ use super::oracle::{ ConstantOracle, FeeOracle, ProviderOracle, UsageBasedFeeOracle, UsageBasedFeeOracleConfig, }; -/// Gas overheads for user operations used in calculating the pre-verification gas. See: https://github.com/eth-infinitism/bundler/blob/main/packages/sdk/src/calcPreVerificationGas.ts -#[derive(Clone, Copy, Debug)] -pub struct GasOverheads { - /// The Entrypoint requires a gas buffer for the bundle to account for the gas spent outside of the major steps in the processing of UOs - pub bundle_transaction_gas_buffer: U256, - /// The fixed gas overhead for any EVM transaction - pub transaction_gas_overhead: U256, - per_user_op: U256, - per_user_op_word: U256, - zero_byte: U256, - non_zero_byte: U256, -} - -impl Default for GasOverheads { - fn default() -> Self { - Self { - bundle_transaction_gas_buffer: 5_000.into(), - transaction_gas_overhead: 21_000.into(), - per_user_op: 18_300.into(), - per_user_op_word: 4.into(), - zero_byte: 4.into(), - non_zero_byte: 16.into(), - } - } -} - /// Returns the required pre_verification_gas for the given user operation /// /// `full_op` is either the user operation submitted via `sendUserOperation` @@ -66,14 +40,17 @@ impl Default for GasOverheads { /// /// 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, - provider: Arc

, - full_op: &UserOperation, - random_op: &UserOperation, + entry_point: &E, + full_op: &UO, + random_op: &UO, gas_price: U256, ) -> anyhow::Result { - let static_gas = calc_static_pre_verification_gas(full_op, true); + let static_gas = full_op.calc_static_pre_verification_gas(true); if !chain_spec.calldata_pre_verification_gas { return Ok(static_gas); } @@ -81,15 +58,13 @@ pub async fn estimate_pre_verification_gas( let dynamic_gas = match chain_spec.l1_gas_oracle_contract_type { L1GasOracleContractType::None => panic!("Chain spec requires calldata pre_verification_gas but no l1_gas_oracle_contract_type is set"), L1GasOracleContractType::ArbitrumNitro => { - provider - .clone() - .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 => { - provider - .clone() - .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? }, }; @@ -100,13 +75,16 @@ pub async fn estimate_pre_verification_gas( /// Calculate the required pre_verification_gas for the given user operation and the provided base fee. /// /// The effective gas price is calculated as min(base_fee + max_priority_fee_per_gas, max_fee_per_gas) -pub async fn calc_required_pre_verification_gas( +pub async fn calc_required_pre_verification_gas< + UO: UserOperation, + E: EntryPoint + L1GasProvider, +>( chain_spec: &ChainSpec, - provider: Arc

, - op: &UserOperation, + entry_point: &E, + op: &UO, base_fee: U256, ) -> anyhow::Result { - let static_gas = calc_static_pre_verification_gas(op, true); + let static_gas = op.calc_static_pre_verification_gas(true); if !chain_spec.calldata_pre_verification_gas { return Ok(static_gas); } @@ -114,17 +92,15 @@ pub async fn calc_required_pre_verification_gas( let dynamic_gas = match chain_spec.l1_gas_oracle_contract_type { L1GasOracleContractType::None => panic!("Chain spec requires calldata pre_verification_gas but no l1_gas_oracle_contract_type is set"), L1GasOracleContractType::ArbitrumNitro => { - provider - .clone() - .calc_arbitrum_l1_gas(chain_spec.entry_point_address, op.clone()) + entry_point + .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); + let gas_price = cmp::min(base_fee + op.max_priority_fee_per_gas(), op.max_fee_per_gas()); - provider - .clone() - .calc_optimism_l1_gas(chain_spec.entry_point_address, op.clone(), gas_price) + entry_point + .calc_optimism_l1_gas(entry_point.address(), op.clone(), gas_price) .await? }, }; @@ -149,102 +125,71 @@ pub async fn calc_required_pre_verification_gas( /// If limiting the size of a bundle transaction to adhere to block gas limit, use the execution gas limit functions. /// Returns the gas limit for the user operation that applies to bundle transaction's limit -pub fn user_operation_gas_limit( +/// +/// On an L2 this is the total gas limit for the bundle transaction ~including~ any potential L1 costs +/// if the chain requires it. +/// +/// This is needed to set the gas limit for the bundle transaction. +pub fn user_operation_gas_limit( chain_spec: &ChainSpec, - uo: &UserOperation, + uo: &UO, assume_single_op_bundle: bool, - paymaster_post_op: bool, ) -> U256 { user_operation_pre_verification_gas_limit(chain_spec, uo, assume_single_op_bundle) - + uo.call_gas_limit - + uo.verification_gas_limit - * verification_gas_limit_multiplier(assume_single_op_bundle, paymaster_post_op) + + uo.total_verification_gas_limit() + + uo.required_pre_execution_buffer() + + uo.call_gas_limit() } /// Returns the gas limit for the user operation that applies to bundle transaction's execution limit -pub fn user_operation_execution_gas_limit( +/// +/// On an L2 this is the total gas limit for the bundle transaction ~excluding~ any potential L1 costs. +/// +/// This is needed to limit the size of the bundle transaction to adhere to the block gas limit. +pub fn user_operation_execution_gas_limit( chain_spec: &ChainSpec, - uo: &UserOperation, + uo: &UO, assume_single_op_bundle: bool, - paymaster_post_op: bool, ) -> U256 { user_operation_pre_verification_execution_gas_limit(chain_spec, uo, assume_single_op_bundle) - + uo.call_gas_limit - + uo.verification_gas_limit - * verification_gas_limit_multiplier(assume_single_op_bundle, paymaster_post_op) + + uo.total_verification_gas_limit() + + uo.required_pre_execution_buffer() + + uo.call_gas_limit() } /// Returns the static pre-verification gas cost of a user operation -pub fn user_operation_pre_verification_execution_gas_limit( +/// +/// On an L2 this is the total gas limit for the bundle transaction ~excluding~ any potential L1 costs +pub fn user_operation_pre_verification_execution_gas_limit( chain_spec: &ChainSpec, - uo: &UserOperation, + uo: &UO, include_fixed_gas_overhead: bool, ) -> U256 { // On some chains (OP bedrock, Arbitrum) the L1 gas fee is charged via pre_verification_gas // but this not part of the EXECUTION gas limit of the transaction. // In such cases we only consider the static portion of the pre_verification_gas in the gas limit. if chain_spec.calldata_pre_verification_gas { - calc_static_pre_verification_gas(uo, include_fixed_gas_overhead) + uo.calc_static_pre_verification_gas(include_fixed_gas_overhead) } else { - uo.pre_verification_gas + uo.pre_verification_gas() } } /// Returns the gas limit for the user operation that applies to bundle transaction's limit -pub fn user_operation_pre_verification_gas_limit( +/// +/// On an L2 this is the total gas limit for the bundle transaction ~including~ any potential L1 costs +pub fn user_operation_pre_verification_gas_limit( chain_spec: &ChainSpec, - uo: &UserOperation, + uo: &UO, include_fixed_gas_overhead: bool, ) -> U256 { // On some chains (OP bedrock) the L1 gas fee is charged via pre_verification_gas // but this not part of the execution TOTAL limit of the transaction. // In such cases we only consider the static portion of the pre_verification_gas in the gas limit. if chain_spec.calldata_pre_verification_gas && !chain_spec.include_l1_gas_in_gas_limit { - calc_static_pre_verification_gas(uo, include_fixed_gas_overhead) - } else { - uo.pre_verification_gas - } -} - -fn calc_static_pre_verification_gas(op: &UserOperation, include_fixed_gas_overhead: bool) -> U256 { - let ov = GasOverheads::default(); - let encoded_op = op.clone().encode(); - let length_in_words = encoded_op.len() / 32; // size of packed user op is always a multiple of 32 bytes - let call_data_cost: U256 = encoded_op - .iter() - .map(|&x| { - if x == 0 { - ov.zero_byte - } else { - ov.non_zero_byte - } - }) - .reduce(|a, b| a + b) - .unwrap_or_default(); - - call_data_cost - + ov.per_user_op - + ov.per_user_op_word * length_in_words - + (if include_fixed_gas_overhead { - ov.transaction_gas_overhead - } else { - 0.into() - }) -} - -fn verification_gas_limit_multiplier( - assume_single_op_bundle: bool, - paymaster_post_op: bool, -) -> u64 { - // If using a paymaster that has a postOp, we need to account for potentially 2 postOp calls which can each use up to verification_gas_limit gas. - // otherwise the entrypoint expects the gas for 1 postOp call that uses verification_gas_limit plus the actual verification call - // we only add the additional verification_gas_limit only if we know for sure that this is a single op bundle, which what we do to get a worst-case upper bound - if paymaster_post_op { - 3 - } else if assume_single_op_bundle { - 2 + uo.calc_static_pre_verification_gas(include_fixed_gas_overhead) } else { - 1 + uo.pre_verification_gas() } } @@ -312,7 +257,7 @@ pub struct FeeEstimator

{ provider: Arc

, priority_fee_mode: PriorityFeeMode, bundle_priority_fee_overhead_percent: u64, - fee_oracle: Arc>, + fee_oracle: Arc, } impl FeeEstimator

{ @@ -388,26 +333,26 @@ impl FeeEstimator

{ } } -fn get_fee_oracle

(chain_spec: &ChainSpec, provider: Arc

) -> Arc> +fn get_fee_oracle

(chain_spec: &ChainSpec, provider: Arc

) -> Arc where P: Provider + Debug, { if !chain_spec.eip1559_enabled { - return Arc::new(Box::new(ConstantOracle::new(U256::zero()))); + return Arc::new(ConstantOracle::new(U256::zero())); } match chain_spec.priority_fee_oracle_type { - chain::PriorityFeeOracleType::Provider => Arc::new(Box::new(ProviderOracle::new( + chain::PriorityFeeOracleType::Provider => Arc::new(ProviderOracle::new( provider, chain_spec.min_max_priority_fee_per_gas, - ))), + )), chain::PriorityFeeOracleType::UsageBased => { let config = UsageBasedFeeOracleConfig { minimum_fee: chain_spec.min_max_priority_fee_per_gas, maximum_fee: chain_spec.max_max_priority_fee_per_gas, ..Default::default() }; - Arc::new(Box::new(UsageBasedFeeOracle::new(provider, config))) + Arc::new(UsageBasedFeeOracle::new(provider, config)) } } } diff --git a/crates/sim/src/lib.rs b/crates/sim/src/lib.rs index db94aeb37..2cbaa4952 100644 --- a/crates/sim/src/lib.rs +++ b/crates/sim/src/lib.rs @@ -30,10 +30,11 @@ //! //! - `test-utils`: Export mocks and utilities for testing. +/// Gas estimation mod estimation; pub use estimation::{ - GasEstimate, GasEstimationError, GasEstimator, GasEstimatorImpl, - Settings as EstimationSettings, UserOperationOptionalGas, + GasEstimationError, GasEstimator, GasEstimatorV0_6, GasEstimatorV0_7, + Settings as EstimationSettings, }; pub mod gas; @@ -43,17 +44,15 @@ mod precheck; #[cfg(feature = "test-utils")] pub use precheck::MockPrechecker; pub use precheck::{ - PrecheckError, PrecheckViolation, Prechecker, PrecheckerImpl, Settings as PrecheckSettings, - MIN_CALL_GAS_LIMIT, + PrecheckError, Prechecker, PrecheckerImpl, Settings as PrecheckSettings, MIN_CALL_GAS_LIMIT, }; -mod simulation; +/// Simulation and violation checking +pub mod simulation; #[cfg(feature = "test-utils")] pub use simulation::MockSimulator; pub use simulation::{ - EntityInfo, EntityInfos, MempoolConfig, NeedsStakeInformation, Settings as SimulationSettings, - SimulateValidationTracer, SimulateValidationTracerImpl, SimulationError, SimulationResult, - SimulationViolation, Simulator, SimulatorImpl, ViolationOpCode, + MempoolConfig, Settings as SimulationSettings, SimulationError, SimulationResult, Simulator, }; mod types; diff --git a/crates/sim/src/precheck.rs b/crates/sim/src/precheck.rs index 0558b0d8d..cdf83791b 100644 --- a/crates/sim/src/precheck.rs +++ b/crates/sim/src/precheck.rs @@ -11,15 +11,22 @@ // 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::sync::{Arc, RwLock}; +use std::{ + marker::PhantomData, + sync::{Arc, RwLock}, +}; use anyhow::Context; use arrayvec::ArrayVec; use ethers::types::{Address, U256}; #[cfg(feature = "test-utils")] use mockall::automock; -use rundler_provider::{EntryPoint, Provider}; -use rundler_types::{chain::ChainSpec, GasFees, UserOperation}; +use rundler_provider::{EntryPoint, L1GasProvider, Provider}; +use rundler_types::{ + chain::ChainSpec, + pool::{MempoolError, PrecheckViolation}, + GasFees, UserOperation, +}; use rundler_utils::math; use crate::{gas, types::ViolationError}; @@ -29,11 +36,14 @@ pub const MIN_CALL_GAS_LIMIT: U256 = U256([9100, 0, 0, 0]); /// Trait for checking if a user operation is valid before simulation /// according to the spec rules. -#[cfg_attr(feature = "test-utils", automock)] +#[cfg_attr(feature = "test-utils", automock(type UO = rundler_types::v0_6::UserOperation;))] #[async_trait::async_trait] pub trait Prechecker: Send + Sync + 'static { + /// The user operation type + type UO: UserOperation; + /// Run the precheck on the given operation and return an error if it fails. - async fn check(&self, op: &UserOperation) -> Result<(), PrecheckError>; + async fn check(&self, op: &Self::UO) -> Result<(), PrecheckError>; /// Update and return the bundle fees. async fn update_fees(&self) -> anyhow::Result<(GasFees, U256)>; } @@ -41,16 +51,34 @@ pub trait Prechecker: Send + Sync + 'static { /// Precheck error pub type PrecheckError = ViolationError; +impl From for MempoolError { + fn from(mut error: PrecheckError) -> Self { + let PrecheckError::Violations(violations) = &mut error else { + return Self::Other(error.into()); + }; + + let Some(violation) = violations.iter_mut().min() else { + return Self::Other(error.into()); + }; + + // extract violation and replace with dummy + Self::PrecheckViolation(std::mem::replace( + violation, + PrecheckViolation::SenderIsNotContractAndNoInitCode(Address::zero()), + )) + } +} + /// Prechecker implementation #[derive(Debug)] -pub struct PrecheckerImpl { +pub struct PrecheckerImpl { chain_spec: ChainSpec, provider: Arc

, entry_point: E, settings: Settings, fee_estimator: gas::FeeEstimator

, - cache: RwLock, + _uo_type: PhantomData, } /// Precheck settings @@ -107,8 +135,15 @@ struct FeeCache { } #[async_trait::async_trait] -impl Prechecker for PrecheckerImpl { - async fn check(&self, op: &UserOperation) -> Result<(), PrecheckError> { +impl Prechecker for PrecheckerImpl +where + P: Provider, + E: EntryPoint + L1GasProvider, + UO: UserOperation, +{ + type UO = UO; + + async fn check(&self, op: &Self::UO) -> Result<(), PrecheckError> { let async_data = self.load_async_data(op).await?; let mut violations: Vec = vec![]; violations.extend(self.check_init_code(op, async_data)); @@ -133,7 +168,12 @@ impl Prechecker for PrecheckerImpl { } } -impl PrecheckerImpl { +impl PrecheckerImpl +where + P: Provider, + E: EntryPoint + L1GasProvider, + UO: UserOperation, +{ /// Create a new prechecker pub fn new( chain_spec: ChainSpec, @@ -155,47 +195,37 @@ impl PrecheckerImpl { settings, fee_estimator, cache: RwLock::new(AsyncDataCache { fees: None }), + _uo_type: PhantomData, } } - fn check_init_code( - &self, - op: &UserOperation, - async_data: AsyncData, - ) -> ArrayVec { + fn check_init_code(&self, op: &UO, async_data: AsyncData) -> ArrayVec { let AsyncData { factory_exists, sender_exists, .. } = async_data; let mut violations = ArrayVec::new(); - let len = op.init_code.len(); - if len == 0 { + if op.factory().is_none() { if !sender_exists { violations.push(PrecheckViolation::SenderIsNotContractAndNoInitCode( - op.sender, + op.sender(), )); } } else { - if len < 20 { - violations.push(PrecheckViolation::InitCodeTooShort(len)); - } else if !factory_exists { + if !factory_exists { violations.push(PrecheckViolation::FactoryIsNotContract( op.factory().unwrap(), )) } if sender_exists { - violations.push(PrecheckViolation::ExistingSenderWithInitCode(op.sender)); + violations.push(PrecheckViolation::ExistingSenderWithInitCode(op.sender())); } } violations } - fn check_gas( - &self, - op: &UserOperation, - async_data: AsyncData, - ) -> ArrayVec { + fn check_gas(&self, op: &UO, async_data: AsyncData) -> ArrayVec { let Settings { max_verification_gas, max_total_execution_gas, @@ -208,16 +238,16 @@ impl PrecheckerImpl { } = async_data; let mut violations = ArrayVec::new(); - if op.verification_gas_limit > max_verification_gas { + if op.verification_gas_limit() > max_verification_gas { violations.push(PrecheckViolation::VerificationGasLimitTooHigh( - op.verification_gas_limit, + op.verification_gas_limit(), max_verification_gas, )); } // compute the worst case total gas limit by assuming the UO is in its own bundle and has a postOp call. // This is conservative and potentially may invalidate some very large UOs that would otherwise be valid. - let gas_limit = gas::user_operation_execution_gas_limit(&self.chain_spec, op, true, true); + let gas_limit = gas::user_operation_execution_gas_limit(&self.chain_spec, op, true); if gas_limit > max_total_execution_gas { violations.push(PrecheckViolation::TotalGasLimitTooHigh( gas_limit, @@ -231,9 +261,9 @@ impl PrecheckerImpl { min_pre_verification_gas, self.settings.pre_verification_gas_accept_percent, ); - if op.pre_verification_gas < min_pre_verification_gas { + if op.pre_verification_gas() < min_pre_verification_gas { violations.push(PrecheckViolation::PreVerificationGasTooLow( - op.pre_verification_gas, + op.pre_verification_gas(), min_pre_verification_gas, )); } @@ -248,47 +278,42 @@ impl PrecheckerImpl { let min_max_fee = min_base_fee + min_priority_fee; // check priority fee first, since once ruled out we can check max fee - if op.max_priority_fee_per_gas < min_priority_fee { + if op.max_priority_fee_per_gas() < min_priority_fee { violations.push(PrecheckViolation::MaxPriorityFeePerGasTooLow( - op.max_priority_fee_per_gas, + op.max_priority_fee_per_gas(), min_priority_fee, )); } - if op.max_fee_per_gas < min_max_fee { + if op.max_fee_per_gas() < min_max_fee { violations.push(PrecheckViolation::MaxFeePerGasTooLow( - op.max_fee_per_gas, + op.max_fee_per_gas(), min_max_fee, )); } - if op.call_gas_limit < MIN_CALL_GAS_LIMIT { + if op.call_gas_limit() < MIN_CALL_GAS_LIMIT { violations.push(PrecheckViolation::CallGasLimitTooLow( - op.call_gas_limit, + op.call_gas_limit(), MIN_CALL_GAS_LIMIT, )); } violations } - fn check_payer(&self, op: &UserOperation, async_data: AsyncData) -> Option { + fn check_payer(&self, op: &UO, async_data: AsyncData) -> Option { let AsyncData { paymaster_exists, payer_funds, .. } = async_data; - if !op.paymaster_and_data.is_empty() { - let Some(paymaster) = op.paymaster() else { - return Some(PrecheckViolation::PaymasterTooShort( - op.paymaster_and_data.len(), - )); - }; + if let Some(paymaster) = op.paymaster() { if !paymaster_exists { return Some(PrecheckViolation::PaymasterIsNotContract(paymaster)); } } let max_gas_cost = op.max_gas_cost(); if payer_funds < max_gas_cost { - if op.paymaster_and_data.is_empty() { + if op.paymaster().is_none() { return Some(PrecheckViolation::SenderFundsTooLow( payer_funds, max_gas_cost, @@ -303,7 +328,7 @@ impl PrecheckerImpl { None } - async fn load_async_data(&self, op: &UserOperation) -> anyhow::Result { + async fn load_async_data(&self, op: &UO) -> anyhow::Result { let (_, base_fee) = self.get_fees().await?; let ( @@ -314,7 +339,7 @@ impl PrecheckerImpl { min_pre_verification_gas, ) = tokio::try_join!( self.is_contract(op.factory()), - self.is_contract(Some(op.sender)), + self.is_contract(Some(op.sender())), self.is_contract(op.paymaster()), self.get_payer_funds(op), self.get_required_pre_verification_gas(op.clone(), base_fee) @@ -341,16 +366,16 @@ impl PrecheckerImpl { Ok(!bytecode.is_empty()) } - async fn get_payer_funds(&self, op: &UserOperation) -> anyhow::Result { + async fn get_payer_funds(&self, op: &UO) -> anyhow::Result { let (deposit, balance) = tokio::try_join!(self.get_payer_deposit(op), self.get_payer_balance(op),)?; Ok(deposit + balance) } - async fn get_payer_deposit(&self, op: &UserOperation) -> anyhow::Result { + async fn get_payer_deposit(&self, op: &UO) -> anyhow::Result { let payer = match op.paymaster() { Some(paymaster) => paymaster, - None => op.sender, + None => op.sender(), }; self.entry_point .balance_of(payer, None) @@ -358,13 +383,13 @@ impl PrecheckerImpl { .context("precheck should get payer balance") } - async fn get_payer_balance(&self, op: &UserOperation) -> anyhow::Result { - if !op.paymaster_and_data.is_empty() { + async fn get_payer_balance(&self, op: &UO) -> anyhow::Result { + if op.paymaster().is_some() { // Paymasters must deposit eth, and cannot pay with their own. return Ok(0.into()); } self.provider - .get_balance(op.sender, None) + .get_balance(op.sender(), None) .await .context("precheck should get sender balance") } @@ -378,85 +403,32 @@ impl PrecheckerImpl { async fn get_required_pre_verification_gas( &self, - op: UserOperation, + op: UO, base_fee: U256, ) -> anyhow::Result { - gas::calc_required_pre_verification_gas( - &self.chain_spec, - self.provider.clone(), - &op, - base_fee, - ) - .await - .context("should calculate pre-verification gas") + gas::calc_required_pre_verification_gas(&self.chain_spec, &self.entry_point, &op, base_fee) + .await + .context("should calculate pre-verification gas") } } -/// Precheck violation enumeration -/// -/// All possible errors that can be returned from a precheck. -#[derive(Clone, Debug, parse_display::Display, Eq, PartialEq, Ord, PartialOrd)] -pub enum PrecheckViolation { - /// The init code is too short to contain a factory address. - #[display("initCode must start with a 20-byte factory address, but was only {0} bytes")] - InitCodeTooShort(usize), - /// The sender is not deployed, and no init code is provided. - #[display("sender {0:?} is not a contract and initCode is empty")] - SenderIsNotContractAndNoInitCode(Address), - /// The sender is already deployed, and an init code is provided. - #[display("sender {0:?} is an existing contract, but initCode is nonempty")] - ExistingSenderWithInitCode(Address), - /// An init code contains a factory address that is not deployed. - #[display("initCode indicates factory with no code: {0:?}")] - FactoryIsNotContract(Address), - /// The total gas limit of the user operation is too high. - /// See `gas::user_operation_execution_gas_limit` for calculation. - #[display("total gas limit is {0} but must be at most {1}")] - TotalGasLimitTooHigh(U256, U256), - /// The verification gas limit of the user operation is too high. - #[display("verificationGasLimit is {0} but must be at most {1}")] - VerificationGasLimitTooHigh(U256, U256), - /// The pre-verification gas of the user operation is too low. - #[display("preVerificationGas is {0} but must be at least {1}")] - PreVerificationGasTooLow(U256, U256), - /// The paymaster and data is too short to contain a paymaster address. - #[display("paymasterAndData must start a 20-byte paymaster address, but was only {0} bytes")] - PaymasterTooShort(usize), - /// A paymaster is provided, but the address is not deployed. - #[display("paymasterAndData indicates paymaster with no code: {0:?}")] - PaymasterIsNotContract(Address), - /// The paymaster deposit is too low to pay for the user operation's maximum cost. - #[display("paymaster deposit is {0} but must be at least {1} to pay for this operation")] - PaymasterDepositTooLow(U256, U256), - /// The sender balance is too low to pay for the user operation's maximum cost. - /// (when not using a paymaster) - #[display("sender balance and deposit together is {0} but must be at least {1} to pay for this operation")] - SenderFundsTooLow(U256, U256), - /// The provided max priority fee per gas is too low based on the current network rate. - #[display("maxPriorityFeePerGas is {0} but must be at least {1}")] - MaxPriorityFeePerGasTooLow(U256, U256), - /// The provided max fee per gas is too low based on the current network rate. - #[display("maxFeePerGas is {0} but must be at least {1}")] - MaxFeePerGasTooLow(U256, U256), - /// The call gas limit is too low to account for any possible call. - #[display("callGasLimit is {0} but must be at least {1}")] - CallGasLimitTooLow(U256, U256), -} - #[cfg(test)] mod tests { use std::str::FromStr; use ethers::types::Bytes; - use rundler_provider::{MockEntryPoint, MockProvider}; + use rundler_provider::{MockEntryPointV0_6, MockProvider}; + // TODO: these tests should be made generic on any UserOperation type, can use a mock + // First, need to fix the EntryPoint interface for DepositInfo + use rundler_types::v0_6::UserOperation; use super::*; - fn create_base_config() -> (ChainSpec, MockProvider, MockEntryPoint) { + fn create_base_config() -> (ChainSpec, MockProvider, MockEntryPointV0_6) { ( ChainSpec::default(), MockProvider::new(), - MockEntryPoint::new(), + MockEntryPointV0_6::new(), ) } @@ -479,7 +451,7 @@ mod tests { let op = UserOperation { sender: Address::from_str("0x3f8a2b6c4d5e1079286fa1b3c0d4e5f6902b7c8d").unwrap(), nonce: 100.into(), - init_code: Bytes::from_str("0x1000").unwrap(), + init_code: Bytes::from_str("0x3f8a2b6c4d5e1079286fa1b3c0d4e5f6902b7c8d").unwrap(), call_data: Bytes::default(), call_gas_limit: 9_000.into(), // large call gas limit high to trigger TotalGasLimitTooHigh verification_gas_limit: 10_000_000.into(), @@ -491,15 +463,11 @@ mod tests { }; let res = prechecker.check_init_code(&op, get_test_async_data()); - assert_eq!( - res, - ArrayVec::::from([ - PrecheckViolation::InitCodeTooShort(2), - PrecheckViolation::ExistingSenderWithInitCode( - Address::from_str("0x3f8a2b6c4d5e1079286fa1b3c0d4e5f6902b7c8d").unwrap() - ) - ]) - ); + let mut expected = ArrayVec::new(); + expected.push(PrecheckViolation::ExistingSenderWithInitCode( + Address::from_str("0x3f8a2b6c4d5e1079286fa1b3c0d4e5f6902b7c8d").unwrap(), + )); + assert_eq!(res, expected); } #[tokio::test] @@ -534,7 +502,7 @@ mod tests { res, ArrayVec::::from([ PrecheckViolation::VerificationGasLimitTooHigh(10_000_000.into(), 5_000_000.into(),), - PrecheckViolation::TotalGasLimitTooHigh(30_009_000.into(), 10_000_000.into(),), + PrecheckViolation::TotalGasLimitTooHigh(20_014_000.into(), 10_000_000.into(),), PrecheckViolation::PreVerificationGasTooLow(0.into(), 1_000.into(),), PrecheckViolation::MaxPriorityFeePerGasTooLow(2_000.into(), 4_000.into(),), PrecheckViolation::MaxFeePerGasTooLow(5_000.into(), 8_000.into(),), diff --git a/crates/sim/src/simulation/mempool.rs b/crates/sim/src/simulation/mempool.rs index 4b4ca1302..9947bdb39 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 { @@ -201,10 +211,9 @@ pub(crate) fn match_mempools( #[cfg(test)] mod tests { use ethers::types::U256; - use rundler_types::StorageSlot; + use rundler_types::{pool::NeedsStakeInformation, StorageSlot, ViolationOpCode}; use super::*; - use crate::simulation::{simulation::NeedsStakeInformation, ViolationOpCode}; #[test] fn test_allow_entity_any() { @@ -469,6 +478,7 @@ mod tests { ( H256::random(), MempoolConfig { + entry_point: Address::random(), allowlist: vec![AllowlistEntry::new( AllowEntity::Type(EntityType::Account), AllowRule::ForbiddenOpcode { @@ -501,6 +511,7 @@ mod tests { ( H256::random(), MempoolConfig { + entry_point: Address::random(), allowlist: vec![AllowlistEntry::new( AllowEntity::Type(EntityType::Account), AllowRule::ForbiddenOpcode { @@ -545,6 +556,7 @@ mod tests { ( mempool1, MempoolConfig { + entry_point: Address::random(), allowlist: vec![AllowlistEntry::new( AllowEntity::Type(EntityType::Account), AllowRule::ForbiddenOpcode { @@ -580,6 +592,7 @@ mod tests { ( mempool1, MempoolConfig { + entry_point: Address::random(), allowlist: vec![ AllowlistEntry::new( AllowEntity::Type(EntityType::Account), @@ -601,6 +614,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 5b6a99b76..9a955e2b9 100644 --- a/crates/sim/src/simulation/mod.rs +++ b/crates/sim/src/simulation/mod.rs @@ -11,18 +11,378 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -#[allow(clippy::module_inception)] -mod simulation; +use std::collections::{BTreeSet, HashMap, HashSet}; + +use anyhow::Error; +use ethers::types::{Address, H256, U256}; #[cfg(feature = "test-utils")] -pub use simulation::MockSimulator; -pub(crate) use simulation::REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER; -pub use simulation::{ - EntityInfo, EntityInfos, NeedsStakeInformation, Settings, SimulationError, SimulationResult, - SimulationViolation, Simulator, SimulatorImpl, ViolationOpCode, +use mockall::automock; +use rundler_provider::AggregatorSimOut; +use rundler_types::{ + pool::{MempoolError, SimulationViolation}, + Entity, EntityInfo, EntityInfos, EntityType, StakeInfo, UserOperation, ValidTimeRange, + ValidationOutput, }; +use serde::{Deserialize, Serialize}; + +/// Simulation module for Entry Point v0.6 +pub mod v0_6; mod mempool; pub use mempool::MempoolConfig; -mod tracer; -pub use tracer::{SimulateValidationTracer, SimulateValidationTracerImpl}; +mod unsafe_sim; +pub use unsafe_sim::UnsafeSimulator; + +use crate::{ExpectedStorage, ViolationError}; + +/// The result of a successful simulation +#[derive(Clone, Debug, Default)] +pub struct SimulationResult { + /// The mempool IDs that support this operation + pub mempools: Vec, + /// Block hash this operation was simulated against + pub block_hash: H256, + /// Block number this operation was simulated against + pub block_number: Option, + /// Gas used in the pre-op phase of simulation measured + /// by the entry point + pub pre_op_gas: U256, + /// The time range for which this operation is valid + pub valid_time_range: ValidTimeRange, + /// If using an aggregator, the result of the aggregation + /// simulation + pub aggregator: Option, + /// Code hash of all accessed contracts + pub code_hash: H256, + /// List of used entities that need to be staked for this operation + /// to be valid + pub entities_needing_stake: Vec, + /// Whether the sender account is staked + pub account_is_staked: bool, + /// List of all addresses accessed during validation + pub accessed_addresses: HashSet

, + /// List of addresses that have associated storage slots + /// accessed within the simulation + pub associated_addresses: HashSet
, + /// Expected storage values for all accessed slots during validation + pub expected_storage: ExpectedStorage, + /// Whether the operation requires a post-op + pub requires_post_op: bool, + /// All the entities used in this operation and their staking state + pub entity_infos: EntityInfos, +} + +impl SimulationResult { + /// Get the aggregator address if one was used + pub fn aggregator_address(&self) -> Option
{ + self.aggregator.as_ref().map(|agg| agg.address) + } +} + +/// The result of a failed simulation. We return a list of the violations that ocurred during the failed simulation +/// and also information about all the entities used in the op to handle entity penalties +#[derive(Clone, Debug)] +pub struct SimulationError { + /// A list of violations that occurred during simulation, or some other error that occurred not directly related to simulation rules + pub violation_error: ViolationError, + /// The addresses and staking states of all the entities involved in an op. This value is None when simulation fails at a point where we are no + pub entity_infos: Option, +} + +impl From for SimulationError { + fn from(error: Error) -> Self { + SimulationError { + violation_error: ViolationError::Other(error), + entity_infos: None, + } + } +} + +impl From for MempoolError { + fn from(mut error: SimulationError) -> Self { + let SimulationError { + violation_error, .. + } = &mut error; + let ViolationError::Violations(violations) = violation_error else { + return Self::Other((*violation_error).clone().into()); + }; + + let Some(violation) = violations.iter_mut().min() else { + return Self::Other((*violation_error).clone().into()); + }; + + // extract violation and replace with dummy + Self::SimulationViolation(std::mem::replace( + violation, + SimulationViolation::DidNotRevert, + )) + } +} + +/// Simulator trait for running user operation simulations +#[cfg_attr(feature = "test-utils", automock(type UO = rundler_types::v0_6::UserOperation;))] +#[async_trait::async_trait] +pub trait Simulator: Send + Sync + 'static { + /// The type of user operation that this simulator can handle + type UO: UserOperation; + + /// Simulate a user operation, returning simulation information + /// upon success, or simulation violations. + async fn simulate_validation( + &self, + op: Self::UO, + block_hash: Option, + expected_code_hash: Option, + ) -> Result; +} + +fn entity_type_from_simulation_phase(i: usize) -> Option { + match i { + 0 => Some(EntityType::Factory), + 1 => Some(EntityType::Account), + 2 => Some(EntityType::Paymaster), + _ => None, + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum StorageRestriction { + /// (Entity needing stake, accessing entity type, accessed entity type, accessed address, accessed slot) + NeedsStake(EntityType, EntityType, Option, Address, U256), + Banned(U256), +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct AccessInfo { + // slot value, just prior this current operation + pub(crate) reads: HashMap, + // count of writes. + pub(crate) writes: HashMap, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct AssociatedSlotsByAddress(HashMap>); + +impl AssociatedSlotsByAddress { + pub(crate) fn is_associated_slot(&self, address: Address, slot: U256) -> bool { + if slot == address.as_bytes().into() { + return true; + } + let Some(associated_slots) = self.0.get(&address) else { + return false; + }; + let Some(&next_smallest_slot) = associated_slots.range(..(slot + 1)).next_back() else { + return false; + }; + slot - next_smallest_slot < 128.into() + } + + pub(crate) fn addresses(&self) -> HashSet
{ + self.0.clone().into_keys().collect() + } +} + +#[derive(Clone, Debug)] +struct ParseStorageAccess<'a> { + access_info: &'a AccessInfo, + slots_by_address: &'a AssociatedSlotsByAddress, + address: Address, + sender: Address, + entrypoint: Address, + has_factory: bool, + entity: &'a Entity, +} + +fn parse_storage_accesses(args: ParseStorageAccess<'_>) -> Result, Error> { + let ParseStorageAccess { + access_info, + address, + sender, + entrypoint, + entity, + slots_by_address, + has_factory, + .. + } = args; + + let mut restrictions = vec![]; + + // STO-010 - always allowed to access storage on the account + // [OP-051, OP-054] - block access to the entrypoint, except for depositTo and fallback + // - this is handled at another level, so we don't need to check for it here + // - at this level we can allow any entry point access through + if address.eq(&sender) || address.eq(&entrypoint) { + return Ok(restrictions); + } + + let slots: Vec<&U256> = access_info + .reads + .keys() + .chain(access_info.writes.keys()) + .collect(); + + for slot in slots { + let is_sender_associated = slots_by_address.is_associated_slot(sender, *slot); + // [STO-032] + let is_entity_associated = slots_by_address.is_associated_slot(entity.address, *slot); + // [STO-031] + let is_same_address = address.eq(&entity.address); + // [STO-033] + let is_read_permission = !access_info.writes.contains_key(slot); + + // STO-021 - Associated storage on external contracts is allowed + if is_sender_associated && !is_same_address { + // STO-022 - Factory must be staked to access associated storage in a deploy + if has_factory { + match entity.kind { + EntityType::Paymaster | EntityType::Aggregator => { + // If its a paymaster/aggregator, then the paymaster must be staked to access associated storage + // during a deploy + restrictions.push(StorageRestriction::NeedsStake( + entity.kind, + entity.kind, + Some(EntityType::Account), + address, + *slot, + )); + } + EntityType::Account | EntityType::Factory => { + restrictions.push(StorageRestriction::NeedsStake( + EntityType::Factory, + entity.kind, + Some(EntityType::Account), + address, + *slot, + )); + } + } + } + } else if is_entity_associated || is_same_address { + restrictions.push(StorageRestriction::NeedsStake( + entity.kind, + entity.kind, + Some(entity.kind), + address, + *slot, + )); + } else if is_read_permission { + restrictions.push(StorageRestriction::NeedsStake( + entity.kind, + entity.kind, + None, + address, + *slot, + )); + } else { + restrictions.push(StorageRestriction::Banned(*slot)); + } + } + + Ok(restrictions) +} + +/// Simulation Settings +#[derive(Debug, Copy, Clone)] +pub struct Settings { + /// The minimum amount of time that a staked entity must have configured as + /// their unstake delay on the entry point contract in order to be considered staked. + pub min_unstake_delay: u32, + /// The minimum amount of stake that a staked entity must have on the entry point + /// contract in order to be considered staked. + pub min_stake_value: u128, + /// The maximum amount of gas that can be used during the simulation call + pub max_simulate_handle_ops_gas: u64, + /// The maximum amount of verification gas that can be used during the simulation call + pub max_verification_gas: u64, +} + +impl Settings { + /// Create new settings + pub fn new( + min_unstake_delay: u32, + min_stake_value: u128, + max_simulate_handle_ops_gas: u64, + max_verification_gas: u64, + ) -> Self { + Self { + min_unstake_delay, + min_stake_value, + max_simulate_handle_ops_gas, + max_verification_gas, + } + } +} + +#[cfg(any(test, feature = "test-utils"))] +impl Default for Settings { + fn default() -> Self { + Self { + // one day in seconds: defined in the ERC-4337 spec + min_unstake_delay: 84600, + // 10^18 wei = 1 eth + min_stake_value: 1_000_000_000_000_000_000, + // 550 million gas: currently the defaults for Alchemy eth_call + max_simulate_handle_ops_gas: 550_000_000, + max_verification_gas: 5_000_000, + } + } +} + +fn override_is_staked(ei: &mut EntityInfo, allow_unstaked_addresses: &HashSet
) { + ei.is_staked = allow_unstaked_addresses.contains(&ei.address) || ei.is_staked; +} + +fn override_infos_staked(eis: &mut EntityInfos, allow_unstaked_addresses: &HashSet
) { + override_is_staked(&mut eis.sender, allow_unstaked_addresses); + + if let Some(mut factory) = eis.factory { + override_is_staked(&mut factory, allow_unstaked_addresses); + } + if let Some(mut paymaster) = eis.paymaster { + override_is_staked(&mut paymaster, allow_unstaked_addresses); + } + if let Some(mut aggregator) = eis.aggregator { + override_is_staked(&mut aggregator, allow_unstaked_addresses); + } +} + +fn infos_from_validation_output( + factory_address: Option
, + sender_address: Address, + paymaster_address: Option
, + entry_point_out: &ValidationOutput, + sim_settings: Settings, +) -> EntityInfos { + let factory = factory_address.map(|address| EntityInfo { + address, + is_staked: is_staked(entry_point_out.factory_info, sim_settings), + }); + let sender = EntityInfo { + address: sender_address, + is_staked: is_staked(entry_point_out.sender_info, sim_settings), + }; + let paymaster = paymaster_address.map(|address| EntityInfo { + address, + is_staked: is_staked(entry_point_out.paymaster_info, sim_settings), + }); + let aggregator = entry_point_out + .aggregator_info + .map(|aggregator_info| EntityInfo { + address: aggregator_info.address, + is_staked: is_staked(aggregator_info.stake_info, sim_settings), + }); + + EntityInfos { + factory, + sender, + paymaster, + aggregator, + } +} + +pub(crate) fn is_staked(info: StakeInfo, sim_settings: Settings) -> bool { + info.stake >= sim_settings.min_stake_value.into() + && info.unstake_delay_sec >= sim_settings.min_unstake_delay.into() +} diff --git a/crates/sim/src/simulation/unsafe_sim.rs b/crates/sim/src/simulation/unsafe_sim.rs new file mode 100644 index 000000000..cb2c7b675 --- /dev/null +++ b/crates/sim/src/simulation/unsafe_sim.rs @@ -0,0 +1,194 @@ +// 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::{marker::PhantomData, sync::Arc}; + +use ethers::types::H256; +use rundler_provider::{ + AggregatorOut, EntryPoint, Provider, SignatureAggregator, SimulationProvider, +}; +use rundler_types::{ + pool::SimulationViolation, EntityInfo, EntityInfos, UserOperation, ValidTimeRange, + ValidationError, +}; + +use crate::{ + SimulationError, SimulationResult, SimulationSettings as Settings, Simulator, ViolationError, +}; + +/// An unsafe simulator that can be used in place of a regular simulator +/// to extract the information needed from simulation while avoiding the use +/// of debug_traceCall. +/// +/// WARNING: This is "unsafe" for a reason. None of the ERC-7562 checks are +/// performed. +pub struct UnsafeSimulator { + provider: Arc

, + entry_point: E, + sim_settings: Settings, + _uo_type: PhantomData, +} + +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: PhantomData, + } + } +} + +#[async_trait::async_trait] +impl Simulator for UnsafeSimulator +where + UO: UserOperation, + P: Provider, + E: EntryPoint + SimulationProvider + SignatureAggregator + Clone, +{ + type UO = UO; + + // Run an unsafe simulation + // + // The only validation checks that are performed are signature checks + async fn simulate_validation( + &self, + op: UO, + block_hash: Option, + _expected_code_hash: Option, + ) -> Result { + tracing::info!("Performing unsafe simulation"); + + let (block_hash, block_number) = match block_hash { + // If we are given a block_hash, we return a None block number, avoiding an extra call + Some(block_hash) => (block_hash, None), + None => { + let hash_and_num = self + .provider + .get_latest_block_hash_and_number() + .await + .map_err(anyhow::Error::from)?; + (hash_and_num.0, Some(hash_and_num.1.as_u64())) + } + }; + + // simulate the validation + let validation_result = self + .entry_point + .call_simulate_validation( + op.clone(), + self.sim_settings.max_verification_gas, + Some(block_hash), + ) + .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, valid_until); + let requires_post_op = !validation_result.return_info.paymaster_context.is_empty(); + + let entity_infos = EntityInfos { + sender: EntityInfo { + address: op.sender(), + is_staked: false, + }, + factory: op.factory().map(|f| EntityInfo { + address: f, + is_staked: false, + }), + paymaster: op.paymaster().map(|p| EntityInfo { + address: p, + is_staked: false, + }), + aggregator: validation_result.aggregator_info.map(|a| EntityInfo { + address: a.address, + is_staked: false, + }), + }; + + let mut violations = vec![]; + + let aggregator = if let Some(aggregator_info) = validation_result.aggregator_info { + let agg_out = self + .entry_point + .validate_user_op_signature( + aggregator_info.address, + op, + self.sim_settings.max_verification_gas, + ) + .await?; + + match agg_out { + AggregatorOut::NotNeeded => None, + AggregatorOut::SuccessWithInfo(info) => Some(info), + AggregatorOut::ValidationReverted => { + violations.push(SimulationViolation::AggregatorValidationFailed); + None + } + } + } else { + None + }; + + if validation_result.return_info.account_sig_failed + || validation_result.return_info.paymaster_sig_failed + { + violations.push(SimulationViolation::InvalidSignature); + } + + if !violations.is_empty() { + Err(SimulationError { + violation_error: ViolationError::Violations(violations), + entity_infos: Some(entity_infos), + })? + } else { + Ok(SimulationResult { + mempools: vec![H256::zero()], + block_hash, + block_number, + pre_op_gas, + valid_time_range, + requires_post_op, + entity_infos, + aggregator, + ..Default::default() + }) + } + } +} diff --git a/crates/sim/src/simulation/v0_6/mod.rs b/crates/sim/src/simulation/v0_6/mod.rs new file mode 100644 index 000000000..40dbc3a14 --- /dev/null +++ b/crates/sim/src/simulation/v0_6/mod.rs @@ -0,0 +1,23 @@ +// 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 ethers::types::U256; + +mod simulator; +pub use simulator::Simulator; + +mod tracer; +pub use tracer::{SimulateValidationTracer, SimulateValidationTracerImpl}; + +/// 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/sim/src/simulation/simulation.rs b/crates/sim/src/simulation/v0_6/simulator.rs similarity index 67% rename from crates/sim/src/simulation/simulation.rs rename to crates/sim/src/simulation/v0_6/simulator.rs index 4a13c55df..2176ead0b 100644 --- a/crates/sim/src/simulation/simulation.rs +++ b/crates/sim/src/simulation/v0_6/simulator.rs @@ -18,114 +18,37 @@ use std::{ sync::Arc, }; -use anyhow::Error; use async_trait::async_trait; use ethers::{ abi::AbiDecode, - types::{Address, BlockId, Opcode, H256, U256}, + types::{Address, BlockId, Opcode, H256}, }; use indexmap::IndexSet; -#[cfg(feature = "test-utils")] -use mockall::automock; -use rundler_provider::{AggregatorOut, AggregatorSimOut, Provider}; +use rundler_provider::{ + AggregatorOut, AggregatorSimOut, EntryPoint, Provider, SignatureAggregator, SimulationProvider, +}; use rundler_types::{ - contracts::v0_6::i_entry_point::FailedOp, Entity, EntityType, StakeInfo, StorageSlot, - UserOperation, ValidTimeRange, ValidationOutput, ValidationReturnInfo, + contracts::v0_6::i_entry_point::FailedOp, + pool::{NeedsStakeInformation, SimulationViolation}, + v0_6::UserOperation, + Entity, EntityInfos, EntityType, StorageSlot, UserOperation as UserOperationTrait, + ValidTimeRange, ValidationOutput, ValidationReturnInfo, ViolationOpCode, }; -use strum::IntoEnumIterator; use super::{ - mempool::{match_mempools, AllowEntity, AllowRule, MempoolConfig, MempoolMatchResult}, - tracer::{ - parse_combined_tracer_str, AccessInfo, AssociatedSlotsByAddress, SimulateValidationTracer, - SimulationTracerOutput, - }, + tracer::{parse_combined_tracer_str, SimulateValidationTracer, SimulationTracerOutput}, + REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER, }; use crate::{ - types::{ExpectedStorage, ViolationError}, - utils, + simulation::{ + self, entity_type_from_simulation_phase, + mempool::{match_mempools, AllowEntity, AllowRule, MempoolConfig, MempoolMatchResult}, + ParseStorageAccess, Settings, StorageRestriction, + }, + types::ViolationError, + utils, SimulationError, SimulationResult, }; -/// 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]); - -/// The result of a successful simulation -#[derive(Clone, Debug, Default)] -pub struct SimulationResult { - /// The mempool IDs that support this operation - pub mempools: Vec, - /// Block hash this operation was simulated against - pub block_hash: H256, - /// Block number this operation was simulated against - pub block_number: Option, - /// Gas used in the pre-op phase of simulation measured - /// by the entry point - pub pre_op_gas: U256, - /// The time range for which this operation is valid - pub valid_time_range: ValidTimeRange, - /// If using an aggregator, the result of the aggregation - /// simulation - pub aggregator: Option, - /// Code hash of all accessed contracts - pub code_hash: H256, - /// List of used entities that need to be staked for this operation - /// to be valid - pub entities_needing_stake: Vec, - /// Whether the sender account is staked - pub account_is_staked: bool, - /// List of all addresses accessed during validation - pub accessed_addresses: HashSet

, - /// List of addresses that have associated storage slots - /// accessed within the simulation - pub associated_addresses: HashSet
, - /// Expected storage values for all accessed slots during validation - pub expected_storage: ExpectedStorage, - /// Whether the operation requires a post-op - pub requires_post_op: bool, - /// All the entities used in this operation and their staking state - pub entity_infos: EntityInfos, -} - -impl SimulationResult { - /// Get the aggregator address if one was used - pub fn aggregator_address(&self) -> Option
{ - self.aggregator.as_ref().map(|agg| agg.address) - } -} - -/// The result of a failed simulation. We return a list of the violations that ocurred during the failed simulation -/// and also information about all the entities used in the op to handle entity penalties -#[derive(Clone, Debug)] -pub struct SimulationError { - /// A list of violations that occurred during simulation, or some other error that occurred not directly related to simulation rules - pub violation_error: ViolationError, - /// The addresses and staking states of all the entities involved in an op. This value is None when simulation fails at a point where we are no - pub entity_infos: Option, -} - -impl From for SimulationError { - fn from(error: Error) -> Self { - SimulationError { - violation_error: ViolationError::Other(error), - entity_infos: None, - } - } -} - -/// Simulator trait for running user operation simulations -#[cfg_attr(feature = "test-utils", automock)] -#[async_trait::async_trait] -pub trait Simulator: Send + Sync + 'static { - /// Simulate a user operation, returning simulation information - /// upon success, or simulation violations. - async fn simulate_validation( - &self, - op: UserOperation, - block_hash: Option, - expected_code_hash: Option, - ) -> Result; -} - /// Simulator implementation. /// /// This simulator supports the use of "alternative mempools". @@ -138,18 +61,22 @@ pub trait Simulator: Send + Sync + 'static { /// If no mempools are found, the simulator will return an error containing /// the violations. #[derive(Debug)] -pub struct SimulatorImpl { +pub struct Simulator { provider: Arc

, - entry_point_address: Address, + entry_point: E, simulate_validation_tracer: T, sim_settings: Settings, mempool_configs: HashMap, allow_unstaked_addresses: HashSet

, } -impl SimulatorImpl +impl Simulator where P: Provider, + E: EntryPoint + + SimulationProvider + + SignatureAggregator + + Clone, T: SimulateValidationTracer, { /// Create a new simulator @@ -159,7 +86,7 @@ where /// the violations found during simulation. pub fn new( provider: Arc

, - entry_point_address: Address, + entry_point: E, simulate_validation_tracer: T, sim_settings: Settings, mempool_configs: HashMap, @@ -178,7 +105,7 @@ where Self { provider, - entry_point_address, + entry_point, simulate_validation_tracer, sim_settings, mempool_configs, @@ -186,11 +113,6 @@ where } } - /// Return the associated settings - pub fn settings(&self) -> &Settings { - &self.sim_settings - } - // Run the tracer and transform the output. // Any violations during this stage are errors. async fn create_context( @@ -248,7 +170,7 @@ where entity_infos: None, })? } - let Ok(entry_point_out) = ValidationOutput::decode_hex(revert_data) else { + let Ok(entry_point_out) = ValidationOutput::decode_v0_6_hex(revert_data) else { let entity_addr = match last_entity_type { EntityType::Factory => factory_address, EntityType::Paymaster => paymaster_address, @@ -262,7 +184,7 @@ where entity_infos: None, })? }; - let entity_infos = EntityInfos::new( + let entity_infos = simulation::infos_from_validation_output( factory_address, sender_address, paymaster_address, @@ -279,8 +201,7 @@ where }; let associated_addresses = tracer_out.associated_slots_by_address.addresses(); - - let initcode_length = op.init_code.len(); + let has_factory = op.factory().is_some(); Ok(ValidationContext { op, block_id, @@ -290,7 +211,7 @@ where associated_addresses, entities_needing_stake: vec![], accessed_addresses: HashSet::new(), - initcode_length, + has_factory, }) } @@ -304,11 +225,10 @@ where return Ok(AggregatorOut::NotNeeded); }; - Ok(self - .provider + self.entry_point .clone() .validate_user_op_signature(aggregator_address, op, gas_cap) - .await?) + .await } // Parse the output from tracing and return a list of violations. @@ -325,13 +245,17 @@ where ref entry_point_out, ref mut entities_needing_stake, ref mut accessed_addresses, - initcode_length, + has_factory, .. } = context; let mut violations = vec![]; - if entry_point_out.return_info.sig_failed { + // v0.6 doesn't distinguish between the different types of signature failures + // both of these will be set to true if the signature failed. + if entry_point_out.return_info.account_sig_failed + || entry_point_out.return_info.paymaster_sig_failed + { violations.push(SimulationViolation::InvalidSignature); } @@ -362,7 +286,7 @@ where } for (addr, opcode) in &phase.ext_code_access_info { - if *addr == self.entry_point_address { + if *addr == self.entry_point.address() { violations.push(SimulationViolation::UsedForbiddenOpcode( entity, *addr, @@ -391,18 +315,18 @@ where let address = *addr; accessed_addresses.insert(address); - let violations = parse_storage_accesses(ParseStorageAccess { + let restrictions = simulation::parse_storage_accesses(ParseStorageAccess { access_info, slots_by_address: &tracer_out.associated_slots_by_address, address, sender: sender_address, - entrypoint: self.entry_point_address, - initcode_length, + entrypoint: self.entry_point.address(), + has_factory, entity: &entity, })?; - for violation in violations { - match violation { + for restriction in restrictions { + match restriction { StorageRestriction::NeedsStake( needs_stake, accessing_entity, @@ -453,7 +377,7 @@ where } if let Some(aggregator_info) = entry_point_out.aggregator_info { - if !is_staked(aggregator_info.stake_info, self.sim_settings) { + if !simulation::is_staked(aggregator_info.stake_info, self.sim_settings) { violations.push(SimulationViolation::UnstakedAggregator) } } @@ -488,7 +412,7 @@ where // weird case where CREATE2 is called > 1, but there isn't a factory // defined. This should never happen, blame the violation on the entry point. violations.push(SimulationViolation::FactoryCalledCreate2Twice( - self.entry_point_address, + self.entry_point.address(), )); } } @@ -502,11 +426,11 @@ where .pre_op_gas .saturating_sub(op.pre_verification_gas); let verification_buffer = op - .verification_gas_limit + .total_verification_gas_limit() .saturating_sub(verification_gas_used); if verification_buffer < REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER { violations.push(SimulationViolation::VerificationGasLimitBufferTooLow( - op.verification_gas_limit, + op.total_verification_gas_limit(), verification_gas_used + REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER, )); } @@ -574,11 +498,17 @@ where } #[async_trait] -impl Simulator for SimulatorImpl +impl simulation::Simulator for Simulator where P: Provider, + E: EntryPoint + + SimulationProvider + + SignatureAggregator + + Clone, T: SimulateValidationTracer, { + type UO = UserOperation; + async fn simulate_validation( &self, op: UserOperation, @@ -639,7 +569,7 @@ where sender_info, .. } = entry_point_out; - let account_is_staked = is_staked(sender_info, self.sim_settings); + let account_is_staked = simulation::is_staked(sender_info, self.sim_settings); let ValidationReturnInfo { pre_op_gas, valid_after, @@ -649,9 +579,10 @@ where } = return_info; // Conduct any stake overrides before assigning entity_infos - context - .entity_infos - .override_is_staked(&self.allow_unstaked_addresses); + simulation::override_infos_staked( + &mut context.entity_infos, + &self.allow_unstaked_addresses, + ); Ok(SimulationResult { mempools, @@ -672,102 +603,6 @@ where } } -/// All possible simulation violations -#[derive(Clone, Debug, parse_display::Display, Ord, Eq, PartialOrd, PartialEq)] -pub enum SimulationViolation { - // Make sure to maintain the order here based on the importance - // of the violation for converting to an JSON RPC error - /// The user operation signature is invalid - #[display("invalid signature")] - InvalidSignature, - /// The user operation used an opcode that is not allowed - #[display("{0.kind} uses banned opcode: {2} in contract {1:?}")] - UsedForbiddenOpcode(Entity, Address, ViolationOpCode), - /// The user operation used a precompile that is not allowed - #[display("{0.kind} uses banned precompile: {2:?} in contract {1:?}")] - UsedForbiddenPrecompile(Entity, Address, Address), - /// The user operation accessed a contract that has not been deployed - #[display( - "{0.kind} tried to access code at {1} during validation, but that address is not a contract" - )] - AccessedUndeployedContract(Entity, Address), - /// The user operation factory entity called CREATE2 more than once during initialization - #[display("factory may only call CREATE2 once during initialization")] - FactoryCalledCreate2Twice(Address), - /// The user operation accessed a storage slot that is not allowed - #[display("{0.kind} accessed forbidden storage at address {1:?} during validation")] - InvalidStorageAccess(Entity, StorageSlot), - /// The user operation called an entry point method that is not allowed - #[display("{0.kind} called entry point method other than depositTo")] - CalledBannedEntryPointMethod(Entity), - /// The user operation made a call that contained value to a contract other than the entrypoint - /// during validation - #[display("{0.kind} must not send ETH during validation (except from account to entry point)")] - CallHadValue(Entity), - /// The code hash of accessed contracts changed on the second simulation - #[display("code accessed by validation has changed since the last time validation was run")] - CodeHashChanged, - /// The user operation contained an entity that accessed storage without being staked - #[display("{0.needs_stake} needs to be staked: {0.accessing_entity} accessed storage at {0.accessed_address} slot {0.slot} (associated with {0.accessed_entity:?})")] - NotStaked(Box), - /// The user operation uses a paymaster that returns a context while being unstaked - #[display("Unstaked paymaster must not return context")] - UnstakedPaymasterContext, - /// The user operation uses an aggregator entity and it is not staked - #[display("An aggregator must be staked, regardless of storager usage")] - UnstakedAggregator, - /// Simulation reverted with an unintended reason, containing a message - #[display("reverted while simulating {0} validation: {1}")] - UnintendedRevertWithMessage(EntityType, String, Option

), - /// Simulation reverted with an unintended reason - #[display("reverted while simulating {0} validation")] - UnintendedRevert(EntityType, Option
), - /// Simulation did not revert, a revert is always expected - #[display("simulateValidation did not revert. Make sure your EntryPoint is valid")] - DidNotRevert, - /// Simulation had the wrong number of phases - #[display("simulateValidation should have 3 parts but had {0} instead. Make sure your EntryPoint is valid")] - WrongNumberOfPhases(u32), - /// The user operation ran out of gas during validation - #[display("ran out of gas during {0.kind} validation")] - OutOfGas(Entity), - /// The user operation aggregator signature validation failed - #[display("aggregator signature validation failed")] - AggregatorValidationFailed, - /// Verification gas limit doesn't have the required buffer on the measured gas - #[display("verification gas limit doesn't have the required buffer on the measured gas, limit: {0}, needed: {1}")] - VerificationGasLimitBufferTooLow(U256, U256), -} - -/// A wrapper around Opcode that implements extra traits -#[derive(Debug, PartialEq, Clone, parse_display::Display, Eq)] -#[display("{0:?}")] -pub struct ViolationOpCode(pub Opcode); - -impl PartialOrd for ViolationOpCode { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for ViolationOpCode { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - let left = self.0 as i32; - let right = other.0 as i32; - - left.cmp(&right) - } -} - -fn entity_type_from_simulation_phase(i: usize) -> Option { - match i { - 0 => Some(EntityType::Factory), - 1 => Some(EntityType::Account), - 2 => Some(EntityType::Paymaster), - _ => None, - } -} - #[derive(Debug)] struct ValidationContext { op: UserOperation, @@ -777,300 +612,38 @@ struct ValidationContext { entry_point_out: ValidationOutput, entities_needing_stake: Vec, accessed_addresses: HashSet
, - initcode_length: usize, + has_factory: bool, associated_addresses: HashSet
, } -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -/// additional context about an entity -pub struct EntityInfo { - /// The address of an entity - pub address: Address, - /// Whether the entity is staked or not - pub is_staked: bool, -} - -impl EntityInfo { - fn override_is_staked(&mut self, allow_unstaked_addresses: &HashSet
) { - self.is_staked = allow_unstaked_addresses.contains(&self.address) || self.is_staked; - } -} - -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -/// additional context for all the entities used in an op -pub struct EntityInfos { - /// The entity info for the factory - pub factory: Option, - /// The entity info for the op sender - pub sender: EntityInfo, - /// The entity info for the paymaster - pub paymaster: Option, - /// The entity info for the aggregator - pub aggregator: Option, -} - -impl EntityInfos { - fn new( - factory_address: Option
, - sender_address: Address, - paymaster_address: Option
, - entry_point_out: &ValidationOutput, - sim_settings: Settings, - ) -> Self { - let factory = factory_address.map(|address| EntityInfo { - address, - is_staked: is_staked(entry_point_out.factory_info, sim_settings), - }); - let sender = EntityInfo { - address: sender_address, - is_staked: is_staked(entry_point_out.sender_info, sim_settings), - }; - let paymaster = paymaster_address.map(|address| EntityInfo { - address, - is_staked: is_staked(entry_point_out.paymaster_info, sim_settings), - }); - let aggregator = entry_point_out - .aggregator_info - .map(|aggregator_info| EntityInfo { - address: aggregator_info.address, - is_staked: is_staked(aggregator_info.stake_info, sim_settings), - }); - - Self { - factory, - sender, - paymaster, - aggregator, - } - } - - /// Get iterator over the entities - pub fn entities(&'_ self) -> impl Iterator + '_ { - EntityType::iter().filter_map(|t| self.get(t).map(|info| (t, info))) - } - - fn override_is_staked(&mut self, allow_unstaked_addresses: &HashSet
) { - if let Some(mut factory) = self.factory { - factory.override_is_staked(allow_unstaked_addresses) - } - self.sender.override_is_staked(allow_unstaked_addresses); - if let Some(mut paymaster) = self.paymaster { - paymaster.override_is_staked(allow_unstaked_addresses) - } - if let Some(mut aggregator) = self.aggregator { - aggregator.override_is_staked(allow_unstaked_addresses) - } - } - - /// Get the EntityInfo of a specific entity - pub fn get(self, entity: EntityType) -> Option { - match entity { - EntityType::Factory => self.factory, - EntityType::Account => Some(self.sender), - EntityType::Paymaster => self.paymaster, - EntityType::Aggregator => self.aggregator, - } - } - - fn sender_address(self) -> Address { - self.sender.address - } -} - -fn is_staked(info: StakeInfo, sim_settings: Settings) -> bool { - info.stake >= sim_settings.min_stake_value.into() - && info.unstake_delay_sec >= sim_settings.min_unstake_delay.into() -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum StorageRestriction { - /// (Entity needing stake, accessing entity type, accessed entity type, accessed address, accessed slot) - NeedsStake(EntityType, EntityType, Option, Address, U256), - Banned(U256), -} - -/// Information about a storage violation based on stake status -#[derive(Debug, PartialEq, Clone, PartialOrd, Eq, Ord)] -pub struct NeedsStakeInformation { - /// Entity needing stake info - pub needs_stake: Entity, - /// The entity that accessed the storage requiring stake - pub accessing_entity: EntityType, - /// Type of accessed entity, if it is a known entity - pub accessed_entity: Option, - /// Address that was accessed while unstaked - pub accessed_address: Address, - /// The accessed slot number - pub slot: U256, - /// Minumum stake - pub min_stake: U256, - /// Minumum delay after an unstake event - pub min_unstake_delay: U256, -} - -#[derive(Clone, Debug)] -struct ParseStorageAccess<'a> { - access_info: &'a AccessInfo, - slots_by_address: &'a AssociatedSlotsByAddress, - address: Address, - sender: Address, - entrypoint: Address, - initcode_length: usize, - entity: &'a Entity, -} - -fn parse_storage_accesses(args: ParseStorageAccess<'_>) -> Result, Error> { - let ParseStorageAccess { - access_info, - address, - sender, - entrypoint, - entity, - slots_by_address, - initcode_length, - .. - } = args; - - let mut restrictions = vec![]; - - // STO-010 - always allowed to access storage on the account - // [OP-051, OP-054] - block access to the entrypoint, except for depositTo and fallback - // - this is handled at another level, so we don't need to check for it here - // - at this level we can allow any entry point access through - if address.eq(&sender) || address.eq(&entrypoint) { - return Ok(restrictions); - } - - let slots: Vec<&U256> = access_info - .reads - .keys() - .chain(access_info.writes.keys()) - .collect(); - - for slot in slots { - let is_sender_associated = slots_by_address.is_associated_slot(sender, *slot); - // [STO-032] - let is_entity_associated = slots_by_address.is_associated_slot(entity.address, *slot); - // [STO-031] - let is_same_address = address.eq(&entity.address); - // [STO-033] - let is_read_permission = !access_info.writes.contains_key(slot); - - // STO-021 - Associated storage on external contracts is allowed - if is_sender_associated && !is_same_address { - // STO-022 - Factory must be staked to access associated storage in a deploy - if initcode_length > 2 { - match entity.kind { - EntityType::Paymaster | EntityType::Aggregator => { - // If its a paymaster/aggregator, then the paymaster must be staked to access associated storage - // during a deploy - restrictions.push(StorageRestriction::NeedsStake( - entity.kind, - entity.kind, - Some(EntityType::Account), - address, - *slot, - )); - } - EntityType::Account | EntityType::Factory => { - restrictions.push(StorageRestriction::NeedsStake( - EntityType::Factory, - entity.kind, - Some(EntityType::Account), - address, - *slot, - )); - } - } - } - } else if is_entity_associated || is_same_address { - restrictions.push(StorageRestriction::NeedsStake( - entity.kind, - entity.kind, - Some(entity.kind), - address, - *slot, - )); - } else if is_read_permission { - restrictions.push(StorageRestriction::NeedsStake( - entity.kind, - entity.kind, - None, - address, - *slot, - )); - } else { - restrictions.push(StorageRestriction::Banned(*slot)); - } - } - - Ok(restrictions) -} - -/// Simulation Settings -#[derive(Debug, Copy, Clone)] -pub struct Settings { - /// The minimum amount of time that a staked entity must have configured as - /// their unstake delay on the entry point contract in order to be considered staked. - pub min_unstake_delay: u32, - /// The minimum amount of stake that a staked entity must have on the entry point - /// contract in order to be considered staked. - pub min_stake_value: u128, - /// The maximum amount of gas that can be used during the simulation call - pub max_simulate_handle_ops_gas: u64, - /// The maximum amount of verification gas that can be used during the simulation call - pub max_verification_gas: u64, -} - -impl Settings { - /// Create new settings - pub fn new( - min_unstake_delay: u32, - min_stake_value: u128, - max_simulate_handle_ops_gas: u64, - max_verification_gas: u64, - ) -> Self { - Self { - min_unstake_delay, - min_stake_value, - max_simulate_handle_ops_gas, - max_verification_gas, - } - } -} - -#[cfg(any(test, feature = "test-utils"))] -impl Default for Settings { - fn default() -> Self { - Self { - // one day in seconds: defined in the ERC-4337 spec - min_unstake_delay: 84600, - // 10^18 wei = 1 eth - min_stake_value: 1_000_000_000_000_000_000, - // 550 million gas: currently the defaults for Alchemy eth_call - max_simulate_handle_ops_gas: 550_000_000, - max_verification_gas: 5_000_000, - } - } -} - #[cfg(test)] mod tests { use std::str::FromStr; use ethers::{ abi::AbiEncode, - types::{Address, BlockNumber, Bytes, U64}, + types::{Address, BlockNumber, Bytes, Opcode, U256, U64}, utils::hex, }; - use rundler_provider::{AggregatorOut, MockProvider}; - use rundler_types::contracts::utils::get_code_hashes::CodeHashesResult; + use rundler_provider::{AggregatorOut, MockEntryPointV0_6, MockProvider}; + use rundler_types::{contracts::utils::get_code_hashes::CodeHashesResult, StakeInfo}; use super::*; - use crate::simulation::tracer::{MockSimulateValidationTracer, Phase}; + use crate::simulation::{ + v0_6::tracer::{MockSimulateValidationTracer, Phase}, + AccessInfo, Simulator as SimulatorTrait, + }; - fn create_base_config() -> (MockProvider, MockSimulateValidationTracer) { - (MockProvider::new(), MockSimulateValidationTracer::new()) + fn create_base_config() -> ( + MockProvider, + MockEntryPointV0_6, + MockSimulateValidationTracer, + ) { + ( + MockProvider::new(), + MockEntryPointV0_6::new(), + MockSimulateValidationTracer::new(), + ) } fn get_test_tracer_output() -> SimulationTracerOutput { @@ -1143,8 +716,9 @@ mod tests { fn create_simulator( provider: MockProvider, + entry_point: MockEntryPointV0_6, simulate_validation_tracer: MockSimulateValidationTracer, - ) -> SimulatorImpl { + ) -> Simulator, MockSimulateValidationTracer> { let settings = Settings::default(); let mut mempool_configs = HashMap::new(); @@ -1152,21 +726,24 @@ mod tests { let provider = Arc::new(provider); - let simulator: SimulatorImpl = - SimulatorImpl::new( - Arc::clone(&provider), - Address::from_str("0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789").unwrap(), - simulate_validation_tracer, - settings, - mempool_configs, - ); + let simulator: Simulator< + MockProvider, + Arc, + MockSimulateValidationTracer, + > = Simulator::new( + Arc::clone(&provider), + Arc::new(entry_point), + simulate_validation_tracer, + settings, + mempool_configs, + ); simulator } #[tokio::test] async fn test_simulate_validation() { - let (mut provider, mut tracer) = create_base_config(); + let (mut provider, mut entry_point, mut tracer) = create_base_config(); provider .expect_get_latest_block_hash_and_number() @@ -1197,7 +774,7 @@ mod tests { }) }); - provider + entry_point .expect_validate_user_op_signature() .returning(|_, _, _| Ok(AggregatorOut::NotNeeded)); @@ -1215,7 +792,7 @@ mod tests { signature: Bytes::from_str("0x98f89993ce573172635b44ef3b0741bd0c19dd06909d3539159f6d66bef8c0945550cc858b1cf5921dfce0986605097ba34c2cf3fc279154dd25e161ea7b3d0f1c").unwrap(), }; - let simulator = create_simulator(provider, tracer); + let simulator = create_simulator(provider, entry_point, tracer); let res = simulator .simulate_validation(user_operation, None, None) .await; @@ -1224,7 +801,7 @@ mod tests { #[tokio::test] async fn test_create_context_two_phases_unintended_revert() { - let (provider, mut tracer) = create_base_config(); + let (provider, entry_point, mut tracer) = create_base_config(); tracer .expect_trace_simulate_validation() @@ -1254,7 +831,7 @@ mod tests { signature: Bytes::from_str("0x98f89993ce573172635b44ef3b0741bd0c19dd06909d3539159f6d66bef8c0945550cc858b1cf5921dfce0986605097ba34c2cf3fc279154dd25e161ea7b3d0f1c").unwrap(), }; - let simulator = create_simulator(provider, tracer); + let simulator = create_simulator(provider, entry_point, tracer); let res = simulator .create_context(user_operation, BlockId::Number(BlockNumber::Latest)) .await; @@ -1274,7 +851,10 @@ mod tests { #[tokio::test] async fn test_gather_context_violations() { - let (provider, tracer) = create_base_config(); + let (provider, mut entry_point, tracer) = create_base_config(); + entry_point + .expect_address() + .returning(|| Address::from_str("0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789").unwrap()); let mut tracer_output = get_test_tracer_output(); @@ -1312,10 +892,10 @@ mod tests { pre_verification_gas: U256::from(1000), ..Default::default() }, - initcode_length: 10, + has_factory: true, associated_addresses: HashSet::new(), block_id: BlockId::Number(BlockNumber::Latest), - entity_infos: EntityInfos::new( + entity_infos: simulation::infos_from_validation_output( Some(Address::from_str("0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789").unwrap()), Address::from_str("0xb856dbd4fa1a79a46d426f537455e7d3e79ab7c4").unwrap(), Some(Address::from_str("0x8abb13360b87be5eeb1b98647a016add927a136c").unwrap()), @@ -1354,7 +934,7 @@ mod tests { accessed_addresses: HashSet::new(), }; - let simulator = create_simulator(provider, tracer); + let simulator = create_simulator(provider, entry_point, tracer); let res = simulator.gather_context_violations(&mut validation_context); assert_eq!( @@ -1410,7 +990,7 @@ mod tests { #[tokio::test] async fn test_op_080() { - let (provider, tracer) = create_base_config(); + let (provider, ep, tracer) = create_base_config(); let mut tracer_output = get_test_tracer_output(); @@ -1426,10 +1006,10 @@ mod tests { pre_verification_gas: U256::from(1000), ..Default::default() }, - initcode_length: 10, + has_factory: true, associated_addresses: HashSet::new(), block_id: BlockId::Number(BlockNumber::Latest), - entity_infos: EntityInfos::new( + entity_infos: simulation::infos_from_validation_output( Some(Address::from_str("0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789").unwrap()), Address::from_str("0xb856dbd4fa1a79a46d426f537455e7d3e79ab7c4").unwrap(), Some(Address::from_str("0x8abb13360b87be5eeb1b98647a016add927a136c").unwrap()), @@ -1468,7 +1048,7 @@ mod tests { accessed_addresses: HashSet::new(), }; - let simulator = create_simulator(provider, tracer); + let simulator = create_simulator(provider, ep, tracer); let res = simulator.gather_context_violations(&mut validation_context); // unstaked causes errors @@ -1510,7 +1090,9 @@ mod tests { #[tokio::test] async fn test_factory_staking_logic() { - let (provider, tracer) = create_base_config(); + let (provider, mut ep, tracer) = create_base_config(); + ep.expect_address() + .returning(|| Address::from_str("0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789").unwrap()); let mut writes: HashMap = HashMap::new(); @@ -1541,10 +1123,10 @@ mod tests { pre_verification_gas: U256::from(1000), ..Default::default() }, - initcode_length: 10, + has_factory: true, associated_addresses: HashSet::new(), block_id: BlockId::Number(BlockNumber::Latest), - entity_infos: EntityInfos::new( + entity_infos: simulation::infos_from_validation_output( Some(factory_address), sender_address, None, @@ -1584,7 +1166,7 @@ mod tests { }; // Create the simulator using the provider and tracer - let simulator = create_simulator(provider, tracer); + let simulator = create_simulator(provider, ep, tracer); let res = simulator.gather_context_violations(&mut validation_context); let expected = NeedsStakeInformation { diff --git a/crates/sim/src/simulation/tracer.rs b/crates/sim/src/simulation/v0_6/tracer.rs similarity index 73% rename from crates/sim/src/simulation/tracer.rs rename to crates/sim/src/simulation/v0_6/tracer.rs index bbe93eb25..27af6b1e3 100644 --- a/crates/sim/src/simulation/tracer.rs +++ b/crates/sim/src/simulation/v0_6/tracer.rs @@ -10,26 +10,24 @@ // 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::{BTreeSet, HashMap, HashSet}, - convert::TryFrom, - fmt::Debug, - sync::Arc, -}; +use std::{collections::HashMap, convert::TryFrom, fmt::Debug, sync::Arc}; use anyhow::{bail, Context}; use async_trait::async_trait; use ethers::types::{ Address, BlockId, GethDebugTracerType, GethDebugTracingCallOptions, GethDebugTracingOptions, - GethTrace, Opcode, U256, + GethTrace, Opcode, }; #[cfg(test)] use mockall::automock; -use rundler_provider::{EntryPoint, Provider}; -use rundler_types::UserOperation; +use rundler_provider::{Provider, SimulationProvider}; +use rundler_types::v0_6::UserOperation; use serde::{Deserialize, Serialize}; -use crate::ExpectedStorage; +use crate::{ + simulation::{AccessInfo, AssociatedSlotsByAddress}, + ExpectedStorage, +}; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -68,37 +66,6 @@ pub(crate) struct Phase { pub(crate) ext_code_access_info: HashMap, } -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct AccessInfo { - // slot value, just prior this current operation - pub(crate) reads: HashMap, - // count of writes. - pub(crate) writes: HashMap, -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub(crate) struct AssociatedSlotsByAddress(HashMap>); - -impl AssociatedSlotsByAddress { - pub(crate) fn is_associated_slot(&self, address: Address, slot: U256) -> bool { - if slot == address.as_bytes().into() { - return true; - } - let Some(associated_slots) = self.0.get(&address) else { - return false; - }; - let Some(&next_smallest_slot) = associated_slots.range(..(slot + 1)).next_back() else { - return false; - }; - slot - next_smallest_slot < 128.into() - } - - pub(crate) fn addresses(&self) -> HashSet
{ - self.0.clone().into_keys().collect() - } -} - /// Trait for tracing the simulation of a user operation. #[cfg_attr(test, automock)] #[async_trait] @@ -114,11 +81,7 @@ pub trait SimulateValidationTracer: Send + Sync + 'static { /// Tracer implementation for the bundler's custom tracer. #[derive(Debug)] -pub struct SimulateValidationTracerImpl -where - P: Provider, - E: EntryPoint, -{ +pub struct SimulateValidationTracerImpl { provider: Arc

, entry_point: E, } @@ -130,7 +93,7 @@ where impl SimulateValidationTracer for SimulateValidationTracerImpl where P: Provider, - E: EntryPoint, + E: SimulationProvider, { async fn trace_simulate_validation( &self, @@ -138,10 +101,9 @@ where block_id: BlockId, max_validation_gas: u64, ) -> anyhow::Result { - let tx = self + let (tx, state_override) = self .entry_point - .get_simulate_validation_call(op, max_validation_gas) - .await?; + .get_tracer_simulate_validation_call(op, max_validation_gas); SimulationTracerOutput::try_from( self.provider @@ -155,7 +117,7 @@ where )), ..Default::default() }, - ..Default::default() + state_overrides: Some(state_override), }, ) .await?, @@ -163,11 +125,7 @@ where } } -impl SimulateValidationTracerImpl -where - P: Provider, - E: EntryPoint, -{ +impl SimulateValidationTracerImpl { /// Creates a new instance of the bundler's custom tracer. pub fn new(provider: Arc

, entry_point: E) -> Self { Self { @@ -178,7 +136,7 @@ where } fn validation_tracer_js() -> &'static str { - include_str!("../../tracer/dist/validationTracer.js").trim_end_matches(";export{};") + include_str!("../../../tracer/dist/validationTracer.js").trim_end_matches(";export{};") } pub(crate) fn parse_combined_tracer_str(combined: &str) -> anyhow::Result<(A, B)> 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 bd0bbfeff..7a41e1ac3 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -10,13 +10,25 @@ repository.workspace = true rundler-utils = { path = "../utils" } anyhow.workspace = true +async-trait.workspace = true chrono = "0.4.24" constcat = "0.4.1" ethers.workspace = true -parse-display = "0.9.0" +futures-util.workspace = true +parse-display.workspace = true +rand.workspace = true serde.workspace = true serde_json.workspace = true strum.workspace = true +thiserror.workspace = true + +mockall = {workspace = true, optional = true } [build-dependencies] ethers.workspace = true + +[dev-dependencies] +rundler-types = { path = ".", features = ["test-utils"] } + +[features] +test-utils = [ "mockall" ] diff --git a/crates/types/build.rs b/crates/types/build.rs index e47ae8fdc..b6ffe04e2 100644 --- a/crates/types/build.rs +++ b/crates/types/build.rs @@ -38,7 +38,6 @@ fn generate_v0_6_bindings() -> Result<(), Box> { MultiAbigen::from_abigens([ abigen_of("v0_6", "IEntryPoint")?, - abigen_of("v0_6", "EntryPoint")?, abigen_of("v0_6", "IAggregator")?, abigen_of("v0_6", "IStakeManager")?, abigen_of("v0_6", "GetBalances")?, @@ -64,9 +63,10 @@ fn generate_v0_7_bindings() -> Result<(), Box> { MultiAbigen::from_abigens([ abigen_of("v0_7", "IEntryPoint")?, - abigen_of("v0_7", "EntryPoint")?, abigen_of("v0_7", "IAggregator")?, abigen_of("v0_7", "IStakeManager")?, + abigen_of("v0_7", "GetBalances")?, + abigen_of("v0_7", "EntryPointSimulations")?, ]) .build()? .write_to_module("src/contracts/v0_7", false)?; diff --git a/crates/types/contracts/src/v0_6/imports.sol b/crates/types/contracts/src/v0_6/imports.sol index fbb9f4ab1..625263d49 100644 --- a/crates/types/contracts/src/v0_6/imports.sol +++ b/crates/types/contracts/src/v0_6/imports.sol @@ -6,6 +6,6 @@ pragma solidity ^0.8.13; import "account-abstraction/v0_6/samples/SimpleAccount.sol"; import "account-abstraction/v0_6/samples/SimpleAccountFactory.sol"; import "account-abstraction/v0_6/samples/VerifyingPaymaster.sol"; -import "account-abstraction/v0_6/core/EntryPoint.sol"; +import "account-abstraction/v0_6/interfaces/IEntryPoint.sol"; import "account-abstraction/v0_6/interfaces/IAggregator.sol"; import "account-abstraction/v0_6/interfaces/IStakeManager.sol"; diff --git a/crates/types/contracts/src/v0_7/GetBalances.sol b/crates/types/contracts/src/v0_7/GetBalances.sol new file mode 100644 index 000000000..bda81addc --- /dev/null +++ b/crates/types/contracts/src/v0_7/GetBalances.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.12; + +import "account-abstraction/v0_7/interfaces/IStakeManager.sol"; + +contract GetBalances { + error GetBalancesResult(uint256[] balances); + + constructor(address stakeManager, address[] memory addresses) { + revert GetBalancesResult(getBalancesHelper(stakeManager, addresses)); + } + + function getBalancesHelper(address stakeManager, address[] memory addresses) public view returns (uint256[] memory) { + uint256[] memory balances = new uint256[](addresses.length); + IStakeManager istakeManager = IStakeManager(stakeManager); + + for (uint256 i = 0; i < addresses.length; i++) { + balances[i] = istakeManager.balanceOf(addresses[i]); + } + + return balances; + } +} diff --git a/crates/types/contracts/src/v0_7/imports.sol b/crates/types/contracts/src/v0_7/imports.sol index 72be598ea..592dddd3a 100644 --- a/crates/types/contracts/src/v0_7/imports.sol +++ b/crates/types/contracts/src/v0_7/imports.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.13; // Simply importing a dependency is enough for Forge to include it in builds. -import "account-abstraction/v0_7/core/EntryPoint.sol"; +import "account-abstraction/v0_7/interfaces/IEntryPoint.sol"; import "account-abstraction/v0_7/interfaces/IAggregator.sol"; import "account-abstraction/v0_7/interfaces/IStakeManager.sol"; +import "account-abstraction/v0_7/core/EntryPointSimulations.sol"; diff --git a/crates/pool/src/server/error.rs b/crates/types/src/builder/error.rs similarity index 59% rename from crates/pool/src/server/error.rs rename to crates/types/src/builder/error.rs index fc07035ad..bfe06d6ee 100644 --- a/crates/pool/src/server/error.rs +++ b/crates/types/src/builder/error.rs @@ -11,27 +11,13 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use crate::mempool::MempoolError; - -/// Pool server error type +/// Builder server errors #[derive(Debug, thiserror::Error)] -pub enum PoolServerError { - /// Mempool error occurred - #[error(transparent)] - MempoolError(MempoolError), - /// Unexpected response from PoolServer - #[error("Unexpected response from PoolServer")] +pub enum BuilderError { + /// Builder returned an unexpected response type for the given request + #[error("Unexpected response from Builder")] UnexpectedResponse, - /// Internal error + /// Internal errors #[error(transparent)] Other(#[from] anyhow::Error), } - -impl From for PoolServerError { - fn from(error: MempoolError) -> Self { - match error { - MempoolError::Other(e) => Self::Other(e), - _ => Self::MempoolError(error), - } - } -} diff --git a/crates/types/src/builder/mod.rs b/crates/types/src/builder/mod.rs new file mode 100644 index 000000000..152a572eb --- /dev/null +++ b/crates/types/src/builder/mod.rs @@ -0,0 +1,23 @@ +// 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/. + +//! Rundler builder types + +mod error; +pub use error::*; + +mod traits; +pub use traits::*; + +mod types; +pub use types::*; diff --git a/crates/types/src/builder/traits.rs b/crates/types/src/builder/traits.rs new file mode 100644 index 000000000..208969a79 --- /dev/null +++ b/crates/types/src/builder/traits.rs @@ -0,0 +1,37 @@ +// 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 ethers::types::{Address, H256}; +#[cfg(feature = "test-utils")] +use mockall::automock; + +use super::{error::BuilderError, types::BundlingMode}; + +/// Builder result +pub type BuilderResult = std::result::Result; + +/// Builder +#[cfg_attr(feature = "test-utils", automock)] +#[async_trait::async_trait] +pub trait Builder: Send + Sync + 'static { + /// Get the supported entry points of this builder + async fn get_supported_entry_points(&self) -> BuilderResult>; + + /// Trigger the builder to send a bundle now, used for debugging. + /// + /// Bundling mode must be set to `Manual`, or this will error + async fn debug_send_bundle_now(&self) -> BuilderResult<(H256, u64)>; + + /// Set the bundling mode + async fn debug_set_bundling_mode(&self, mode: BundlingMode) -> BuilderResult<()>; +} diff --git a/crates/types/src/builder/types.rs b/crates/types/src/builder/types.rs new file mode 100644 index 000000000..52a9e00e8 --- /dev/null +++ b/crates/types/src/builder/types.rs @@ -0,0 +1,30 @@ +// 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 parse_display::Display; +use serde::{Deserialize, Serialize}; + +/// Builder bundling mode +#[derive(Display, Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +#[display(style = "lowercase")] +#[serde(rename_all = "lowercase")] +pub enum BundlingMode { + /// Manual bundling mode for debugging. + /// + /// Bundles will only be sent when `debug_send_bundle_now` is called. + Manual, + /// Auto bundling mode for normal operation. + /// + /// Bundles will be sent automatically. + Auto, +} diff --git a/crates/types/src/chain.rs b/crates/types/src/chain.rs index 66c140b4d..1024a1107 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, /// Overhead when preforming gas estimation to account for the deposit storage /// and transfer overhead. /// @@ -119,7 +122,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(), deposit_transfer_overhead: U256::from(30000), eip1559_enabled: true, calldata_pre_verification_gas: false, diff --git a/crates/types/src/entity.rs b/crates/types/src/entity.rs index 8da03ebb7..55896a6b3 100644 --- a/crates/types/src/entity.rs +++ b/crates/types/src/entity.rs @@ -17,7 +17,7 @@ use anyhow::bail; use ethers::{types::Address, utils::to_checksum}; use parse_display::Display; use serde::{ser::SerializeStruct, Deserialize, Serialize}; -use strum::EnumIter; +use strum::{EnumIter, IntoEnumIterator}; /// The type of an entity #[derive( @@ -156,3 +156,74 @@ pub struct EntityUpdate { /// The kind of update to perform for the entity pub update_type: EntityUpdateType, } + +/// additional context about an entity +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct EntityInfo { + /// The address of an entity + pub address: Address, + /// Whether the entity is staked or not + pub is_staked: bool, +} + +/// additional context for all the entities used in an op +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct EntityInfos { + /// The entity info for the factory + pub factory: Option, + /// The entity info for the op sender + pub sender: EntityInfo, + /// The entity info for the paymaster + pub paymaster: Option, + /// The entity info for the aggregator + pub aggregator: Option, +} + +impl EntityInfos { + /// Get iterator over the entities + pub fn entities(&'_ self) -> impl Iterator + '_ { + EntityType::iter().filter_map(|t| self.get(t).map(|info| (t, info))) + } + + /// Get the EntityInfo of a specific entity + pub fn get(self, entity: EntityType) -> Option { + match entity { + EntityType::Factory => self.factory, + EntityType::Account => Some(self.sender), + EntityType::Paymaster => self.paymaster, + EntityType::Aggregator => self.aggregator, + } + } + + /// Get the type of an entity from its address, if any + pub fn type_from_address(self, address: Address) -> Option { + if address.eq(&self.sender.address) { + return Some(EntityType::Account); + } + + if let Some(factory) = self.factory { + if address.eq(&factory.address) { + return Some(EntityType::Factory); + } + } + + if let Some(paymaster) = self.paymaster { + if address.eq(&paymaster.address) { + return Some(EntityType::Paymaster); + } + } + + if let Some(aggregator) = self.aggregator { + if address.eq(&aggregator.address) { + return Some(EntityType::Aggregator); + } + } + + None + } + + /// Get the sender address + pub fn sender_address(self) -> Address { + self.sender.address + } +} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index ce2f45b71..30d11dd48 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -20,26 +20,35 @@ //! Rundler common types +pub mod builder; + pub mod chain; #[rustfmt::skip] pub mod contracts; -pub use contracts::v0_6::shared_types::{DepositInfo, UserOperation, UserOpsPerAggregator}; mod entity; -pub use entity::{Entity, EntityType, EntityUpdate, EntityUpdateType}; +pub use entity::{Entity, EntityInfo, EntityInfos, EntityType, EntityUpdate, EntityUpdateType}; + +mod opcode; +pub use opcode::ViolationOpCode; mod gas; pub use gas::GasFees; +pub mod pool; + mod timestamp; pub use timestamp::{Timestamp, ValidTimeRange}; mod user_operation; -pub use user_operation::UserOperationId; +pub use user_operation::*; mod storage; pub use storage::StorageSlot; mod validation_results; -pub use validation_results::{AggregatorInfo, StakeInfo, ValidationOutput, ValidationReturnInfo}; +pub use validation_results::{ + parse_validation_data, AggregatorInfo, StakeInfo, ValidationError, ValidationOutput, + ValidationReturnInfo, ValidationRevert, +}; diff --git a/crates/types/src/opcode.rs b/crates/types/src/opcode.rs new file mode 100644 index 000000000..85a478175 --- /dev/null +++ b/crates/types/src/opcode.rs @@ -0,0 +1,34 @@ +// 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 ethers::types::Opcode; + +/// A wrapper around Opcode that implements extra traits +#[derive(Debug, PartialEq, Clone, parse_display::Display, Eq)] +#[display("{0:?}")] +pub struct ViolationOpCode(pub Opcode); + +impl PartialOrd for ViolationOpCode { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ViolationOpCode { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let left = self.0 as i32; + let right = other.0 as i32; + + left.cmp(&right) + } +} diff --git a/crates/types/src/pool/error.rs b/crates/types/src/pool/error.rs new file mode 100644 index 000000000..fbc483cbe --- /dev/null +++ b/crates/types/src/pool/error.rs @@ -0,0 +1,230 @@ +// 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 ethers::types::{Address, U256}; + +use crate::{ + validation_results::ValidationRevert, Entity, EntityType, StorageSlot, ViolationOpCode, +}; + +/// Pool server error type +#[derive(Debug, thiserror::Error)] +pub enum PoolError { + /// Mempool error occurred + #[error(transparent)] + MempoolError(MempoolError), + /// Unexpected response from PoolServer + #[error("Unexpected response from PoolServer")] + UnexpectedResponse, + /// Internal error + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +impl From for PoolError { + fn from(error: MempoolError) -> Self { + match error { + MempoolError::Other(e) => Self::Other(e), + _ => Self::MempoolError(error), + } + } +} + +/// Mempool error type. +#[derive(Debug, thiserror::Error)] +pub enum MempoolError { + /// Some other error occurred + #[error(transparent)] + Other(#[from] anyhow::Error), + /// Operation with the same hash already in pool + #[error("Operation already known")] + OperationAlreadyKnown, + /// Operation with same sender/nonce already in pool + /// and the replacement operation has lower gas price. + #[error("Replacement operation underpriced. Existing priority fee: {0}. Existing fee: {1}")] + ReplacementUnderpriced(U256, U256), + /// Max operations reached for unstaked sender [UREP-010] or unstaked non-sender entity [UREP-020] + #[error("Max operations ({0}) reached for entity {1}")] + MaxOperationsReached(usize, Address), + /// Multiple roles violation + /// Spec rule: STO-040 + #[error("A {} at {} in this UserOperation is used as a sender entity in another UserOperation currently in mempool.", .0.kind, .0.address)] + MultipleRolesViolation(Entity), + /// An associated storage slot that is accessed in the UserOperation is being used as a sender by another UserOperation in the mempool. + /// Spec rule: STO-041 + #[error("An associated storage slot that is accessed in the UserOperation is being used as a sender by another UserOperation in the mempool")] + AssociatedStorageIsAlternateSender, + /// Sender address used as different entity in another UserOperation currently in the mempool. + /// Spec rule: STO-040 + #[error("The sender address {0} is used as a different entity in another UserOperation currently in mempool")] + SenderAddressUsedAsAlternateEntity(Address), + /// An entity associated with the operation is throttled/banned. + #[error("Entity {0} is throttled/banned")] + EntityThrottled(Entity), + /// Operation was discarded on inserting due to size limit + #[error("Operation was discarded on inserting")] + DiscardedOnInsert, + /// Paymaster balance too low + /// Spec rule: EREP-010 + #[error("Paymaster balance too low. Required balance: {0}. Current balance {1}")] + PaymasterBalanceTooLow(U256, U256), + /// Operation was rejected due to a precheck violation + #[error("Operation violation during precheck {0}")] + PrecheckViolation(PrecheckViolation), + /// Operation was rejected due to a simulation violation + #[error("Operation violation during simulation {0}")] + SimulationViolation(SimulationViolation), + /// Operation was rejected because it used an unsupported aggregator + #[error("Unsupported aggregator {0}")] + UnsupportedAggregator(Address), + /// An unknown entry point was specified + #[error("Unknown entry point {0}")] + UnknownEntryPoint(Address), + /// The operation drop attempt too soon after being added to the pool + #[error("Operation drop attempt too soon after being added to the pool. Added at {0}, attempted to drop at {1}, must wait {2} blocks.")] + OperationDropTooSoon(u64, u64, u64), +} + +/// Precheck violation enumeration +/// +/// All possible errors that can be returned from a precheck. +#[derive(Clone, Debug, parse_display::Display, Eq, PartialEq, Ord, PartialOrd)] +pub enum PrecheckViolation { + /// The sender is not deployed, and no init code is provided. + #[display("sender {0:?} is not a contract and initCode is empty")] + SenderIsNotContractAndNoInitCode(Address), + /// The sender is already deployed, and an init code is provided. + #[display("sender {0:?} is an existing contract, but initCode is nonempty")] + ExistingSenderWithInitCode(Address), + /// An init code contains a factory address that is not deployed. + #[display("initCode indicates factory with no code: {0:?}")] + FactoryIsNotContract(Address), + /// The total gas limit of the user operation is too high. + /// See `gas::user_operation_execution_gas_limit` for calculation. + #[display("total gas limit is {0} but must be at most {1}")] + TotalGasLimitTooHigh(U256, U256), + /// The verification gas limit of the user operation is too high. + #[display("verificationGasLimit is {0} but must be at most {1}")] + VerificationGasLimitTooHigh(U256, U256), + /// The pre-verification gas of the user operation is too low. + #[display("preVerificationGas is {0} but must be at least {1}")] + PreVerificationGasTooLow(U256, U256), + /// A paymaster is provided, but the address is not deployed. + #[display("paymasterAndData indicates paymaster with no code: {0:?}")] + PaymasterIsNotContract(Address), + /// The paymaster deposit is too low to pay for the user operation's maximum cost. + #[display("paymaster deposit is {0} but must be at least {1} to pay for this operation")] + PaymasterDepositTooLow(U256, U256), + /// The sender balance is too low to pay for the user operation's maximum cost. + /// (when not using a paymaster) + #[display("sender balance and deposit together is {0} but must be at least {1} to pay for this operation")] + SenderFundsTooLow(U256, U256), + /// The provided max priority fee per gas is too low based on the current network rate. + #[display("maxPriorityFeePerGas is {0} but must be at least {1}")] + MaxPriorityFeePerGasTooLow(U256, U256), + /// The provided max fee per gas is too low based on the current network rate. + #[display("maxFeePerGas is {0} but must be at least {1}")] + MaxFeePerGasTooLow(U256, U256), + /// The call gas limit is too low to account for any possible call. + #[display("callGasLimit is {0} but must be at least {1}")] + CallGasLimitTooLow(U256, U256), +} + +/// All possible simulation violations +#[derive(Clone, Debug, parse_display::Display, Ord, Eq, PartialOrd, PartialEq)] +pub enum SimulationViolation { + // Make sure to maintain the order here based on the importance + // of the violation for converting to an JSON RPC error + /// The user operation signature is invalid + #[display("invalid signature")] + InvalidSignature, + /// The user operation used an opcode that is not allowed + #[display("{0.kind} uses banned opcode: {2} in contract {1:?}")] + UsedForbiddenOpcode(Entity, Address, ViolationOpCode), + /// The user operation used a precompile that is not allowed + #[display("{0.kind} uses banned precompile: {2:?} in contract {1:?}")] + UsedForbiddenPrecompile(Entity, Address, Address), + /// The user operation accessed a contract that has not been deployed + #[display( + "{0.kind} tried to access code at {1} during validation, but that address is not a contract" + )] + AccessedUndeployedContract(Entity, Address), + /// The user operation factory entity called CREATE2 more than once during initialization + #[display("factory may only call CREATE2 once during initialization")] + FactoryCalledCreate2Twice(Address), + /// The user operation accessed a storage slot that is not allowed + #[display("{0.kind} accessed forbidden storage at address {1:?} during validation")] + InvalidStorageAccess(Entity, StorageSlot), + /// The user operation called an entry point method that is not allowed + #[display("{0.kind} called entry point method other than depositTo")] + CalledBannedEntryPointMethod(Entity), + /// The user operation made a call that contained value to a contract other than the entrypoint + /// during validation + #[display("{0.kind} must not send ETH during validation (except from account to entry point)")] + CallHadValue(Entity), + /// The code hash of accessed contracts changed on the second simulation + #[display("code accessed by validation has changed since the last time validation was run")] + CodeHashChanged, + /// The user operation contained an entity that accessed storage without being staked + #[display("{0.needs_stake} needs to be staked: {0.accessing_entity} accessed storage at {0.accessed_address} slot {0.slot} (associated with {0.accessed_entity:?})")] + NotStaked(Box), + /// The user operation uses a paymaster that returns a context while being unstaked + #[display("Unstaked paymaster must not return context")] + UnstakedPaymasterContext, + /// The user operation uses an aggregator entity and it is not staked + #[display("An aggregator must be staked, regardless of storager usage")] + UnstakedAggregator, + /// Simulation reverted with an unintended reason, containing a message + #[display("reverted while simulating {0} validation: {1}")] + UnintendedRevertWithMessage(EntityType, String, Option

), + /// 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, + /// Simulation had the wrong number of phases + #[display("simulateValidation should have 3 parts but had {0} instead. Make sure your EntryPoint is valid")] + WrongNumberOfPhases(u32), + /// The user operation ran out of gas during validation + #[display("ran out of gas during {0.kind} validation")] + OutOfGas(Entity), + /// The user operation aggregator signature validation failed + #[display("aggregator signature validation failed")] + AggregatorValidationFailed, + /// Verification gas limit doesn't have the required buffer on the measured gas + #[display("verification gas limit doesn't have the required buffer on the measured gas, limit: {0}, needed: {1}")] + VerificationGasLimitBufferTooLow(U256, U256), +} + +/// Information about a storage violation based on stake status +#[derive(Debug, PartialEq, Clone, PartialOrd, Eq, Ord)] +pub struct NeedsStakeInformation { + /// Entity needing stake info + pub needs_stake: Entity, + /// The entity that accessed the storage requiring stake + pub accessing_entity: EntityType, + /// Type of accessed entity, if it is a known entity + pub accessed_entity: Option, + /// Address that was accessed while unstaked + pub accessed_address: Address, + /// The accessed slot number + pub slot: U256, + /// Minumum stake + pub min_stake: U256, + /// Minumum delay after an unstake event + pub min_unstake_delay: U256, +} diff --git a/crates/types/src/pool/mod.rs b/crates/types/src/pool/mod.rs new file mode 100644 index 000000000..d17f5bfd9 --- /dev/null +++ b/crates/types/src/pool/mod.rs @@ -0,0 +1,36 @@ +// 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/. + +// 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/. + +//! Rundler pool types + +mod error; +pub use error::*; + +mod traits; +pub use traits::*; + +mod types; +pub use types::*; diff --git a/crates/types/src/pool/traits.rs b/crates/types/src/pool/traits.rs new file mode 100644 index 000000000..8b9db7104 --- /dev/null +++ b/crates/types/src/pool/traits.rs @@ -0,0 +1,124 @@ +// 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::pin::Pin; + +use ethers::types::{Address, H256}; +use futures_util::Stream; +#[cfg(feature = "test-utils")] +use mockall::automock; + +use super::{ + error::PoolError, + types::{NewHead, PaymasterMetadata, PoolOperation, Reputation, ReputationStatus, StakeStatus}, +}; +use crate::{EntityUpdate, UserOperationId, UserOperationVariant}; + +/// Result type for pool server operations. +pub type PoolResult = std::result::Result; + +/// Pool server trait +#[cfg_attr(feature = "test-utils", automock)] +#[async_trait::async_trait] +pub trait Pool: Send + Sync + 'static { + /// Get the supported entry points of the pool + async fn get_supported_entry_points(&self) -> PoolResult>; + + /// Add an operation to the pool + async fn add_op(&self, entry_point: Address, op: UserOperationVariant) -> PoolResult; + + /// Get operations from the pool + async fn get_ops( + &self, + entry_point: Address, + max_ops: u64, + shard_index: u64, + ) -> PoolResult>; + + /// Get an operation from the pool by hash + /// Checks each entry point in order until the operation is found + /// Returns None if the operation is not found + async fn get_op_by_hash(&self, hash: H256) -> PoolResult>; + + /// Remove operations from the pool by hash + async fn remove_ops(&self, entry_point: Address, ops: Vec) -> PoolResult<()>; + + /// Remove an operation from the pool by id + async fn remove_op_by_id( + &self, + entry_point: Address, + id: UserOperationId, + ) -> PoolResult>; + + /// Update operations associated with entities from the pool + async fn update_entities( + &self, + entry_point: Address, + entities: Vec, + ) -> PoolResult<()>; + + /// Subscribe to new chain heads from the pool. + /// + /// The pool will notify the subscriber when a new chain head is received, and the pool + /// has processed all operations up to that head. + async fn subscribe_new_heads(&self) -> PoolResult + Send>>>; + + /// Get reputation status given entrypoint and address + async fn get_reputation_status( + &self, + entry_point: Address, + address: Address, + ) -> PoolResult; + + /// Get stake status given entrypoint and address + async fn get_stake_status( + &self, + entry_point: Address, + address: Address, + ) -> PoolResult; + + /// Clear the pool state, used for debug methods + async fn debug_clear_state( + &self, + clear_mempool: bool, + clear_paymaster: bool, + clear_reputation: bool, + ) -> PoolResult<()>; + + /// Dump all operations in the pool, used for debug methods + async fn debug_dump_mempool(&self, entry_point: Address) -> PoolResult>; + + /// Set reputations for entities, used for debug methods + async fn debug_set_reputations( + &self, + entry_point: Address, + reputations: Vec, + ) -> PoolResult<()>; + + /// Dump reputations for entities, used for debug methods + async fn debug_dump_reputation(&self, entry_point: Address) -> PoolResult>; + + /// Dump paymaster balances, used for debug methods + async fn debug_dump_paymaster_balances( + &self, + entry_point: Address, + ) -> PoolResult>; + + /// Controls whether or not the certain tracking data structures are used to block user operations + async fn admin_set_tracking( + &self, + entry_point: Address, + paymaster: bool, + reputation: bool, + ) -> PoolResult<()>; +} diff --git a/crates/types/src/pool/types.rs b/crates/types/src/pool/types.rs new file mode 100644 index 000000000..694f29f02 --- /dev/null +++ b/crates/types/src/pool/types.rs @@ -0,0 +1,189 @@ +// 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 ethers::types::{Address, H256, U256}; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +use crate::{ + entity::EntityInfos, Entity, EntityType, StakeInfo, UserOperation, UserOperationVariant, + ValidTimeRange, +}; + +/// The new head of the chain, as viewed by the pool +#[derive(Clone, Debug, Default)] +pub struct NewHead { + /// The hash of the new head + pub block_hash: H256, + /// The number of the new head + pub block_number: u64, +} + +/// The reputation of an entity +#[derive(Debug, Clone)] +pub struct Reputation { + /// The entity's address + pub address: Address, + /// Number of ops seen in the current interval + pub ops_seen: u64, + /// Number of ops included in the current interval + pub ops_included: u64, +} + +/// Reputation status for an entity +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ReputationStatus { + /// Entity is not throttled or banned + Ok, + /// Entity is throttled + Throttled, + /// Entity is banned + Banned, +} + +impl Serialize for ReputationStatus { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + ReputationStatus::Ok => serializer.serialize_str("ok"), + ReputationStatus::Throttled => serializer.serialize_str("throttled"), + ReputationStatus::Banned => serializer.serialize_str("banned"), + } + } +} + +impl<'de> Deserialize<'de> for ReputationStatus { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + "ok" => Ok(ReputationStatus::Ok), + "throttled" => Ok(ReputationStatus::Throttled), + "banned" => Ok(ReputationStatus::Banned), + _ => Err(de::Error::custom(format!("Invalid reputation status {s}"))), + } + } +} + +/// Stake status structure +#[derive(Debug, Clone, Copy)] +pub struct StakeStatus { + /// Address is staked + pub is_staked: bool, + /// Stake information about address + pub stake_info: StakeInfo, +} + +/// The metadata for a paymaster +#[derive(Debug, Default, Clone, Eq, PartialEq, Copy)] +pub struct PaymasterMetadata { + /// Paymaster address + pub address: Address, + /// The on-chain balance of the paymaster + pub confirmed_balance: U256, + /// The pending balance is the confirm balance subtracted by + /// the max cost of all the pending user operations that use the paymaster + pub pending_balance: U256, +} + +/// A user operation with additional metadata from validation. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct PoolOperation { + /// The user operation stored in the pool + pub uo: UserOperationVariant, + /// The entry point address for this operation + pub entry_point: Address, + /// The aggregator address for this operation, if any. + pub aggregator: Option
, + /// The valid time range for this operation. + pub valid_time_range: ValidTimeRange, + /// The expected code hash for all contracts accessed during validation for this operation. + pub expected_code_hash: H256, + /// The block hash simulation was completed at + pub sim_block_hash: H256, + /// The block number simulation was completed at + pub sim_block_number: u64, + /// List of entities that need to stake for this operation. + pub entities_needing_stake: Vec, + /// Whether the account is staked. + pub account_is_staked: bool, + /// Staking information about all the entities. + pub entity_infos: EntityInfos, +} + +impl PoolOperation { + /// Returns true if the operation contains the given entity. + pub fn contains_entity(&self, entity: &Entity) -> bool { + if let Some(e) = self.entity_infos.get(entity.kind) { + e.address == entity.address + } else { + false + } + } + + /// Returns true if the operation requires the given entity to stake. + /// + /// For non-accounts, its possible that the entity is staked, but doesn't + /// _need_ to stake for this operation. For example, if the operation does not + /// access any storage slots that require staking. In that case this function + /// will return false. + /// + /// For staked accounts, this function will always return true. Staked accounts + /// are able to circumvent the mempool operation limits always need their reputation + /// checked to prevent them from filling the pool. + pub fn requires_stake(&self, entity: EntityType) -> bool { + match entity { + EntityType::Account => self.account_is_staked, + _ => self.entities_needing_stake.contains(&entity), + } + } + + /// Returns an iterator over all entities that are included in this operation. + pub fn entities(&'_ self) -> impl Iterator + '_ { + self.entity_infos + .entities() + .map(|(t, entity)| Entity::new(t, entity.address)) + } + + /// Returns an iterator over all entities that need stake in this operation. This can be a subset of entities that are staked in the operation. + pub fn entities_requiring_stake(&'_ self) -> impl Iterator + '_ { + self.entity_infos.entities().filter_map(|(t, entity)| { + if self.requires_stake(t) { + Entity::new(t, entity.address).into() + } else { + None + } + }) + } + + /// Return all the unstaked entities that are used in this operation. + pub fn unstaked_entities(&'_ self) -> impl Iterator + '_ { + self.entity_infos.entities().filter_map(|(t, entity)| { + if entity.is_staked { + None + } else { + Entity::new(t, entity.address).into() + } + }) + } + + /// Compute the amount of heap memory the PoolOperation takes up. + pub fn mem_size(&self) -> usize { + std::mem::size_of::() + + self.uo.heap_size() + + self.entities_needing_stake.len() * std::mem::size_of::() + } +} diff --git a/crates/types/src/timestamp.rs b/crates/types/src/timestamp.rs index 78d6bb14f..5e5187d24 100644 --- a/crates/types/src/timestamp.rs +++ b/crates/types/src/timestamp.rs @@ -192,6 +192,14 @@ impl ValidTimeRange { pub fn contains(self, timestamp: Timestamp, buffer: Duration) -> bool { self.valid_after <= timestamp && (timestamp + buffer) <= self.valid_until } + + /// Intersect two time ranges into a single time range that is valid whenever both are valid + pub fn intersect(self, other: Self) -> Self { + Self { + valid_after: self.valid_after.max(other.valid_after), + valid_until: self.valid_until.min(other.valid_until), + } + } } #[cfg(test)] @@ -286,6 +294,15 @@ mod test { assert_eq!(json, "\"0x64\""); } + #[test] + fn test_merge_time_ranges() { + let range1 = ValidTimeRange::new(Timestamp::new(100), Timestamp::new(200)); + let range2 = ValidTimeRange::new(Timestamp::new(150), Timestamp::new(250)); + let intersect = range1.intersect(range2); + assert_eq!(intersect.valid_after, Timestamp::new(150)); + assert_eq!(intersect.valid_until, Timestamp::new(200)); + } + fn get_timestamp_out_of_bounds_for_datetime() -> Timestamp { // This is just a bit further in the future than the maximum allowed // DateTime, which is just before the start of year 2^18 = 262144. diff --git a/crates/types/src/user_operation.rs b/crates/types/src/user_operation.rs deleted file mode 100644 index fb3e633dc..000000000 --- a/crates/types/src/user_operation.rs +++ /dev/null @@ -1,308 +0,0 @@ -// 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 ethers::{ - abi::{encode, Token}, - types::{Address, Bytes, H256, U256}, - utils::keccak256, -}; -use strum::IntoEnumIterator; - -use crate::{ - entity::{Entity, EntityType}, - UserOperation, -}; - -/// Number of bytes in the fixed size portion of an ABI encoded user operation -const PACKED_USER_OPERATION_FIXED_LEN: usize = 480; - -/// Unique identifier for a user operation from a given sender -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct UserOperationId { - /// sender of user operation - pub sender: Address, - /// nonce of user operation - pub nonce: U256, -} - -impl UserOperation { - /// Hash a user operation with the given entry point and chain ID. - /// - /// The hash is used to uniquely identify a user operation in the entry point. - /// It does not include the signature field. - pub fn op_hash(&self, entry_point: Address, chain_id: u64) -> H256 { - keccak256(encode(&[ - Token::FixedBytes(keccak256(self.pack_for_hash()).to_vec()), - Token::Address(entry_point), - Token::Uint(chain_id.into()), - ])) - .into() - } - - /// Get the unique identifier for this user operation from its sender - pub fn id(&self) -> UserOperationId { - UserOperationId { - sender: self.sender, - nonce: self.nonce, - } - } - - /// Get the address of the factory entity associated with this user operation, if any - pub fn factory(&self) -> Option
{ - Self::get_address_from_field(&self.init_code) - } - - /// Returns the maximum cost, in wei, of this user operation - pub fn max_gas_cost(&self) -> U256 { - let mul = if self.paymaster().is_some() { 3 } else { 1 }; - self.max_fee_per_gas - * (self.pre_verification_gas + self.call_gas_limit + self.verification_gas_limit * mul) - } - - /// Get the address of the paymaster entity associated with this user operation, if any - pub fn paymaster(&self) -> Option
{ - Self::get_address_from_field(&self.paymaster_and_data) - } - - /// Extracts an address from the beginning of a data field - /// Useful to extract the paymaster address from paymaster_and_data - /// and the factory address from init_code - pub fn get_address_from_field(data: &Bytes) -> Option
{ - if data.len() < 20 { - None - } else { - Some(Address::from_slice(&data[..20])) - } - } - - /// Efficient calculation of the size of a packed user operation - pub fn abi_encoded_size(&self) -> usize { - PACKED_USER_OPERATION_FIXED_LEN - + pad_len(&self.init_code) - + pad_len(&self.call_data) - + pad_len(&self.paymaster_and_data) - + pad_len(&self.signature) - } - - /// Compute the amount of heap memory the UserOperation takes up. - pub fn heap_size(&self) -> usize { - self.init_code.len() - + self.call_data.len() - + self.paymaster_and_data.len() - + self.signature.len() - } - - /// Gets the byte array representation of the user operation to be used in the signature - pub fn pack_for_hash(&self) -> Bytes { - let hash_init_code = keccak256(self.init_code.clone()); - let hash_call_data = keccak256(self.call_data.clone()); - let hash_paymaster_and_data = keccak256(self.paymaster_and_data.clone()); - - encode(&[ - Token::Address(self.sender), - Token::Uint(self.nonce), - Token::FixedBytes(hash_init_code.to_vec()), - Token::FixedBytes(hash_call_data.to_vec()), - Token::Uint(self.call_gas_limit), - Token::Uint(self.verification_gas_limit), - Token::Uint(self.pre_verification_gas), - Token::Uint(self.max_fee_per_gas), - Token::Uint(self.max_priority_fee_per_gas), - Token::FixedBytes(hash_paymaster_and_data.to_vec()), - ]) - .into() - } - - /// Gets an iterator on all entities associated with this user operation - pub fn entities(&'_ self) -> impl Iterator + '_ { - EntityType::iter().filter_map(|entity| { - self.entity_address(entity) - .map(|address| Entity::new(entity, address)) - }) - } - - /// Gets the address of the entity of the given type associated with this user operation, if any - fn entity_address(&self, entity: EntityType) -> Option
{ - match entity { - EntityType::Account => Some(self.sender), - EntityType::Paymaster => self.paymaster(), - EntityType::Factory => self.factory(), - EntityType::Aggregator => None, - } - } -} - -/// Calculates the size a byte array padded to the next largest multiple of 32 -fn pad_len(b: &Bytes) -> usize { - (b.len() + 31) & !31 -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use ethers::{ - abi::AbiEncode, - types::{Bytes, U256}, - }; - - use super::*; - - #[test] - fn test_hash_zeroed() { - // Testing a user operation hash against the hash generated by the - // entrypoint contract getUserOpHash() function with entrypoint address - // at 0x66a15edcc3b50a663e72f1457ffd49b9ae284ddc and chain ID 1337. - // - // UserOperation = { - // sender: '0x0000000000000000000000000000000000000000', - // nonce: 0, - // initCode: '0x', - // callData: '0x', - // callGasLimit: 0, - // verificationGasLimit: 0, - // preVerificationGas: 0, - // maxFeePerGas: 0, - // maxPriorityFeePerGas: 0, - // paymasterAndData: '0x', - // signature: '0x', - // } - // - // Hash: 0xdca97c3b49558ab360659f6ead939773be8bf26631e61bb17045bb70dc983b2d - let operation = UserOperation { - sender: "0x0000000000000000000000000000000000000000" - .parse() - .unwrap(), - nonce: U256::zero(), - init_code: Bytes::default(), - call_data: Bytes::default(), - call_gas_limit: U256::zero(), - verification_gas_limit: U256::zero(), - pre_verification_gas: U256::zero(), - max_fee_per_gas: U256::zero(), - max_priority_fee_per_gas: U256::zero(), - paymaster_and_data: Bytes::default(), - signature: Bytes::default(), - }; - let entry_point = "0x66a15edcc3b50a663e72f1457ffd49b9ae284ddc" - .parse() - .unwrap(); - let chain_id = 1337; - let hash = operation.op_hash(entry_point, chain_id); - assert_eq!( - hash, - "0xdca97c3b49558ab360659f6ead939773be8bf26631e61bb17045bb70dc983b2d" - .parse() - .unwrap() - ); - } - - #[test] - fn test_hash() { - // Testing a user operation hash against the hash generated by the - // entrypoint contract getUserOpHash() function with entrypoint address - // at 0x66a15edcc3b50a663e72f1457ffd49b9ae284ddc and chain ID 1337. - // - // UserOperation = { - // sender: '0x1306b01bc3e4ad202612d3843387e94737673f53', - // nonce: 8942, - // initCode: '0x6942069420694206942069420694206942069420', - // callData: '0x0000000000000000000000000000000000000000080085', - // callGasLimit: 10000, - // verificationGasLimit: 100000, - // preVerificationGas: 100, - // maxFeePerGas: 99999, - // maxPriorityFeePerGas: 9999999, - // paymasterAndData: - // '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', - // signature: - // '0xda0929f527cded8d0a1eaf2e8861d7f7e2d8160b7b13942f99dd367df4473a', - // } - // - // Hash: 0x484add9e4d8c3172d11b5feb6a3cc712280e176d278027cfa02ee396eb28afa1 - let operation = UserOperation { - sender: "0x1306b01bc3e4ad202612d3843387e94737673f53" - .parse() - .unwrap(), - nonce: 8942.into(), - init_code: "0x6942069420694206942069420694206942069420" - .parse() - .unwrap(), - call_data: "0x0000000000000000000000000000000000000000080085" - .parse() - .unwrap(), - call_gas_limit: 10000.into(), - verification_gas_limit: 100000.into(), - pre_verification_gas: 100.into(), - max_fee_per_gas: 99999.into(), - max_priority_fee_per_gas: 9999999.into(), - paymaster_and_data: - "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - .parse() - .unwrap(), - signature: "0xda0929f527cded8d0a1eaf2e8861d7f7e2d8160b7b13942f99dd367df4473a" - .parse() - .unwrap(), - }; - let entry_point = "0x66a15edcc3b50a663e72f1457ffd49b9ae284ddc" - .parse() - .unwrap(); - let chain_id = 1337; - let hash = operation.op_hash(entry_point, chain_id); - assert_eq!( - hash, - "0x484add9e4d8c3172d11b5feb6a3cc712280e176d278027cfa02ee396eb28afa1" - .parse() - .unwrap() - ); - } - - #[test] - fn test_get_address_from_field() { - let paymaster_and_data: Bytes = - "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - .parse() - .unwrap(); - let address = UserOperation::get_address_from_field(&paymaster_and_data).unwrap(); - assert_eq!( - address, - "0x0123456789abcdef0123456789abcdef01234567" - .parse() - .unwrap() - ); - } - - #[test] - fn test_abi_encoded_size() { - let user_operation = UserOperation { - sender: "0xe29a7223a7e040d70b5cd460ef2f4ac6a6ab304d" - .parse() - .unwrap(), - nonce: U256::from_dec_str("3937668929043450082210854285941660524781292117276598730779").unwrap(), - init_code: Bytes::default(), - call_data: Bytes::from_str("0x5194544700000000000000000000000058440a3e78b190e5bd07905a08a60e30bb78cb5b000000000000000000000000000000000000000000000000000009184e72a000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap(), - call_gas_limit: 40_960.into(), - verification_gas_limit: 75_099.into(), - pre_verification_gas: 46_330.into(), - max_fee_per_gas: 105_000_000.into(), - max_priority_fee_per_gas: 105_000_000.into(), - paymaster_and_data: Bytes::from_str("0xc03aac639bb21233e0139381970328db8bceeb6700006508996f000065089a9b0000000000000000000000000000000000000000ca7517be4e51ca2cde69bc44c4c3ce00ff7f501ce4ee1b3c6b2a742f579247292e4f9a672522b15abee8eaaf1e1487b8e3121d61d42ba07a47f5ccc927aa7eb61b").unwrap(), - signature: Bytes::from_str("0x00000000f8a0655423f2dfbb104e0ff906b7b4c64cfc12db0ac5ef0fb1944076650ce92a1a736518e5b6cd46c6ff6ece7041f2dae199fb4c8e7531704fbd629490b712dc1b").unwrap(), - }; - - assert_eq!( - user_operation.clone().encode().len(), - user_operation.abi_encoded_size() - ); - } -} diff --git a/crates/types/src/user_operation/mod.rs b/crates/types/src/user_operation/mod.rs new file mode 100644 index 000000000..246950e7b --- /dev/null +++ b/crates/types/src/user_operation/mod.rs @@ -0,0 +1,399 @@ +// 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::fmt::Debug; + +use ethers::{ + abi::AbiEncode, + types::{Address, Bytes, H256, U256}, +}; + +/// User Operation types for Entry Point v0.6 +pub mod v0_6; +/// User Operation types for Entry Point v0.7 +pub mod v0_7; + +use crate::Entity; + +/// ERC-4337 Entry point version +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum EntryPointVersion { + /// Unspecified version + Unspecified, + /// Version 0.6 + V0_6, + /// Version 0.7 + V0_7, +} + +/// Unique identifier for a user operation from a given sender +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct UserOperationId { + /// sender of user operation + pub sender: Address, + /// nonce of user operation + pub nonce: U256, +} + +/// User operation trait +pub trait UserOperation: Debug + Clone + Send + Sync + 'static { + /// Optional gas type + /// + /// Associated type for the version of a user operation that has optional gas and fee fields + type OptionalGas; + + /// Get the entry point version for this UO + fn entry_point_version() -> EntryPointVersion; + + /* + * Getters + */ + + /// Get the user operation sender address + fn sender(&self) -> Address; + + /// Get the user operation nonce + fn nonce(&self) -> U256; + + /// Get the user operation paymaster address, if any + fn paymaster(&self) -> Option
; + + /// Get the user operation factory address, if any + fn factory(&self) -> Option
; + + /// Get the user operation calldata + fn call_data(&self) -> &Bytes; + + /// Returns the call gas limit + fn call_gas_limit(&self) -> U256; + + /// Returns the verification gas limit + fn verification_gas_limit(&self) -> U256; + + /// Returns the max fee per gas + fn max_fee_per_gas(&self) -> U256; + + /// Returns the max priority fee per gas + fn max_priority_fee_per_gas(&self) -> U256; + + /// Returns the maximum cost, in wei, of this user operation + fn max_gas_cost(&self) -> U256; + + /* + * Enhanced functions + */ + + /// Hash a user operation with the given entry point and chain ID. + /// + /// The hash is used to uniquely identify a user operation in the entry point. + /// It does not include the signature field. + fn hash(&self, entry_point: Address, chain_id: u64) -> H256; + + /// Get the user operation id + fn id(&self) -> UserOperationId; + + /// Gets an iterator on all entities associated with this user operation + fn entities(&'_ self) -> Vec; + + /// Returns the heap size of the user operation + fn heap_size(&self) -> usize; + + /// Returns the total verification gas limit + fn total_verification_gas_limit(&self) -> U256; + + /// Returns the required pre-execution buffer + /// + /// This should capture all of the gas that is needed to execute the user operation, + /// minus the call gas limit. The entry point will check for this buffer before + /// executing the user operation. + fn required_pre_execution_buffer(&self) -> U256; + + /// Returns the pre-verification gas + fn pre_verification_gas(&self) -> U256; + + /// Calculate the static portion of the pre-verification gas for this user operation + fn calc_static_pre_verification_gas(&self, include_fixed_gas_overhead: bool) -> U256; + + /// Clear the signature field of the user op + /// + /// Used when a user op is using a signature aggregator prior to being submitted + fn clear_signature(&mut self); +} + +/// User operation enum +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum UserOperationVariant { + /// User operation version 0.6 + V0_6(v0_6::UserOperation), + /// User operation version 0.7 + V0_7(v0_7::UserOperation), +} + +impl UserOperation for UserOperationVariant { + type OptionalGas = UserOperationOptionalGas; + + fn entry_point_version() -> EntryPointVersion { + EntryPointVersion::Unspecified + } + + fn hash(&self, entry_point: Address, chain_id: u64) -> H256 { + match self { + UserOperationVariant::V0_6(op) => op.hash(entry_point, chain_id), + UserOperationVariant::V0_7(op) => op.hash(entry_point, chain_id), + } + } + + fn id(&self) -> UserOperationId { + match self { + UserOperationVariant::V0_6(op) => op.id(), + UserOperationVariant::V0_7(op) => op.id(), + } + } + + fn sender(&self) -> Address { + match self { + UserOperationVariant::V0_6(op) => op.sender(), + UserOperationVariant::V0_7(op) => op.sender(), + } + } + + fn nonce(&self) -> U256 { + match self { + UserOperationVariant::V0_6(op) => op.nonce(), + UserOperationVariant::V0_7(op) => op.nonce(), + } + } + + fn paymaster(&self) -> Option
{ + match self { + UserOperationVariant::V0_6(op) => op.paymaster(), + UserOperationVariant::V0_7(op) => op.paymaster(), + } + } + + fn factory(&self) -> Option
{ + match self { + UserOperationVariant::V0_6(op) => op.factory(), + UserOperationVariant::V0_7(op) => op.factory(), + } + } + + fn call_data(&self) -> &Bytes { + match self { + UserOperationVariant::V0_6(op) => op.call_data(), + UserOperationVariant::V0_7(op) => op.call_data(), + } + } + + fn max_gas_cost(&self) -> U256 { + match self { + UserOperationVariant::V0_6(op) => op.max_gas_cost(), + UserOperationVariant::V0_7(op) => op.max_gas_cost(), + } + } + + fn entities(&'_ self) -> Vec { + match self { + UserOperationVariant::V0_6(op) => op.entities(), + UserOperationVariant::V0_7(op) => op.entities(), + } + } + + fn heap_size(&self) -> usize { + match self { + UserOperationVariant::V0_6(op) => op.heap_size(), + UserOperationVariant::V0_7(op) => op.heap_size(), + } + } + + fn call_gas_limit(&self) -> U256 { + match self { + UserOperationVariant::V0_6(op) => op.call_gas_limit(), + UserOperationVariant::V0_7(op) => op.call_gas_limit(), + } + } + + fn verification_gas_limit(&self) -> U256 { + match self { + UserOperationVariant::V0_6(op) => op.verification_gas_limit(), + UserOperationVariant::V0_7(op) => op.verification_gas_limit(), + } + } + + fn total_verification_gas_limit(&self) -> U256 { + match self { + UserOperationVariant::V0_6(op) => op.total_verification_gas_limit(), + UserOperationVariant::V0_7(op) => op.total_verification_gas_limit(), + } + } + + fn required_pre_execution_buffer(&self) -> U256 { + match self { + UserOperationVariant::V0_6(op) => op.required_pre_execution_buffer(), + UserOperationVariant::V0_7(op) => op.required_pre_execution_buffer(), + } + } + + fn pre_verification_gas(&self) -> U256 { + match self { + UserOperationVariant::V0_6(op) => op.pre_verification_gas(), + UserOperationVariant::V0_7(op) => op.pre_verification_gas(), + } + } + + fn calc_static_pre_verification_gas(&self, include_fixed_gas_overhead: bool) -> U256 { + match self { + UserOperationVariant::V0_6(op) => { + op.calc_static_pre_verification_gas(include_fixed_gas_overhead) + } + UserOperationVariant::V0_7(op) => { + op.calc_static_pre_verification_gas(include_fixed_gas_overhead) + } + } + } + + fn max_fee_per_gas(&self) -> U256 { + match self { + UserOperationVariant::V0_6(op) => op.max_fee_per_gas(), + UserOperationVariant::V0_7(op) => op.max_fee_per_gas(), + } + } + + fn max_priority_fee_per_gas(&self) -> U256 { + match self { + UserOperationVariant::V0_6(op) => op.max_priority_fee_per_gas(), + UserOperationVariant::V0_7(op) => op.max_priority_fee_per_gas(), + } + } + + fn clear_signature(&mut self) { + match self { + UserOperationVariant::V0_6(op) => op.clear_signature(), + UserOperationVariant::V0_7(op) => op.clear_signature(), + } + } +} + +impl UserOperationVariant { + fn into_v0_6(self) -> Option { + match self { + UserOperationVariant::V0_6(op) => Some(op), + _ => None, + } + } + + fn into_v0_7(self) -> Option { + match self { + UserOperationVariant::V0_7(op) => Some(op), + _ => None, + } + } + + /// Returns the user operation type + pub fn uo_type(&self) -> EntryPointVersion { + match self { + UserOperationVariant::V0_6(_) => EntryPointVersion::V0_6, + UserOperationVariant::V0_7(_) => EntryPointVersion::V0_7, + } + } +} + +/// User operation optional gas enum +#[derive(Debug, Clone)] +pub enum UserOperationOptionalGas { + /// User operation optional gas for version 0.6 + V0_6(v0_6::UserOperationOptionalGas), + /// User operation optional gas for version 0.7 + V0_7(v0_7::UserOperationOptionalGas), +} + +/// Gas estimate +#[derive(Debug, Clone)] +pub struct GasEstimate { + /// Pre verification gas + pub pre_verification_gas: U256, + /// Call gas limit + pub call_gas_limit: U256, + /// Verification gas limit + pub verification_gas_limit: U256, + /// Paymaster verification gas limit + /// + /// v0.6: unused + /// + /// v0.7: populated only if the user operation has a paymaster + pub paymaster_verification_gas_limit: Option, + /// Paymaster post op gas limit + /// + /// v0.6: unused + /// + /// v0.7: populated only if the user operation has a paymaster + pub paymaster_post_op_gas_limit: Option, +} + +/// User operations per aggregator +#[derive(Debug, Eq, PartialEq, Clone, Default)] +pub struct UserOpsPerAggregator { + /// User operations + pub user_ops: Vec, + /// Aggregator address, zero if no aggregator is used + pub aggregator: Address, + /// Aggregator signature, empty if no aggregator is used + pub signature: Bytes, +} + +// TODO(danc): move this to chain spec + +/// Gas overheads for user operations used in calculating the pre-verification gas. See: https://github.com/eth-infinitism/bundler/blob/main/packages/sdk/src/calcPreVerificationGas.ts +#[derive(Clone, Copy, Debug)] +pub struct GasOverheads { + /// The Entrypoint requires a gas buffer for the bundle to account for the gas spent outside of the major steps in the processing of UOs + pub bundle_transaction_gas_buffer: U256, + /// The fixed gas overhead for any EVM transaction + pub transaction_gas_overhead: U256, + per_user_op: U256, + per_user_op_word: U256, + zero_byte: U256, + non_zero_byte: U256, +} + +impl Default for GasOverheads { + fn default() -> Self { + Self { + bundle_transaction_gas_buffer: 5_000.into(), + transaction_gas_overhead: 21_000.into(), + per_user_op: 18_300.into(), + per_user_op_word: 4.into(), + zero_byte: 4.into(), + non_zero_byte: 16.into(), + } + } +} + +pub(crate) fn op_calldata_gas_cost(uo: UO) -> U256 { + let ov = GasOverheads::default(); + let encoded_op = uo.encode(); + let length_in_words = encoded_op.len() / 32; // size of packed user op is always a multiple of 32 bytes + let call_data_cost: U256 = encoded_op + .iter() + .map(|&x| { + if x == 0 { + ov.zero_byte + } else { + ov.non_zero_byte + } + }) + .reduce(|a, b| a + b) + .unwrap_or_default(); + + call_data_cost + ov.per_user_op + ov.per_user_op_word * length_in_words +} diff --git a/crates/types/src/user_operation/v0_6.rs b/crates/types/src/user_operation/v0_6.rs new file mode 100644 index 000000000..d13757346 --- /dev/null +++ b/crates/types/src/user_operation/v0_6.rs @@ -0,0 +1,463 @@ +// 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 ethers::{ + abi::{encode, Token}, + types::{Address, Bytes, H256, U256}, + utils::keccak256, +}; +use rand::{self, RngCore}; +use serde::{Deserialize, Serialize}; +use strum::IntoEnumIterator; + +use super::{ + GasOverheads, UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant, +}; +pub use crate::contracts::v0_6::i_entry_point::{UserOperation, UserOpsPerAggregator}; +use crate::{ + entity::{Entity, EntityType}, + EntryPointVersion, +}; + +impl UserOperationTrait for UserOperation { + type OptionalGas = UserOperationOptionalGas; + + fn entry_point_version() -> EntryPointVersion { + EntryPointVersion::V0_6 + } + + fn hash(&self, entry_point: Address, chain_id: u64) -> H256 { + keccak256(encode(&[ + Token::FixedBytes(keccak256(self.pack_for_hash()).to_vec()), + Token::Address(entry_point), + Token::Uint(chain_id.into()), + ])) + .into() + } + + fn id(&self) -> UserOperationId { + UserOperationId { + sender: self.sender, + nonce: self.nonce, + } + } + + fn sender(&self) -> Address { + self.sender + } + + fn nonce(&self) -> U256 { + self.nonce + } + + fn factory(&self) -> Option
{ + Self::get_address_from_field(&self.init_code) + } + + fn paymaster(&self) -> Option
{ + Self::get_address_from_field(&self.paymaster_and_data) + } + + fn call_data(&self) -> &Bytes { + &self.call_data + } + + fn max_gas_cost(&self) -> U256 { + let mul = if self.paymaster().is_some() { 3 } else { 1 }; + self.max_fee_per_gas + * (self.pre_verification_gas + self.call_gas_limit + self.verification_gas_limit * mul) + } + + fn heap_size(&self) -> usize { + self.init_code.len() + + self.call_data.len() + + self.paymaster_and_data.len() + + self.signature.len() + } + + fn entities(&self) -> Vec { + EntityType::iter() + .filter_map(|entity| { + self.entity_address(entity) + .map(|address| Entity::new(entity, address)) + }) + .collect() + } + + fn max_fee_per_gas(&self) -> U256 { + self.max_fee_per_gas + } + + fn max_priority_fee_per_gas(&self) -> U256 { + self.max_priority_fee_per_gas + } + + fn call_gas_limit(&self) -> U256 { + self.call_gas_limit + } + + fn pre_verification_gas(&self) -> U256 { + self.pre_verification_gas + } + + fn verification_gas_limit(&self) -> U256 { + self.verification_gas_limit + } + + fn total_verification_gas_limit(&self) -> U256 { + let mul = if self.paymaster().is_some() { 2 } else { 1 }; + self.verification_gas_limit * mul + } + + fn required_pre_execution_buffer(&self) -> U256 { + self.verification_gas_limit + U256::from(5_000) + } + + fn calc_static_pre_verification_gas(&self, include_fixed_gas_overhead: bool) -> U256 { + let ov = GasOverheads::default(); + super::op_calldata_gas_cost(self.clone()) + + (if include_fixed_gas_overhead { + ov.transaction_gas_overhead + } else { + 0.into() + }) + } + + fn clear_signature(&mut self) { + self.signature = Bytes::default(); + } +} + +impl UserOperation { + fn get_address_from_field(data: &Bytes) -> Option
{ + if data.len() < 20 { + None + } else { + Some(Address::from_slice(&data[..20])) + } + } + + fn pack_for_hash(&self) -> Bytes { + let hash_init_code = keccak256(self.init_code.clone()); + let hash_call_data = keccak256(self.call_data.clone()); + let hash_paymaster_and_data = keccak256(self.paymaster_and_data.clone()); + + encode(&[ + Token::Address(self.sender), + Token::Uint(self.nonce), + Token::FixedBytes(hash_init_code.to_vec()), + Token::FixedBytes(hash_call_data.to_vec()), + Token::Uint(self.call_gas_limit), + Token::Uint(self.verification_gas_limit), + Token::Uint(self.pre_verification_gas), + Token::Uint(self.max_fee_per_gas), + Token::Uint(self.max_priority_fee_per_gas), + Token::FixedBytes(hash_paymaster_and_data.to_vec()), + ]) + .into() + } + + fn entity_address(&self, entity: EntityType) -> Option
{ + match entity { + EntityType::Account => Some(self.sender), + EntityType::Paymaster => self.paymaster(), + EntityType::Factory => self.factory(), + EntityType::Aggregator => None, + } + } +} + +impl From for UserOperation { + /// Converts a UserOperationVariant to a UserOperation 0.6 + /// + /// # Panics + /// + /// Panics if the variant is not v0.6. This is for use in contexts + /// where the variant is known to be v0.6. + fn from(value: UserOperationVariant) -> Self { + value.into_v0_6().expect("Expected UserOperationV0_6") + } +} + +impl From for super::UserOperationVariant { + fn from(op: UserOperation) -> Self { + super::UserOperationVariant::V0_6(op) + } +} + +impl AsRef for super::UserOperationVariant { + fn as_ref(&self) -> &UserOperation { + match self { + super::UserOperationVariant::V0_6(op) => op, + _ => panic!("Expected UserOperationV0_6"), + } + } +} + +impl AsMut for super::UserOperationVariant { + fn as_mut(&mut self) -> &mut UserOperation { + match self { + super::UserOperationVariant::V0_6(op) => op, + _ => panic!("Expected UserOperationV0_6"), + } + } +} + +/// User operation with optional gas fields for gas estimation +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct UserOperationOptionalGas { + /// Sender (required) + pub sender: Address, + /// Nonce (required) + pub nonce: U256, + /// Init code (required) + pub init_code: Bytes, + /// Call data (required) + pub call_data: Bytes, + /// Call gas limit (optional, set to maximum if unset) + pub call_gas_limit: Option, + /// Verification gas limit (optional, set to maximum if unset) + pub verification_gas_limit: Option, + /// Pre verification gas (optional, ignored if set) + pub pre_verification_gas: Option, + /// Max fee per gas (optional, ignored if set) + pub max_fee_per_gas: Option, + /// Max priority fee per gas (optional, ignored if set) + pub max_priority_fee_per_gas: Option, + /// Paymaster and data (required, dummy value for gas estimation) + pub paymaster_and_data: Bytes, + /// Signature (required, dummy value for gas estimation) + pub signature: 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, max_call_gas: U256, max_verification_gas: U256) -> UserOperation { + UserOperation { + call_gas_limit: U256::MAX, + verification_gas_limit: U256::MAX, + pre_verification_gas: U256::MAX, + max_fee_per_gas: U256::MAX, + max_priority_fee_per_gas: U256::MAX, + signature: vec![255_u8; self.signature.len()].into(), + paymaster_and_data: vec![255_u8; self.paymaster_and_data.len()].into(), + ..self + .clone() + .into_user_operation(max_call_gas, max_verification_gas) + } + } + + /// 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, max_call_gas: U256, max_verification_gas: U256) -> UserOperation { + UserOperation { + call_gas_limit: U256::from_big_endian(&Self::random_bytes(4)), // 30M max + verification_gas_limit: U256::from_big_endian(&Self::random_bytes(4)), // 30M max + pre_verification_gas: U256::from_big_endian(&Self::random_bytes(4)), // 30M max + max_fee_per_gas: U256::from_big_endian(&Self::random_bytes(8)), // 2^64 max + max_priority_fee_per_gas: U256::from_big_endian(&Self::random_bytes(8)), // 2^64 max + signature: Self::random_bytes(self.signature.len()), + paymaster_and_data: Self::random_bytes(self.paymaster_and_data.len()), + ..self + .clone() + .into_user_operation(max_call_gas, max_verification_gas) + } + } + + /// Convert into a full user operation. + /// Fill in the optional fields of the user operation with default values if unset + pub fn into_user_operation( + self, + max_call_gas: U256, + max_verification_gas: U256, + ) -> UserOperation { + UserOperation { + sender: self.sender, + nonce: self.nonce, + init_code: self.init_code, + call_data: self.call_data, + paymaster_and_data: self.paymaster_and_data, + signature: self.signature, + // If unset, default these to gas limits from settings + // Cap their values to the gas limits from settings + verification_gas_limit: self + .verification_gas_limit + .unwrap_or(max_verification_gas) + .min(max_verification_gas), + call_gas_limit: self + .call_gas_limit + .unwrap_or(max_call_gas) + .min(max_call_gas), + // These aren't used in gas estimation, set to if unset 0 so that there are no payment attempts during gas estimation + pre_verification_gas: self.pre_verification_gas.unwrap_or_default(), + max_fee_per_gas: self.max_fee_per_gas.unwrap_or_default(), + max_priority_fee_per_gas: self.max_priority_fee_per_gas.unwrap_or_default(), + } + } + + 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_6(op) => op, + _ => panic!("Expected UserOperationOptionalGasV0_6"), + } + } +} + +#[cfg(test)] +mod tests { + + use ethers::types::{Bytes, U256}; + + use super::*; + + #[test] + fn test_hash_zeroed() { + // Testing a user operation hash against the hash generated by the + // entrypoint contract getUserOpHash() function with entrypoint address + // at 0x66a15edcc3b50a663e72f1457ffd49b9ae284ddc and chain ID 1337. + // + // UserOperation = { + // sender: '0x0000000000000000000000000000000000000000', + // nonce: 0, + // initCode: '0x', + // callData: '0x', + // callGasLimit: 0, + // verificationGasLimit: 0, + // preVerificationGas: 0, + // maxFeePerGas: 0, + // maxPriorityFeePerGas: 0, + // paymasterAndData: '0x', + // signature: '0x', + // } + // + // Hash: 0xdca97c3b49558ab360659f6ead939773be8bf26631e61bb17045bb70dc983b2d + let operation = UserOperation { + sender: "0x0000000000000000000000000000000000000000" + .parse() + .unwrap(), + nonce: U256::zero(), + init_code: Bytes::default(), + call_data: Bytes::default(), + call_gas_limit: U256::zero(), + verification_gas_limit: U256::zero(), + pre_verification_gas: U256::zero(), + max_fee_per_gas: U256::zero(), + max_priority_fee_per_gas: U256::zero(), + paymaster_and_data: Bytes::default(), + signature: Bytes::default(), + }; + let entry_point = "0x66a15edcc3b50a663e72f1457ffd49b9ae284ddc" + .parse() + .unwrap(); + let chain_id = 1337; + let hash = operation.hash(entry_point, chain_id); + assert_eq!( + hash, + "0xdca97c3b49558ab360659f6ead939773be8bf26631e61bb17045bb70dc983b2d" + .parse() + .unwrap() + ); + } + + #[test] + fn test_hash() { + // Testing a user operation hash against the hash generated by the + // entrypoint contract getUserOpHash() function with entrypoint address + // at 0x66a15edcc3b50a663e72f1457ffd49b9ae284ddc and chain ID 1337. + // + // UserOperation = { + // sender: '0x1306b01bc3e4ad202612d3843387e94737673f53', + // nonce: 8942, + // initCode: '0x6942069420694206942069420694206942069420', + // callData: '0x0000000000000000000000000000000000000000080085', + // callGasLimit: 10000, + // verificationGasLimit: 100000, + // preVerificationGas: 100, + // maxFeePerGas: 99999, + // maxPriorityFeePerGas: 9999999, + // paymasterAndData: + // '0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', + // signature: + // '0xda0929f527cded8d0a1eaf2e8861d7f7e2d8160b7b13942f99dd367df4473a', + // } + // + // Hash: 0x484add9e4d8c3172d11b5feb6a3cc712280e176d278027cfa02ee396eb28afa1 + let operation = UserOperation { + sender: "0x1306b01bc3e4ad202612d3843387e94737673f53" + .parse() + .unwrap(), + nonce: 8942.into(), + init_code: "0x6942069420694206942069420694206942069420" + .parse() + .unwrap(), + call_data: "0x0000000000000000000000000000000000000000080085" + .parse() + .unwrap(), + call_gas_limit: 10000.into(), + verification_gas_limit: 100000.into(), + pre_verification_gas: 100.into(), + max_fee_per_gas: 99999.into(), + max_priority_fee_per_gas: 9999999.into(), + paymaster_and_data: + "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + .parse() + .unwrap(), + signature: "0xda0929f527cded8d0a1eaf2e8861d7f7e2d8160b7b13942f99dd367df4473a" + .parse() + .unwrap(), + }; + let entry_point = "0x66a15edcc3b50a663e72f1457ffd49b9ae284ddc" + .parse() + .unwrap(); + let chain_id = 1337; + let hash = operation.hash(entry_point, chain_id); + assert_eq!( + hash, + "0x484add9e4d8c3172d11b5feb6a3cc712280e176d278027cfa02ee396eb28afa1" + .parse() + .unwrap() + ); + } + + #[test] + fn test_get_address_from_field() { + let paymaster_and_data: Bytes = + "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + .parse() + .unwrap(); + let address = UserOperation::get_address_from_field(&paymaster_and_data).unwrap(); + assert_eq!( + address, + "0x0123456789abcdef0123456789abcdef01234567" + .parse() + .unwrap() + ); + } +} diff --git a/crates/types/src/user_operation/v0_7.rs b/crates/types/src/user_operation/v0_7.rs new file mode 100644 index 000000000..c18b5647d --- /dev/null +++ b/crates/types/src/user_operation/v0_7.rs @@ -0,0 +1,767 @@ +// 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 ethers::{ + abi::{encode, Token}, + types::{Address, Bytes, H256, U128, U256}, + utils::keccak256, +}; +use rand::RngCore; + +use super::{UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant}; +use crate::{ + contracts::v0_7::shared_types::PackedUserOperation, Entity, EntryPointVersion, GasOverheads, +}; + +const ENTRY_POINT_INNER_GAS_OVERHEAD: U256 = U256([10_000, 0, 0, 0]); + +/// User Operation for Entry Point v0.7 +/// +/// Offchain version, must be packed before sending onchain +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct UserOperation { + /* + * Required fields + */ + /// Sender + pub sender: Address, + /// Semi-abstracted nonce + /// + /// The first 192 bits are the nonce key, the last 64 bits are the nonce value + pub nonce: U256, + /// Calldata + pub call_data: Bytes, + /// Call gas limit + pub call_gas_limit: U128, + /// Verification gas limit + pub verification_gas_limit: U128, + /// Pre-verification gas + pub pre_verification_gas: U256, + /// Max priority fee per gas + pub max_priority_fee_per_gas: U128, + /// Max fee per gas + pub max_fee_per_gas: U128, + /// Signature + pub signature: Bytes, + /* + * Optional fields + */ + /// Factory, populated if deploying a new sender contract + pub factory: Option
, + /// Factory data + pub factory_data: Bytes, + /// Paymaster, populated if using a paymaster + pub paymaster: Option
, + /// Paymaster verification gas limit + pub paymaster_verification_gas_limit: U128, + /// Paymaster post-op gas limit + pub paymaster_post_op_gas_limit: U128, + /// Paymaster data + pub paymaster_data: Bytes, + /* + * Cached fields, not part of the UO + */ + /// 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 { + type OptionalGas = UserOperationOptionalGas; + + fn entry_point_version() -> EntryPointVersion { + EntryPointVersion::V0_7 + } + + fn hash(&self, _entry_point: Address, _chain_id: u64) -> H256 { + self.hash + } + + fn id(&self) -> UserOperationId { + UserOperationId { + sender: self.sender, + nonce: self.nonce, + } + } + + fn sender(&self) -> Address { + self.sender + } + + fn nonce(&self) -> U256 { + self.nonce + } + + fn paymaster(&self) -> Option
{ + self.paymaster + } + + fn factory(&self) -> Option
{ + self.factory + } + + fn call_data(&self) -> &Bytes { + &self.call_data + } + + fn max_gas_cost(&self) -> U256 { + U256::from(self.max_fee_per_gas) + * (self.pre_verification_gas + + self.call_gas_limit + + self.verification_gas_limit + + self.paymaster_verification_gas_limit + + self.paymaster_post_op_gas_limit) + } + + fn entities(&self) -> Vec { + let mut ret = vec![Entity::account(self.sender)]; + if let Some(factory) = self.factory { + ret.push(Entity::factory(factory)); + } + if let Some(paymaster) = self.paymaster { + ret.push(Entity::paymaster(paymaster)); + } + ret + } + + fn heap_size(&self) -> usize { + self.packed.heap_size() + + self.call_data.len() + + self.signature.len() + + self.factory_data.len() + + self.paymaster_data.len() + } + + fn max_fee_per_gas(&self) -> U256 { + U256::from(self.max_fee_per_gas) + } + + fn max_priority_fee_per_gas(&self) -> U256 { + U256::from(self.max_priority_fee_per_gas) + } + + fn pre_verification_gas(&self) -> U256 { + self.pre_verification_gas + } + + fn call_gas_limit(&self) -> U256 { + U256::from(self.call_gas_limit) + } + + fn verification_gas_limit(&self) -> U256 { + U256::from(self.verification_gas_limit) + } + + fn total_verification_gas_limit(&self) -> U256 { + U256::from(self.verification_gas_limit) + U256::from(self.paymaster_verification_gas_limit) + } + + fn calc_static_pre_verification_gas(&self, include_fixed_gas_overhead: bool) -> U256 { + let ov = GasOverheads::default(); + self.calldata_gas_cost + + (if include_fixed_gas_overhead { + ov.transaction_gas_overhead + } else { + 0.into() + }) + } + + fn required_pre_execution_buffer(&self) -> U256 { + // See EntryPoint::innerHandleOp + // + // Overhead prior to execution of the user operation is required to be + // At least the call gas limit, plus the paymaster post-op gas limit, plus + // a static overhead of 10K gas. + // + // To handle the 63/64ths rule also need to add a buffer of 1/63rd of that total* + ENTRY_POINT_INNER_GAS_OVERHEAD + + U256::from(self.paymaster_post_op_gas_limit) + + (U256::from(64) + * (U256::from(self.call_gas_limit) + + U256::from(self.paymaster_post_op_gas_limit) + + ENTRY_POINT_INNER_GAS_OVERHEAD) + / U256::from(63)) + } + + fn clear_signature(&mut self) { + // TODO: repack and rehash + self.signature = Bytes::new(); + } +} + +impl UserOperation { + /// Packs the user operation to its offchain representation + pub fn pack(self) -> PackedUserOperation { + self.packed + } + + /// Returns a reference to the packed user operation + pub fn packed(&self) -> &PackedUserOperation { + &self.packed + } +} + +impl From for UserOperation { + /// Converts a UserOperationVariant to a UserOperation 0.7 + /// + /// # Panics + /// + /// Panics if the variant is not v0.7. This is for use in contexts + /// where the variant is known to be v0.7. + fn from(value: UserOperationVariant) -> Self { + value.into_v0_7().expect("Expected UserOperationV0_7") + } +} + +impl From for super::UserOperationVariant { + fn from(op: UserOperation) -> Self { + super::UserOperationVariant::V0_7(op) + } +} + +impl AsRef for super::UserOperationVariant { + fn as_ref(&self) -> &UserOperation { + match self { + super::UserOperationVariant::V0_7(op) => op, + _ => panic!("Expected UserOperationV0_7"), + } + } +} + +impl AsMut for super::UserOperationVariant { + fn as_mut(&mut self) -> &mut UserOperation { + match self { + super::UserOperationVariant::V0_7(op) => op, + _ => panic!("Expected UserOperationV0_7"), + } + } +} + +/// User Operation with optional gas for Entry Point v0.7 +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct UserOperationOptionalGas { + /* + * Required fields + */ + /// Sender + pub sender: Address, + /// Semi-abstracted nonce + pub nonce: U256, + /// Calldata + pub call_data: Bytes, + /// Signature, typically a dummy value for optional gas + pub signature: Bytes, + /* + * Optional fields + */ + /// Call gas limit + pub call_gas_limit: Option, + /// Verification gas limit + pub verification_gas_limit: Option, + /// Pre-verification gas + pub pre_verification_gas: Option, + /// Max priority fee per gas + pub max_priority_fee_per_gas: Option, + /// Max fee per gas + pub max_fee_per_gas: Option, + /// Factory + pub factory: Option
, + /// Factory data + pub factory_data: Bytes, + /// Paymaster + pub paymaster: Option
, + /// Paymaster verification gas limit + pub paymaster_verification_gas_limit: Option, + /// Paymaster post-op gas limit + pub paymaster_post_op_gas_limit: Option, + /// Paymaster data + 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 +pub struct UserOperationBuilder { + // required fields for hash + entry_point: Address, + chain_id: u64, + + // required fields + required: UserOperationRequiredFields, + + // optional fields + factory: Option
, + factory_data: Bytes, + paymaster: Option
, + paymaster_verification_gas_limit: U128, + paymaster_post_op_gas_limit: U128, + paymaster_data: Bytes, + packed_uo: Option, +} + +/// Required fields for UserOperation v0.7 +pub struct UserOperationRequiredFields { + /// Sender + pub sender: Address, + /// Semi-abstracted nonce + pub nonce: U256, + /// Calldata + pub call_data: Bytes, + /// Call gas limit + pub call_gas_limit: U128, + /// Verification gas limit + pub verification_gas_limit: U128, + /// Pre-verification gas + pub pre_verification_gas: U256, + /// Max priority fee per gas + pub max_priority_fee_per_gas: U128, + /// Max fee per gas + pub max_fee_per_gas: U128, + /// Signature + pub signature: Bytes, +} + +impl UserOperationBuilder { + /// Creates a new builder + pub fn new(entry_point: Address, chain_id: u64, required: UserOperationRequiredFields) -> Self { + Self { + entry_point, + chain_id, + required, + factory: None, + factory_data: Bytes::new(), + paymaster: None, + paymaster_verification_gas_limit: U128::zero(), + paymaster_post_op_gas_limit: U128::zero(), + paymaster_data: Bytes::new(), + packed_uo: None, + } + } + + /// Sets the factory and factory data + pub fn factory(mut self, factory: Address, factory_data: Bytes) -> Self { + self.factory = Some(factory); + self.factory_data = factory_data; + self + } + + /// Sets the paymaster and associated fields + pub fn paymaster( + mut self, + paymaster: Address, + paymaster_verification_gas_limit: U128, + paymaster_post_op_gas_limit: U128, + paymaster_data: Bytes, + ) -> Self { + self.paymaster = Some(paymaster); + self.paymaster_verification_gas_limit = paymaster_verification_gas_limit; + self.paymaster_post_op_gas_limit = paymaster_post_op_gas_limit; + self.paymaster_data = paymaster_data; + 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 { + sender: self.required.sender, + nonce: self.required.nonce, + factory: self.factory, + factory_data: self.factory_data, + call_data: self.required.call_data, + call_gas_limit: self.required.call_gas_limit, + verification_gas_limit: self.required.verification_gas_limit, + pre_verification_gas: self.required.pre_verification_gas, + max_priority_fee_per_gas: self.required.max_priority_fee_per_gas, + max_fee_per_gas: self.required.max_fee_per_gas, + paymaster: self.paymaster, + paymaster_verification_gas_limit: self.paymaster_verification_gas_limit, + 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 = 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()); + + UserOperation { + hash, + packed, + calldata_gas_cost, + ..uo + } + } +} + +fn pack_user_operation(uo: UserOperation) -> PackedUserOperation { + let init_code = if let Some(factory) = uo.factory { + let mut init_code = factory.as_bytes().to_vec(); + init_code.extend_from_slice(&uo.factory_data); + Bytes::from(init_code) + } else { + Bytes::new() + }; + + let account_gas_limits = concat_128( + 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_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_be_bytes()); + paymaster_and_data + .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 { + Bytes::new() + }; + + PackedUserOperation { + sender: uo.sender, + nonce: uo.nonce, + init_code, + call_data: uo.call_data, + account_gas_limits, + pre_verification_gas: uo.pre_verification_gas, + gas_fees, + paymaster_and_data, + signature: uo.signature, + } +} + +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() { + 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() { + 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( + puo: &PackedUserOperation, + entry_point: Address, + chain_id: u64, +) -> H256 { + let hash_init_code = keccak256(&puo.init_code); + let hash_call_data = keccak256(&puo.call_data); + let hash_paymaster_and_data = keccak256(&puo.paymaster_and_data); + + let encoded: Bytes = encode(&[ + Token::Address(puo.sender), + Token::Uint(puo.nonce), + Token::FixedBytes(hash_init_code.to_vec()), + Token::FixedBytes(hash_call_data.to_vec()), + Token::FixedBytes(puo.account_gas_limits.to_vec()), + Token::Uint(puo.pre_verification_gas), + Token::FixedBytes(puo.gas_fees.to_vec()), + Token::FixedBytes(hash_paymaster_and_data.to_vec()), + ]) + .into(); + + let hashed = keccak256(encoded); + + keccak256(encode(&[ + Token::FixedBytes(hashed.to_vec()), + Token::Address(entry_point), + Token::Uint(chain_id.into()), + ])) + .into() +} + +fn concat_128(a: [u8; 16], b: [u8; 16]) -> [u8; 32] { + std::array::from_fn(|i| { + if let Some(i) = i.checked_sub(a.len()) { + b[i] + } else { + a[i] + } + }) +} + +impl PackedUserOperation { + /// Unpacks the user operation to its offchain representation + pub fn unpack(self, entry_point: Address, chain_id: u64) -> UserOperation { + unpack_user_operation(self.clone(), entry_point, chain_id) + } + + fn heap_size(&self) -> usize { + self.init_code.len() + self.call_data.len() + self.paymaster_and_data.len() + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use ethers::utils::hex::{self, FromHex}; + + use super::*; + + #[test] + fn test_pack_unpack() { + let builder = UserOperationBuilder::new( + Address::zero(), + 1, + UserOperationRequiredFields { + sender: Address::zero(), + nonce: 0.into(), + call_data: Bytes::new(), + call_gas_limit: 0.into(), + verification_gas_limit: 0.into(), + pre_verification_gas: 0.into(), + max_priority_fee_per_gas: 0.into(), + max_fee_per_gas: 0.into(), + signature: Bytes::new(), + }, + ); + + let uo = builder.build(); + let packed = uo.clone().pack(); + let unpacked = packed.unpack(Address::zero(), 1); + + assert_eq!(uo, unpacked); + } + + #[test] + fn test_hash() { + // From https://sepolia.etherscan.io/tx/0x51c1f40ce6e997a54b39a0eb783e472c2afa4ed3f2f11f97986f7f3a347b9d50 + let entry_point = Address::from_str("0x0000000071727De22E5E9d8BAf0edAc6f37da032").unwrap(); + let chain_id = 11155111; + + let puo = PackedUserOperation { + sender: Address::from_str("0xb292Cf4a8E1fF21Ac27C4f94071Cd02C022C414b").unwrap(), + nonce: U256::from("0xF83D07238A7C8814A48535035602123AD6DBFA63000000000000000000000001"), + init_code: Bytes::from_hex("0x").unwrap(), + call_data: Bytes::from_hex("0xe9ae5c530000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001d8b292cf4a8e1ff21ac27c4f94071cd02c022c414b00000000000000000000000000000000000000000000000000000000000000009517e29f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000ad6330089d9a1fe89f4020292e1afe9969a5a2fc00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000001518000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018e2fbe8980000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000800000000000000000000000002372912728f93ab3daaaebea4f87e6e28476d987000000000000000000000000000000000000000000000000002386f26fc10000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap(), + account_gas_limits: hex::decode("0x000000000000000000000000000114fc0000000000000000000000000012c9b5") + .unwrap() + .try_into() + .unwrap(), + pre_verification_gas: U256::from(48916), + gas_fees: hex::decode("0x000000000000000000000000524121000000000000000000000000109a4a441a") + .unwrap() + .try_into() + .unwrap(), + paymaster_and_data: Bytes::from_hex("0x").unwrap(), + signature: Bytes::from_hex("0x3c7bfe22c9c2ef8994a9637bcc4df1741c5dc0c25b209545a7aeb20f7770f351479b683bd17c4d55bc32e2a649c8d2dff49dcfcc1f3fd837bcd88d1e69a434cf1c").unwrap(), + }; + + let hash = + H256::from_str("0xe486401370d145766c3cf7ba089553214a1230d38662ae532c9b62eb6dadcf7e") + .unwrap(); + let uo = puo.unpack(entry_point, chain_id); + assert_eq!(uo.hash(entry_point, chain_id), hash); + } + + #[test] + fn test_builder() { + let entry_point = Address::zero(); + let chain_id = 1; + + let factory_address = Address::random(); + let paymaster_address = Address::random(); + + let uo = UserOperationBuilder::new( + entry_point, + chain_id, + UserOperationRequiredFields { + sender: Address::zero(), + nonce: 0.into(), + call_data: Bytes::new(), + call_gas_limit: 0.into(), + verification_gas_limit: 0.into(), + pre_verification_gas: 0.into(), + max_priority_fee_per_gas: 0.into(), + max_fee_per_gas: 0.into(), + signature: Bytes::new(), + }, + ) + .factory(factory_address, Bytes::new()) + .paymaster(paymaster_address, 10.into(), 20.into(), Bytes::new()) + .build(); + + assert_eq!(uo.factory, Some(factory_address)); + assert_eq!(uo.paymaster, Some(paymaster_address)); + assert_eq!(uo.paymaster_verification_gas_limit, 10.into()); + assert_eq!(uo.paymaster_post_op_gas_limit, 20.into()); + } +} diff --git a/crates/types/src/validation_results.rs b/crates/types/src/validation_results.rs index dad3659be..1af3cbfd0 100644 --- a/crates/types/src/validation_results.rs +++ b/crates/types/src/validation_results.rs @@ -12,16 +12,54 @@ // If not, see https://www.gnu.org/licenses/. use ethers::{ - abi, - abi::{AbiDecode, AbiError}, - types::{Address, Bytes, U256}, + abi::{self, AbiDecode, AbiError}, + types::{Address, Bytes, H160, U256}, }; use crate::{ - contracts::v0_6::entry_point::{ValidationResult, ValidationResultWithAggregation}, - Timestamp, + contracts::{ + v0_6::i_entry_point::{ + ValidationResult as ValidationResultV0_6, + ValidationResultWithAggregation as ValidationResultWithAggregationV0_6, + }, + v0_7::entry_point_simulations::{ + AggregatorStakeInfo as AggregatorStakeInfoV0_7, ReturnInfo as ReturnInfoV0_7, + StakeInfo as StakeInfoV0_7, ValidationResult as ValidationResultV0_7, + }, + }, + Timestamp, ValidTimeRange, }; +/// Both v0.6 and v0.7 contracts use this aggregator address to indicate that the signature validation failed +/// Zero is also used to indicate that no aggregator is used AND that the signature validation failed. +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. @@ -39,21 +77,42 @@ pub struct ValidationOutput { pub aggregator_info: Option, } -impl AbiDecode for ValidationOutput { - fn decode(bytes: impl AsRef<[u8]>) -> Result { - if let Ok(result) = ValidationResult::decode(bytes.as_ref()) { +impl ValidationOutput { + /// Decode a v0.6 validation result from bytes. + pub fn decode_v0_6(bytes: impl AsRef<[u8]>) -> Result { + if let Ok(result) = ValidationResultV0_6::decode(bytes.as_ref()) { return Ok(result.into()); } - if let Ok(result) = ValidationResultWithAggregation::decode(bytes) { + if let Ok(result) = ValidationResultWithAggregationV0_6::decode(bytes) { return Ok(result.into()); } Err(AbiError::DecodingError(abi::Error::InvalidData)) } + + /// Decode a v0.6 validation result from hex. + pub fn decode_v0_6_hex(hex: impl AsRef) -> Result { + let bytes: Bytes = hex.as_ref().parse()?; + Self::decode_v0_6(&bytes) + } + + /// Decode a v0.7 validation result from bytes. + pub fn decode_v0_7(bytes: impl AsRef<[u8]>) -> Result { + if let Ok(result) = ValidationResultV0_7::decode(bytes.as_ref()) { + return Ok(result.into()); + } + Err(AbiError::DecodingError(abi::Error::InvalidData)) + } + + /// Decode a v0.7 validation result from hex. + pub fn decode_v0_7_hex(hex: impl AsRef) -> Result { + let bytes: Bytes = hex.as_ref().parse()?; + Self::decode_v0_7(&bytes) + } } -impl From for ValidationOutput { - fn from(value: ValidationResult) -> Self { - let ValidationResult { +impl From for ValidationOutput { + fn from(value: ValidationResultV0_6) -> Self { + let ValidationResultV0_6 { return_info, sender_info, factory_info, @@ -69,9 +128,9 @@ impl From for ValidationOutput { } } -impl From for ValidationOutput { - fn from(value: ValidationResultWithAggregation) -> Self { - let ValidationResultWithAggregation { +impl From for ValidationOutput { + fn from(value: ValidationResultWithAggregationV0_6) -> Self { + let ValidationResultWithAggregationV0_6 { return_info, sender_info, factory_info, @@ -88,13 +147,41 @@ impl From for ValidationOutput { } } +impl From for ValidationOutput { + fn from(value: ValidationResultV0_7) -> Self { + let ValidationResultV0_7 { + return_info, + sender_info, + factory_info, + paymaster_info, + aggregator_info, + } = value; + + let aggregator_info = if aggregator_info.aggregator.is_zero() { + None + } else { + Some(aggregator_info.into()) + }; + + Self { + return_info: return_info.into(), + sender_info: sender_info.into(), + factory_info: factory_info.into(), + paymaster_info: paymaster_info.into(), + aggregator_info, + } + } +} + /// ValidationReturnInfo from EntryPoint contract #[derive(Debug)] pub struct ValidationReturnInfo { /// The amount of gas used before the op was executed (pre verification gas and validation gas) pub pre_op_gas: U256, - /// Whether the signature verification failed - pub sig_failed: bool, + /// Whether the account signature verification failed + pub account_sig_failed: bool, + /// Whether the paymaster signature verification failed + pub paymaster_sig_failed: bool, /// The time after which the op is valid pub valid_after: Timestamp, /// The time until which the op is valid @@ -103,6 +190,7 @@ pub struct ValidationReturnInfo { pub paymaster_context: Bytes, } +// Conversion for v0.6 impl From<(U256, U256, bool, u64, u64, Bytes)> for ValidationReturnInfo { fn from(value: (U256, U256, bool, u64, u64, Bytes)) -> Self { let ( @@ -113,9 +201,11 @@ impl From<(U256, U256, bool, u64, u64, Bytes)> for ValidationReturnInfo { valid_until, paymaster_context, ) = value; + // In v0.6 if one signature fails both do Self { pre_op_gas, - sig_failed, + account_sig_failed: sig_failed, + paymaster_sig_failed: sig_failed, valid_after: valid_after.into(), valid_until: valid_until.into(), paymaster_context, @@ -123,6 +213,84 @@ impl From<(U256, U256, bool, u64, u64, Bytes)> for ValidationReturnInfo { } } +impl From for ValidationReturnInfo { + fn from(value: ReturnInfoV0_7) -> Self { + let ReturnInfoV0_7 { + pre_op_gas, + prefund: _, + account_validation_data, + paymaster_validation_data, + paymaster_context, + } = value; + + let account = parse_validation_data(account_validation_data); + let paymaster = parse_validation_data(paymaster_validation_data); + + let intersect_range = account + .valid_time_range() + .intersect(paymaster.valid_time_range()); + + Self { + pre_op_gas, + account_sig_failed: !account.signature_valid(), + paymaster_sig_failed: !paymaster.signature_valid(), + valid_after: intersect_range.valid_after, + valid_until: intersect_range.valid_until, + paymaster_context, + } + } +} + +/// ValidationData from EntryPoint contract +pub struct ValidationData { + aggregator: Address, + valid_after: u64, + valid_until: u64, +} + +impl ValidationData { + /// Valid time range for the validation data + pub fn valid_time_range(&self) -> ValidTimeRange { + ValidTimeRange::new(self.valid_after.into(), self.valid_until.into()) + } + + /// Whether the signature is valid + pub fn signature_valid(&self) -> bool { + self.aggregator != SIG_VALIDATION_FAILED + } + + /// The aggregator address, if any + pub fn aggregator(&self) -> Option
{ + if self.aggregator == SIG_VALIDATION_FAILED || self.aggregator.is_zero() { + None + } else { + Some(self.aggregator) + } + } +} + +/// Parse the validation data from a U256 +/// +/// Works for both v0.6 and v0.7 validation data +pub fn parse_validation_data(data: U256) -> ValidationData { + let slice: [u8; 32] = data.into(); + let aggregator = Address::from_slice(&slice[0..20]); + + let mut buf = [0_u8; 8]; + buf[..6].copy_from_slice(&slice[20..26]); + let valid_after = u64::from_le_bytes(buf); + + let mut buf = [0_u8; 8]; + buf[..6].copy_from_slice(&slice[26..32]); + let valid_until = u64::from_le_bytes(buf); + + ValidationData { + aggregator, + valid_after, + valid_until, + } +} + /// StakeInfo from EntryPoint contract #[derive(Clone, Copy, Debug)] pub struct StakeInfo { @@ -141,6 +309,19 @@ impl From<(U256, U256)> for StakeInfo { } } +impl From for StakeInfo { + fn from(value: StakeInfoV0_7) -> Self { + let StakeInfoV0_7 { + stake, + unstake_delay_sec, + } = value; + Self { + stake, + unstake_delay_sec, + } + } +} + /// AggregatorInfo from EntryPoint contract #[derive(Clone, Copy, Debug)] pub struct AggregatorInfo { @@ -158,3 +339,35 @@ impl From<(Address, (U256, U256))> for AggregatorInfo { } } } + +impl From for AggregatorInfo { + fn from(value: AggregatorStakeInfoV0_7) -> Self { + let AggregatorStakeInfoV0_7 { + aggregator, + stake_info, + } = value; + Self { + address: aggregator, + stake_info: stake_info.into(), + } + } +} + +#[cfg(test)] +mod tests { + use super::parse_validation_data; + + #[test] + fn test_parse_validation_data() { + let data = "0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"; + let parsed = parse_validation_data(data.into()); + assert_eq!( + parsed.aggregator, + "0x00112233445566778899aabbccddeeff00112233" + .parse() + .unwrap() + ); + assert_eq!(parsed.valid_after, 0x998877665544); // solidity is LE + assert_eq!(parsed.valid_until, 0xffeeddccbbaa); + } +} 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