From d951f1465da155b03f9ac0e191b80e1c21b47d55 Mon Sep 17 00:00:00 2001 From: dancoombs Date: Tue, 12 Mar 2024 06:59:12 -0400 Subject: [PATCH 01/14] feat: add and use a user operation trait --- Cargo.lock | 34 +- crates/builder/src/bundle_proposer.rs | 359 +++++----- crates/builder/src/bundle_sender.rs | 34 +- crates/builder/src/task.rs | 16 +- crates/dev/src/lib.rs | 24 +- crates/pool/proto/op_pool/op_pool.proto | 34 +- crates/pool/src/emit.rs | 8 +- crates/pool/src/lib.rs | 3 +- crates/pool/src/mempool/error.rs | 2 +- crates/pool/src/mempool/mod.rs | 81 ++- crates/pool/src/mempool/paymaster.rs | 46 +- crates/pool/src/mempool/pool.rs | 139 ++-- crates/pool/src/mempool/uo_pool.rs | 106 +-- crates/pool/src/server/local.rs | 76 ++- crates/pool/src/server/mod.rs | 16 +- crates/pool/src/server/remote/client.rs | 16 +- crates/pool/src/server/remote/error.rs | 40 +- crates/pool/src/server/remote/protos.rs | 52 +- crates/pool/src/task.rs | 29 +- crates/provider/Cargo.toml | 4 + crates/provider/src/ethers/entry_point/mod.rs | 14 + .../{entry_point.rs => entry_point/v0_6.rs} | 344 +++++++--- crates/provider/src/ethers/mod.rs | 2 +- crates/provider/src/ethers/provider.rs | 112 +--- crates/provider/src/lib.rs | 12 +- crates/provider/src/traits/entry_point.rs | 138 +++- crates/provider/src/traits/mod.rs | 11 +- crates/provider/src/traits/provider.rs | 53 +- crates/provider/src/traits/test_utils.rs | 116 ++++ crates/rpc/src/debug.rs | 3 +- crates/rpc/src/eth/api.rs | 100 +-- crates/rpc/src/eth/mod.rs | 8 +- crates/rpc/src/eth/server.rs | 14 +- crates/rpc/src/rundler.rs | 12 +- crates/rpc/src/task.rs | 18 +- crates/rpc/src/types.rs | 59 +- crates/sim/src/estimation/mod.rs | 76 ++- .../src/estimation/{estimation.rs => v0_6.rs} | 137 ++-- crates/sim/src/gas/gas.rs | 155 ++--- crates/sim/src/lib.rs | 14 +- crates/sim/src/precheck.rs | 150 ++--- crates/sim/src/simulation/mempool.rs | 2 +- crates/sim/src/simulation/mod.rs | 503 +++++++++++++- crates/sim/src/simulation/v0_6/mod.rs | 23 + .../{simulation.rs => v0_6/simulator.rs} | 616 +++--------------- .../sim/src/simulation/{ => v0_6}/tracer.rs | 65 +- crates/types/Cargo.toml | 1 + crates/types/src/lib.rs | 4 +- crates/types/src/user_operation.rs | 308 --------- crates/types/src/user_operation/mod.rs | 371 +++++++++++ crates/types/src/user_operation/v0_6.rs | 430 ++++++++++++ crates/types/src/user_operation/v0_7.rs | 613 +++++++++++++++++ 52 files changed, 3592 insertions(+), 2011 deletions(-) create mode 100644 crates/provider/src/ethers/entry_point/mod.rs rename crates/provider/src/ethers/{entry_point.rs => entry_point/v0_6.rs} (57%) create mode 100644 crates/provider/src/traits/test_utils.rs rename crates/sim/src/estimation/{estimation.rs => v0_6.rs} (93%) create mode 100644 crates/sim/src/simulation/v0_6/mod.rs rename crates/sim/src/simulation/{simulation.rs => v0_6/simulator.rs} (68%) rename crates/sim/src/simulation/{ => v0_6}/tracer.rs (75%) delete mode 100644 crates/types/src/user_operation.rs create mode 100644 crates/types/src/user_operation/mod.rs create mode 100644 crates/types/src/user_operation/v0_6.rs create mode 100644 crates/types/src/user_operation/v0_7.rs diff --git a/Cargo.lock b/Cargo.lock index 324504f08..41808c810 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" @@ -4216,11 +4191,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", @@ -4331,6 +4308,7 @@ dependencies = [ "constcat", "ethers", "parse-display", + "rand", "rundler-utils", "serde", "serde_json", diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index d2780e3a5..204d9a778 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -12,7 +12,6 @@ // If not, see https://www.gnu.org/licenses/. use std::{ - cmp, collections::{BTreeMap, HashMap, HashSet}, future::Future, mem, @@ -29,16 +28,17 @@ 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_pool::{FromPoolOperationVariant, PoolOperation, PoolServer}; +use rundler_provider::{ + BundleHandler, EntryPoint, HandleOpsOut, L1GasProvider, Provider, SignatureAggregator, +}; use rundler_sim::{ - gas::{self, GasOverheads}, - EntityInfo, EntityInfos, ExpectedStorage, FeeEstimator, PriorityFeeMode, SimulationError, + gas, EntityInfo, EntityInfos, ExpectedStorage, FeeEstimator, PriorityFeeMode, SimulationError, SimulationResult, SimulationViolation, Simulator, ViolationError, }; use rundler_types::{ - chain::ChainSpec, Entity, EntityType, EntityUpdate, EntityUpdateType, GasFees, Timestamp, - UserOperation, UserOpsPerAggregator, + chain::ChainSpec, Entity, EntityType, EntityUpdate, EntityUpdateType, GasFees, GasOverheads, + Timestamp, UserOperation, UserOperationVariant, UserOpsPerAggregator, }; use rundler_utils::{emit::WithEntryPoint, math}; use tokio::{sync::broadcast, try_join}; @@ -51,17 +51,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,29 +86,25 @@ 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, simulator: S, @@ -104,6 +113,7 @@ where settings: Settings, fee_estimator: FeeEstimator

, event_sender: broadcast::Sender>, + _uo_type: std::marker::PhantomData, } #[derive(Debug)] @@ -117,18 +127,21 @@ pub(crate) struct Settings { } #[async_trait] -impl BundleProposer for BundleProposerImpl +impl BundleProposer for BundleProposerImpl where - S: Simulator, - E: EntryPoint, + UO: UserOperation + From, + S: Simulator, + E: EntryPoint + SignatureAggregator + BundleHandler + L1GasProvider, P: Provider, C: PoolServer, { + 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,10 +232,11 @@ where } } -impl BundleProposerImpl +impl BundleProposerImpl where - S: Simulator, - E: EntryPoint, + UO: UserOperation + From, + S: Simulator, + E: EntryPoint + SignatureAggregator + BundleHandler + L1GasProvider, P: Provider, C: PoolServer, { @@ -249,6 +263,7 @@ where ), settings, event_sender, + _uo_type: std::marker::PhantomData, } } @@ -260,14 +275,14 @@ where // - any errors async fn filter_and_simulate( &self, - op: PoolOperation, + op: PoolOperation, block_hash: H256, base_fee: U256, required_op_fees: GasFees, - ) -> Option<(PoolOperation, Result)> { + ) -> Option<(PoolOperation, Result)> { // 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 +290,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,7 +301,7 @@ where // Check if the pvg is enough let required_pvg = gas::calc_required_pre_verification_gas( &self.settings.chain_spec, - self.provider.clone(), + &self.entry_point, &op.uo, base_fee, ) @@ -305,18 +320,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), 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; @@ -355,12 +370,12 @@ where async fn assemble_context( &self, - ops_with_simulations: Vec<(PoolOperation, Result)>, + 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 paymasters_to_reject = Vec::::new(); @@ -410,13 +425,8 @@ where } // 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 +434,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,12 +462,8 @@ 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 @@ -475,19 +481,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 +511,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 +531,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 @@ -566,20 +577,24 @@ where } } - async fn get_ops_from_pool(&self) -> anyhow::Result> { + async fn get_ops_from_pool(&self) -> anyhow::Result>> { // Use builder's index as the shard index to ensure that two builders don't // attempt to bundle the same operations. // // 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() + .map(PoolOperation::::from_variant) + .collect()) } async fn get_balances_by_paymaster( @@ -603,14 +618,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 +635,7 @@ where async fn process_failed_op( &self, - context: &mut ProposalContext, + context: &mut ProposalContext, index: usize, message: String, ) -> anyhow::Result<()> { @@ -679,7 +695,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 +748,7 @@ where async fn check_for_post_op_revert_single_op( &self, - op: UserOperation, + op: UO, gas: U256, op_index: usize, ) -> Vec { @@ -768,7 +784,7 @@ where async fn check_for_post_op_revert_agg_ops( &self, - group: UserOpsPerAggregator, + group: UserOpsPerAggregator, gas: U256, start_index: usize, ) -> Vec { @@ -801,20 +817,16 @@ where fn limit_user_operations_for_simulation( &self, - ops: Vec, - ) -> (Vec, u64) { + ops: Vec>, + ) -> (Vec>, u64) { // Make the bundle gas limit 10% higher here so that we simulate more UOs than we need in case that we end up dropping some UOs later so we can still pack a full bundle let mut gas_left = math::increase_by_percent(U256::from(self.settings.max_bundle_gas), 10); let mut ops_in_bundle = Vec::new(); 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 +853,23 @@ 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: &UO) -> H256 { + 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 +880,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 +930,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 +1014,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 +1040,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 +1056,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 +1182,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 +1239,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 +1246,10 @@ mod tests { types::{H160, U64}, utils::parse_units, }; - use rundler_pool::MockPoolServer; - use rundler_provider::{AggregatorSimOut, MockEntryPoint, MockProvider}; + use rundler_pool::{IntoPoolOperationVariant, MockPoolServer}; + use rundler_provider::{AggregatorSimOut, MockEntryPointV0_6, MockProvider}; use rundler_sim::{MockSimulator, SimulationViolation, ViolationError}; - use rundler_types::ValidTimeRange; + use rundler_types::{v0_6::UserOperation, UserOperation as UserOperationTrait, ValidTimeRange}; use super::*; @@ -1541,7 +1534,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 +1594,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 +1610,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 +1623,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 +1755,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 +1795,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 +1843,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 +1993,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 +2012,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); @@ -2027,18 +2023,29 @@ mod tests { .map(|MockOp { op, .. }| PoolOperation { uo: op.clone(), 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(); - pool_client - .expect_get_ops() - .returning(move |_, _, _| Ok(ops.clone())); + pool_client.expect_get_ops().returning(move |_, _, _| { + Ok(ops + .iter() + .cloned() + .map(IntoPoolOperationVariant::into_variant) + .collect()) + }); 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 +2053,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 +2086,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..af6eb2748 100644 --- a/crates/builder/src/bundle_sender.rs +++ b/crates/builder/src/bundle_sender.rs @@ -18,7 +18,7 @@ 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_utils::emit::WithEntryPoint; @@ -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: std::marker::PhantomData, } #[derive(Debug)] @@ -99,10 +94,11 @@ 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, { @@ -247,10 +243,11 @@ where } } -impl BundleSenderImpl +impl BundleSenderImpl where - P: BundleProposer, - E: EntryPoint, + UO: UserOperation, + P: BundleProposer, + E: EntryPoint + BundleHandler, T: TransactionTracker, C: PoolServer, { @@ -278,6 +275,7 @@ where pool, settings, event_sender, + _uo_type: std::marker::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/task.rs b/crates/builder/src/task.rs index a69dc3b73..7ad6e3def 100644 --- a/crates/builder/src/task.rs +++ b/crates/builder/src/task.rs @@ -23,9 +23,13 @@ use ethers_signers::Signer; use futures::future; use futures_util::TryFutureExt; use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, EthersEntryPoint}; +use rundler_provider::EthersEntryPointV0_6; use rundler_sim::{ - MempoolConfig, PriorityFeeMode, SimulateValidationTracerImpl, SimulationSettings, SimulatorImpl, + simulation::v0_6::{ + SimulateValidationTracerImpl as SimulateValidationTracerImplV0_6, + Simulator as SimulatorV0_6, + }, + MempoolConfig, PriorityFeeMode, SimulationSettings, }; use rundler_task::Task; use rundler_types::chain::ChainSpec; @@ -262,15 +266,15 @@ where bundle_priority_fee_overhead_percent: self.args.bundle_priority_fee_overhead_percent, }; - let ep = EthersEntryPoint::new( + let ep = EthersEntryPointV0_6::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( + SimulateValidationTracerImplV0_6::new(Arc::clone(&provider), ep.clone()); + let simulator = SimulatorV0_6::new( Arc::clone(&provider), - ep.address(), + ep.clone(), simulate_validation_tracer, self.args.sim_settings, self.args.mempool_configs.clone(), diff --git a/crates/dev/src/lib.rs b/crates/dev/src/lib.rs index 7fe999e55..9206fa63e 100644 --- a/crates/dev/src/lib.rs +++ b/crates/dev/src/lib.rs @@ -46,7 +46,7 @@ use rundler_types::{ entry_point::EntryPoint, 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() } } @@ -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 @@ -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/proto/op_pool/op_pool.proto b/crates/pool/proto/op_pool/op_pool.proto index b6152bfb6..062a559e3 100644 --- a/crates/pool/proto/op_pool/op_pool.proto +++ b/crates/pool/proto/op_pool/op_pool.proto @@ -17,9 +17,15 @@ syntax = "proto3"; package op_pool; +message UserOperation { + oneof uo { + UserOperationV06 v06 = 1; + } +} + // 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” ) @@ -502,20 +508,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; } } 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..2a44d2509 100644 --- a/crates/pool/src/lib.rs +++ b/crates/pool/src/lib.rs @@ -26,7 +26,8 @@ pub use emit::OpPoolEvent as PoolEvent; mod mempool; pub use mempool::{ - MempoolError, PoolConfig, PoolOperation, Reputation, ReputationStatus, StakeStatus, + FromPoolOperationVariant, IntoPoolOperationVariant, MempoolError, PoolConfig, PoolOperation, + Reputation, ReputationStatus, StakeStatus, }; mod server; diff --git a/crates/pool/src/mempool/error.rs b/crates/pool/src/mempool/error.rs index 3d3370bbc..69500459c 100644 --- a/crates/pool/src/mempool/error.rs +++ b/crates/pool/src/mempool/error.rs @@ -108,7 +108,7 @@ impl From for MempoolError { // extract violation and replace with dummy Self::PrecheckViolation(mem::replace( violation, - PrecheckViolation::InitCodeTooShort(0), + PrecheckViolation::SenderIsNotContractAndNoInitCode(Address::zero()), )) } } diff --git a/crates/pool/src/mempool/mod.rs b/crates/pool/src/mempool/mod.rs index c9c22fd78..df2c80728 100644 --- a/crates/pool/src/mempool/mod.rs +++ b/crates/pool/src/mempool/mod.rs @@ -37,7 +37,8 @@ use ethers::types::{Address, H256, U256}; use mockall::automock; use rundler_sim::{EntityInfos, MempoolConfig, PrecheckSettings, SimulationSettings}; use rundler_types::{ - Entity, EntityType, EntityUpdate, UserOperation, UserOperationId, ValidTimeRange, + Entity, EntityType, EntityUpdate, UserOperation, UserOperationId, UserOperationVariant, + ValidTimeRange, }; use tonic::async_trait; pub(crate) use uo_pool::UoPool; @@ -45,10 +46,13 @@ pub(crate) use uo_pool::UoPool; use self::error::MempoolResult; use super::chain::ChainUpdate; -#[cfg_attr(test, automock)] +#[cfg_attr(test, automock(type UO = rundler_types::v0_6::UserOperation;))] #[async_trait] /// In-memory operation pool pub trait Mempool: Send + Sync + 'static { + /// The type of user operation this pool stores + type UO: UserOperation; + /// Call to update the mempool with a new chain update async fn on_chain_update(&self, update: &ChainUpdate); @@ -56,11 +60,7 @@ pub trait Mempool: Send + Sync + 'static { fn entry_point(&self) -> Address; /// Adds a user operation to the pool - async fn add_operation( - &self, - origin: OperationOrigin, - op: UserOperation, - ) -> MempoolResult; + async fn add_operation(&self, origin: OperationOrigin, op: Self::UO) -> MempoolResult; /// Removes a set of operations from the pool. fn remove_operations(&self, hashes: &[H256]); @@ -83,13 +83,13 @@ pub trait Mempool: Send + Sync + 'static { &self, max: usize, shard_index: u64, - ) -> MempoolResult>>; + ) -> MempoolResult>>>; /// Returns the all operations from the pool up to a max size - fn all_operations(&self, max: usize) -> Vec>; + fn all_operations(&self, max: usize) -> Vec>>; /// Looks up a user operation by hash, returns None if not found - fn get_user_operation_by_hash(&self, hash: H256) -> Option>; + fn get_user_operation_by_hash(&self, hash: H256) -> Option>>; /// Debug methods @@ -192,10 +192,10 @@ pub enum OperationOrigin { } /// A user operation with additional metadata from validation. -#[derive(Debug, Default, Clone, Eq, PartialEq)] -pub struct PoolOperation { +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct PoolOperation { /// The user operation stored in the pool - pub uo: UserOperation, + pub uo: UO, /// The entry point address for this operation pub entry_point: Address, /// The aggregator address for this operation, if any. @@ -227,7 +227,7 @@ pub struct PaymasterMetadata { pub pending_balance: U256, } -impl PoolOperation { +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) { @@ -291,9 +291,62 @@ impl PoolOperation { } } +/// Trait to convert a [PoolOperation] holding a [UserOperationVariant] to a [PoolOperation] with a different user operation type. +pub trait FromPoolOperationVariant { + /// Conversion + fn from_variant(op: PoolOperation) -> Self; +} + +/// Trait to convert a [PoolOperation] holding a user operation to a [PoolOperation] with a [UserOperationVariant]. +pub trait IntoPoolOperationVariant { + /// Conversion + fn into_variant(self) -> PoolOperation; +} + +impl FromPoolOperationVariant for PoolOperation +where + UO: UserOperation + From, +{ + fn from_variant(op: PoolOperation) -> Self { + PoolOperation { + uo: op.uo.into(), + entry_point: op.entry_point, + aggregator: op.aggregator, + valid_time_range: op.valid_time_range, + expected_code_hash: op.expected_code_hash, + sim_block_hash: op.sim_block_hash, + sim_block_number: op.sim_block_number, + entities_needing_stake: op.entities_needing_stake, + account_is_staked: op.account_is_staked, + entity_infos: op.entity_infos, + } + } +} + +impl IntoPoolOperationVariant for PoolOperation +where + UO: UserOperation + Into, +{ + fn into_variant(self) -> PoolOperation { + PoolOperation { + uo: self.uo.into(), + entry_point: self.entry_point, + aggregator: self.aggregator, + valid_time_range: self.valid_time_range, + expected_code_hash: self.expected_code_hash, + sim_block_hash: self.sim_block_hash, + sim_block_number: self.sim_block_number, + entities_needing_stake: self.entities_needing_stake, + account_is_staked: self.account_is_staked, + entity_infos: self.entity_infos, + } + } +} + #[cfg(test)] mod tests { use rundler_sim::EntityInfo; + use rundler_types::v0_6::UserOperation; use super::*; diff --git a/crates/pool/src/mempool/paymaster.rs b/crates/pool/src/mempool/paymaster.rs index 5b77c718f..471a55ef2 100644 --- a/crates/pool/src/mempool/paymaster.rs +++ b/crates/pool/src/mempool/paymaster.rs @@ -30,9 +30,9 @@ use crate::{ /// Keeps track of current and pending paymaster balances #[derive(Debug)] -pub(crate) struct PaymasterTracker { +pub(crate) struct PaymasterTracker { entry_point: E, - state: RwLock, + state: RwLock>, config: PaymasterConfig, } @@ -60,8 +60,9 @@ impl PaymasterConfig { } } -impl PaymasterTracker +impl PaymasterTracker where + UO: UserOperation, E: EntryPoint, { pub(crate) fn new(entry_point: E, config: PaymasterConfig) -> Self { @@ -150,7 +151,7 @@ where Ok(paymaster_meta) } - pub(crate) async fn check_operation_cost(&self, op: &UserOperation) -> MempoolResult<()> { + pub(crate) async fn check_operation_cost(&self, op: &UO) -> MempoolResult<()> { if let Some(paymaster) = op.paymaster() { let balance = self.paymaster_balance(paymaster).await?; self.state.read().check_operation_cost(op, &balance)? @@ -205,7 +206,7 @@ where .unmine_actual_cost(paymaster, actual_cost); } - pub(crate) async fn add_or_update_balance(&self, po: &PoolOperation) -> MempoolResult<()> { + pub(crate) async fn add_or_update_balance(&self, po: &PoolOperation) -> MempoolResult<()> { if let Some(paymaster) = po.uo.paymaster() { let paymaster_metadata = self.paymaster_balance(paymaster).await?; return self @@ -218,23 +219,25 @@ 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 +struct PaymasterTrackerInner { + // 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, + _uo_type: std::marker::PhantomData, } -impl PaymasterTrackerInner { +impl PaymasterTrackerInner { fn new(tracker_enabled: bool, cache_size: u32) -> Self { Self { user_op_fees: HashMap::new(), tracker_enabled, paymaster_balances: LruMap::new(cache_size), + _uo_type: std::marker::PhantomData, } } @@ -248,7 +251,7 @@ impl PaymasterTrackerInner { fn check_operation_cost( &self, - op: &UserOperation, + op: &UO, paymaster_metadata: &PaymasterMetadata, ) -> MempoolResult<()> { let max_op_cost = op.max_gas_cost(); @@ -367,7 +370,7 @@ impl PaymasterTrackerInner { fn add_or_update_balance( &mut self, - po: &PoolOperation, + po: &PoolOperation, paymaster_metadata: &PaymasterMetadata, ) -> MempoolResult<()> { let id = po.uo.id(); @@ -520,9 +523,12 @@ impl PaymasterBalance { #[cfg(test)] mod tests { use ethers::types::{Address, H256, U256}; - use rundler_provider::MockEntryPoint; + use rundler_provider::MockEntryPointV0_6; use rundler_sim::EntityInfos; - use rundler_types::{DepositInfo, UserOperation, UserOperationId, ValidTimeRange}; + use rundler_types::{ + contracts::v0_6::verifying_paymaster::DepositInfo, v0_6::UserOperation, + UserOperation as UserOperationTrait, UserOperationId, ValidTimeRange, + }; use super::*; use crate::{ @@ -531,7 +537,7 @@ mod tests { PoolOperation, }; - fn demo_pool_op(uo: UserOperation) -> PoolOperation { + fn demo_pool_op(uo: UserOperation) -> PoolOperation { PoolOperation { uo, entry_point: Address::random(), @@ -944,7 +950,7 @@ mod tests { #[test] fn test_inner_cache_full() { - let mut inner = PaymasterTrackerInner::new(true, 2); + let mut inner = PaymasterTrackerInner::::new(true, 2); let paymaster_0 = Address::random(); let paymaster_1 = Address::random(); @@ -963,8 +969,8 @@ 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 { @@ -989,7 +995,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..5bfca439d 100644 --- a/crates/pool/src/mempool/pool.rs +++ b/crates/pool/src/mempool/pool.rs @@ -59,19 +59,19 @@ impl From for PoolInnerConfig { /// Pool of user operations #[derive(Debug)] -pub(crate) struct PoolInner { +pub(crate) struct PoolInner { /// Pool settings config: PoolInnerConfig, /// Operations by hash - by_hash: HashMap, + by_hash: HashMap>, /// Operations by operation ID - by_id: HashMap, + by_id: HashMap>, /// Best operations, sorted by gas price - best: BTreeSet, + best: BTreeSet>, /// Removed operations, temporarily kept around in case their blocks are /// reorged away. Stored along with the block number at which it was /// removed. - mined_at_block_number_by_hash: HashMap, + mined_at_block_number_by_hash: HashMap, u64)>, /// Removed operation hashes sorted by block number, so we can forget them /// when enough new blocks have passed. mined_hashes_with_block_numbers: BTreeSet<(u64, H256)>, @@ -85,7 +85,7 @@ pub(crate) struct PoolInner { cache_size: SizeTracker, } -impl PoolInner { +impl PoolInner { pub(crate) fn new(config: PoolInnerConfig) -> Self { Self { config, @@ -102,11 +102,11 @@ 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: &UO) -> 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,32 +115,32 @@ 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) } } - pub(crate) fn add_operation(&mut self, op: PoolOperation) -> MempoolResult { + pub(crate) fn add_operation(&mut self, op: PoolOperation) -> MempoolResult { let ret = self.add_operation_internal(Arc::new(op), None); self.update_metrics(); ret } - pub(crate) fn best_operations(&self) -> impl Iterator> { + pub(crate) fn best_operations(&self) -> impl Iterator>> { self.best.clone().into_iter().map(|v| v.po) } @@ -171,25 +171,33 @@ impl PoolInner { 0 } - pub(crate) fn get_operation_by_hash(&self, hash: H256) -> Option> { + pub(crate) fn get_operation_by_hash(&self, hash: H256) -> Option>> { self.by_hash.get(&hash).map(|o| o.po.clone()) } - pub(crate) fn get_operation_by_id(&self, id: &UserOperationId) -> Option> { + pub(crate) fn get_operation_by_id( + &self, + id: &UserOperationId, + ) -> Option>> { self.by_id.get(id).map(|o| o.po.clone()) } - pub(crate) fn remove_operation_by_hash(&mut self, hash: H256) -> Option> { + pub(crate) fn remove_operation_by_hash( + &mut self, + hash: H256, + ) -> Option>> { let ret = self.remove_operation_internal(hash, None); self.update_metrics(); ret } // 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: &UO) -> 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 +221,11 @@ impl PoolInner { pub(crate) fn check_associated_storage( &self, accessed_storage: &HashSet
, - uo: &UserOperation, + uo: &UO, ) -> 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) { @@ -235,12 +243,12 @@ impl PoolInner { &mut self, mined_op: &MinedOp, block_number: u64, - ) -> Option> { + ) -> Option>> { let tx_in_pool = self.by_id.get(&mined_op.id())?; 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)); @@ -248,7 +256,10 @@ impl PoolInner { ret } - pub(crate) fn unmine_operation(&mut self, mined_op: &MinedOp) -> Option> { + pub(crate) fn unmine_operation( + &mut self, + mined_op: &MinedOp, + ) -> Option>> { let hash = mined_op.hash; let (op, block_number) = self.mined_at_block_number_by_hash.remove(&hash)?; self.mined_hashes_with_block_numbers @@ -285,10 +296,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 +354,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) @@ -359,13 +367,13 @@ impl PoolInner { Ok(removed) } - fn put_back_unmined_operation(&mut self, op: OrderedPoolOperation) -> MempoolResult { + fn put_back_unmined_operation(&mut self, op: OrderedPoolOperation) -> MempoolResult { self.add_operation_internal(op.po, Some(op.submission_id)) } fn add_operation_internal( &mut self, - op: Arc, + op: Arc>, submission_id: Option, ) -> MempoolResult { // Check if operation already known or replacing an existing operation @@ -390,7 +398,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()); @@ -412,7 +420,7 @@ impl PoolInner { &mut self, hash: H256, block_number: Option, - ) -> Option> { + ) -> Option>> { let op = self.by_hash.remove(&hash)?; let id = &op.po.uo.id(); self.by_id.remove(id); @@ -449,13 +457,13 @@ impl PoolInner { id } - fn get_min_replacement_fees(&self, op: &UserOperation) -> (U256, U256) { + fn get_min_replacement_fees(&self, op: &UO) -> (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) @@ -478,41 +486,41 @@ impl PoolInner { /// Wrapper around PoolOperation that adds a submission ID to implement /// a custom ordering for the best operations #[derive(Debug, Clone)] -struct OrderedPoolOperation { - po: Arc, +struct OrderedPoolOperation { + po: Arc>, submission_id: u64, } -impl OrderedPoolOperation { - fn uo(&self) -> &UserOperation { +impl OrderedPoolOperation { + fn uo(&self) -> &UO { &self.po.uo } fn mem_size(&self) -> usize { - std::mem::size_of::() + self.po.mem_size() + std::mem::size_of::() + self.po.mem_size() } } -impl Eq for OrderedPoolOperation {} +impl Eq for OrderedPoolOperation {} -impl Ord for OrderedPoolOperation { +impl Ord for OrderedPoolOperation { fn cmp(&self, other: &Self) -> Ordering { // 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)) } } -impl PartialOrd for OrderedPoolOperation { +impl PartialOrd for OrderedPoolOperation { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl PartialEq for OrderedPoolOperation { +impl PartialEq for OrderedPoolOperation { fn eq(&self, other: &Self) -> bool { self.cmp(other) == Ordering::Equal } @@ -538,6 +546,7 @@ impl PoolMetrics { #[cfg(test)] mod tests { use rundler_sim::{EntityInfo, EntityInfos}; + use rundler_types::{v0_6::UserOperation, UserOperation as UserOperationTrait, ValidTimeRange}; use super::*; @@ -689,7 +698,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 +727,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(); @@ -968,7 +975,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 +997,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 { @@ -1013,7 +1020,11 @@ mod tests { .mem_size() } - fn create_op(sender: Address, nonce: usize, max_fee_per_gas: usize) -> PoolOperation { + fn create_op( + sender: Address, + nonce: usize, + max_fee_per_gas: usize, + ) -> PoolOperation { PoolOperation { uo: UserOperation { sender, @@ -1031,11 +1042,21 @@ 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, } } - fn check_map_entry(actual: Option<&OrderedPoolOperation>, expected: Option<&PoolOperation>) { + fn check_map_entry( + actual: Option<&OrderedPoolOperation>, + expected: Option<&PoolOperation>, + ) { match (actual, expected) { (Some(actual), Some(expected)) => assert_eq!(*actual.po, *expected), (None, None) => (), diff --git a/crates/pool/src/mempool/uo_pool.rs b/crates/pool/src/mempool/uo_pool.rs index 6e9e4054c..20c36705b 100644 --- a/crates/pool/src/mempool/uo_pool.rs +++ b/crates/pool/src/mempool/uo_pool.rs @@ -21,7 +21,9 @@ 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::{ + Entity, EntityUpdate, EntityUpdateType, UserOperation, UserOperationId, UserOperationVariant, +}; use rundler_utils::emit::WithEntryPoint; use tokio::sync::broadcast; use tonic::async_trait; @@ -45,26 +47,27 @@ 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, + state: RwLock>, + paymaster: PaymasterTracker, reputation: Arc, event_sender: broadcast::Sender>, prechecker: P, simulator: S, } -struct UoPoolState { - pool: PoolInner, +struct UoPoolState { + pool: PoolInner, throttled_ops: HashSet, block_number: u64, } -impl UoPool +impl UoPool where - P: Prechecker, - S: Simulator, + UO: UserOperation, + P: Prechecker, + S: Simulator, E: EntryPoint, { pub(crate) fn new( @@ -72,7 +75,7 @@ where event_sender: broadcast::Sender>, prechecker: P, simulator: S, - paymaster: PaymasterTracker, + paymaster: PaymasterTracker, reputation: Arc, ) -> Self { Self { @@ -131,12 +134,15 @@ where } #[async_trait] -impl Mempool for UoPool +impl Mempool for UoPool where - P: Prechecker, - S: Simulator, + UO: UserOperation + Into, + P: Prechecker, + S: Simulator, E: EntryPoint, { + type UO = UO; + async fn on_chain_update(&self, update: &ChainUpdate) { { let deduped_ops = update.deduped_ops(); @@ -320,11 +326,7 @@ where self.paymaster.get_stake_status(address).await } - async fn add_operation( - &self, - origin: OperationOrigin, - op: UserOperation, - ) -> MempoolResult { + async fn add_operation(&self, origin: OperationOrigin, op: UO) -> MempoolResult { // TODO(danc) aggregator reputation is not implemented // TODO(danc) catch ops with aggregators prior to simulation and reject @@ -412,12 +414,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,12 +466,12 @@ 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 { op_hash, - op: pool_op.uo, + op: pool_op.uo.into(), block_number: pool_op.sim_block_number, origin, valid_after, @@ -522,7 +524,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 @@ -562,7 +564,7 @@ where &self, max: usize, shard_index: u64, - ) -> MempoolResult>> { + ) -> MempoolResult>>> { if shard_index >= self.config.num_shards { Err(anyhow::anyhow!("Invalid shard ID"))?; } @@ -577,22 +579,22 @@ 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) .collect()) } - fn all_operations(&self, max: usize) -> Vec> { + fn all_operations(&self, max: usize) -> Vec>> { self.state.read().pool.best_operations().take(max).collect() } - fn get_user_operation_by_hash(&self, hash: H256) -> Option> { + fn get_user_operation_by_hash(&self, hash: H256) -> Option>> { self.state.read().pool.get_operation_by_hash(hash) } @@ -669,13 +671,16 @@ mod tests { use std::collections::HashMap; use ethers::types::{Bytes, H160}; - use rundler_provider::MockEntryPoint; + use rundler_provider::MockEntryPointV0_6; use rundler_sim::{ EntityInfo, EntityInfos, MockPrechecker, MockSimulator, PrecheckError, PrecheckSettings, PrecheckViolation, SimulationError, SimulationResult, SimulationSettings, SimulationViolation, ViolationError, }; - use rundler_types::{DepositInfo, EntityType, GasFees, ValidTimeRange}; + use rundler_types::{ + contracts::v0_6::verifying_paymaster::DepositInfo, v0_6::UserOperation, EntityType, + GasFees, UserOperation as UserOperationTrait, ValidTimeRange, + }; use super::*; use crate::{ @@ -765,7 +770,7 @@ 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), + hash: uos[0].hash(pool.config.entry_point, 1), sender: uos[0].sender, nonce: uos[0].nonce, actual_gas_cost: U256::zero(), @@ -827,7 +832,7 @@ 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), + hash: uos[0].hash(pool.config.entry_point, 1), sender: uos[0].sender, nonce: uos[0].nonce, actual_gas_cost: 10.into(), @@ -867,7 +872,7 @@ 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), + hash: uos[0].hash(pool.config.entry_point, 1), sender: uos[0].sender, nonce: uos[0].nonce, actual_gas_cost: 10.into(), @@ -908,7 +913,7 @@ mod tests { reorg_depth: 0, mined_ops: vec![MinedOp { entry_point: Address::random(), - hash: uos[0].op_hash(pool.config.entry_point, 1), + hash: uos[0].hash(pool.config.entry_point, 1), sender: uos[0].sender, nonce: uos[0].nonce, actual_gas_cost: U256::zero(), @@ -950,7 +955,7 @@ 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), + hash: uos[0].hash(pool.config.entry_point, 1), sender: uos[0].sender, nonce: uos[0].nonce, actual_gas_cost: U256::zero(), @@ -1026,7 +1031,7 @@ 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), + hash: uos[0].hash(pool.config.entry_point, 1), sender: uos[0].sender, nonce: uos[0].nonce, actual_gas_cost: U256::zero(), @@ -1101,11 +1106,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 +1119,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![]); @@ -1314,7 +1322,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, @@ -1371,7 +1379,12 @@ mod tests { fn create_pool( ops: Vec, - ) -> UoPool { + ) -> UoPool< + UserOperation, + impl Prechecker, + impl Simulator, + impl EntryPoint, + > { let args = PoolConfig { entry_point: Address::random(), chain_id: 1, @@ -1394,7 +1407,7 @@ 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, @@ -1483,7 +1496,12 @@ mod tests { async fn create_pool_insert_ops( ops: Vec, ) -> ( - UoPool, + UoPool< + UserOperation, + impl Prechecker, + impl Simulator, + impl EntryPoint, + >, Vec, ) { let uos = ops.iter().map(|op| op.op.clone()).collect::>(); @@ -1543,7 +1561,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..ef14abf3b 100644 --- a/crates/pool/src/server/local.rs +++ b/crates/pool/src/server/local.rs @@ -19,7 +19,7 @@ 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::{v0_6, EntityUpdate, UserOperationId, UserOperationVariant}; use tokio::{ sync::{broadcast, mpsc, oneshot}, task::JoinHandle, @@ -31,7 +31,8 @@ use super::{PoolResult, PoolServerError}; use crate::{ chain::ChainUpdate, mempool::{ - Mempool, MempoolError, OperationOrigin, PaymasterMetadata, PoolOperation, StakeStatus, + IntoPoolOperationVariant, Mempool, MempoolError, OperationOrigin, PaymasterMetadata, + PoolOperation, StakeStatus, }, server::{NewHead, PoolServer, Reputation}, ReputationStatus, @@ -65,12 +66,15 @@ impl LocalPoolBuilder { } /// Run the local pool server, consumes the builder - pub fn run( + pub fn run( self, mempools: HashMap>, chain_updates: broadcast::Receiver>, shutdown_token: CancellationToken, - ) -> JoinHandle> { + ) -> JoinHandle> + where + M: Mempool, + { let mut runner = LocalPoolServerRunner::new( self.req_receiver, self.block_sender, @@ -122,7 +126,7 @@ impl PoolServer for LocalPoolHandle { } } - 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, @@ -140,7 +144,7 @@ impl PoolServer for LocalPoolHandle { entry_point: Address, max_ops: u64, shard_index: u64, - ) -> PoolResult> { + ) -> PoolResult>> { let req = ServerRequestKind::GetOps { entry_point, max_ops, @@ -153,7 +157,10 @@ impl PoolServer for LocalPoolHandle { } } - async fn get_op_by_hash(&self, hash: H256) -> PoolResult> { + async fn get_op_by_hash( + &self, + hash: H256, + ) -> PoolResult>> { let req = ServerRequestKind::GetOpByHash { hash }; let resp = self.send(req).await?; match resp { @@ -236,7 +243,10 @@ impl PoolServer for LocalPoolHandle { } } - async fn debug_dump_mempool(&self, entry_point: Address) -> PoolResult> { + async fn debug_dump_mempool( + &self, + entry_point: Address, + ) -> PoolResult>> { let req = ServerRequestKind::DebugDumpMempool { entry_point }; let resp = self.send(req).await?; match resp { @@ -354,7 +364,7 @@ impl HealthCheck for LocalPoolHandle { impl LocalPoolServerRunner where - M: Mempool, + M: Mempool, { fn new( req_receiver: mpsc::Receiver, @@ -381,19 +391,22 @@ where entry_point: Address, max_ops: u64, shard_index: u64, - ) -> PoolResult> { + ) -> PoolResult>> { let mempool = self.get_pool(entry_point)?; Ok(mempool .best_operations(max_ops as usize, shard_index)? .iter() - .map(|op| (**op).clone()) + .map(|op| (**op).clone().into_variant()) .collect()) } - fn get_op_by_hash(&self, hash: H256) -> PoolResult> { + fn get_op_by_hash( + &self, + hash: H256, + ) -> PoolResult>> { for mempool in self.mempools.values() { if let Some(op) = mempool.get_user_operation_by_hash(hash) { - return Ok(Some((*op).clone())); + return Ok(Some((*op).clone().into_variant())); } } Ok(None) @@ -449,12 +462,15 @@ where Ok(()) } - fn debug_dump_mempool(&self, entry_point: Address) -> PoolResult> { + fn debug_dump_mempool( + &self, + entry_point: Address, + ) -> PoolResult>> { let mempool = self.get_pool(entry_point)?; Ok(mempool .all_operations(usize::MAX) .iter() - .map(|op| (**op).clone()) + .map(|op| (**op).clone().into_variant()) .collect()) } @@ -549,7 +565,7 @@ where // 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 { + let resp = match mempool.add_operation(origin, op.into()).await { Ok(hash) => Ok(ServerResponse::AddOp { hash }), Err(e) => Err(e.into()), }; @@ -680,7 +696,7 @@ enum ServerRequestKind { GetSupportedEntryPoints, AddOp { entry_point: Address, - op: UserOperation, + op: UserOperationVariant, origin: OperationOrigin, }, GetOps { @@ -746,10 +762,10 @@ enum ServerResponse { hash: H256, }, GetOps { - ops: Vec, + ops: Vec>, }, GetOpByHash { - op: Option, + op: Option>, }, RemoveOps, RemoveOpById { @@ -759,7 +775,7 @@ enum ServerResponse { DebugClearState, AdminSetTracking, DebugDumpMempool { - ops: Vec, + ops: Vec>, }, DebugSetReputations, DebugDumpReputation { @@ -784,6 +800,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}; @@ -799,11 +816,7 @@ mod tests { let ep = Address::random(); let state = setup(HashMap::from([(ep, Arc::new(mock_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); } @@ -875,14 +888,7 @@ mod tests { ); 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()); } } @@ -903,4 +909,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..a016bc669 100644 --- a/crates/pool/src/server/mod.rs +++ b/crates/pool/src/server/mod.rs @@ -26,7 +26,7 @@ pub use local::{LocalPoolBuilder, LocalPoolHandle}; use mockall::automock; pub(crate) use remote::spawn_remote_mempool_server; pub use remote::RemotePoolClient; -use rundler_types::{EntityUpdate, UserOperation, UserOperationId}; +use rundler_types::{EntityUpdate, UserOperationId, UserOperationVariant}; use crate::{ mempool::{PaymasterMetadata, PoolOperation, Reputation, StakeStatus}, @@ -59,7 +59,7 @@ pub trait PoolServer: Send + Sync + 'static { 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; + async fn add_op(&self, entry_point: Address, op: UserOperationVariant) -> PoolResult; /// Get operations from the pool async fn get_ops( @@ -67,12 +67,15 @@ pub trait PoolServer: Send + Sync + 'static { entry_point: Address, max_ops: u64, shard_index: u64, - ) -> PoolResult>; + ) -> 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>; + 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<()>; @@ -120,7 +123,10 @@ pub trait PoolServer: Send + Sync + 'static { ) -> PoolResult<()>; /// Dump all operations in the pool, used for debug methods - async fn debug_dump_mempool(&self, entry_point: Address) -> PoolResult>; + async fn debug_dump_mempool( + &self, + entry_point: Address, + ) -> PoolResult>>; /// Set reputations for entities, used for debug methods async fn debug_set_reputations( diff --git a/crates/pool/src/server/remote/client.rs b/crates/pool/src/server/remote/client.rs index 91a045597..a00c55d48 100644 --- a/crates/pool/src/server/remote/client.rs +++ b/crates/pool/src/server/remote/client.rs @@ -19,7 +19,7 @@ use rundler_task::{ grpc::protos::{from_bytes, to_le_bytes, ConversionError}, server::{HealthCheck, ServerStatus}, }; -use rundler_types::{EntityUpdate, UserOperation, UserOperationId}; +use rundler_types::{EntityUpdate, UserOperationId, UserOperationVariant}; use rundler_utils::retry::{self, UnlimitedRetryOpts}; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -137,7 +137,7 @@ impl PoolServer for RemotePoolClient { .collect::>()?) } - 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() @@ -163,7 +163,7 @@ impl PoolServer for RemotePoolClient { entry_point: Address, max_ops: u64, shard_index: u64, - ) -> PoolResult> { + ) -> PoolResult>> { let res = self .op_pool_client .clone() @@ -190,7 +190,10 @@ impl PoolServer for RemotePoolClient { } } - async fn get_op_by_hash(&self, hash: H256) -> PoolResult> { + async fn get_op_by_hash( + &self, + hash: H256, + ) -> PoolResult>> { let res = self .op_pool_client .clone() @@ -352,7 +355,10 @@ impl PoolServer for RemotePoolClient { } } - async fn debug_dump_mempool(&self, entry_point: Address) -> PoolResult> { + async fn debug_dump_mempool( + &self, + entry_point: Address, + ) -> PoolResult>> { let res = self .op_pool_client .clone() diff --git a/crates/pool/src/server/remote/error.rs b/crates/pool/src/server/remote/error.rs index d155724ff..5f51faacd 100644 --- a/crates/pool/src/server/remote/error.rs +++ b/crates/pool/src/server/remote/error.rs @@ -22,11 +22,11 @@ use super::protos::{ AccessedUndeployedContract, AggregatorValidationFailed, AssociatedStorageIsAlternateSender, CallGasLimitTooLow, CallHadValue, CalledBannedEntryPointMethod, CodeHashChanged, DidNotRevert, DiscardedOnInsertError, Entity, EntityThrottledError, EntityType, ExistingSenderWithInitCode, - FactoryCalledCreate2Twice, FactoryIsNotContract, InitCodeTooShort, InvalidSignature, - InvalidStorageAccess, MaxFeePerGasTooLow, MaxOperationsReachedError, - MaxPriorityFeePerGasTooLow, MempoolError as ProtoMempoolError, MultipleRolesViolation, - NotStaked, OperationAlreadyKnownError, OperationDropTooSoon, OutOfGas, PaymasterBalanceTooLow, - PaymasterDepositTooLow, PaymasterIsNotContract, PaymasterTooShort, PreVerificationGasTooLow, + FactoryCalledCreate2Twice, FactoryIsNotContract, InvalidSignature, InvalidStorageAccess, + MaxFeePerGasTooLow, MaxOperationsReachedError, MaxPriorityFeePerGasTooLow, + MempoolError as ProtoMempoolError, MultipleRolesViolation, NotStaked, + OperationAlreadyKnownError, OperationDropTooSoon, OutOfGas, PaymasterBalanceTooLow, + PaymasterDepositTooLow, PaymasterIsNotContract, PreVerificationGasTooLow, PrecheckViolationError as ProtoPrecheckViolationError, ReplacementUnderpricedError, SenderAddressUsedAsAlternateEntity, SenderFundsTooLow, SenderIsNotContractAndNoInitCode, SimulationViolationError as ProtoSimulationViolationError, TotalGasLimitTooHigh, @@ -244,13 +244,6 @@ 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( @@ -310,13 +303,6 @@ impl From for ProtoPrecheckViolationError { ), } } - 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 { @@ -377,9 +363,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 +390,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)?) } @@ -787,12 +767,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..e4966cecd 100644 --- a/crates/pool/src/server/remote/protos.rs +++ b/crates/pool/src/server/remote/protos.rs @@ -15,9 +15,9 @@ use anyhow::{anyhow, Context}; use ethers::types::{Address, H256}; use rundler_task::grpc::protos::{from_bytes, to_le_bytes, ConversionError}; use rundler_types::{ - Entity as RundlerEntity, EntityType as RundlerEntityType, EntityUpdate as RundlerEntityUpdate, - EntityUpdateType as RundlerEntityUpdateType, UserOperation as RundlerUserOperation, - ValidTimeRange, + v0_6, Entity as RundlerEntity, EntityType as RundlerEntityType, + EntityUpdate as RundlerEntityUpdate, EntityUpdateType as RundlerEntityUpdateType, + UserOperationVariant, ValidTimeRange, }; use crate::{ @@ -34,9 +34,20 @@ 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 { - UserOperation { +impl From<&UserOperationVariant> for UserOperation { + fn from(op: &UserOperationVariant) -> Self { + match op { + UserOperationVariant::V0_6(op) => op.into(), + UserOperationVariant::V0_7(_) => { + unimplemented!("V0_7 user operation is not supported") + } + } + } +} + +impl From<&v0_6::UserOperation> for UserOperation { + fn from(op: &v0_6::UserOperation) -> Self { + let op = UserOperationV06 { sender: op.sender.0.to_vec(), nonce: to_le_bytes(op.nonce), init_code: op.init_code.to_vec(), @@ -48,15 +59,18 @@ impl From<&RundlerUserOperation> for UserOperation { 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(), + }; + UserOperation { + 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 +86,20 @@ impl TryFrom for RundlerUserOperation { } } +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()?)), + } + } +} + impl TryFrom for RundlerEntityType { type Error = ConversionError; @@ -246,8 +274,8 @@ impl From for StakeStatus { } } -impl From<&PoolOperation> for MempoolOp { - fn from(op: &PoolOperation) -> Self { +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(), @@ -267,7 +295,7 @@ impl From<&PoolOperation> for MempoolOp { } pub const MISSING_USER_OP_ERR_STR: &str = "Mempool op should contain user operation"; -impl TryFrom for PoolOperation { +impl TryFrom for PoolOperation { type Error = anyhow::Error; fn try_from(op: MempoolOp) -> Result { diff --git a/crates/pool/src/task.rs b/crates/pool/src/task.rs index 7a556d13c..6cd9a9808 100644 --- a/crates/pool/src/task.rs +++ b/crates/pool/src/task.rs @@ -16,12 +16,10 @@ 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_sim::{ - Prechecker, PrecheckerImpl, SimulateValidationTracerImpl, Simulator, SimulatorImpl, -}; +use rundler_provider::{EntryPoint, EthersEntryPointV0_6, Provider}; +use rundler_sim::{simulation::v0_6 as sim_v0_6, Prechecker, PrecheckerImpl, Simulator}; use rundler_task::Task; -use rundler_types::chain::ChainSpec; +use rundler_types::{chain::ChainSpec, v0_6}; use rundler_utils::{emit::WithEntryPoint, handle}; use tokio::{sync::broadcast, try_join}; use tokio_util::sync::CancellationToken; @@ -89,7 +87,7 @@ impl Task for PoolTask { // create mempools let mut mempools = HashMap::new(); for pool_config in &self.args.pool_configs { - let pool = PoolTask::create_mempool( + let pool = PoolTask::create_mempool_v0_6( self.args.chain_spec.clone(), pool_config, self.event_sender.clone(), @@ -157,13 +155,20 @@ impl PoolTask { Box::new(self) } - async fn create_mempool( + async fn create_mempool_v0_6( chain_spec: ChainSpec, pool_config: &PoolConfig, event_sender: broadcast::Sender>, provider: Arc

, - ) -> anyhow::Result> { - let ep = EthersEntryPoint::new(pool_config.entry_point, Arc::clone(&provider)); + ) -> anyhow::Result< + UoPool< + v0_6::UserOperation, + impl Prechecker, + impl Simulator, + impl EntryPoint, + >, + > { + let ep = EthersEntryPointV0_6::new(pool_config.entry_point, Arc::clone(&provider)); let prechecker = PrecheckerImpl::new( chain_spec, @@ -173,10 +178,10 @@ impl PoolTask { ); let simulate_validation_tracer = - SimulateValidationTracerImpl::new(Arc::clone(&provider), ep.clone()); - let simulator = SimulatorImpl::new( + sim_v0_6::SimulateValidationTracerImpl::new(Arc::clone(&provider), ep.clone()); + let simulator = sim_v0_6::Simulator::new( Arc::clone(&provider), - ep.address(), + ep.clone(), simulate_validation_tracer, pool_config.sim_settings, pool_config.mempool_channel_configs.clone(), 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/mod.rs b/crates/provider/src/ethers/entry_point/mod.rs new file mode 100644 index 000000000..b93f2790a --- /dev/null +++ b/crates/provider/src/ethers/entry_point/mod.rs @@ -0,0 +1,14 @@ +// 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/. + +pub(crate) mod v0_6; diff --git a/crates/provider/src/ethers/entry_point.rs b/crates/provider/src/ethers/entry_point/v0_6.rs similarity index 57% rename from crates/provider/src/ethers/entry_point.rs rename to crates/provider/src/ethers/entry_point/v0_6.rs index f7b8ef158..a2223e039 100644 --- a/crates/provider/src/ethers/entry_point.rs +++ b/crates/provider/src/ethers/entry_point/v0_6.rs @@ -20,35 +20,51 @@ use ethers::{ providers::{spoof, Middleware, RawCall}, types::{ transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, Eip1559TransactionRequest, - H256, U256, + H160, H256, U256, U64, }, utils::hex, }; use rundler_types::{ - contracts::v0_6::{ - get_balances::{GetBalancesResult, GETBALANCES_BYTECODE}, - i_entry_point::{ExecutionResult, FailedOp, IEntryPoint, SignatureValidationFailed}, - shared_types::UserOpsPerAggregator, + contracts::{ + arbitrum::node_interface::NodeInterface, + optimism::gas_price_oracle::GasPriceOracle, + v0_6::{ + get_balances::{GetBalancesResult, GETBALANCES_BYTECODE}, + i_aggregator::IAggregator, + i_entry_point::{ExecutionResult, FailedOp, IEntryPoint, SignatureValidationFailed}, + shared_types::UserOpsPerAggregator as UserOpsPerAggregatorV0_6, + }, }, - DepositInfo, GasFees, UserOperation, ValidationOutput, + v0_6::UserOperation, + DepositInfoV0_6, GasFees, UserOpsPerAggregator, ValidationOutput, }; use rundler_utils::eth::{self, ContractRevertError}; use crate::{ - traits::{EntryPoint, HandleOpsOut}, - Provider, + traits::HandleOpsOut, AggregatorOut, AggregatorSimOut, BundleHandler, 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 EntryPointImpl { +pub struct EntryPoint { i_entry_point: IEntryPoint

, provider: Arc

, + arb_node: NodeInterface

, + opt_gas_oracle: GasPriceOracle

, } -impl

Clone for EntryPointImpl

+impl

Clone for EntryPoint

where P: Provider + Middleware, { @@ -56,25 +72,32 @@ where 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

EntryPointImpl

+impl

EntryPoint

where P: Provider + Middleware, { - /// Create a new `EntryPointImpl` instance + /// 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, + 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

EntryPoint for EntryPointImpl

+impl

crate::traits::EntryPoint for EntryPoint

where P: Provider + Middleware + Send + Sync + 'static, { @@ -82,43 +105,100 @@ where self.i_entry_point.address() } - async fn get_simulate_validation_call( + async fn balance_of( &self, - user_op: UserOperation, - max_validation_gas: u64, - ) -> anyhow::Result { - let pvg = user_op.pre_verification_gas; - let tx = 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 - .simulate_validation(user_op) - .gas(U256::from(max_validation_gas) + pvg) - .tx; - Ok(tx) + .get_deposit_info(address) + .await + .context("should get deposit info")?) } - async fn call_simulate_validation( + 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, - 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) + 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 - { - 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")?, + .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, + ops_per_aggregator: Vec>, beneficiary: Address, gas: U256, ) -> anyhow::Result { @@ -150,47 +230,9 @@ where 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, + ops_per_aggregator: Vec>, beneficiary: Address, gas: U256, gas_fees: GasFees, @@ -203,6 +245,104 @@ where .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")?; + + let gas = self + .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, + 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")?; + + // 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 l1_fee = self.opt_gas_oracle.get_l1_fee(tx).call().await?; + Ok(l1_fee.checked_div(gas_price).unwrap_or(U256::MAX)) + } +} + +#[async_trait::async_trait] +impl

SimulationProvider for EntryPoint

+where + P: Provider + Middleware + Send + Sync + 'static, +{ + type UO = UserOperation; + + 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")?, + } + } fn decode_simulate_handle_ops_revert( &self, @@ -219,35 +359,45 @@ where } } - async fn get_deposit_info(&self, address: Address) -> anyhow::Result { - Ok(self + 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 - .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(), - ) + .simulate_handle_op(user_op, target, target_call_data) + .block(block_hash) + .gas(gas) + .call_raw() + .state(spoofed_state) .await - .context("should compute balances")?; - Ok(out.balances) + .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_handle_ops_call( entry_point: &IEntryPoint, - mut ops_per_aggregator: Vec, + 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) diff --git a/crates/provider/src/ethers/mod.rs b/crates/provider/src/ethers/mod.rs index 072d4bc8f..8ddcb7d21 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; 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..bf8a5efeb 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}; mod traits; +#[cfg(any(test, feature = "test-utils"))] +pub use traits::test_utils::*; +#[cfg(any(test, feature = "test-utils"))] +pub use traits::MockProvider; pub use traits::{ - AggregatorOut, AggregatorSimOut, EntryPoint, HandleOpsOut, Provider, ProviderError, - ProviderResult, + AggregatorOut, AggregatorSimOut, BundleHandler, EntryPoint, HandleOpsOut, L1GasProvider, + Provider, ProviderError, ProviderResult, SignatureAggregator, SimulationProvider, }; -#[cfg(any(test, feature = "test-utils"))] -pub use traits::{MockEntryPoint, MockProvider}; diff --git a/crates/provider/src/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index 6a67c3563..ea69fd015 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -14,13 +14,31 @@ 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, + contracts::v0_6::i_entry_point::ExecutionResult, DepositInfoV0_6, GasFees, UserOperation, + UserOpsPerAggregator, 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 { @@ -36,37 +54,114 @@ pub enum HandleOpsOut { } /// 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( &self, - user_op: UserOperation, + user_op: Self::UO, max_validation_gas: u64, ) -> anyhow::Result; /// 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; @@ -74,7 +169,7 @@ pub trait EntryPoint: Send + Sync + 'static { /// 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 +177,9 @@ 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>; } diff --git a/crates/provider/src/traits/mod.rs b/crates/provider/src/traits/mod.rs index 87bde2210..24fb1b441 100644 --- a/crates/provider/src/traits/mod.rs +++ b/crates/provider/src/traits/mod.rs @@ -17,11 +17,14 @@ 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::{ + AggregatorOut, AggregatorSimOut, BundleHandler, EntryPoint, HandleOpsOut, L1GasProvider, + SignatureAggregator, SimulationProvider, +}; 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..7b5492e01 --- /dev/null +++ b/crates/provider/src/traits/test_utils.rs @@ -0,0 +1,116 @@ +// 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::{ + contracts::v0_6::i_entry_point::ExecutionResult, v0_6, DepositInfoV0_6, GasFees, + UserOpsPerAggregator, ValidationOutput, +}; + +use crate::{ + AggregatorOut, BundleHandler, EntryPoint, 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; + async fn get_simulate_validation_call( + &self, + user_op: v0_6::UserOperation, + max_validation_gas: u64, + ) -> anyhow::Result; + async fn call_simulate_validation( + &self, + user_op: v0_6::UserOperation, + max_validation_gas: u64, + ) -> anyhow::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/src/debug.rs b/crates/rpc/src/debug.rs index a7b3b20bd..982320eb7 100644 --- a/crates/rpc/src/debug.rs +++ b/crates/rpc/src/debug.rs @@ -17,6 +17,7 @@ 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::v0_6; use crate::{ error::rpc_err, @@ -126,7 +127,7 @@ where .await .map_err(|e| rpc_err(INTERNAL_ERROR_CODE, e.to_string()))? .into_iter() - .map(|pop| pop.uo.into()) + .map(|pop| v0_6::UserOperation::from(pop.uo).into()) .collect::>()) } diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 15f1ab060..99fb4c49a 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -28,23 +28,23 @@ use ethers::{ utils::to_checksum, }; use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, Provider}; +use rundler_provider::{EntryPoint, L1GasProvider, Provider, SimulationProvider}; use rundler_sim::{ - EstimationSettings, FeeEstimator, GasEstimate, GasEstimationError, GasEstimator, - GasEstimatorImpl, PrecheckSettings, UserOperationOptionalGas, + estimation::v0_6::GasEstimator as GasEstimatorV0_6, EstimationSettings, FeeEstimator, + GasEstimationError, GasEstimator, PrecheckSettings, }; use rundler_types::{ chain::ChainSpec, contracts::v0_6::i_entry_point::{ IEntryPointCalls, UserOperationEventFilter, UserOperationRevertReasonFilter, }, - UserOperation, + v0_6, UserOperation, }; use rundler_utils::{eth::log_to_raw_log, log::LogOnError}; use tracing::Level; use super::error::{EthResult, EthRpcError, ExecutionRevertedWithBytesData}; -use crate::types::{RichUserOperation, RpcUserOperation, UserOperationReceipt}; +use crate::types::{RichUserOperation, RpcGasEstimate, RpcUserOperation, UserOperationReceipt}; /// Settings for the `eth_` API #[derive(Copy, Clone, Debug)] @@ -64,13 +64,15 @@ impl Settings { #[derive(Debug)] struct EntryPointContext { - gas_estimator: GasEstimatorImpl, + gas_estimator: GasEstimatorV0_6, } impl EntryPointContext where P: Provider, - E: EntryPoint, + E: EntryPoint + + L1GasProvider + + SimulationProvider, { fn new( chain_spec: ChainSpec, @@ -79,7 +81,7 @@ where estimation_settings: EstimationSettings, fee_estimator: FeeEstimator

, ) -> Self { - let gas_estimator = GasEstimatorImpl::new( + let gas_estimator = GasEstimatorV0_6::new( chain_spec, provider, entry_point, @@ -102,7 +104,9 @@ pub(crate) struct EthApi { impl EthApi where P: Provider, - E: EntryPoint, + E: EntryPoint + + L1GasProvider + + SimulationProvider, PS: PoolServer, { pub(crate) fn new( @@ -157,6 +161,8 @@ where "supplied entry point addr is not a known entry point".to_string(), )); } + let op: v0_6::UserOperation = op.try_into()?; + self.pool .add_op(entry_point, op.into()) .await @@ -166,10 +172,10 @@ where pub(crate) async fn estimate_user_operation_gas( &self, - op: UserOperationOptionalGas, + op: v0_6::UserOperationOptionalGas, entry_point: Address, state_override: Option, - ) -> EthResult { + ) -> EthResult { let context = self .contexts_by_entry_point .get(&entry_point) @@ -179,12 +185,22 @@ where ) })?; + if op.init_code.len() > 0 && op.init_code.len() < 20 { + return Err(EthRpcError::InvalidParams( + "init_code must be empty or at least 20 bytes".to_string(), + )); + } else if op.paymaster_and_data.len() > 0 && op.paymaster_and_data.len() < 20 { + return Err(EthRpcError::InvalidParams( + "paymaster_and_data must be empty or at least 20 bytes".to_string(), + )); + } + let result = context .gas_estimator .estimate_op_gas(op, state_override.unwrap_or_default()) .await; match result { - Ok(estimate) => Ok(estimate), + Ok(estimate) => Ok(estimate.into()), Err(GasEstimationError::RevertInValidation(message)) => { Err(EthRpcError::EntryPointValidationRejected(message))? } @@ -346,7 +362,7 @@ where 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) + .find(|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) @@ -378,7 +394,7 @@ where .await .map_err(EthRpcError::from)?; Ok(res.map(|op| RichUserOperation { - user_operation: op.uo.into(), + user_operation: v0_6::UserOperation::from(op.uo).into(), entry_point: op.entry_point.into(), block_number: None, block_hash: None, @@ -410,7 +426,7 @@ where Ok(logs.into_iter().next()) } - fn get_user_operations_from_tx_data(&self, tx_data: Bytes) -> Vec { + 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![], @@ -510,7 +526,7 @@ where &self, tx_hash: H256, user_op_hash: H256, - ) -> EthResult> { + ) -> 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( @@ -543,7 +559,7 @@ where 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) + .find(|op| op.hash(*to, self.chain_spec.id) == user_op_hash) { return Ok(Some(uo)); } @@ -564,10 +580,13 @@ mod tests { utils::keccak256, }; 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_pool::{IntoPoolOperationVariant, MockPoolServer, PoolOperation}; + use rundler_provider::{MockEntryPointV0_6, MockProvider}; + use rundler_sim::{EntityInfos, PriorityFeeMode}; + use rundler_types::{ + contracts::v0_6::i_entry_point::HandleOpsCall, v0_6::UserOperation, + UserOperation as UserOperationTrait, ValidTimeRange, + }; use super::*; @@ -584,7 +603,7 @@ mod tests { ]); let result = - EthApi::::filter_receipt_logs_matching_user_op( + EthApi::::filter_receipt_logs_matching_user_op( &reference_log, &receipt, ); @@ -607,7 +626,7 @@ mod tests { ]); let result = - EthApi::::filter_receipt_logs_matching_user_op( + EthApi::::filter_receipt_logs_matching_user_op( &reference_log, &receipt, ); @@ -630,7 +649,7 @@ mod tests { ]); let result = - EthApi::::filter_receipt_logs_matching_user_op( + EthApi::::filter_receipt_logs_matching_user_op( &reference_log, &receipt, ); @@ -657,7 +676,7 @@ mod tests { ]); let result = - EthApi::::filter_receipt_logs_matching_user_op( + EthApi::::filter_receipt_logs_matching_user_op( &reference_log, &receipt, ); @@ -683,7 +702,7 @@ mod tests { ]); let result = - EthApi::::filter_receipt_logs_matching_user_op( + EthApi::::filter_receipt_logs_matching_user_op( &reference_log, &receipt, ); @@ -705,7 +724,7 @@ mod tests { ]); let result = - EthApi::::filter_receipt_logs_matching_user_op( + EthApi::::filter_receipt_logs_matching_user_op( &reference_log, &receipt, ); @@ -717,25 +736,32 @@ mod tests { 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(), 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(); pool.expect_get_op_by_hash() .with(eq(hash)) .times(1) - .returning(move |_| Ok(Some(po.clone()))); + .returning(move |_| Ok(Some(po.clone().into_variant()))); let mut provider = MockProvider::default(); 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); @@ -754,7 +780,7 @@ 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(); @@ -794,7 +820,7 @@ 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); @@ -813,7 +839,7 @@ 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(); pool.expect_get_op_by_hash() @@ -825,7 +851,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); @@ -852,9 +878,9 @@ mod tests { fn create_api( provider: MockProvider, - ep: MockEntryPoint, + ep: MockEntryPointV0_6, pool: MockPoolServer, - ) -> EthApi { + ) -> EthApi { let mut contexts_by_entry_point = HashMap::new(); let provider = Arc::new(provider); let chain_spec = ChainSpec { diff --git a/crates/rpc/src/eth/mod.rs b/crates/rpc/src/eth/mod.rs index 3cb50e52b..c4f560b91 100644 --- a/crates/rpc/src/eth/mod.rs +++ b/crates/rpc/src/eth/mod.rs @@ -21,9 +21,9 @@ mod server; use ethers::types::{spoof, Address, H256, U64}; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; -use rundler_sim::{GasEstimate, UserOperationOptionalGas}; +use rundler_types::v0_6; -use crate::types::{RichUserOperation, RpcUserOperation, UserOperationReceipt}; +use crate::types::{RichUserOperation, RpcGasEstimate, RpcUserOperation, UserOperationReceipt}; /// Eth API #[rpc(client, server, namespace = "eth")] @@ -41,10 +41,10 @@ pub trait EthApi { #[method(name = "estimateUserOperationGas")] async fn estimate_user_operation_gas( &self, - op: UserOperationOptionalGas, + op: v0_6::UserOperationOptionalGas, entry_point: Address, state_override: Option, - ) -> RpcResult; + ) -> RpcResult; /// Returns the user operation with the given hash. #[method(name = "getUserOperationByHash")] diff --git a/crates/rpc/src/eth/server.rs b/crates/rpc/src/eth/server.rs index 58a0d290d..755279f8c 100644 --- a/crates/rpc/src/eth/server.rs +++ b/crates/rpc/src/eth/server.rs @@ -15,17 +15,19 @@ 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_provider::{EntryPoint, L1GasProvider, Provider, SimulationProvider}; +use rundler_types::v0_6; use super::{api::EthApi, EthApiServer}; -use crate::types::{RichUserOperation, RpcUserOperation, UserOperationReceipt}; +use crate::types::{RichUserOperation, RpcGasEstimate, RpcUserOperation, UserOperationReceipt}; #[async_trait] impl EthApiServer for EthApi where P: Provider, - E: EntryPoint, + E: EntryPoint + + L1GasProvider + + SimulationProvider, PS: PoolServer, { async fn send_user_operation( @@ -38,10 +40,10 @@ where async fn estimate_user_operation_gas( &self, - op: UserOperationOptionalGas, + op: v0_6::UserOperationOptionalGas, entry_point: Address, state_override: Option, - ) -> RpcResult { + ) -> RpcResult { Ok(EthApi::estimate_user_operation_gas(self, op, entry_point, state_override).await?) } diff --git a/crates/rpc/src/rundler.rs b/crates/rpc/src/rundler.rs index 5f11815bf..98ef5c6bc 100644 --- a/crates/rpc/src/rundler.rs +++ b/crates/rpc/src/rundler.rs @@ -21,9 +21,13 @@ use jsonrpsee::{ types::error::{INTERNAL_ERROR_CODE, INVALID_REQUEST_CODE}, }; use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, Provider}; +use rundler_provider::{EntryPoint, Provider, SimulationProvider}; use rundler_sim::{gas, FeeEstimator}; -use rundler_types::{chain::ChainSpec, UserOperation, UserOperationId}; +use rundler_types::{ + chain::ChainSpec, + v0_6::{self, UserOperation}, + UserOperationId, +}; use crate::{error::rpc_err, eth::EthRpcError, RpcUserOperation}; @@ -99,7 +103,7 @@ where impl RundlerApiServer for RundlerApi where P: Provider, - E: EntryPoint, + E: EntryPoint + SimulationProvider, PS: PoolServer, { async fn max_priority_fee_per_gas(&self) -> RpcResult { @@ -126,7 +130,7 @@ where )); } - let uo: UserOperation = user_op.into(); + let uo: v0_6::UserOperation = user_op.try_into()?; let id = UserOperationId { sender: uo.sender, nonce: uo.nonce, diff --git a/crates/rpc/src/task.rs b/crates/rpc/src/task.rs index 54b05e6d4..cf5435590 100644 --- a/crates/rpc/src/task.rs +++ b/crates/rpc/src/task.rs @@ -22,13 +22,13 @@ use jsonrpsee::{ }; use rundler_builder::BuilderServer; use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, EthersEntryPoint}; +use rundler_provider::{EntryPoint, EthersEntryPointV0_6, L1GasProvider, SimulationProvider}; use rundler_sim::{EstimationSettings, PrecheckSettings}; use rundler_task::{ server::{format_socket_addr, HealthCheck}, Task, }; -use rundler_types::chain::ChainSpec; +use rundler_types::{chain::ChainSpec, v0_6::UserOperation}; use tokio_util::sync::CancellationToken; use tracing::info; @@ -88,7 +88,8 @@ where 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 = + EthersEntryPointV0_6::new(self.args.chain_spec.entry_point_address, provider.clone()); let mut module = RpcModule::new(()); self.attach_namespaces(provider, ep, &mut module)?; @@ -147,12 +148,19 @@ where Box::new(self) } - fn attach_namespaces( + fn attach_namespaces( &self, provider: Arc>, entry_point: E, module: &mut RpcModule<()>, - ) -> anyhow::Result<()> { + ) -> anyhow::Result<()> + where + E: EntryPoint + + SimulationProvider + + L1GasProvider + + Clone, + C: JsonRpcClient + 'static, + { for api in &self.args.api_namespaces { match api { ApiNamespace::Eth => module.merge( diff --git a/crates/rpc/src/types.rs b/crates/rpc/src/types.rs index 30c182858..24fcbb54c 100644 --- a/crates/rpc/src/types.rs +++ b/crates/rpc/src/types.rs @@ -16,9 +16,11 @@ use ethers::{ utils::to_checksum, }; use rundler_pool::{Reputation, ReputationStatus}; -use rundler_types::UserOperation; +use rundler_types::{v0_6, GasEstimate}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use crate::eth::EthRpcError; + /// API namespace #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumString)] #[strum(serialize_all = "lowercase", ascii_case_insensitive)] @@ -96,8 +98,8 @@ pub struct RpcUserOperation { signature: Bytes, } -impl From for RpcUserOperation { - fn from(op: UserOperation) -> Self { +impl From for RpcUserOperation { + fn from(op: v0_6::UserOperation) -> Self { RpcUserOperation { sender: op.sender.into(), nonce: op.nonce, @@ -114,9 +116,21 @@ impl From for RpcUserOperation { } } -impl From for UserOperation { - fn from(def: RpcUserOperation) -> Self { - UserOperation { +impl TryFrom for v0_6::UserOperation { + type Error = EthRpcError; + + fn try_from(def: RpcUserOperation) -> Result { + if def.init_code.len() > 0 && def.init_code.len() < 20 { + return Err(EthRpcError::InvalidParams( + "init_code must be empty or at least 20 bytes".to_string(), + )); + } else if def.paymaster_and_data.len() > 0 && def.paymaster_and_data.len() < 20 { + return Err(EthRpcError::InvalidParams( + "paymaster_and_data must be empty or at least 20 bytes".to_string(), + )); + } + + Ok(v0_6::UserOperation { sender: def.sender.into(), nonce: def.nonce, init_code: def.init_code, @@ -128,7 +142,7 @@ impl From for UserOperation { max_priority_fee_per_gas: def.max_priority_fee_per_gas, paymaster_and_data: def.paymaster_and_data, signature: def.signature, - } + }) } } @@ -259,3 +273,34 @@ pub struct RpcDebugPaymasterBalance { /// Paymaster confirmed balance onchain pub confirmed_balance: U256, } + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RpcGasEstimate { + /// The pre-verification gas estimate + pub pre_verification_gas: U256, + /// The call gas limit estimate + pub call_gas_limit: U256, + /// The verification gas limit estimate + pub verification_gas_limit: U256, + /// The paymaster verification gas limit estimate + /// 0.6: unused + /// 0.7: populated if a paymaster is used + pub paymaster_verification_gas_limit: Option, + /// The paymaster post op gas limit + /// 0.6: unused + /// 0.7: populated if a paymaster is used + pub 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, + } + } +} diff --git a/crates/sim/src/estimation/mod.rs b/crates/sim/src/estimation/mod.rs index ce3a96c45..feacea06e 100644 --- a/crates/sim/src/estimation/mod.rs +++ b/crates/sim/src/estimation/mod.rs @@ -11,9 +11,75 @@ // 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 +pub mod v0_6; + +/// 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 93% rename from crates/sim/src/estimation/estimation.rs rename to crates/sim/src/estimation/v0_6.rs index a32b61dd8..8342020fc 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,18 @@ 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 crate::{ + estimation::{GasEstimationError, Settings}, + gas, + precheck::MIN_CALL_GAS_LIMIT, + simulation, utils, FeeEstimator, +}; /// 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 +72,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 +83,13 @@ pub struct GasEstimatorImpl { } #[async_trait::async_trait] -impl GasEstimator for GasEstimatorImpl { +impl crate::estimation::GasEstimator for GasEstimator +where + P: Provider, + E: EntryPoint + SimulationProvider + L1GasProvider, +{ + type UserOperationOptionalGas = UserOperationOptionalGas; + async fn estimate_op_gas( &self, op: UserOperationOptionalGas, @@ -139,7 +118,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 +145,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 +158,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 +440,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 +470,19 @@ mod tests { types::U64, utils::hex, }; - use rundler_provider::{MockEntryPoint, MockProvider}; + use rundler_provider::{MockEntryPointV0_6, MockProvider}; use rundler_types::{ chain::L1GasOracleContractType, contracts::{utils::get_gas_used::GasUsedResult, v0_6::i_entry_point::ExecutionResult}, + 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 +493,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 +510,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 +520,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 +587,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 +612,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 +633,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 +647,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 +673,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 +695,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 +709,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; @@ -747,7 +755,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())); } @@ -1170,7 +1178,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())); } @@ -1239,7 +1247,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 ) ); @@ -1315,7 +1323,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/gas/gas.rs b/crates/sim/src/gas/gas.rs index 002c1a3ce..8652a2a2a 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::{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,14 @@ 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>( chain_spec: &ChainSpec, - provider: Arc

, - full_op: &UserOperation, - random_op: &UserOperation, + enty_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,14 +55,12 @@ 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() + enty_point .calc_arbitrum_l1_gas(chain_spec.entry_point_address, random_op.clone()) .await? }, L1GasOracleContractType::OptimismBedrock => { - provider - .clone() + enty_point .calc_optimism_l1_gas(chain_spec.entry_point_address, random_op.clone(), gas_price) .await? }, @@ -100,13 +72,13 @@ 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>( 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,16 +86,14 @@ 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() + entry_point .calc_arbitrum_l1_gas(chain_spec.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() + entry_point .calc_optimism_l1_gas(chain_spec.entry_point_address, op.clone(), gas_price) .await? }, @@ -149,102 +119,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() } } diff --git a/crates/sim/src/lib.rs b/crates/sim/src/lib.rs index db94aeb37..8b1d54c36 100644 --- a/crates/sim/src/lib.rs +++ b/crates/sim/src/lib.rs @@ -30,11 +30,9 @@ //! //! - `test-utils`: Export mocks and utilities for testing. -mod estimation; -pub use estimation::{ - GasEstimate, GasEstimationError, GasEstimator, GasEstimatorImpl, - Settings as EstimationSettings, UserOperationOptionalGas, -}; +/// Gas estimation +pub mod estimation; +pub use estimation::{GasEstimationError, GasEstimator, Settings as EstimationSettings}; pub mod gas; pub use gas::{FeeEstimator, PriorityFeeMode}; @@ -47,13 +45,13 @@ pub use precheck::{ 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, + SimulationError, SimulationResult, SimulationViolation, Simulator, ViolationOpCode, }; mod types; diff --git a/crates/sim/src/precheck.rs b/crates/sim/src/precheck.rs index 0558b0d8d..fb0443c57 100644 --- a/crates/sim/src/precheck.rs +++ b/crates/sim/src/precheck.rs @@ -18,7 +18,7 @@ use arrayvec::ArrayVec; use ethers::types::{Address, U256}; #[cfg(feature = "test-utils")] use mockall::automock; -use rundler_provider::{EntryPoint, Provider}; +use rundler_provider::{EntryPoint, L1GasProvider, Provider}; use rundler_types::{chain::ChainSpec, GasFees, UserOperation}; use rundler_utils::math; @@ -29,11 +29,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)>; } @@ -43,14 +46,14 @@ pub type PrecheckError = ViolationError; /// 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: std::marker::PhantomData, } /// Precheck settings @@ -107,8 +110,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 +143,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 +170,37 @@ impl PrecheckerImpl { settings, fee_estimator, cache: RwLock::new(AsyncDataCache { fees: None }), + _uo_type: std::marker::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 +213,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 +236,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 +253,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 +303,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 +314,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 +341,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 +358,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,17 +378,12 @@ 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") } } @@ -397,9 +392,6 @@ impl PrecheckerImpl { /// 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), @@ -419,9 +411,6 @@ pub enum PrecheckViolation { /// 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), @@ -448,15 +437,18 @@ 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 +471,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 +483,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 +522,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..913284004 100644 --- a/crates/sim/src/simulation/mempool.rs +++ b/crates/sim/src/simulation/mempool.rs @@ -204,7 +204,7 @@ mod tests { use rundler_types::StorageSlot; use super::*; - use crate::simulation::{simulation::NeedsStakeInformation, ViolationOpCode}; + use crate::simulation::{NeedsStakeInformation, ViolationOpCode}; #[test] fn test_allow_entity_any() { diff --git a/crates/sim/src/simulation/mod.rs b/crates/sim/src/simulation/mod.rs index 5b6a99b76..1b289db43 100644 --- a/crates/sim/src/simulation/mod.rs +++ b/crates/sim/src/simulation/mod.rs @@ -11,18 +11,503 @@ // 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, Opcode, 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::{ + Entity, EntityType, StakeInfo, StorageSlot, UserOperation, ValidTimeRange, ValidationOutput, }; +use serde::{Deserialize, Serialize}; +use strum::IntoEnumIterator; + +/// Simulation module for Entry Point v0.6 +pub mod v0_6; mod mempool; pub use mempool::MempoolConfig; -mod tracer; -pub use tracer::{SimulateValidationTracer, SimulateValidationTracerImpl}; +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, + } + } +} + +/// 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; +} + +/// 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(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, 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, + } + } +} 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 68% rename from crates/sim/src/simulation/simulation.rs rename to crates/sim/src/simulation/v0_6/simulator.rs index 4a13c55df..961fe45e4 100644 --- a/crates/sim/src/simulation/simulation.rs +++ b/crates/sim/src/simulation/v0_6/simulator.rs @@ -18,114 +18,35 @@ 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, v0_6::UserOperation, Entity, EntityType, StorageSlot, + UserOperation as UserOperationTrait, ValidTimeRange, ValidationOutput, ValidationReturnInfo, }; -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, EntityInfos, NeedsStakeInformation, SimulationError, SimulationResult, + SimulationViolation, ViolationOpCode, }; -/// 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 +59,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 +84,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 +103,7 @@ where Self { provider, - entry_point_address, + entry_point, simulate_validation_tracer, sim_settings, mempool_configs, @@ -279,8 +204,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 +214,7 @@ where associated_addresses, entities_needing_stake: vec![], accessed_addresses: HashSet::new(), - initcode_length, + has_factory, }) } @@ -304,11 +228,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,7 +248,7 @@ where ref entry_point_out, ref mut entities_needing_stake, ref mut accessed_addresses, - initcode_length, + has_factory, .. } = context; @@ -362,7 +285,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 +314,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 +376,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 +411,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 +425,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 +497,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 +568,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, @@ -672,102 +601,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 +610,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 +714,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 +724,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 +772,7 @@ mod tests { }) }); - provider + entry_point .expect_validate_user_op_signature() .returning(|_, _, _| Ok(AggregatorOut::NotNeeded)); @@ -1215,7 +790,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 +799,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 +829,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 +849,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,7 +890,7 @@ 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( @@ -1354,7 +932,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 +988,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,7 +1004,7 @@ 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( @@ -1468,7 +1046,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 +1088,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,7 +1121,7 @@ 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( @@ -1584,7 +1164,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 75% rename from crates/sim/src/simulation/tracer.rs rename to crates/sim/src/simulation/v0_6/tracer.rs index bbe93eb25..15854408a 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, @@ -163,11 +126,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 +137,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/types/Cargo.toml b/crates/types/Cargo.toml index bd0bbfeff..cdef39c41 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -14,6 +14,7 @@ chrono = "0.4.24" constcat = "0.4.1" ethers.workspace = true parse-display = "0.9.0" +rand.workspace = true serde.workspace = true serde_json.workspace = true strum.workspace = true diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index ce2f45b71..7df0bde63 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -24,7 +24,7 @@ pub mod chain; #[rustfmt::skip] pub mod contracts; -pub use contracts::v0_6::shared_types::{DepositInfo, UserOperation, UserOpsPerAggregator}; +pub use contracts::v0_6::shared_types::DepositInfo as DepositInfoV0_6; mod entity; pub use entity::{Entity, EntityType, EntityUpdate, EntityUpdateType}; @@ -36,7 +36,7 @@ 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; 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..84513fb1f --- /dev/null +++ b/crates/types/src/user_operation/mod.rs @@ -0,0 +1,371 @@ +// 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 { + /// 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; + + /// 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; + + /// Get the user operation sender address + fn sender(&self) -> Address; + + /// Get the user operation paymaster address, if any + fn paymaster(&self) -> Option
; + + /// Get the user operation factory address, if any + fn factory(&self) -> Option
; + + /// Returns the maximum cost, in wei, of this user operation + fn max_gas_cost(&self) -> U256; + + /// 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 call gas limit + fn call_gas_limit(&self) -> U256; + + /// Returns the verification gas limit + fn verification_gas_limit(&self) -> U256; + + /// 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; + + /// 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; + + /// 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 type enum +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum UserOperationType { + /// User operation type for EntryPoint v0.6 + V0_6, + /// User operation type for EntryPoint v0.7 + V0_7, +} + +/// 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 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 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 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) -> UserOperationType { + match self { + UserOperationVariant::V0_6(_) => UserOperationType::V0_6, + UserOperationVariant::V0_7(_) => UserOperationType::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..958d445b1 --- /dev/null +++ b/crates/types/src/user_operation/v0_6.rs @@ -0,0 +1,430 @@ +// 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::shared_types::{UserOperation, UserOpsPerAggregator}; +use crate::entity::{Entity, EntityType}; + +impl UserOperationTrait for UserOperation { + type OptionalGas = UserOperationOptionalGas; + + 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 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 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) + } +} + +/// 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..dbe1a66e6 --- /dev/null +++ b/crates/types/src/user_operation/v0_7.rs @@ -0,0 +1,613 @@ +// 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 super::{UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant}; +use crate::{contracts::v0_7::shared_types::PackedUserOperation, Entity, 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 + */ + // The hash of the user operation + hash: H256, + // The packed user operation + packed: PackedUserOperation, + // The gas cost of the calldata + calldata_gas_cost: U256, +} + +impl UserOperationTrait for UserOperation { + type OptionalGas = UserOperationOptionalGas; + + 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 paymaster(&self) -> Option
{ + self.paymaster + } + + fn factory(&self) -> Option
{ + self.factory + } + + 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) + } +} + +/// 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, +} + +/// 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, +} + +/// 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(), + } + } + + /// 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 + } + + /// 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, + hash: H256::zero(), + packed: PackedUserOperation::default(), + calldata_gas_cost: U256::zero(), + }; + + let packed = 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_le_bytes(), + uo.call_gas_limit.low_u128().to_le_bytes(), + ); + + let gas_fees = concat_128( + uo.max_priority_fee_per_gas.low_u128().to_le_bytes(), + uo.max_fee_per_gas.low_u128().to_le_bytes(), + ); + + let paymaster_and_data = if let Some(paymaster) = uo.paymaster { + let mut paymaster_and_data = paymaster.as_bytes().to_vec(); + paymaster_and_data + .extend_from_slice(&uo.paymaster_verification_gas_limit.low_u128().to_le_bytes()); + paymaster_and_data + .extend_from_slice(&uo.paymaster_post_op_gas_limit.low_u128().to_le_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) -> UserOperation { + let mut factory = None; + let mut factory_data = Bytes::new(); + let mut paymaster = None; + let mut paymaster_verification_gas_limit = U128::zero(); + let mut paymaster_post_op_gas_limit = U128::zero(); + let mut paymaster_data = Bytes::new(); + + if !puo.init_code.is_empty() { + factory = Some(Address::from_slice(&puo.init_code)); + factory_data = Bytes::from_iter(&puo.init_code[20..]); + } + + if !puo.paymaster_and_data.is_empty() { + paymaster = Some(Address::from_slice(&puo.paymaster_and_data)); + paymaster_verification_gas_limit = U128::from_big_endian(&puo.paymaster_and_data[20..36]); + paymaster_post_op_gas_limit = U128::from_big_endian(&puo.paymaster_and_data[36..52]); + paymaster_data = Bytes::from_iter(&puo.paymaster_and_data[52..]); + } + + UserOperation { + sender: puo.sender, + nonce: puo.nonce, + call_data: puo.call_data.clone(), + call_gas_limit: U128::from_big_endian(&puo.account_gas_limits[..16]), + verification_gas_limit: U128::from_big_endian(&puo.account_gas_limits[16..]), + pre_verification_gas: puo.pre_verification_gas, + max_priority_fee_per_gas: U128::from_big_endian(&puo.gas_fees[..16]), + max_fee_per_gas: U128::from_big_endian(&puo.gas_fees[16..]), + signature: puo.signature.clone(), + factory, + factory_data, + paymaster, + paymaster_verification_gas_limit, + paymaster_post_op_gas_limit, + paymaster_data, + calldata_gas_cost: super::op_calldata_gas_cost(puo.clone()), + packed: puo, + hash: H256::zero(), + } +} + +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 { + let hash = hash_packed_user_operation(&self, entry_point, chain_id); + let unpacked = unpack_user_operation(self.clone()); + UserOperation { hash, ..unpacked } + } + + 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()); + } +} From 5d1d8854c6a9365eeee800c7cfd881c9291da6dd Mon Sep 17 00:00:00 2001 From: dancoombs Date: Tue, 5 Mar 2024 17:01:43 -0500 Subject: [PATCH 02/14] feat(types): add entrypoint 0.7 contract types --- crates/pool/src/mempool/paymaster.rs | 7 ++- .../provider/src/ethers/entry_point/v0_6.rs | 45 +++++++++++++++---- crates/provider/src/lib.rs | 5 ++- crates/provider/src/traits/entry_point.rs | 39 +++++++++++++--- crates/provider/src/traits/mod.rs | 4 +- crates/provider/src/traits/test_utils.rs | 11 ++--- crates/rpc/src/eth/api.rs | 4 ++ crates/sim/src/estimation/v0_6.rs | 32 ++++++------- crates/types/src/lib.rs | 1 - 9 files changed, 103 insertions(+), 45 deletions(-) diff --git a/crates/pool/src/mempool/paymaster.rs b/crates/pool/src/mempool/paymaster.rs index 471a55ef2..f1e3fee97 100644 --- a/crates/pool/src/mempool/paymaster.rs +++ b/crates/pool/src/mempool/paymaster.rs @@ -523,11 +523,10 @@ impl PaymasterBalance { #[cfg(test)] mod tests { use ethers::types::{Address, H256, U256}; - use rundler_provider::MockEntryPointV0_6; + use rundler_provider::{DepositInfo, MockEntryPointV0_6}; use rundler_sim::EntityInfos; use rundler_types::{ - contracts::v0_6::verifying_paymaster::DepositInfo, v0_6::UserOperation, - UserOperation as UserOperationTrait, UserOperationId, ValidTimeRange, + v0_6::UserOperation, UserOperation as UserOperationTrait, UserOperationId, ValidTimeRange, }; use super::*; @@ -974,7 +973,7 @@ mod tests { entrypoint.expect_get_deposit_info().returning(|_| { Ok(DepositInfo { - deposit: 1000, + deposit: 1000.into(), staked: true, stake: 10000, unstake_delay_sec: 100, diff --git a/crates/provider/src/ethers/entry_point/v0_6.rs b/crates/provider/src/ethers/entry_point/v0_6.rs index a2223e039..9bc7ad76e 100644 --- a/crates/provider/src/ethers/entry_point/v0_6.rs +++ b/crates/provider/src/ethers/entry_point/v0_6.rs @@ -31,18 +31,21 @@ use rundler_types::{ v0_6::{ get_balances::{GetBalancesResult, GETBALANCES_BYTECODE}, i_aggregator::IAggregator, - i_entry_point::{ExecutionResult, FailedOp, IEntryPoint, SignatureValidationFailed}, + i_entry_point::{ + DepositInfo as DepositInfoV0_6, ExecutionResult as ExecutionResultV0_6, FailedOp, + IEntryPoint, SignatureValidationFailed, + }, shared_types::UserOpsPerAggregator as UserOpsPerAggregatorV0_6, }, }, v0_6::UserOperation, - DepositInfoV0_6, GasFees, UserOpsPerAggregator, ValidationOutput, + GasFees, UserOpsPerAggregator, ValidationOutput, }; use rundler_utils::eth::{self, ContractRevertError}; use crate::{ - traits::HandleOpsOut, AggregatorOut, AggregatorSimOut, BundleHandler, L1GasProvider, Provider, - SignatureAggregator, SimulationProvider, + traits::HandleOpsOut, AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, + ExecutionResult, L1GasProvider, Provider, SignatureAggregator, SimulationProvider, }; const ARBITRUM_NITRO_NODE_INTERFACE_ADDRESS: Address = H160([ @@ -119,12 +122,13 @@ where .context("entry point should return balance") } - async fn get_deposit_info(&self, address: Address) -> anyhow::Result { + 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")?) + .context("should get deposit info")? + .into()) } async fn get_balances(&self, addresses: Vec
) -> anyhow::Result> { @@ -348,8 +352,8 @@ where &self, revert_data: Bytes, ) -> Result { - if let Ok(result) = ExecutionResult::decode(&revert_data) { - Ok(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) { @@ -406,3 +410,28 @@ fn get_handle_ops_call( }; 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/lib.rs b/crates/provider/src/lib.rs index bf8a5efeb..e05a69cb1 100644 --- a/crates/provider/src/lib.rs +++ b/crates/provider/src/lib.rs @@ -30,6 +30,7 @@ pub use traits::test_utils::*; #[cfg(any(test, feature = "test-utils"))] pub use traits::MockProvider; pub use traits::{ - AggregatorOut, AggregatorSimOut, BundleHandler, EntryPoint, HandleOpsOut, L1GasProvider, - Provider, ProviderError, ProviderResult, SignatureAggregator, SimulationProvider, + AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint, ExecutionResult, + HandleOpsOut, L1GasProvider, Provider, ProviderError, ProviderResult, SignatureAggregator, + SimulationProvider, }; diff --git a/crates/provider/src/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index ea69fd015..8e8678b17 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -14,10 +14,7 @@ use ethers::types::{ spoof, transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, H256, U256, }; -use rundler_types::{ - contracts::v0_6::i_entry_point::ExecutionResult, DepositInfoV0_6, GasFees, UserOperation, - UserOpsPerAggregator, ValidationOutput, -}; +use rundler_types::{GasFees, Timestamp, UserOperation, UserOpsPerAggregator, ValidationOutput}; /// Output of a successful signature aggregator simulation call #[derive(Clone, Debug, Default)] @@ -53,6 +50,38 @@ 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. #[async_trait::async_trait] #[auto_impl::auto_impl(&, Arc)] @@ -65,7 +94,7 @@ pub trait EntryPoint: Send + Sync + 'static { -> anyhow::Result; /// Get the deposit info for an address - async fn get_deposit_info(&self, address: Address) -> anyhow::Result; + 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>; diff --git a/crates/provider/src/traits/mod.rs b/crates/provider/src/traits/mod.rs index 24fb1b441..42aa230d4 100644 --- a/crates/provider/src/traits/mod.rs +++ b/crates/provider/src/traits/mod.rs @@ -18,8 +18,8 @@ pub use error::ProviderError; mod entry_point; pub use entry_point::{ - AggregatorOut, AggregatorSimOut, BundleHandler, EntryPoint, HandleOpsOut, L1GasProvider, - SignatureAggregator, SimulationProvider, + AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint, ExecutionResult, + HandleOpsOut, L1GasProvider, SignatureAggregator, SimulationProvider, }; mod provider; diff --git a/crates/provider/src/traits/test_utils.rs b/crates/provider/src/traits/test_utils.rs index 7b5492e01..62cf78815 100644 --- a/crates/provider/src/traits/test_utils.rs +++ b/crates/provider/src/traits/test_utils.rs @@ -14,14 +14,11 @@ use ethers::types::{ spoof, transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, H256, U256, }; -use rundler_types::{ - contracts::v0_6::i_entry_point::ExecutionResult, v0_6, DepositInfoV0_6, GasFees, - UserOpsPerAggregator, ValidationOutput, -}; +use rundler_types::{v0_6, GasFees, UserOpsPerAggregator, ValidationOutput}; use crate::{ - AggregatorOut, BundleHandler, EntryPoint, HandleOpsOut, L1GasProvider, SignatureAggregator, - SimulationProvider, + AggregatorOut, BundleHandler, DepositInfo, EntryPoint, ExecutionResult, HandleOpsOut, + L1GasProvider, SignatureAggregator, SimulationProvider, }; mockall::mock! { @@ -32,7 +29,7 @@ mockall::mock! { 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_deposit_info(&self, address: Address) -> anyhow::Result; async fn get_balances(&self, addresses: Vec
) -> anyhow::Result>; } diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 99fb4c49a..bbbac1b87 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -587,6 +587,10 @@ mod tests { contracts::v0_6::i_entry_point::HandleOpsCall, v0_6::UserOperation, UserOperation as UserOperationTrait, ValidTimeRange, }; + 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 super::*; diff --git a/crates/sim/src/estimation/v0_6.rs b/crates/sim/src/estimation/v0_6.rs index 8342020fc..9f09b752e 100644 --- a/crates/sim/src/estimation/v0_6.rs +++ b/crates/sim/src/estimation/v0_6.rs @@ -470,10 +470,10 @@ mod tests { types::U64, utils::hex, }; - use rundler_provider::{MockEntryPointV0_6, 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, }; @@ -746,8 +746,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(), }) @@ -804,8 +804,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(), }) @@ -861,8 +861,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(), }) @@ -957,8 +957,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(), }) @@ -999,8 +999,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(), }) @@ -1199,8 +1199,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(), }) @@ -1283,8 +1283,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(), }) diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 7df0bde63..10366d99c 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -24,7 +24,6 @@ pub mod chain; #[rustfmt::skip] pub mod contracts; -pub use contracts::v0_6::shared_types::DepositInfo as DepositInfoV0_6; mod entity; pub use entity::{Entity, EntityType, EntityUpdate, EntityUpdateType}; From 0c74023bf1309718d76e1cf62e8c5722a904a9be Mon Sep 17 00:00:00 2001 From: dancoombs Date: Tue, 12 Mar 2024 06:59:12 -0400 Subject: [PATCH 03/14] feat: add and use a user operation trait --- crates/rpc/src/eth/api.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index bbbac1b87..01dc61509 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -581,16 +581,12 @@ mod tests { }; use mockall::predicate::eq; use rundler_pool::{IntoPoolOperationVariant, MockPoolServer, PoolOperation}; - use rundler_provider::{MockEntryPointV0_6, MockProvider}; + use rundler_provider::{MockEntryPoint, MockEntryPointV0_6, MockProvider}; use rundler_sim::{EntityInfos, PriorityFeeMode}; use rundler_types::{ contracts::v0_6::i_entry_point::HandleOpsCall, v0_6::UserOperation, UserOperation as UserOperationTrait, ValidTimeRange, }; - 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 super::*; From 94d30f360c4d27f6862260c42d7b48f9f7e16853 Mon Sep 17 00:00:00 2001 From: dancoombs Date: Thu, 14 Mar 2024 13:53:42 -0400 Subject: [PATCH 04/14] feat(rpc): introduce entry point routing --- bin/tools/src/bin/get_example_ops.rs | 38 -- crates/rpc/src/debug.rs | 3 +- crates/rpc/src/eth/api.rs | 735 +++------------------- crates/rpc/src/eth/error.rs | 21 +- crates/rpc/src/eth/events/mod.rs | 219 +++++++ crates/rpc/src/eth/events/v0_6.rs | 287 +++++++++ crates/rpc/src/eth/events/v0_7.rs | 34 + crates/rpc/src/eth/mod.rs | 20 +- crates/rpc/src/eth/router.rs | 305 +++++++++ crates/rpc/src/eth/server.rs | 32 +- crates/rpc/src/lib.rs | 1 - crates/rpc/src/rundler.rs | 64 +- crates/rpc/src/task.rs | 109 ++-- crates/rpc/src/{types.rs => types/mod.rs} | 184 +++--- crates/rpc/src/types/v0_6.rs | 105 ++++ crates/rpc/src/types/v0_7.rs | 88 +++ crates/sim/src/estimation/mod.rs | 5 +- crates/sim/src/estimation/v0_6.rs | 8 +- crates/sim/src/estimation/v0_7.rs | 36 ++ crates/sim/src/lib.rs | 7 +- crates/types/src/user_operation/mod.rs | 65 +- crates/types/src/user_operation/v0_6.rs | 13 +- crates/types/src/user_operation/v0_7.rs | 12 +- 23 files changed, 1472 insertions(+), 919 deletions(-) delete mode 100644 bin/tools/src/bin/get_example_ops.rs create mode 100644 crates/rpc/src/eth/events/mod.rs create mode 100644 crates/rpc/src/eth/events/v0_6.rs create mode 100644 crates/rpc/src/eth/events/v0_7.rs create mode 100644 crates/rpc/src/eth/router.rs rename crates/rpc/src/{types.rs => types/mod.rs} (66%) create mode 100644 crates/rpc/src/types/v0_6.rs create mode 100644 crates/rpc/src/types/v0_7.rs create mode 100644 crates/sim/src/estimation/v0_7.rs 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/rpc/src/debug.rs b/crates/rpc/src/debug.rs index 982320eb7..a7b3b20bd 100644 --- a/crates/rpc/src/debug.rs +++ b/crates/rpc/src/debug.rs @@ -17,7 +17,6 @@ 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::v0_6; use crate::{ error::rpc_err, @@ -127,7 +126,7 @@ where .await .map_err(|e| rpc_err(INTERNAL_ERROR_CODE, e.to_string()))? .into_iter() - .map(|pop| v0_6::UserOperation::from(pop.uo).into()) + .map(|pop| pop.uo.into()) .collect::>()) } diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 01dc61509..85304281b 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -11,40 +11,23 @@ // 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 futures_util::future; use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, L1GasProvider, Provider, SimulationProvider}; -use rundler_sim::{ - estimation::v0_6::GasEstimator as GasEstimatorV0_6, EstimationSettings, FeeEstimator, - GasEstimationError, GasEstimator, PrecheckSettings, -}; -use rundler_types::{ - chain::ChainSpec, - contracts::v0_6::i_entry_point::{ - IEntryPointCalls, UserOperationEventFilter, UserOperationRevertReasonFilter, - }, - v0_6, UserOperation, -}; -use rundler_utils::{eth::log_to_raw_log, log::LogOnError}; +use rundler_types::{chain::ChainSpec, UserOperationOptionalGas, UserOperationVariant}; +use rundler_utils::log::LogOnError; use tracing::Level; -use super::error::{EthResult, EthRpcError, ExecutionRevertedWithBytesData}; -use crate::types::{RichUserOperation, RpcGasEstimate, 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,109 +45,36 @@ impl Settings { } } -#[derive(Debug)] -struct EntryPointContext { - gas_estimator: GasEstimatorV0_6, -} - -impl EntryPointContext +pub(crate) struct EthApi where - P: Provider, - E: EntryPoint - + L1GasProvider - + SimulationProvider, + PS: PoolServer, { - fn new( - chain_spec: ChainSpec, - provider: Arc

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

, - ) -> Self { - let gas_estimator = GasEstimatorV0_6::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, + router: EntryPointRouter, } -impl EthApi +impl EthApi where - P: Provider, - E: EntryPoint - + L1GasProvider - + SimulationProvider, 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: PS) -> 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(), - )); - } - let op: v0_6::UserOperation = op.try_into()?; + 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") @@ -172,152 +82,63 @@ where pub(crate) async fn estimate_user_operation_gas( &self, - op: v0_6::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(), - ) - })?; - - if op.init_code.len() > 0 && op.init_code.len() < 20 { - return Err(EthRpcError::InvalidParams( - "init_code must be empty or at least 20 bytes".to_string(), - )); - } else if op.paymaster_and_data.len() > 0 && op.paymaster_and_data.len() < 20 { - return Err(EthRpcError::InvalidParams( - "paymaster_and_data must be empty or at least 20 bytes".to_string(), - )); - } - - let result = context - .gas_estimator - .estimate_op_gas(op, state_override.unwrap_or_default()) - .await; - match result { - Ok(estimate) => Ok(estimate.into()), - 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)?, - } + 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 futs = self + .router + .entry_points() + .map(|ep| self.router.get_receipt(ep, hash)); - let Some(log) = log else { return Ok(None) }; - let entry_point = log.address; - - // 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()) } @@ -326,411 +147,48 @@ 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.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 { - user_operation: v0_6::UserOperation::from(op.uo).into(), + + Ok(res.map(|op| RpcUserOperationByHash { + user_operation: op.uo.into(), entry_point: op.entry_point.into(), block_number: None, block_hash: None, 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.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::{IntoPoolOperationVariant, MockPoolServer, PoolOperation}; use rundler_provider::{MockEntryPoint, MockEntryPointV0_6, MockProvider}; use rundler_sim::{EntityInfos, PriorityFeeMode}; use rundler_types::{ - contracts::v0_6::i_entry_point::HandleOpsCall, v0_6::UserOperation, + contracts::v0_6::i_entry_point::{HandleOpsCall, IEntryPointCalls}, + v0_6::UserOperation, 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() { @@ -766,8 +224,8 @@ mod tests { 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, @@ -825,8 +283,8 @@ mod tests { 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), @@ -859,60 +317,49 @@ 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: MockEntryPointV0_6, pool: MockPoolServer, - ) -> EthApi { - let mut contexts_by_entry_point = HashMap::new(); + ) -> EthApi { 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(), - 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, + contexts_by_entry_point + .insert( + ep.address(), + EntryPointContext::new( + chain_spec.clone(), Arc::clone(&provider), - PriorityFeeMode::BaseFeePercent(0), - 0, + 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, + ), + 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..3b356c8c3 100644 --- a/crates/rpc/src/eth/error.rs +++ b/crates/rpc/src/eth/error.rs @@ -18,7 +18,7 @@ use jsonrpsee::types::{ }; use rundler_pool::{MempoolError, PoolServerError}; use rundler_provider::ProviderError; -use rundler_sim::{PrecheckViolation, SimulationViolation}; +use rundler_sim::{GasEstimationError, PrecheckViolation, SimulationViolation}; use rundler_types::{Entity, EntityType, Timestamp}; use serde::Serialize; @@ -373,3 +373,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/mod.rs b/crates/rpc/src/eth/events/mod.rs new file mode 100644 index 000000000..3ef20a9f5 --- /dev/null +++ b/crates/rpc/src/eth/events/mod.rs @@ -0,0 +1,219 @@ +// 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 v0_6; +pub(crate) use v0_6::UserOperationEventProviderV0_6; +mod v0_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..d2efdacd8 --- /dev/null +++ b/crates/rpc/src/eth/events/v0_6.rs @@ -0,0 +1,287 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use std::{collections::VecDeque, sync::Arc}; + +use anyhow::Context; +use ethers::{ + abi::{AbiDecode, RawLog}, + prelude::EthEvent, + types::{ + Address, Bytes, Filter, GethDebugBuiltInTracerType, GethDebugTracerType, + GethDebugTracingOptions, GethTrace, GethTraceFrame, Log, H256, U256, + }, +}; +use rundler_provider::Provider; +use rundler_types::{ + contracts::v0_6::i_entry_point::{ + IEntryPointCalls, UserOperationEventFilter, UserOperationRevertReasonFilter, + }, + v0_6::UserOperation, + UserOperation as UserOperationTrait, UserOperationVariant, +}; +use rundler_utils::{eth, log::LogOnError}; + +use super::UserOperationEventProvider; +use crate::types::{RpcUserOperationByHash, RpcUserOperationReceipt}; + +#[derive(Debug)] +pub(crate) struct UserOperationEventProviderV0_6 { + chain_id: u64, + address: Address, + provider: Arc

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

{ + async fn get_mined_by_hash( + &self, + hash: H256, + ) -> anyhow::Result> { + // Get event associated with hash (need to check all entry point addresses associated with this API) + let event = self + .get_event_by_hash(hash) + .await + .log_on_error("should have successfully queried for user op events by hash")?; + + let Some(event) = event else { return Ok(None) }; + + // If the event is found, get the TX and entry point + let transaction_hash = event + .transaction_hash + .context("tx_hash should be present")?; + + let tx = self + .provider + .get_transaction(transaction_hash) + .await + .context("should have fetched tx from provider")? + .context("should have found tx")?; + + // We should return null if the tx isn't included in the block yet + if tx.block_hash.is_none() && tx.block_number.is_none() { + return Ok(None); + } + let to = tx + .to + .context("tx.to should be present on transaction containing user operation event")?; + + // Find first op matching the hash + let user_operation = if self.address == to { + self.get_user_operations_from_tx_data(tx.input) + .into_iter() + .find(|op| op.hash(to, self.chain_id) == hash) + .context("matching user operation should be found in tx data")? + } else { + self.trace_find_user_operation(transaction_hash, hash) + .await + .context("error running trace")? + .context("should have found user operation in trace")? + }; + + Ok(Some(RpcUserOperationByHash { + user_operation: UserOperationVariant::from(user_operation).into(), + entry_point: event.address.into(), + block_number: Some( + tx.block_number + .map(|n| U256::from(n.as_u64())) + .unwrap_or_default(), + ), + block_hash: Some(tx.block_hash.unwrap_or_default()), + transaction_hash: Some(transaction_hash), + })) + } + + async fn get_receipt(&self, hash: H256) -> anyhow::Result> { + let event = self + .get_event_by_hash(hash) + .await + .log_on_error("should have successfully queried for user op events by hash")?; + let Some(event) = event else { return Ok(None) }; + + let entry_point = event.address; + + 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")?; + + // get failure reason + let reason: String = if uo_event.success { + "".to_owned() + } else { + Self::get_failure_reason(&tx_receipt.logs, hash) + .context("should have found revert reason if tx wasn't successful")? + .unwrap_or_default() + }; + + Ok(Some(RpcUserOperationReceipt { + user_op_hash: hash, + entry_point: entry_point.into(), + sender: uo_event.sender.into(), + nonce: uo_event.nonce, + paymaster: uo_event.paymaster.into(), + actual_gas_cost: uo_event.actual_gas_cost, + actual_gas_used: uo_event.actual_gas_used, + success: uo_event.success, + logs: filtered_logs, + receipt: tx_receipt, + reason, + })) + } +} + +impl UserOperationEventProviderV0_6

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

, + event_block_distance: Option, + ) -> Self { + Self { + chain_id, + address, + provider, + event_block_distance, + } + } + + async fn get_event_by_hash(&self, hash: H256) -> anyhow::Result> { + let to_block = self.provider.get_block_number().await?; + + let from_block = match self.event_block_distance { + Some(distance) => to_block.saturating_sub(distance), + None => 0, + }; + + let filter = Filter::new() + .address(self.address) + .event(&UserOperationEventFilter::abi_signature()) + .from_block(from_block) + .to_block(to_block) + .topic1(hash); + + let logs = self.provider.get_logs(&filter).await?; + Ok(logs.into_iter().next()) + } + + fn get_user_operations_from_tx_data(&self, tx_data: Bytes) -> Vec { + 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) -> anyhow::Result { + UserOperationEventFilter::decode_log(ð::log_to_raw_log(log)) + .context("log should be a user operation event") + } + + /// This method takes a transaction hash and a user operation hash and returns the full user operation if it exists. + /// This is meant to be used when a user operation event is found in the logs of a transaction, but the top level call + /// wasn't to an entrypoint, so we need to trace the transaction to find the user operation by inspecting each call frame + /// and returning the user operation that matches the hash. + async fn trace_find_user_operation( + &self, + tx_hash: H256, + user_op_hash: H256, + ) -> anyhow::Result> { + // initial call wasn't to an entrypoint, so we need to trace the transaction to find the user operation + let trace_options = GethDebugTracingOptions { + tracer: Some(GethDebugTracerType::BuiltInTracer( + GethDebugBuiltInTracerType::CallTracer, + )), + ..Default::default() + }; + let trace = self + .provider + .debug_trace_transaction(tx_hash, trace_options) + .await + .context("should have fetched trace from provider")?; + + // breadth first search for the user operation in the trace + let mut frame_queue = VecDeque::new(); + + if let GethTrace::Known(GethTraceFrame::CallTracer(call_frame)) = trace { + frame_queue.push_back(call_frame); + } + + while let Some(call_frame) = frame_queue.pop_front() { + // check if the call is to an entrypoint, if not enqueue the child calls if any + if let Some(to) = call_frame + .to + .as_ref() + .and_then(|to| to.as_address()) + .filter(|to| **to == self.address) + { + // check if the user operation is in the call frame + if let Some(uo) = self + .get_user_operations_from_tx_data(call_frame.input) + .into_iter() + .find(|op| op.hash(*to, self.chain_id) == user_op_hash) + { + return Ok(Some(uo)); + } + } else if let Some(calls) = call_frame.calls { + frame_queue.extend(calls) + } + } + + Ok(None) + } + + fn get_failure_reason(logs: &[Log], hash: H256) -> anyhow::Result> { + let revert_reason_evt: Option = logs + .iter() + .filter(|l| l.topics.len() > 1 && l.topics[1] == hash) + .map_while(|l| { + UserOperationRevertReasonFilter::decode_log(&RawLog { + topics: l.topics.clone(), + data: l.data.to_vec(), + }) + .ok() + }) + .next(); + + Ok(revert_reason_evt.map(|r| r.revert_reason.to_string())) + } +} diff --git a/crates/rpc/src/eth/events/v0_7.rs b/crates/rpc/src/eth/events/v0_7.rs new file mode 100644 index 000000000..f892b64bb --- /dev/null +++ b/crates/rpc/src/eth/events/v0_7.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::H256; + +use super::UserOperationEventProvider; +use crate::types::{RpcUserOperationByHash, RpcUserOperationReceipt}; + +#[derive(Debug)] +pub(crate) struct UserOperationEventProviderV0_7; + +#[async_trait::async_trait] +impl UserOperationEventProvider for UserOperationEventProviderV0_7 { + async fn get_mined_by_hash( + &self, + _hash: H256, + ) -> anyhow::Result> { + unimplemented!() + } + + async fn get_receipt(&self, _hash: H256) -> anyhow::Result> { + unimplemented!() + } +} diff --git a/crates/rpc/src/eth/mod.rs b/crates/rpc/src/eth/mod.rs index c4f560b91..dfd9b4f71 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; mod server; use ethers::types::{spoof, Address, H256, U64}; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; -use rundler_types::v0_6; -use crate::types::{RichUserOperation, RpcGasEstimate, 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: v0_6::UserOperationOptionalGas, + op: RpcUserOperationOptionalGas, entry_point: Address, state_override: Option, ) -> 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..285c44608 --- /dev/null +++ b/crates/rpc/src/eth/router.rs @@ -0,0 +1,305 @@ +// 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, 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(Box::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(Box::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); + } + } else 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: std::marker::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) + .await?; + + Ok(!output.return_info.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: std::marker::PhantomData, + } + } +} diff --git a/crates/rpc/src/eth/server.rs b/crates/rpc/src/eth/server.rs index 755279f8c..8ec47a777 100644 --- a/crates/rpc/src/eth/server.rs +++ b/crates/rpc/src/eth/server.rs @@ -11,23 +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 async_trait::async_trait; use ethers::types::{spoof, Address, H256, U64}; use jsonrpsee::core::RpcResult; use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, L1GasProvider, Provider, SimulationProvider}; -use rundler_types::v0_6; use super::{api::EthApi, EthApiServer}; -use crate::types::{RichUserOperation, RpcGasEstimate, RpcUserOperation, UserOperationReceipt}; +use crate::types::{ + RpcGasEstimate, RpcUserOperation, RpcUserOperationByHash, RpcUserOperationOptionalGas, + RpcUserOperationReceipt, +}; -#[async_trait] -impl EthApiServer for EthApi +#[async_trait::async_trait] +impl EthApiServer for EthApi where - P: Provider, - E: EntryPoint - + L1GasProvider - + SimulationProvider, PS: PoolServer, { async fn send_user_operation( @@ -35,26 +31,32 @@ where op: RpcUserOperation, entry_point: Address, ) -> RpcResult { - Ok(EthApi::send_user_operation(self, op, entry_point).await?) + Ok(EthApi::send_user_operation(self, op.into(), entry_point).await?) } async fn estimate_user_operation_gas( &self, - op: v0_6::UserOperationOptionalGas, + op: RpcUserOperationOptionalGas, entry_point: Address, state_override: Option, ) -> RpcResult { - Ok(EthApi::estimate_user_operation_gas(self, op, entry_point, state_override).await?) + 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 98ef5c6bc..14b1f8beb 100644 --- a/crates/rpc/src/rundler.rs +++ b/crates/rpc/src/rundler.rs @@ -21,15 +21,15 @@ use jsonrpsee::{ types::error::{INTERNAL_ERROR_CODE, INVALID_REQUEST_CODE}, }; use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, Provider, SimulationProvider}; +use rundler_provider::Provider; use rundler_sim::{gas, FeeEstimator}; -use rundler_types::{ - chain::ChainSpec, - v0_6::{self, UserOperation}, - UserOperationId, -}; +use rundler_types::{chain::ChainSpec, UserOperation, UserOperationVariant}; -use crate::{error::rpc_err, eth::EthRpcError, RpcUserOperation}; +use crate::{ + error::rpc_err, + eth::{EntryPointRouter, EthRpcError}, + types::RpcUserOperation, +}; /// Settings for the `rundler_` API #[derive(Copy, Clone, Debug)] @@ -65,23 +65,22 @@ pub trait RundlerApi { ) -> RpcResult>; } -pub(crate) struct RundlerApi { +pub(crate) struct RundlerApi { settings: Settings, fee_estimator: FeeEstimator

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

, - entry_point: E, + entry_point_router: EntryPointRouter, pool_server: PS, settings: Settings, ) -> Self { @@ -93,17 +92,16 @@ 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 + SimulationProvider, PS: PoolServer, { async fn max_priority_fee_per_gas(&self) -> RpcResult { @@ -123,23 +121,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: v0_6::UserOperation = user_op.try_into()?; - let id = UserOperationId { - sender: uo.sender, - nonce: uo.nonce, - }; + let uo = UserOperationVariant::from(user_op); + let id = uo.id(); - 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, @@ -147,16 +135,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 cf5435590..533cce4c4 100644 --- a/crates/rpc/src/task.rs +++ b/crates/rpc/src/task.rs @@ -22,20 +22,23 @@ use jsonrpsee::{ }; use rundler_builder::BuilderServer; use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, EthersEntryPointV0_6, L1GasProvider, SimulationProvider}; -use rundler_sim::{EstimationSettings, PrecheckSettings}; +use rundler_provider::EthersEntryPointV0_6; +use rundler_sim::{EstimationSettings, FeeEstimator, GasEstimatorV0_6, PrecheckSettings}; use rundler_task::{ server::{format_socket_addr, HealthCheck}, Task, }; -use rundler_types::{chain::ChainSpec, v0_6::UserOperation}; +use rundler_types::chain::ChainSpec; 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, + }, health::{HealthChecker, SystemApiServer}, metrics::RpcMetricsLogger, rundler::{RundlerApi, RundlerApiServer, Settings as RundlerApiSettings}, @@ -91,8 +94,37 @@ where let ep = EthersEntryPointV0_6::new(self.args.chain_spec.entry_point_address, provider.clone()); + // create the entry point router + let router = EntryPointRouterBuilder::default() + .v0_6(EntryPointRouteImpl::new( + ep.clone(), + GasEstimatorV0_6::new( + self.args.chain_spec.clone(), + provider.clone(), + ep, + 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, + provider.clone(), + self.args + .eth_api_settings + .user_operation_event_block_distance, + ), + )) + .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())]; @@ -148,48 +180,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<()> where - E: EntryPoint - + SimulationProvider - + L1GasProvider - + Clone, C: JsonRpcClient + 'static, { - 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(), - )?, - } + 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 66% rename from crates/rpc/src/types.rs rename to crates/rpc/src/types/mod.rs index 24fcbb54c..c5d383533 100644 --- a/crates/rpc/src/types.rs +++ b/crates/rpc/src/types/mod.rs @@ -12,14 +12,23 @@ // 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::{v0_6, GasEstimate}; +use rundler_types::{GasEstimate, UserOperationOptionalGas, UserOperationVariant}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::eth::EthRpcError; +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)] @@ -68,104 +77,102 @@ 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: v0_6::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 TryFrom for v0_6::UserOperation { - type Error = EthRpcError; - - fn try_from(def: RpcUserOperation) -> Result { - if def.init_code.len() > 0 && def.init_code.len() < 20 { - return Err(EthRpcError::InvalidParams( - "init_code must be empty or at least 20 bytes".to_string(), - )); - } else if def.paymaster_and_data.len() > 0 && def.paymaster_and_data.len() < 20 { - return Err(EthRpcError::InvalidParams( - "paymaster_and_data must be empty or at least 20 bytes".to_string(), - )); +impl From for UserOperationVariant { + fn from(op: RpcUserOperation) -> Self { + match op { + RpcUserOperation::V0_6(op) => UserOperationVariant::V0_6(op.into()), + RpcUserOperation::V0_7(op) => UserOperationVariant::V0_7(op.into()), } - - Ok(v0_6::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, - }) } } /// 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 @@ -273,34 +280,3 @@ pub struct RpcDebugPaymasterBalance { /// Paymaster confirmed balance onchain pub confirmed_balance: U256, } - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct RpcGasEstimate { - /// The pre-verification gas estimate - pub pre_verification_gas: U256, - /// The call gas limit estimate - pub call_gas_limit: U256, - /// The verification gas limit estimate - pub verification_gas_limit: U256, - /// The paymaster verification gas limit estimate - /// 0.6: unused - /// 0.7: populated if a paymaster is used - pub paymaster_verification_gas_limit: Option, - /// The paymaster post op gas limit - /// 0.6: unused - /// 0.7: populated if a paymaster is used - pub 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, - } - } -} diff --git a/crates/rpc/src/types/v0_6.rs b/crates/rpc/src/types/v0_6.rs new file mode 100644 index 000000000..1c764fc86 --- /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::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 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, + } + } +} + +#[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..8fcb53d83 --- /dev/null +++ b/crates/rpc/src/types/v0_7.rs @@ -0,0 +1,88 @@ +// 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, UserOperationOptionalGas}; +use serde::{Deserialize, Serialize}; + +use super::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 { + todo!() + } +} + +impl From for UserOperation { + fn from(_def: RpcUserOperation) -> Self { + todo!() + } +} + +/// 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 { + todo!() + } +} diff --git a/crates/sim/src/estimation/mod.rs b/crates/sim/src/estimation/mod.rs index feacea06e..7848ff1cf 100644 --- a/crates/sim/src/estimation/mod.rs +++ b/crates/sim/src/estimation/mod.rs @@ -19,7 +19,10 @@ use rundler_types::GasEstimate; use crate::precheck::MIN_CALL_GAS_LIMIT; /// Gas estimation module for Entry Point v0.6 -pub mod 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)] diff --git a/crates/sim/src/estimation/v0_6.rs b/crates/sim/src/estimation/v0_6.rs index 9f09b752e..ab0a11308 100644 --- a/crates/sim/src/estimation/v0_6.rs +++ b/crates/sim/src/estimation/v0_6.rs @@ -42,12 +42,8 @@ use rundler_types::{ use rundler_utils::{eth, math}; use tokio::join; -use crate::{ - estimation::{GasEstimationError, Settings}, - gas, - precheck::MIN_CALL_GAS_LIMIT, - simulation, utils, FeeEstimator, -}; +use super::{GasEstimationError, Settings}; +use crate::{gas, precheck::MIN_CALL_GAS_LIMIT, simulation, utils, FeeEstimator}; /// 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 diff --git a/crates/sim/src/estimation/v0_7.rs b/crates/sim/src/estimation/v0_7.rs new file mode 100644 index 000000000..a4a2b9646 --- /dev/null +++ b/crates/sim/src/estimation/v0_7.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/. + +use ethers::types::spoof; +use rundler_types::{v0_7::UserOperationOptionalGas, GasEstimate}; + +use super::GasEstimationError; + +/// Gas estimator for entry point v0.7 +#[derive(Debug)] +pub struct GasEstimator {} + +#[async_trait::async_trait] +impl super::GasEstimator for GasEstimator { + 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 { + unimplemented!() + } +} diff --git a/crates/sim/src/lib.rs b/crates/sim/src/lib.rs index 8b1d54c36..6111b1c98 100644 --- a/crates/sim/src/lib.rs +++ b/crates/sim/src/lib.rs @@ -31,8 +31,11 @@ //! - `test-utils`: Export mocks and utilities for testing. /// Gas estimation -pub mod estimation; -pub use estimation::{GasEstimationError, GasEstimator, Settings as EstimationSettings}; +mod estimation; +pub use estimation::{ + GasEstimationError, GasEstimator, GasEstimatorV0_6, GasEstimatorV0_7, + Settings as EstimationSettings, +}; pub mod gas; pub use gas::{FeeEstimator, PriorityFeeMode}; diff --git a/crates/types/src/user_operation/mod.rs b/crates/types/src/user_operation/mod.rs index 84513fb1f..906d8f3a0 100644 --- a/crates/types/src/user_operation/mod.rs +++ b/crates/types/src/user_operation/mod.rs @@ -28,6 +28,8 @@ 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 @@ -50,14 +52,12 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { /// Associated type for the version of a user operation that has optional gas and fee fields type OptionalGas; - /// 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 entry point version for this UO + fn entry_point_version() -> EntryPointVersion; - /// Get the user operation id - fn id(&self) -> UserOperationId; + /* + * Getters + */ /// Get the user operation sender address fn sender(&self) -> Address; @@ -68,21 +68,43 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { /// 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 call gas limit - fn call_gas_limit(&self) -> U256; - - /// Returns the verification gas limit - fn verification_gas_limit(&self) -> U256; - /// Returns the total verification gas limit fn total_verification_gas_limit(&self) -> U256; @@ -99,12 +121,6 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { /// 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; - /// 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; - /// Clear the signature field of the user op /// /// Used when a user op is using a signature aggregator prior to being submitted @@ -132,6 +148,10 @@ pub enum UserOperationVariant { 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), @@ -167,6 +187,13 @@ impl UserOperation for UserOperationVariant { } } + 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(), diff --git a/crates/types/src/user_operation/v0_6.rs b/crates/types/src/user_operation/v0_6.rs index 958d445b1..9677b7a3f 100644 --- a/crates/types/src/user_operation/v0_6.rs +++ b/crates/types/src/user_operation/v0_6.rs @@ -24,11 +24,18 @@ use super::{ GasOverheads, UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant, }; pub use crate::contracts::v0_6::shared_types::{UserOperation, UserOpsPerAggregator}; -use crate::entity::{Entity, EntityType}; +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()), @@ -57,6 +64,10 @@ impl UserOperationTrait for UserOperation { 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 diff --git a/crates/types/src/user_operation/v0_7.rs b/crates/types/src/user_operation/v0_7.rs index dbe1a66e6..e9c6af957 100644 --- a/crates/types/src/user_operation/v0_7.rs +++ b/crates/types/src/user_operation/v0_7.rs @@ -18,7 +18,9 @@ use ethers::{ }; use super::{UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant}; -use crate::{contracts::v0_7::shared_types::PackedUserOperation, Entity, GasOverheads}; +use crate::{ + contracts::v0_7::shared_types::PackedUserOperation, Entity, EntryPointVersion, GasOverheads, +}; const ENTRY_POINT_INNER_GAS_OVERHEAD: U256 = U256([10_000, 0, 0, 0]); @@ -79,6 +81,10 @@ pub struct UserOperation { 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 } @@ -102,6 +108,10 @@ impl UserOperationTrait for UserOperation { 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 From f1b8435bc35171ccf630e5ad441e20d7829b9cf4 Mon Sep 17 00:00:00 2001 From: dancoombs Date: Thu, 14 Mar 2024 18:48:14 -0400 Subject: [PATCH 05/14] refactor: move builder and pool traits to types --- Cargo.lock | 8 +- crates/builder/Cargo.toml | 3 +- crates/builder/src/bundle_proposer.rs | 26 +- crates/builder/src/bundle_sender.rs | 10 +- crates/builder/src/lib.rs | 5 +- crates/builder/src/server/local.rs | 14 +- crates/builder/src/server/mod.rs | 55 +--- crates/builder/src/server/remote/client.rs | 20 +- crates/builder/src/server/remote/error.rs | 31 +- crates/builder/src/server/remote/protos.rs | 15 +- crates/builder/src/server/remote/server.rs | 28 +- crates/builder/src/task.rs | 7 +- crates/pool/Cargo.toml | 3 - crates/pool/src/lib.rs | 9 +- crates/pool/src/mempool/error.rs | 114 ------- crates/pool/src/mempool/mod.rs | 189 +----------- crates/pool/src/mempool/paymaster.rs | 22 +- crates/pool/src/mempool/pool.rs | 18 +- crates/pool/src/mempool/reputation.rs | 52 +--- crates/pool/src/mempool/uo_pool.rs | 23 +- crates/pool/src/server/local.rs | 68 ++-- crates/pool/src/server/mod.rs | 139 +-------- crates/pool/src/server/remote/client.rs | 109 ++++--- crates/pool/src/server/remote/error.rs | 37 +-- crates/pool/src/server/remote/protos.rs | 50 ++- crates/pool/src/server/remote/server.rs | 10 +- crates/rpc/Cargo.toml | 4 +- crates/rpc/src/admin.rs | 4 +- crates/rpc/src/debug.rs | 14 +- crates/rpc/src/eth/api.rs | 29 +- crates/rpc/src/eth/error.rs | 18 +- crates/rpc/src/eth/server.rs | 6 +- crates/rpc/src/rundler.rs | 17 +- crates/rpc/src/task.rs | 12 +- crates/rpc/src/types/mod.rs | 6 +- crates/sim/src/lib.rs | 6 +- crates/sim/src/precheck.rs | 69 ++--- crates/sim/src/simulation/mempool.rs | 3 +- crates/sim/src/simulation/mod.rs | 292 +++++------------- crates/sim/src/simulation/v0_6/simulator.rs | 21 +- crates/types/Cargo.toml | 11 + .../src/server => types/src/builder}/error.rs | 24 +- crates/types/src/builder/mod.rs | 23 ++ crates/types/src/builder/traits.rs | 37 +++ crates/types/src/builder/types.rs | 30 ++ crates/types/src/entity.rs | 73 ++++- crates/types/src/lib.rs | 9 +- crates/types/src/opcode.rs | 34 ++ crates/types/src/pool/error.rs | 225 ++++++++++++++ crates/types/src/pool/mod.rs | 36 +++ crates/types/src/pool/traits.rs | 130 ++++++++ crates/types/src/pool/types.rs | 250 +++++++++++++++ 52 files changed, 1293 insertions(+), 1155 deletions(-) delete mode 100644 crates/pool/src/mempool/error.rs rename crates/{pool/src/server => types/src/builder}/error.rs (59%) create mode 100644 crates/types/src/builder/mod.rs create mode 100644 crates/types/src/builder/traits.rs create mode 100644 crates/types/src/builder/types.rs create mode 100644 crates/types/src/opcode.rs create mode 100644 crates/types/src/pool/error.rs create mode 100644 crates/types/src/pool/mod.rs create mode 100644 crates/types/src/pool/traits.rs create mode 100644 crates/types/src/pool/types.rs diff --git a/Cargo.lock b/Cargo.lock index 41808c810..a319778d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4122,7 +4122,6 @@ dependencies = [ "prost", "reqwest", "rslock", - "rundler-pool", "rundler-provider", "rundler-sim", "rundler-task", @@ -4217,8 +4216,6 @@ dependencies = [ "jsonrpsee", "metrics 0.22.1", "mockall", - "rundler-builder", - "rundler-pool", "rundler-provider", "rundler-sim", "rundler-task", @@ -4304,15 +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/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 204d9a778..ed8c5159a 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -28,17 +28,18 @@ use futures_util::TryFutureExt; use linked_hash_map::LinkedHashMap; #[cfg(test)] use mockall::automock; -use rundler_pool::{FromPoolOperationVariant, PoolOperation, PoolServer}; use rundler_provider::{ BundleHandler, EntryPoint, HandleOpsOut, L1GasProvider, Provider, SignatureAggregator, }; use rundler_sim::{ - gas, 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, GasOverheads, - Timestamp, UserOperation, UserOperationVariant, UserOpsPerAggregator, + chain::ChainSpec, + pool::{FromPoolOperationVariant, 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}; @@ -133,7 +134,7 @@ where S: Simulator, E: EntryPoint + SignatureAggregator + BundleHandler + L1GasProvider, P: Provider, - C: PoolServer, + C: Pool, { type UO = UO; @@ -238,7 +239,7 @@ where S: Simulator, E: EntryPoint + SignatureAggregator + BundleHandler + L1GasProvider, P: Provider, - C: PoolServer, + C: Pool, { pub(crate) fn new( builder_index: u64, @@ -1246,10 +1247,13 @@ mod tests { types::{H160, U64}, utils::parse_units, }; - use rundler_pool::{IntoPoolOperationVariant, MockPoolServer}; use rundler_provider::{AggregatorSimOut, MockEntryPointV0_6, MockProvider}; - use rundler_sim::{MockSimulator, SimulationViolation, ViolationError}; - use rundler_types::{v0_6::UserOperation, UserOperation as UserOperationTrait, ValidTimeRange}; + use rundler_sim::MockSimulator; + use rundler_types::{ + pool::{IntoPoolOperationVariant, MockPool, SimulationViolation}, + v0_6::UserOperation, + UserOperation as UserOperationTrait, ValidTimeRange, + }; use super::*; @@ -2034,7 +2038,7 @@ mod tests { }) .collect(); - let mut pool_client = MockPoolServer::new(); + let mut pool_client = MockPool::new(); pool_client.expect_get_ops().returning(move |_, _, _| { Ok(ops .iter() diff --git a/crates/builder/src/bundle_sender.rs b/crates/builder/src/bundle_sender.rs index af6eb2748..3814ae648 100644 --- a/crates/builder/src/bundle_sender.rs +++ b/crates/builder/src/bundle_sender.rs @@ -17,10 +17,11 @@ 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::{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] @@ -100,7 +100,7 @@ where 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 @@ -249,7 +249,7 @@ where P: BundleProposer, E: EntryPoint + BundleHandler, T: TransactionTracker, - C: PoolServer, + C: Pool, { #[allow(clippy::too_many_arguments)] pub(crate) fn new( diff --git a/crates/builder/src/lib.rs b/crates/builder/src/lib.rs index 8e7d9ccb4..66107d3c5 100644 --- a/crates/builder/src/lib.rs +++ b/crates/builder/src/lib.rs @@ -29,10 +29,7 @@ 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; 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 7ad6e3def..80389ea2c 100644 --- a/crates/builder/src/task.rs +++ b/crates/builder/src/task.rs @@ -22,7 +22,6 @@ use ethers::{ use ethers_signers::Signer; use futures::future; use futures_util::TryFutureExt; -use rundler_pool::PoolServer; use rundler_provider::EthersEntryPointV0_6; use rundler_sim::{ simulation::v0_6::{ @@ -32,7 +31,7 @@ use rundler_sim::{ MempoolConfig, PriorityFeeMode, SimulationSettings, }; use rundler_task::Task; -use rundler_types::chain::ChainSpec; +use rundler_types::{chain::ChainSpec, pool::Pool}; use rundler_utils::{emit::WithEntryPoint, handle}; use rusoto_core::Region; use tokio::{ @@ -122,7 +121,7 @@ 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); @@ -190,7 +189,7 @@ where impl

BuilderTask

where - P: PoolServer + Clone, + P: Pool + Clone, { /// Create a new builder task pub fn new( 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/src/lib.rs b/crates/pool/src/lib.rs index 2a44d2509..8dc63e071 100644 --- a/crates/pool/src/lib.rs +++ b/crates/pool/src/lib.rs @@ -25,17 +25,12 @@ mod emit; pub use emit::OpPoolEvent as PoolEvent; mod mempool; -pub use mempool::{ - FromPoolOperationVariant, IntoPoolOperationVariant, 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 69500459c..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::SenderIsNotContractAndNoInitCode(Address::zero()), - )) - } -} diff --git a/crates/pool/src/mempool/mod.rs b/crates/pool/src/mempool/mod.rs index df2c80728..401e2e88e 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,20 +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, UserOperationVariant, - ValidTimeRange, + pool::{ + MempoolError, PaymasterMetadata, PoolOperation, Reputation, ReputationStatus, StakeStatus, + }, + EntityUpdate, UserOperation, UserOperationId, }; 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(type UO = rundler_types::v0_6::UserOperation;))] #[async_trait] /// In-memory operation pool @@ -161,23 +160,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,162 +173,9 @@ pub enum OperationOrigin { ReturnedAfterReorg, } -/// 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: UO, - /// 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::() - } -} - -/// Trait to convert a [PoolOperation] holding a [UserOperationVariant] to a [PoolOperation] with a different user operation type. -pub trait FromPoolOperationVariant { - /// Conversion - fn from_variant(op: PoolOperation) -> Self; -} - -/// Trait to convert a [PoolOperation] holding a user operation to a [PoolOperation] with a [UserOperationVariant]. -pub trait IntoPoolOperationVariant { - /// Conversion - fn into_variant(self) -> PoolOperation; -} - -impl FromPoolOperationVariant for PoolOperation -where - UO: UserOperation + From, -{ - fn from_variant(op: PoolOperation) -> Self { - PoolOperation { - uo: op.uo.into(), - entry_point: op.entry_point, - aggregator: op.aggregator, - valid_time_range: op.valid_time_range, - expected_code_hash: op.expected_code_hash, - sim_block_hash: op.sim_block_hash, - sim_block_number: op.sim_block_number, - entities_needing_stake: op.entities_needing_stake, - account_is_staked: op.account_is_staked, - entity_infos: op.entity_infos, - } - } -} - -impl IntoPoolOperationVariant for PoolOperation -where - UO: UserOperation + Into, -{ - fn into_variant(self) -> PoolOperation { - PoolOperation { - uo: self.uo.into(), - entry_point: self.entry_point, - aggregator: self.aggregator, - valid_time_range: self.valid_time_range, - expected_code_hash: self.expected_code_hash, - sim_block_hash: self.sim_block_hash, - sim_block_number: self.sim_block_number, - entities_needing_stake: self.entities_needing_stake, - account_is_staked: self.account_is_staked, - entity_infos: self.entity_infos, - } - } -} - #[cfg(test)] mod tests { - use rundler_sim::EntityInfo; - use rundler_types::v0_6::UserOperation; + use rundler_types::{v0_6::UserOperation, EntityInfo, EntityInfos, EntityType, ValidTimeRange}; use super::*; diff --git a/crates/pool/src/mempool/paymaster.rs b/crates/pool/src/mempool/paymaster.rs index f1e3fee97..5fda21486 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, +}; 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)] @@ -86,8 +86,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, }; @@ -530,11 +530,7 @@ mod tests { }; 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 { diff --git a/crates/pool/src/mempool/pool.rs b/crates/pool/src/mempool/pool.rs index 5bfca439d..87095ab0e 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, +}; 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)] @@ -545,8 +543,10 @@ impl PoolMetrics { #[cfg(test)] mod tests { - use rundler_sim::{EntityInfo, EntityInfos}; - use rundler_types::{v0_6::UserOperation, UserOperation as UserOperationTrait, ValidTimeRange}; + use rundler_types::{ + v0_6::UserOperation, EntityInfo, EntityInfos, UserOperation as UserOperationTrait, + ValidTimeRange, + }; use super::*; 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 20c36705b..197652bc2 100644 --- a/crates/pool/src/mempool/uo_pool.rs +++ b/crates/pool/src/mempool/uo_pool.rs @@ -22,6 +22,9 @@ use parking_lot::RwLock; use rundler_provider::EntryPoint; use rundler_sim::{Prechecker, Simulator}; use rundler_types::{ + pool::{ + MempoolError, PaymasterMetadata, PoolOperation, Reputation, ReputationStatus, StakeStatus, + }, Entity, EntityUpdate, EntityUpdateType, UserOperation, UserOperationId, UserOperationVariant, }; use rundler_utils::emit::WithEntryPoint; @@ -30,16 +33,12 @@ 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 @@ -673,13 +672,15 @@ mod tests { use ethers::types::{Bytes, H160}; use rundler_provider::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::{ - contracts::v0_6::verifying_paymaster::DepositInfo, v0_6::UserOperation, EntityType, - GasFees, UserOperation as UserOperationTrait, ValidTimeRange, + contracts::v0_6::verifying_paymaster::DepositInfo, + pool::{PrecheckViolation, SimulationViolation}, + v0_6::UserOperation, + EntityInfo, EntityInfos, EntityType, GasFees, UserOperation as UserOperationTrait, + ValidTimeRange, }; use super::*; diff --git a/crates/pool/src/server/local.rs b/crates/pool/src/server/local.rs index ef14abf3b..d68807a47 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::{v0_6, EntityUpdate, UserOperationId, UserOperationVariant}; +use rundler_types::{ + pool::{ + IntoPoolOperationVariant, MempoolError, NewHead, PaymasterMetadata, Pool, PoolError, + PoolOperation, PoolResult, Reputation, ReputationStatus, StakeStatus, + }, + v0_6, EntityUpdate, UserOperationId, UserOperationVariant, +}; use tokio::{ sync::{broadcast, mpsc, oneshot}, task::JoinHandle, @@ -27,15 +33,9 @@ use tokio::{ use tokio_util::sync::CancellationToken; use tracing::error; -use super::{PoolResult, PoolServerError}; use crate::{ chain::ChainUpdate, - mempool::{ - IntoPoolOperationVariant, Mempool, MempoolError, OperationOrigin, PaymasterMetadata, - PoolOperation, StakeStatus, - }, - server::{NewHead, PoolServer, Reputation}, - ReputationStatus, + mempool::{Mempool, OperationOrigin}, }; /// Local pool server builder @@ -116,13 +116,13 @@ 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), } } @@ -135,7 +135,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::AddOp { hash } => Ok(hash), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -153,7 +153,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::GetOps { ops } => Ok(ops), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -165,7 +165,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::GetOpByHash { op } => Ok(op), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -174,7 +174,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::RemoveOps => Ok(()), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -187,7 +187,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::RemoveOpById { hash } => Ok(hash), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -203,7 +203,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::UpdateEntities => Ok(()), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -212,7 +212,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, @@ -221,7 +221,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugClearState => Ok(()), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -230,7 +230,7 @@ impl PoolServer for LocalPoolHandle { entry_point: Address, paymaster: bool, reputation: bool, - ) -> Result<(), PoolServerError> { + ) -> Result<(), PoolError> { let req = ServerRequestKind::AdminSetTracking { entry_point, paymaster, @@ -239,7 +239,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::AdminSetTracking => Ok(()), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -251,7 +251,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugDumpMempool { ops } => Ok(ops), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -267,7 +267,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugSetReputations => Ok(()), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -276,7 +276,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugDumpReputation { reputations } => Ok(reputations), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -288,7 +288,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugDumpPaymasterBalances { balances } => Ok(balances), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -304,7 +304,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::GetStakeStatus { status } => Ok(status), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -320,7 +320,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::GetReputationStatus { status } => Ok(status), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -342,7 +342,7 @@ impl PoolServer for LocalPoolHandle { } } })), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } } @@ -381,9 +381,9 @@ where } fn get_pool(&self, entry_point: Address) -> PoolResult<&Arc> { - self.mempools.get(&entry_point).ok_or_else(|| { - PoolServerError::MempoolError(MempoolError::UnknownEntryPoint(entry_point)) - }) + self.mempools + .get(&entry_point) + .ok_or_else(|| PoolError::MempoolError(MempoolError::UnknownEntryPoint(entry_point))) } fn get_ops( @@ -511,10 +511,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) { @@ -564,7 +564,7 @@ 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 fut = |mempool: Arc, response: oneshot::Sender>| async move { let resp = match mempool.add_operation(origin, op.into()).await { Ok(hash) => Ok(ServerResponse::AddOp { hash }), Err(e) => Err(e.into()), @@ -578,7 +578,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()), diff --git a/crates/pool/src/server/mod.rs b/crates/pool/src/server/mod.rs index a016bc669..2cf32bef2 100644 --- a/crates/pool/src/server/mod.rs +++ b/crates/pool/src/server/mod.rs @@ -11,144 +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, UserOperationId, UserOperationVariant}; - -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: 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/pool/src/server/remote/client.rs b/crates/pool/src/server/remote/client.rs index a00c55d48..11e3b324a 100644 --- a/crates/pool/src/server/remote/client.rs +++ b/crates/pool/src/server/remote/client.rs @@ -19,7 +19,13 @@ use rundler_task::{ grpc::protos::{from_bytes, to_le_bytes, ConversionError}, server::{HealthCheck, ServerStatus}, }; -use rundler_types::{EntityUpdate, UserOperationId, UserOperationVariant}; +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,18 +125,20 @@ 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: UserOperationVariant) -> PoolResult { @@ -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" )))?, } @@ -200,7 +206,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; @@ -210,11 +217,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" )))?, } @@ -228,14 +235,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" )))?, } @@ -254,7 +262,8 @@ impl PoolServer for RemotePoolClient { sender: id.sender.as_bytes().to_vec(), nonce: to_le_bytes(id.nonce), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -267,7 +276,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" )))?, } @@ -288,14 +297,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" )))?, } @@ -315,14 +325,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" )))?, } @@ -342,14 +353,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" )))?, } @@ -365,7 +377,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; @@ -374,10 +387,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" )))?, } @@ -398,14 +411,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" )))?, } @@ -418,7 +432,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; @@ -427,10 +442,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" )))?, } @@ -446,7 +461,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; @@ -455,10 +471,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" )))?, } @@ -476,16 +492,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" )))?, } @@ -503,7 +523,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; @@ -512,7 +533,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 5f51faacd..d9eb975f4 100644 --- a/crates/pool/src/server/remote/error.rs +++ b/crates/pool/src/server/remote/error.rs @@ -13,9 +13,13 @@ 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, to_le_bytes}; +use rundler_types::{ + pool::{ + MempoolError, NeedsStakeInformation, PoolError, PrecheckViolation, SimulationViolation, + }, + StorageSlot, ViolationOpCode, +}; use super::protos::{ mempool_error, precheck_violation_error, simulation_violation_error, @@ -35,38 +39,25 @@ use super::protos::{ UsedForbiddenPrecompile, 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())), }, } diff --git a/crates/pool/src/server/remote/protos.rs b/crates/pool/src/server/remote/protos.rs index e4966cecd..f822b5d49 100644 --- a/crates/pool/src/server/remote/protos.rs +++ b/crates/pool/src/server/remote/protos.rs @@ -15,18 +15,14 @@ use anyhow::{anyhow, Context}; use ethers::types::{Address, H256}; use rundler_task::grpc::protos::{from_bytes, to_le_bytes, ConversionError}; use rundler_types::{ - v0_6, Entity as RundlerEntity, EntityType as RundlerEntityType, - EntityUpdate as RundlerEntityUpdate, EntityUpdateType as RundlerEntityUpdateType, - UserOperationVariant, 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, Entity as RundlerEntity, EntityInfos, EntityType as RundlerEntityType, + EntityUpdate as RundlerEntityUpdate, EntityUpdateType as RundlerEntityUpdateType, + StakeInfo as RundlerStakeInfo, UserOperationVariant, ValidTimeRange, }; tonic::include_proto!("op_pool"); @@ -209,6 +205,19 @@ 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 { @@ -219,19 +228,6 @@ impl From for Reputation { } } -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; @@ -253,7 +249,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(), }, }); } @@ -267,8 +263,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(), }), } } @@ -333,7 +329,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(), }) } } 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/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 85304281b..1f3e550d2 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -18,8 +18,7 @@ use ethers::{ utils::to_checksum, }; use futures_util::future; -use rundler_pool::PoolServer; -use rundler_types::{chain::ChainSpec, UserOperationOptionalGas, UserOperationVariant}; +use rundler_types::{chain::ChainSpec, pool::Pool, UserOperationOptionalGas, UserOperationVariant}; use rundler_utils::log::LogOnError; use tracing::Level; @@ -45,20 +44,17 @@ impl Settings { } } -pub(crate) struct EthApi -where - PS: PoolServer, -{ +pub(crate) struct EthApi

{ chain_spec: ChainSpec, - pool: PS, + pool: P, router: EntryPointRouter, } -impl EthApi +impl

EthApi

where - PS: PoolServer, + P: Pool, { - pub(crate) fn new(chain_spec: ChainSpec, router: EntryPointRouter, pool: PS) -> Self { + pub(crate) fn new(chain_spec: ChainSpec, router: EntryPointRouter, pool: P) -> Self { Self { router, pool, @@ -181,8 +177,9 @@ mod tests { use rundler_sim::{EntityInfos, PriorityFeeMode}; use rundler_types::{ contracts::v0_6::i_entry_point::{HandleOpsCall, IEntryPointCalls}, + pool::{IntoPoolOperationVariant, MockPool, PoolOperation}, v0_6::UserOperation, - UserOperation as UserOperationTrait, ValidTimeRange, + EntityInfos, UserOperation as UserOperationTrait, ValidTimeRange, }; use super::*; @@ -209,7 +206,7 @@ mod tests { 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) @@ -242,7 +239,7 @@ mod tests { 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)); @@ -299,7 +296,7 @@ mod tests { let uo = UserOperation::default(); 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) @@ -320,8 +317,8 @@ mod tests { fn create_api( provider: MockProvider, ep: MockEntryPointV0_6, - pool: MockPoolServer, - ) -> EthApi { + pool: MockPool, + ) -> EthApi { let provider = Arc::new(provider); let chain_spec = ChainSpec { id: 1, diff --git a/crates/rpc/src/eth/error.rs b/crates/rpc/src/eth/error.rs index 3b356c8c3..3a6447897 100644 --- a/crates/rpc/src/eth/error.rs +++ b/crates/rpc/src/eth/error.rs @@ -16,10 +16,12 @@ 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::{GasEstimationError, PrecheckViolation, SimulationViolation}; -use rundler_types::{Entity, EntityType, Timestamp}; +use rundler_sim::GasEstimationError; +use rundler_types::{ + pool::{MempoolError, PoolError, PrecheckViolation, SimulationViolation}, + Entity, EntityType, Timestamp, +}; use serde::Serialize; use crate::error::{rpc_err, rpc_err_with_data}; @@ -199,14 +201,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), } } } diff --git a/crates/rpc/src/eth/server.rs b/crates/rpc/src/eth/server.rs index 8ec47a777..e07736825 100644 --- a/crates/rpc/src/eth/server.rs +++ b/crates/rpc/src/eth/server.rs @@ -13,7 +13,7 @@ use ethers::types::{spoof, Address, H256, U64}; use jsonrpsee::core::RpcResult; -use rundler_pool::PoolServer; +use rundler_types::pool::Pool; use super::{api::EthApi, EthApiServer}; use crate::types::{ @@ -22,9 +22,9 @@ use crate::types::{ }; #[async_trait::async_trait] -impl EthApiServer for EthApi +impl

EthApiServer for EthApi

where - PS: PoolServer, + P: Pool, { async fn send_user_operation( &self, diff --git a/crates/rpc/src/rundler.rs b/crates/rpc/src/rundler.rs index 14b1f8beb..953c81741 100644 --- a/crates/rpc/src/rundler.rs +++ b/crates/rpc/src/rundler.rs @@ -20,10 +20,9 @@ use jsonrpsee::{ proc_macros::rpc, types::error::{INTERNAL_ERROR_CODE, INVALID_REQUEST_CODE}, }; -use rundler_pool::PoolServer; use rundler_provider::Provider; use rundler_sim::{gas, FeeEstimator}; -use rundler_types::{chain::ChainSpec, UserOperation, UserOperationVariant}; +use rundler_types::{chain::ChainSpec, pool::Pool, UserOperation, UserOperationVariant}; use crate::{ error::rpc_err, @@ -65,23 +64,23 @@ pub trait RundlerApi { ) -> RpcResult>; } -pub(crate) struct RundlerApi { +pub(crate) struct RundlerApi { settings: Settings, fee_estimator: FeeEstimator

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

, entry_point_router: EntryPointRouter, - pool_server: PS, + pool_server: PL, settings: Settings, ) -> Self { Self { @@ -99,10 +98,10 @@ where } #[async_trait] -impl RundlerApiServer for RundlerApi +impl RundlerApiServer for RundlerApi where P: Provider, - PS: PoolServer, + PL: Pool, { async fn max_priority_fee_per_gas(&self) -> RpcResult { let (bundle_fees, _) = self diff --git a/crates/rpc/src/task.rs b/crates/rpc/src/task.rs index 533cce4c4..2cc1bb5d4 100644 --- a/crates/rpc/src/task.rs +++ b/crates/rpc/src/task.rs @@ -20,15 +20,13 @@ use jsonrpsee::{ server::{middleware::ProxyGetRequestLayer, ServerBuilder}, RpcModule, }; -use rundler_builder::BuilderServer; -use rundler_pool::PoolServer; use rundler_provider::EthersEntryPointV0_6; use rundler_sim::{EstimationSettings, FeeEstimator, GasEstimatorV0_6, 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; @@ -83,8 +81,8 @@ 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()?; @@ -163,8 +161,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 { diff --git a/crates/rpc/src/types/mod.rs b/crates/rpc/src/types/mod.rs index c5d383533..c97bf3017 100644 --- a/crates/rpc/src/types/mod.rs +++ b/crates/rpc/src/types/mod.rs @@ -15,8 +15,10 @@ use ethers::{ types::{Address, Log, TransactionReceipt, H160, H256, U256}, utils::to_checksum, }; -use rundler_pool::{Reputation, ReputationStatus}; -use rundler_types::{GasEstimate, UserOperationOptionalGas, UserOperationVariant}; +use rundler_types::{ + pool::{Reputation, ReputationStatus}, + GasEstimate, UserOperationOptionalGas, UserOperationVariant, +}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; mod v0_6; diff --git a/crates/sim/src/lib.rs b/crates/sim/src/lib.rs index 6111b1c98..2cbaa4952 100644 --- a/crates/sim/src/lib.rs +++ b/crates/sim/src/lib.rs @@ -44,8 +44,7 @@ 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, }; /// Simulation and violation checking @@ -53,8 +52,7 @@ pub mod simulation; #[cfg(feature = "test-utils")] pub use simulation::MockSimulator; pub use simulation::{ - EntityInfo, EntityInfos, MempoolConfig, NeedsStakeInformation, Settings as SimulationSettings, - SimulationError, SimulationResult, SimulationViolation, Simulator, 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 fb0443c57..95391febe 100644 --- a/crates/sim/src/precheck.rs +++ b/crates/sim/src/precheck.rs @@ -19,7 +19,11 @@ use ethers::types::{Address, U256}; #[cfg(feature = "test-utils")] use mockall::automock; use rundler_provider::{EntryPoint, L1GasProvider, Provider}; -use rundler_types::{chain::ChainSpec, GasFees, UserOperation}; +use rundler_types::{ + chain::ChainSpec, + pool::{MempoolError, PrecheckViolation}, + GasFees, UserOperation, +}; use rundler_utils::math; use crate::{gas, types::ViolationError}; @@ -44,6 +48,24 @@ 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 { @@ -387,51 +409,6 @@ where } } -/// 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), -} - #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/crates/sim/src/simulation/mempool.rs b/crates/sim/src/simulation/mempool.rs index 913284004..44c1dfd1a 100644 --- a/crates/sim/src/simulation/mempool.rs +++ b/crates/sim/src/simulation/mempool.rs @@ -201,10 +201,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::{NeedsStakeInformation, ViolationOpCode}; #[test] fn test_allow_entity_any() { diff --git a/crates/sim/src/simulation/mod.rs b/crates/sim/src/simulation/mod.rs index 1b289db43..70ab685cf 100644 --- a/crates/sim/src/simulation/mod.rs +++ b/crates/sim/src/simulation/mod.rs @@ -14,15 +14,16 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use anyhow::Error; -use ethers::types::{Address, Opcode, H256, U256}; +use ethers::types::{Address, H256, U256}; #[cfg(feature = "test-utils")] use mockall::automock; use rundler_provider::AggregatorSimOut; use rundler_types::{ - Entity, EntityType, StakeInfo, StorageSlot, UserOperation, ValidTimeRange, ValidationOutput, + pool::{MempoolError, SimulationViolation}, + Entity, EntityInfo, EntityInfos, EntityType, StakeInfo, UserOperation, ValidTimeRange, + ValidationOutput, }; use serde::{Deserialize, Serialize}; -use strum::IntoEnumIterator; /// Simulation module for Entry Point v0.6 pub mod v0_6; @@ -95,6 +96,27 @@ impl From for SimulationError { } } +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] @@ -112,93 +134,6 @@ pub trait Simulator: Send + Sync + 'static { ) -> Result; } -/// 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), @@ -208,107 +143,6 @@ fn entity_type_from_simulation_phase(i: usize) -> Option { } } -#[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) @@ -316,25 +150,6 @@ enum StorageRestriction { 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, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct AccessInfo { @@ -511,3 +326,60 @@ impl Default for Settings { } } } + +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/v0_6/simulator.rs b/crates/sim/src/simulation/v0_6/simulator.rs index 961fe45e4..bab4818cf 100644 --- a/crates/sim/src/simulation/v0_6/simulator.rs +++ b/crates/sim/src/simulation/v0_6/simulator.rs @@ -28,8 +28,11 @@ use rundler_provider::{ AggregatorOut, AggregatorSimOut, EntryPoint, Provider, SignatureAggregator, SimulationProvider, }; use rundler_types::{ - contracts::v0_6::i_entry_point::FailedOp, v0_6::UserOperation, Entity, EntityType, StorageSlot, - UserOperation as UserOperationTrait, 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 super::{ @@ -43,8 +46,7 @@ use crate::{ ParseStorageAccess, Settings, StorageRestriction, }, types::ViolationError, - utils, EntityInfos, NeedsStakeInformation, SimulationError, SimulationResult, - SimulationViolation, ViolationOpCode, + utils, SimulationError, SimulationResult, }; /// Simulator implementation. @@ -187,7 +189,7 @@ where entity_infos: None, })? }; - let entity_infos = EntityInfos::new( + let entity_infos = simulation::infos_from_validation_output( factory_address, sender_address, paymaster_address, @@ -578,9 +580,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, @@ -893,7 +896,7 @@ mod tests { 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()), diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index cdef39c41..a53917109 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -10,14 +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 +futures-util.workspace = true parse-display = "0.9.0" 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/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/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 10366d99c..b9faf6d97 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -20,17 +20,24 @@ //! Rundler common types +pub mod builder; + pub mod chain; #[rustfmt::skip] pub mod contracts; 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}; 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..1ea690aee --- /dev/null +++ b/crates/types/src/pool/error.rs @@ -0,0 +1,225 @@ +// 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::{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
), + /// 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..d52ef06c2 --- /dev/null +++ b/crates/types/src/pool/traits.rs @@ -0,0 +1,130 @@ +// 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..6d1ec71cf --- /dev/null +++ b/crates/types/src/pool/types.rs @@ -0,0 +1,250 @@ +// 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)] +pub struct NewHead { + /// The hash of the new head + pub block_hash: H256, + /// The number of the new head + pub block_number: u64, +} + +impl Default for NewHead { + fn default() -> NewHead { + NewHead { + block_hash: H256::zero(), + block_number: 0, + } + } +} + +/// 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: UO, + /// 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::() + } +} + +/// Trait to convert a [PoolOperation] holding a [UserOperationVariant] to a [PoolOperation] with a different user operation type. +pub trait FromPoolOperationVariant { + /// Conversion + fn from_variant(op: PoolOperation) -> Self; +} + +/// Trait to convert a [PoolOperation] holding a user operation to a [PoolOperation] with a [UserOperationVariant]. +pub trait IntoPoolOperationVariant { + /// Conversion + fn into_variant(self) -> PoolOperation; +} + +impl FromPoolOperationVariant for PoolOperation +where + UO: UserOperation + From, +{ + fn from_variant(op: PoolOperation) -> Self { + PoolOperation { + uo: op.uo.into(), + entry_point: op.entry_point, + aggregator: op.aggregator, + valid_time_range: op.valid_time_range, + expected_code_hash: op.expected_code_hash, + sim_block_hash: op.sim_block_hash, + sim_block_number: op.sim_block_number, + entities_needing_stake: op.entities_needing_stake, + account_is_staked: op.account_is_staked, + entity_infos: op.entity_infos, + } + } +} + +impl IntoPoolOperationVariant for PoolOperation +where + UO: UserOperation + Into, +{ + fn into_variant(self) -> PoolOperation { + PoolOperation { + uo: self.uo.into(), + entry_point: self.entry_point, + aggregator: self.aggregator, + valid_time_range: self.valid_time_range, + expected_code_hash: self.expected_code_hash, + sim_block_hash: self.sim_block_hash, + sim_block_number: self.sim_block_number, + entities_needing_stake: self.entities_needing_stake, + account_is_staked: self.account_is_staked, + entity_infos: self.entity_infos, + } + } +} From 78b336196f1602fbe18759c0cc73dac4368fa555 Mon Sep 17 00:00:00 2001 From: dancoombs Date: Thu, 21 Mar 2024 20:21:36 -0400 Subject: [PATCH 06/14] feat(pool): Add entry point routing to pool --- bin/rundler/src/cli/pool.rs | 4 +- crates/builder/src/bundle_proposer.rs | 63 +- crates/pool/src/chain.rs | 738 +++++++++++++++++++----- crates/pool/src/mempool/mod.rs | 27 +- crates/pool/src/mempool/paymaster.rs | 36 +- crates/pool/src/mempool/pool.rs | 121 ++-- crates/pool/src/mempool/uo_pool.rs | 121 ++-- crates/pool/src/server/local.rs | 125 ++-- crates/pool/src/server/remote/client.rs | 12 +- crates/pool/src/server/remote/protos.rs | 6 +- crates/pool/src/task.rs | 69 ++- crates/rpc/src/eth/api.rs | 6 +- crates/rpc/src/task.rs | 1 + crates/types/src/pool/traits.rs | 12 +- crates/types/src/pool/types.rs | 58 +- crates/types/src/user_operation/mod.rs | 10 + crates/types/src/user_operation/v0_6.rs | 22 + crates/types/src/user_operation/v0_7.rs | 22 + 18 files changed, 982 insertions(+), 471 deletions(-) diff --git a/bin/rundler/src/cli/pool.rs b/bin/rundler/src/cli/pool.rs index 66df53cf9..639103f2c 100644 --- a/bin/rundler/src/cli/pool.rs +++ b/bin/rundler/src/cli/pool.rs @@ -19,7 +19,7 @@ use ethers::types::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,8 +181,10 @@ impl PoolArgs { tracing::info!("Mempool channel configs: {:?}", mempool_channel_configs); let chain_id = chain_spec.id; + // TODO(danc): multiple pool configs let pool_config = PoolConfig { entry_point: chain_spec.entry_point_address, + entry_point_version: EntryPointVersion::V0_6, chain_id, // Currently use the same shard count as the number of builders num_shards: common.num_builders, diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index ed8c5159a..6ec830050 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -37,7 +37,7 @@ use rundler_sim::{ }; use rundler_types::{ chain::ChainSpec, - pool::{FromPoolOperationVariant, Pool, PoolOperation, SimulationViolation}, + pool::{Pool, PoolOperation, SimulationViolation}, Entity, EntityInfo, EntityInfos, EntityType, EntityUpdate, EntityUpdateType, GasFees, GasOverheads, Timestamp, UserOperation, UserOperationVariant, UserOpsPerAggregator, }; @@ -131,6 +131,7 @@ pub(crate) struct Settings { impl BundleProposer for BundleProposerImpl where UO: UserOperation + From, + UserOperationVariant: AsRef, S: Simulator, E: EntryPoint + SignatureAggregator + BundleHandler + L1GasProvider, P: Provider, @@ -236,6 +237,7 @@ where impl BundleProposerImpl where UO: UserOperation + From, + UserOperationVariant: AsRef, S: Simulator, E: EntryPoint + SignatureAggregator + BundleHandler + L1GasProvider, P: Provider, @@ -276,11 +278,13 @@ where // - any errors async fn filter_and_simulate( &self, - op: PoolOperation, + op: PoolOperation, block_hash: H256, base_fee: U256, required_op_fees: GasFees, - ) -> Option<(PoolOperation, Result)> { + ) -> 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 @@ -303,14 +307,14 @@ where let required_pvg = gas::calc_required_pre_verification_gas( &self.settings.chain_spec, &self.entry_point, - &op.uo, + 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" @@ -324,7 +328,7 @@ where 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 { @@ -341,7 +345,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)), @@ -356,7 +364,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")), }, @@ -371,14 +379,14 @@ where async fn assemble_context( &self, - ops_with_simulations: Vec<(PoolOperation, Result)>, + ops_with_simulations: Vec<(PoolOperation, Result)>, mut balances_by_paymaster: HashMap, ) -> ProposalContext { let all_sender_addresses: HashSet
= ops_with_simulations .iter() .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(); @@ -403,7 +411,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; } @@ -421,7 +429,7 @@ 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; } @@ -471,7 +479,10 @@ where .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. @@ -578,7 +589,7 @@ where } } - async fn get_ops_from_pool(&self) -> anyhow::Result>> { + async fn get_ops_from_pool(&self) -> anyhow::Result> { // Use builder's index as the shard index to ensure that two builders don't // attempt to bundle the same operations. // @@ -594,7 +605,6 @@ where .await .context("should get ops from pool")? .into_iter() - .map(PoolOperation::::from_variant) .collect()) } @@ -818,8 +828,8 @@ where fn limit_user_operations_for_simulation( &self, - ops: Vec>, - ) -> (Vec>, u64) { + ops: Vec, + ) -> (Vec, u64) { // Make the bundle gas limit 10% higher here so that we simulate more UOs than we need in case that we end up dropping some UOs later so we can still pack a full bundle let mut gas_left = math::increase_by_percent(U256::from(self.settings.max_bundle_gas), 10); let mut ops_in_bundle = Vec::new(); @@ -854,7 +864,10 @@ where }); } - fn op_hash(&self, op: &UO) -> H256 { + fn op_hash(&self, op: &T) -> H256 + where + T: UserOperation, + { op.hash(self.entry_point.address(), self.settings.chain_spec.id) } } @@ -1250,7 +1263,7 @@ mod tests { use rundler_provider::{AggregatorSimOut, MockEntryPointV0_6, MockProvider}; use rundler_sim::MockSimulator; use rundler_types::{ - pool::{IntoPoolOperationVariant, MockPool, SimulationViolation}, + pool::{MockPool, SimulationViolation}, v0_6::UserOperation, UserOperation as UserOperationTrait, ValidTimeRange, }; @@ -2025,7 +2038,7 @@ mod tests { let ops: Vec<_> = mock_ops .iter() .map(|MockOp { op, .. }| PoolOperation { - uo: op.clone(), + uo: op.clone().into(), expected_code_hash, entry_point: entry_point_address, sim_block_hash: current_block_hash, @@ -2039,13 +2052,9 @@ mod tests { .collect(); let mut pool_client = MockPool::new(); - pool_client.expect_get_ops().returning(move |_, _, _| { - Ok(ops - .iter() - .cloned() - .map(IntoPoolOperationVariant::into_variant) - .collect()) - }); + pool_client + .expect_get_ops() + .returning(move |_, _, _| Ok(ops.clone())); let simulations_by_op: HashMap<_, _> = mock_ops .into_iter() diff --git a/crates/pool/src/chain.rs b/crates/pool/src/chain.rs index b35bfcb3c..c62754866 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::entry_point as entry_point_v0_6, v0_7::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,113 @@ 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 + ); + continue; + } } } - 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::EntryPointEvents::decode_log(&log.into()) { + match event { + entry_point_v0_6::EntryPointEvents::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::EntryPointEvents::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::EntryPointEvents::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::EntryPointEvents::decode_log(&log.into()) { + match event { + entry_point_v0_7::EntryPointEvents::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::EntryPointEvents::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::EntryPointEvents::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 +677,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 +762,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 +809,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 +839,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 +856,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 +894,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 +907,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 +932,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 +961,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 +986,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 +1012,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 +1062,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 +1087,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 +1099,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 +1124,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 +1186,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 +1207,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 +1249,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 +1265,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 +1288,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 +1301,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 +1356,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 +1389,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 +1468,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 +1485,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 +1503,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 +1518,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/mempool/mod.rs b/crates/pool/src/mempool/mod.rs index 401e2e88e..d034d2d96 100644 --- a/crates/pool/src/mempool/mod.rs +++ b/crates/pool/src/mempool/mod.rs @@ -36,7 +36,7 @@ use rundler_types::{ pool::{ MempoolError, PaymasterMetadata, PoolOperation, Reputation, ReputationStatus, StakeStatus, }, - EntityUpdate, UserOperation, UserOperationId, + EntityUpdate, EntryPointVersion, UserOperationId, UserOperationVariant, }; use tonic::async_trait; pub(crate) use uo_pool::UoPool; @@ -45,21 +45,25 @@ use super::chain::ChainUpdate; pub(crate) type MempoolResult = std::result::Result; -#[cfg_attr(test, automock(type UO = rundler_types::v0_6::UserOperation;))] +#[cfg_attr(test, automock)] #[async_trait] /// In-memory operation pool pub trait Mempool: Send + Sync + 'static { - /// The type of user operation this pool stores - type UO: UserOperation; - /// Call to update the mempool with a new chain update async fn on_chain_update(&self, update: &ChainUpdate); /// 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: Self::UO) -> MempoolResult; + async fn add_operation( + &self, + origin: OperationOrigin, + op: UserOperationVariant, + ) -> MempoolResult; /// Removes a set of operations from the pool. fn remove_operations(&self, hashes: &[H256]); @@ -82,13 +86,13 @@ pub trait Mempool: Send + Sync + 'static { &self, max: usize, shard_index: u64, - ) -> MempoolResult>>>; + ) -> MempoolResult>>; /// Returns the all operations from the pool up to a max size - fn all_operations(&self, max: usize) -> Vec>>; + fn all_operations(&self, max: usize) -> Vec>; /// Looks up a user operation by hash, returns None if not found - fn get_user_operation_by_hash(&self, hash: H256) -> Option>>; + fn get_user_operation_by_hash(&self, hash: H256) -> Option>; /// Debug methods @@ -122,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 @@ -192,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 5fda21486..a9280188d 100644 --- a/crates/pool/src/mempool/paymaster.rs +++ b/crates/pool/src/mempool/paymaster.rs @@ -21,7 +21,7 @@ use parking_lot::RwLock; use rundler_provider::EntryPoint; use rundler_types::{ pool::{MempoolError, PaymasterMetadata, PoolOperation, StakeStatus}, - StakeInfo, UserOperation, UserOperationId, + StakeInfo, UserOperation, UserOperationId, UserOperationVariant, }; use rundler_utils::cache::LruMap; @@ -30,9 +30,9 @@ use crate::chain::{BalanceUpdate, MinedOp}; /// Keeps track of current and pending paymaster balances #[derive(Debug)] -pub(crate) struct PaymasterTracker { +pub(crate) struct PaymasterTracker { entry_point: E, - state: RwLock>, + state: RwLock, config: PaymasterConfig, } @@ -60,9 +60,8 @@ impl PaymasterConfig { } } -impl PaymasterTracker +impl PaymasterTracker where - UO: UserOperation, E: EntryPoint, { pub(crate) fn new(entry_point: E, config: PaymasterConfig) -> Self { @@ -151,7 +150,10 @@ where Ok(paymaster_meta) } - pub(crate) async fn check_operation_cost(&self, op: &UO) -> 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)? @@ -206,7 +208,7 @@ where .unmine_actual_cost(paymaster, actual_cost); } - pub(crate) async fn add_or_update_balance(&self, po: &PoolOperation) -> MempoolResult<()> { + pub(crate) async fn add_or_update_balance(&self, po: &PoolOperation) -> MempoolResult<()> { if let Some(paymaster) = po.uo.paymaster() { let paymaster_metadata = self.paymaster_balance(paymaster).await?; return self @@ -221,23 +223,21 @@ where // Keeps track of current and pending paymaster balances #[derive(Debug)] -struct PaymasterTrackerInner { +struct PaymasterTrackerInner { // map for userop based on id user_op_fees: HashMap, // map for paymaster balance status paymaster_balances: LruMap, // boolean for operation of tracker tracker_enabled: bool, - _uo_type: std::marker::PhantomData, } -impl PaymasterTrackerInner { +impl PaymasterTrackerInner { fn new(tracker_enabled: bool, cache_size: u32) -> Self { Self { user_op_fees: HashMap::new(), tracker_enabled, paymaster_balances: LruMap::new(cache_size), - _uo_type: std::marker::PhantomData, } } @@ -251,7 +251,7 @@ impl PaymasterTrackerInner { fn check_operation_cost( &self, - op: &UO, + op: &UserOperationVariant, paymaster_metadata: &PaymasterMetadata, ) -> MempoolResult<()> { let max_op_cost = op.max_gas_cost(); @@ -370,7 +370,7 @@ impl PaymasterTrackerInner { fn add_or_update_balance( &mut self, - po: &PoolOperation, + po: &PoolOperation, paymaster_metadata: &PaymasterMetadata, ) -> MempoolResult<()> { let id = po.uo.id(); @@ -532,9 +532,9 @@ mod tests { use super::*; use crate::{chain::BalanceUpdate, mempool::paymaster::PaymasterTracker}; - fn demo_pool_op(uo: UserOperation) -> PoolOperation { + fn demo_pool_op(uo: UserOperation) -> PoolOperation { PoolOperation { - uo, + uo: uo.into(), entry_point: Address::random(), aggregator: None, valid_time_range: ValidTimeRange::all_time(), @@ -945,7 +945,7 @@ mod tests { #[test] fn test_inner_cache_full() { - let mut inner = PaymasterTrackerInner::::new(true, 2); + let mut inner = PaymasterTrackerInner::new(true, 2); let paymaster_0 = Address::random(); let paymaster_1 = Address::random(); @@ -964,7 +964,7 @@ mod tests { assert!(inner.paymaster_exists(paymaster_2)); } - fn new_paymaster_tracker() -> PaymasterTracker { + fn new_paymaster_tracker() -> PaymasterTracker { let mut entrypoint = MockEntryPointV0_6::new(); entrypoint.expect_get_deposit_info().returning(|_| { @@ -990,7 +990,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 87095ab0e..7471b890f 100644 --- a/crates/pool/src/mempool/pool.rs +++ b/crates/pool/src/mempool/pool.rs @@ -24,7 +24,7 @@ use ethers::{ }; use rundler_types::{ pool::{MempoolError, PoolOperation}, - Entity, EntityType, Timestamp, UserOperation, UserOperationId, + Entity, EntityType, Timestamp, UserOperation, UserOperationId, UserOperationVariant, }; use rundler_utils::math; use tracing::info; @@ -57,19 +57,19 @@ impl From for PoolInnerConfig { /// Pool of user operations #[derive(Debug)] -pub(crate) struct PoolInner { +pub(crate) struct PoolInner { /// Pool settings config: PoolInnerConfig, /// Operations by hash - by_hash: HashMap>, + by_hash: HashMap, /// Operations by operation ID - by_id: HashMap>, + by_id: HashMap, /// Best operations, sorted by gas price - best: BTreeSet>, + best: BTreeSet, /// Removed operations, temporarily kept around in case their blocks are /// reorged away. Stored along with the block number at which it was /// removed. - mined_at_block_number_by_hash: HashMap, u64)>, + mined_at_block_number_by_hash: HashMap, /// Removed operation hashes sorted by block number, so we can forget them /// when enough new blocks have passed. mined_hashes_with_block_numbers: BTreeSet<(u64, H256)>, @@ -83,7 +83,7 @@ pub(crate) struct PoolInner { cache_size: SizeTracker, } -impl PoolInner { +impl PoolInner { pub(crate) fn new(config: PoolInnerConfig) -> Self { Self { config, @@ -100,7 +100,10 @@ impl PoolInner { } /// Returns hash of operation to replace if operation is a replacement - pub(crate) fn check_replacement(&self, op: &UO) -> MempoolResult> { + pub(crate) fn check_replacement( + &self, + op: &UserOperationVariant, + ) -> MempoolResult> { // Check if operation already known if self .by_hash @@ -132,13 +135,13 @@ impl PoolInner { } } - pub(crate) fn add_operation(&mut self, op: PoolOperation) -> MempoolResult { + pub(crate) fn add_operation(&mut self, op: PoolOperation) -> MempoolResult { let ret = self.add_operation_internal(Arc::new(op), None); self.update_metrics(); ret } - pub(crate) fn best_operations(&self) -> impl Iterator>> { + pub(crate) fn best_operations(&self) -> impl Iterator> { self.best.clone().into_iter().map(|v| v.po) } @@ -169,28 +172,25 @@ impl PoolInner { 0 } - pub(crate) fn get_operation_by_hash(&self, hash: H256) -> Option>> { + pub(crate) fn get_operation_by_hash(&self, hash: H256) -> Option> { self.by_hash.get(&hash).map(|o| o.po.clone()) } - pub(crate) fn get_operation_by_id( - &self, - id: &UserOperationId, - ) -> Option>> { + pub(crate) fn get_operation_by_id(&self, id: &UserOperationId) -> Option> { self.by_id.get(id).map(|o| o.po.clone()) } - pub(crate) fn remove_operation_by_hash( - &mut self, - hash: H256, - ) -> Option>> { + pub(crate) fn remove_operation_by_hash(&mut self, hash: H256) -> Option> { let ret = self.remove_operation_internal(hash, None); self.update_metrics(); ret } // STO-040 - pub(crate) fn check_multiple_roles_violation(&self, uo: &UO) -> MempoolResult<()> { + 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( @@ -219,7 +219,7 @@ impl PoolInner { pub(crate) fn check_associated_storage( &self, accessed_storage: &HashSet
, - uo: &UO, + uo: &UserOperationVariant, ) -> MempoolResult<()> { for storage_address in accessed_storage { if let Some(ec) = self.count_by_address.get(storage_address) { @@ -241,7 +241,7 @@ impl PoolInner { &mut self, mined_op: &MinedOp, block_number: u64, - ) -> Option>> { + ) -> Option> { let tx_in_pool = self.by_id.get(&mined_op.id())?; let hash = tx_in_pool @@ -254,10 +254,7 @@ impl PoolInner { ret } - pub(crate) fn unmine_operation( - &mut self, - mined_op: &MinedOp, - ) -> Option>> { + pub(crate) fn unmine_operation(&mut self, mined_op: &MinedOp) -> Option> { let hash = mined_op.hash; let (op, block_number) = self.mined_at_block_number_by_hash.remove(&hash)?; self.mined_hashes_with_block_numbers @@ -365,13 +362,13 @@ impl PoolInner { Ok(removed) } - fn put_back_unmined_operation(&mut self, op: OrderedPoolOperation) -> MempoolResult { + fn put_back_unmined_operation(&mut self, op: OrderedPoolOperation) -> MempoolResult { self.add_operation_internal(op.po, Some(op.submission_id)) } fn add_operation_internal( &mut self, - op: Arc>, + op: Arc, submission_id: Option, ) -> MempoolResult { // Check if operation already known or replacing an existing operation @@ -418,7 +415,7 @@ impl PoolInner { &mut self, hash: H256, block_number: Option, - ) -> Option>> { + ) -> Option> { let op = self.by_hash.remove(&hash)?; let id = &op.po.uo.id(); self.by_id.remove(id); @@ -455,7 +452,7 @@ impl PoolInner { id } - fn get_min_replacement_fees(&self, op: &UO) -> (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(), self.config.min_replacement_fee_increase_percentage, @@ -484,13 +481,13 @@ impl PoolInner { /// Wrapper around PoolOperation that adds a submission ID to implement /// a custom ordering for the best operations #[derive(Debug, Clone)] -struct OrderedPoolOperation { - po: Arc>, +struct OrderedPoolOperation { + po: Arc, submission_id: u64, } -impl OrderedPoolOperation { - fn uo(&self) -> &UO { +impl OrderedPoolOperation { + fn uo(&self) -> &UserOperationVariant { &self.po.uo } @@ -499,9 +496,9 @@ impl OrderedPoolOperation { } } -impl Eq for OrderedPoolOperation {} +impl Eq for OrderedPoolOperation {} -impl Ord for OrderedPoolOperation { +impl Ord for OrderedPoolOperation { fn cmp(&self, other: &Self) -> Ordering { // Sort by gas price descending then by id ascending other @@ -512,13 +509,13 @@ impl Ord for OrderedPoolOperation { } } -impl PartialOrd for OrderedPoolOperation { +impl PartialOrd for OrderedPoolOperation { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl PartialEq for OrderedPoolOperation { +impl PartialEq for OrderedPoolOperation { fn eq(&self, other: &Self) -> bool { self.cmp(other) == Ordering::Equal } @@ -783,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, @@ -807,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, @@ -827,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()); } @@ -884,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() { @@ -916,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, @@ -927,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, @@ -953,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); @@ -1020,19 +1026,15 @@ mod tests { .mem_size() } - fn create_op( - sender: Address, - nonce: usize, - max_fee_per_gas: usize, - ) -> PoolOperation { + fn create_op(sender: Address, nonce: usize, max_fee_per_gas: usize) -> PoolOperation { PoolOperation { uo: UserOperation { sender, nonce: nonce.into(), max_fee_per_gas: max_fee_per_gas.into(), - ..UserOperation::default() - }, + } + .into(), entity_infos: EntityInfos { factory: None, sender: EntityInfo { @@ -1053,10 +1055,7 @@ mod tests { } } - fn check_map_entry( - actual: Option<&OrderedPoolOperation>, - expected: Option<&PoolOperation>, - ) { + fn check_map_entry(actual: Option<&OrderedPoolOperation>, expected: Option<&PoolOperation>) { match (actual, expected) { (Some(actual), Some(expected)) => assert_eq!(*actual.po, *expected), (None, None) => (), diff --git a/crates/pool/src/mempool/uo_pool.rs b/crates/pool/src/mempool/uo_pool.rs index 197652bc2..9af9c3794 100644 --- a/crates/pool/src/mempool/uo_pool.rs +++ b/crates/pool/src/mempool/uo_pool.rs @@ -25,7 +25,8 @@ use rundler_types::{ pool::{ MempoolError, PaymasterMetadata, PoolOperation, Reputation, ReputationStatus, StakeStatus, }, - Entity, EntityUpdate, EntityUpdateType, UserOperation, UserOperationId, UserOperationVariant, + Entity, EntityUpdate, EntityUpdateType, EntryPointVersion, UserOperation, UserOperationId, + UserOperationVariant, }; use rundler_utils::emit::WithEntryPoint; use tokio::sync::broadcast; @@ -48,16 +49,17 @@ use crate::{ /// block on write locks. pub(crate) struct UoPool { config: PoolConfig, - state: RwLock>, - paymaster: PaymasterTracker, + state: RwLock, + paymaster: PaymasterTracker, reputation: Arc, event_sender: broadcast::Sender>, prechecker: P, simulator: S, + _uo_type: std::marker::PhantomData, } -struct UoPoolState { - pool: PoolInner, +struct UoPoolState { + pool: PoolInner, throttled_ops: HashSet, block_number: u64, } @@ -74,7 +76,7 @@ where event_sender: broadcast::Sender>, prechecker: P, simulator: S, - paymaster: PaymasterTracker, + paymaster: PaymasterTracker, reputation: Arc, ) -> Self { Self { @@ -89,6 +91,7 @@ where prechecker, simulator, config, + _uo_type: std::marker::PhantomData, } } @@ -135,13 +138,11 @@ where #[async_trait] impl Mempool for UoPool where - UO: UserOperation + Into, + UO: UserOperation + From + Into, P: Prechecker, S: Simulator, E: EntryPoint, { - type UO = UO; - async fn on_chain_update(&self, update: &ChainUpdate) { { let deduped_ops = update.deduped_ops(); @@ -312,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); @@ -325,7 +330,11 @@ where self.paymaster.get_stake_status(address).await } - async fn add_operation(&self, origin: OperationOrigin, op: UO) -> MempoolResult { + async fn add_operation( + &self, + origin: OperationOrigin, + op: UserOperationVariant, + ) -> MempoolResult { // TODO(danc) aggregator reputation is not implemented // TODO(danc) catch ops with aggregators prior to simulation and reject @@ -376,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 @@ -470,7 +480,7 @@ where let valid_until = pool_op.valid_time_range.valid_until; self.emit(OpPoolEvent::ReceivedOp { op_hash, - op: pool_op.uo.into(), + op: pool_op.uo, block_number: pool_op.sim_block_number, origin, valid_after, @@ -563,7 +573,7 @@ where &self, max: usize, shard_index: u64, - ) -> MempoolResult>>> { + ) -> MempoolResult>> { if shard_index >= self.config.num_shards { Err(anyhow::anyhow!("Invalid shard ID"))?; } @@ -586,14 +596,15 @@ where senders.insert(op.uo.sender()) }) .take(max) + .map(Into::into) .collect()) } - fn all_operations(&self, max: usize) -> Vec>> { + fn all_operations(&self, max: usize) -> Vec> { self.state.read().pool.best_operations().take(max).collect() } - fn get_user_operation_by_hash(&self, hash: H256) -> Option>> { + fn get_user_operation_by_hash(&self, hash: H256) -> Option> { self.state.read().pool.get_operation_by_hash(hash) } @@ -679,8 +690,8 @@ mod tests { contracts::v0_6::verifying_paymaster::DepositInfo, pool::{PrecheckViolation, SimulationViolation}, v0_6::UserOperation, - EntityInfo, EntityInfos, EntityType, GasFees, UserOperation as UserOperationTrait, - ValidTimeRange, + EntityInfo, EntityInfos, EntityType, EntryPointVersion, GasFees, + UserOperation as UserOperationTrait, ValidTimeRange, }; use super::*; @@ -772,8 +783,8 @@ mod tests { mined_ops: vec![MinedOp { entry_point: pool.config.entry_point, hash: uos[0].hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: U256::zero(), paymaster: None, }], @@ -812,10 +823,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; @@ -834,8 +846,8 @@ mod tests { mined_ops: vec![MinedOp { entry_point: pool.config.entry_point, hash: uos[0].hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: 10.into(), paymaster: Some(paymaster), }], @@ -874,8 +886,8 @@ mod tests { unmined_ops: vec![MinedOp { entry_point: pool.config.entry_point, hash: uos[0].hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: 10.into(), paymaster: None, }], @@ -915,8 +927,8 @@ mod tests { mined_ops: vec![MinedOp { entry_point: Address::random(), hash: uos[0].hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: U256::zero(), paymaster: None, }], @@ -957,8 +969,8 @@ mod tests { mined_ops: vec![MinedOp { entry_point: pool.config.entry_point, hash: uos[0].hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: U256::zero(), paymaster: None, }], @@ -1033,8 +1045,8 @@ mod tests { mined_ops: vec![MinedOp { entry_point: pool.config.entry_point, hash: uos[0].hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: U256::zero(), paymaster: None, }], @@ -1089,10 +1101,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]); @@ -1178,7 +1191,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) @@ -1207,10 +1221,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()]); @@ -1220,7 +1235,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()) @@ -1233,7 +1249,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); } @@ -1371,7 +1387,7 @@ mod tests { #[derive(Clone, Debug)] struct OpWithErrors { - op: UserOperation, + op: UserOperationVariant, valid_time_range: ValidTimeRange, precheck_error: Option, simulation_error: Option, @@ -1388,6 +1404,7 @@ mod tests { > { 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, @@ -1471,7 +1488,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() @@ -1503,7 +1520,7 @@ mod tests { impl Simulator, impl EntryPoint, >, - Vec, + Vec, ) { let uos = ops.iter().map(|op| op.op.clone()).collect::>(); let pool = create_pool(ops); @@ -1532,7 +1549,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, @@ -1554,7 +1572,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, @@ -1562,7 +1581,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 d68807a47..2b41a79c3 100644 --- a/crates/pool/src/server/local.rs +++ b/crates/pool/src/server/local.rs @@ -21,10 +21,10 @@ use futures_util::Stream; use rundler_task::server::{HealthCheck, ServerStatus}; use rundler_types::{ pool::{ - IntoPoolOperationVariant, MempoolError, NewHead, PaymasterMetadata, Pool, PoolError, - PoolOperation, PoolResult, Reputation, ReputationStatus, StakeStatus, + MempoolError, NewHead, PaymasterMetadata, Pool, PoolError, PoolOperation, PoolResult, + Reputation, ReputationStatus, StakeStatus, }, - v0_6, EntityUpdate, UserOperationId, UserOperationVariant, + EntityUpdate, EntryPointVersion, UserOperationId, UserOperationVariant, }; use tokio::{ sync::{broadcast, mpsc, oneshot}, @@ -66,15 +66,12 @@ 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> - where - M: Mempool, - { + ) -> JoinHandle> { let mut runner = LocalPoolServerRunner::new( self.req_receiver, self.block_sender, @@ -93,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>, } @@ -144,7 +141,7 @@ impl Pool for LocalPoolHandle { entry_point: Address, max_ops: u64, shard_index: u64, - ) -> PoolResult>> { + ) -> PoolResult> { let req = ServerRequestKind::GetOps { entry_point, max_ops, @@ -157,10 +154,7 @@ impl Pool for LocalPoolHandle { } } - async fn get_op_by_hash( - &self, - hash: H256, - ) -> PoolResult>> { + async fn get_op_by_hash(&self, hash: H256) -> PoolResult> { let req = ServerRequestKind::GetOpByHash { hash }; let resp = self.send(req).await?; match resp { @@ -243,10 +237,7 @@ impl Pool for LocalPoolHandle { } } - async fn debug_dump_mempool( - &self, - entry_point: Address, - ) -> PoolResult>> { + async fn debug_dump_mempool(&self, entry_point: Address) -> PoolResult> { let req = ServerRequestKind::DebugDumpMempool { entry_point }; let resp = self.send(req).await?; match resp { @@ -362,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 { @@ -380,7 +368,7 @@ where } } - fn get_pool(&self, entry_point: Address) -> PoolResult<&Arc> { + fn get_pool(&self, entry_point: Address) -> PoolResult<&Arc>> { self.mempools .get(&entry_point) .ok_or_else(|| PoolError::MempoolError(MempoolError::UnknownEntryPoint(entry_point))) @@ -391,22 +379,19 @@ where entry_point: Address, max_ops: u64, shard_index: u64, - ) -> PoolResult>> { + ) -> PoolResult> { let mempool = self.get_pool(entry_point)?; Ok(mempool .best_operations(max_ops as usize, shard_index)? .iter() - .map(|op| (**op).clone().into_variant()) + .map(|op| (**op).clone()) .collect()) } - fn get_op_by_hash( - &self, - hash: H256, - ) -> PoolResult>> { + fn get_op_by_hash(&self, hash: H256) -> PoolResult> { for mempool in self.mempools.values() { if let Some(op) = mempool.get_user_operation_by_hash(hash) { - return Ok(Some((*op).clone().into_variant())); + return Ok(Some((*op).clone())); } } Ok(None) @@ -462,15 +447,12 @@ where Ok(()) } - fn debug_dump_mempool( - &self, - entry_point: Address, - ) -> PoolResult>> { + fn debug_dump_mempool(&self, entry_point: Address) -> PoolResult> { let mempool = self.get_pool(entry_point)?; Ok(mempool .all_operations(usize::MAX) .iter() - .map(|op| (**op).clone().into_variant()) + .map(|op| (**op).clone()) .collect()) } @@ -514,7 +496,7 @@ where 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) { @@ -564,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.into()).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); } @@ -578,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()), @@ -762,10 +763,10 @@ enum ServerResponse { hash: H256, }, GetOps { - ops: Vec>, + ops: Vec, }, GetOpByHash { - op: Option>, + op: Option, }, RemoveOps, RemoveOpById { @@ -775,7 +776,7 @@ enum ServerResponse { DebugClearState, AdminSetTracking, DebugDumpMempool { - ops: Vec>, + ops: Vec, }, DebugSetReputations, DebugDumpReputation { @@ -809,12 +810,16 @@ 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(Box::new(mock_pool)); + let state = setup(HashMap::from([(ep, pool)])); let hash1 = state.handle.add_op(ep, mock_op()).await.unwrap(); assert_eq!(hash0, hash1); @@ -826,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(Box::new(mock_pool)); + let state = setup(HashMap::from([(ep, pool)])); let mut sub = state.handle.subscribe_new_heads().await.unwrap(); @@ -852,7 +858,10 @@ mod tests { let state = setup( eps0.iter() - .map(|ep| (*ep, Arc::new(MockMempool::new()))) + .map(|ep| { + let pool: Arc> = Arc::new(Box::new(MockMempool::new())); + (*ep, pool) + }) .collect(), ); @@ -871,19 +880,31 @@ 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(Box::new(pool)); + (*ep, pool) + }) .collect(), ); @@ -898,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); diff --git a/crates/pool/src/server/remote/client.rs b/crates/pool/src/server/remote/client.rs index 11e3b324a..e8cce9a46 100644 --- a/crates/pool/src/server/remote/client.rs +++ b/crates/pool/src/server/remote/client.rs @@ -168,7 +168,7 @@ impl Pool for RemotePoolClient { entry_point: Address, max_ops: u64, shard_index: u64, - ) -> PoolResult>> { + ) -> PoolResult> { let res = self .op_pool_client .clone() @@ -196,10 +196,7 @@ impl Pool for RemotePoolClient { } } - async fn get_op_by_hash( - &self, - hash: H256, - ) -> PoolResult>> { + async fn get_op_by_hash(&self, hash: H256) -> PoolResult> { let res = self .op_pool_client .clone() @@ -367,10 +364,7 @@ impl Pool for RemotePoolClient { } } - async fn debug_dump_mempool( - &self, - entry_point: Address, - ) -> PoolResult>> { + async fn debug_dump_mempool(&self, entry_point: Address) -> PoolResult> { let res = self .op_pool_client .clone() diff --git a/crates/pool/src/server/remote/protos.rs b/crates/pool/src/server/remote/protos.rs index f822b5d49..ec4f034ce 100644 --- a/crates/pool/src/server/remote/protos.rs +++ b/crates/pool/src/server/remote/protos.rs @@ -270,8 +270,8 @@ impl From for StakeStatus { } } -impl From<&PoolOperation> for MempoolOp { - fn from(op: &PoolOperation) -> Self { +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(), @@ -291,7 +291,7 @@ impl From<&PoolOperation> for MempoolOp { } pub const MISSING_USER_OP_ERR_STR: &str = "Mempool op should contain user operation"; -impl TryFrom for PoolOperation { +impl TryFrom for PoolOperation { type Error = anyhow::Error; fn try_from(op: MempoolOp) -> Result { diff --git a/crates/pool/src/task.rs b/crates/pool/src/task.rs index 6cd9a9808..7dc39540d 100644 --- a/crates/pool/src/task.rs +++ b/crates/pool/src/task.rs @@ -16,10 +16,10 @@ 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, EthersEntryPointV0_6, Provider}; -use rundler_sim::{simulation::v0_6 as sim_v0_6, Prechecker, PrecheckerImpl, Simulator}; +use rundler_provider::{EthersEntryPointV0_6, Provider}; +use rundler_sim::{simulation::v0_6 as sim_v0_6, PrecheckerImpl}; use rundler_task::Task; -use rundler_types::{chain::ChainSpec, v0_6}; +use rundler_types::{chain::ChainSpec, EntryPointVersion}; use rundler_utils::{emit::WithEntryPoint, handle}; use tokio::{sync::broadcast, try_join}; use tokio_util::sync::CancellationToken; @@ -28,7 +28,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}, }; @@ -73,7 +75,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( @@ -87,6 +89,39 @@ impl Task for PoolTask { // create mempools let mut mempools = HashMap::new(); for pool_config in &self.args.pool_configs { + match pool_config.entry_point_version { + EntryPointVersion::V0_6 => { + let pool = PoolTask::create_mempool_v0_6( + self.args.chain_spec.clone(), + pool_config, + self.event_sender.clone(), + provider.clone(), + ) + .await + .context("should have created mempool")?; + + 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.event_sender.clone(), + provider.clone(), + ) + .await + .context("should have created mempool")?; + + mempools.insert(pool_config.entry_point, pool); + } + EntryPointVersion::Unspecified => { + bail!( + "Unsupported entry point version: {:?}", + pool_config.entry_point_version + ); + } + } + let pool = PoolTask::create_mempool_v0_6( self.args.chain_spec.clone(), pool_config, @@ -96,7 +131,7 @@ impl Task for PoolTask { .await .context("should have created mempool")?; - mempools.insert(pool_config.entry_point, Arc::new(pool)); + mempools.insert(pool_config.entry_point, pool); } let pool_handle = self.pool_builder.get_handle(); @@ -155,19 +190,23 @@ impl PoolTask { Box::new(self) } + async fn create_mempool_v0_7( + _chain_spec: ChainSpec, + _pool_config: &PoolConfig, + _event_sender: broadcast::Sender>, + _provider: Arc

, + ) -> anyhow::Result>> { + // TODO: implement + // requires 0.7 simulation + todo!() + } + async fn create_mempool_v0_6( chain_spec: ChainSpec, pool_config: &PoolConfig, event_sender: broadcast::Sender>, provider: Arc

, - ) -> anyhow::Result< - UoPool< - v0_6::UserOperation, - impl Prechecker, - impl Simulator, - impl EntryPoint, - >, - > { + ) -> anyhow::Result>> { let ep = EthersEntryPointV0_6::new(pool_config.entry_point, Arc::clone(&provider)); let prechecker = PrecheckerImpl::new( @@ -216,6 +255,6 @@ impl PoolTask { reputation, ); - Ok(uo_pool) + Ok(Arc::new(Box::new(uo_pool))) } } diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 1f3e550d2..ee74623d4 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -177,7 +177,7 @@ mod tests { use rundler_sim::{EntityInfos, PriorityFeeMode}; use rundler_types::{ contracts::v0_6::i_entry_point::{HandleOpsCall, IEntryPointCalls}, - pool::{IntoPoolOperationVariant, MockPool, PoolOperation}, + pool::{MockPool, PoolOperation}, v0_6::UserOperation, EntityInfos, UserOperation as UserOperationTrait, ValidTimeRange, }; @@ -194,7 +194,7 @@ mod tests { let hash = uo.hash(ep, 1); let po = PoolOperation { - uo: uo.clone(), + uo: uo.clone().into(), entry_point: ep, aggregator: None, valid_time_range: ValidTimeRange::default(), @@ -210,7 +210,7 @@ mod tests { pool.expect_get_op_by_hash() .with(eq(hash)) .times(1) - .returning(move |_| Ok(Some(po.clone().into_variant()))); + .returning(move |_| Ok(Some(po.clone()))); let mut provider = MockProvider::default(); provider.expect_get_logs().returning(move |_| Ok(vec![])); diff --git a/crates/rpc/src/task.rs b/crates/rpc/src/task.rs index 2cc1bb5d4..a72763b8d 100644 --- a/crates/rpc/src/task.rs +++ b/crates/rpc/src/task.rs @@ -93,6 +93,7 @@ where EthersEntryPointV0_6::new(self.args.chain_spec.entry_point_address, provider.clone()); // create the entry point router + // TODO(danc) create 0.7 route, requires 0.7 estimator and 0.7 event provider let router = EntryPointRouterBuilder::default() .v0_6(EntryPointRouteImpl::new( ep.clone(), diff --git a/crates/types/src/pool/traits.rs b/crates/types/src/pool/traits.rs index d52ef06c2..8b9db7104 100644 --- a/crates/types/src/pool/traits.rs +++ b/crates/types/src/pool/traits.rs @@ -43,15 +43,12 @@ pub trait Pool: Send + Sync + 'static { entry_point: Address, max_ops: u64, shard_index: u64, - ) -> PoolResult>>; + ) -> 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>>; + 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<()>; @@ -99,10 +96,7 @@ pub trait Pool: Send + Sync + 'static { ) -> PoolResult<()>; /// Dump all operations in the pool, used for debug methods - async fn debug_dump_mempool( - &self, - entry_point: Address, - ) -> PoolResult>>; + async fn debug_dump_mempool(&self, entry_point: Address) -> PoolResult>; /// Set reputations for entities, used for debug methods async fn debug_set_reputations( diff --git a/crates/types/src/pool/types.rs b/crates/types/src/pool/types.rs index 6d1ec71cf..d2753c159 100644 --- a/crates/types/src/pool/types.rs +++ b/crates/types/src/pool/types.rs @@ -110,9 +110,9 @@ pub struct PaymasterMetadata { /// A user operation with additional metadata from validation. #[derive(Debug, Clone, Eq, PartialEq)] -pub struct PoolOperation { +pub struct PoolOperation { /// The user operation stored in the pool - pub uo: UO, + pub uo: UserOperationVariant, /// The entry point address for this operation pub entry_point: Address, /// The aggregator address for this operation, if any. @@ -133,7 +133,7 @@ pub struct PoolOperation { pub entity_infos: EntityInfos, } -impl PoolOperation { +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) { @@ -196,55 +196,3 @@ impl PoolOperation { + self.entities_needing_stake.len() * std::mem::size_of::() } } - -/// Trait to convert a [PoolOperation] holding a [UserOperationVariant] to a [PoolOperation] with a different user operation type. -pub trait FromPoolOperationVariant { - /// Conversion - fn from_variant(op: PoolOperation) -> Self; -} - -/// Trait to convert a [PoolOperation] holding a user operation to a [PoolOperation] with a [UserOperationVariant]. -pub trait IntoPoolOperationVariant { - /// Conversion - fn into_variant(self) -> PoolOperation; -} - -impl FromPoolOperationVariant for PoolOperation -where - UO: UserOperation + From, -{ - fn from_variant(op: PoolOperation) -> Self { - PoolOperation { - uo: op.uo.into(), - entry_point: op.entry_point, - aggregator: op.aggregator, - valid_time_range: op.valid_time_range, - expected_code_hash: op.expected_code_hash, - sim_block_hash: op.sim_block_hash, - sim_block_number: op.sim_block_number, - entities_needing_stake: op.entities_needing_stake, - account_is_staked: op.account_is_staked, - entity_infos: op.entity_infos, - } - } -} - -impl IntoPoolOperationVariant for PoolOperation -where - UO: UserOperation + Into, -{ - fn into_variant(self) -> PoolOperation { - PoolOperation { - uo: self.uo.into(), - entry_point: self.entry_point, - aggregator: self.aggregator, - valid_time_range: self.valid_time_range, - expected_code_hash: self.expected_code_hash, - sim_block_hash: self.sim_block_hash, - sim_block_number: self.sim_block_number, - entities_needing_stake: self.entities_needing_stake, - account_is_staked: self.account_is_staked, - entity_infos: self.entity_infos, - } - } -} diff --git a/crates/types/src/user_operation/mod.rs b/crates/types/src/user_operation/mod.rs index 906d8f3a0..e925a538e 100644 --- a/crates/types/src/user_operation/mod.rs +++ b/crates/types/src/user_operation/mod.rs @@ -62,6 +62,9 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { /// 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

; @@ -173,6 +176,13 @@ impl UserOperation for UserOperationVariant { } } + 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(), diff --git a/crates/types/src/user_operation/v0_6.rs b/crates/types/src/user_operation/v0_6.rs index 9677b7a3f..e7ec3bbdf 100644 --- a/crates/types/src/user_operation/v0_6.rs +++ b/crates/types/src/user_operation/v0_6.rs @@ -56,6 +56,10 @@ impl UserOperationTrait for UserOperation { self.sender } + fn nonce(&self) -> U256 { + self.nonce + } + fn factory(&self) -> Option
{ Self::get_address_from_field(&self.init_code) } @@ -191,6 +195,24 @@ impl From for super::UserOperationVariant { } } +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")] diff --git a/crates/types/src/user_operation/v0_7.rs b/crates/types/src/user_operation/v0_7.rs index e9c6af957..a4a7af70d 100644 --- a/crates/types/src/user_operation/v0_7.rs +++ b/crates/types/src/user_operation/v0_7.rs @@ -100,6 +100,10 @@ impl UserOperationTrait for UserOperation { self.sender } + fn nonce(&self) -> U256 { + self.nonce + } + fn paymaster(&self) -> Option
{ self.paymaster } @@ -227,6 +231,24 @@ impl From for super::UserOperationVariant { } } +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 { From ba666af395c75b02d314604c1d9c0e3ddb5eeeb9 Mon Sep 17 00:00:00 2001 From: Dan Coombs Date: Fri, 29 Mar 2024 16:49:26 -0500 Subject: [PATCH 07/14] feat(builder): start support for multiple entry points in builder (#645) --- bin/rundler/src/cli/builder.rs | 20 ++- crates/builder/src/lib.rs | 2 +- crates/builder/src/task.rs | 123 ++++++++++++------ .../provider/src/ethers/entry_point/v0_6.rs | 8 +- crates/provider/src/lib.rs | 6 +- crates/provider/src/traits/entry_point.rs | 10 ++ crates/provider/src/traits/mod.rs | 4 +- 7 files changed, 122 insertions(+), 51 deletions(-) diff --git a/bin/rundler/src/cli/builder.rs b/bin/rundler/src/cli/builder.rs index 1b9ebe260..afe26cc3b 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", @@ -191,6 +191,7 @@ impl BuilderArgs { .context("should have a node HTTP URL")?; let submit_url = self.submit_url.clone().unwrap_or_else(|| rpc_url.clone()); + // TODO these should be scoped by entry point let mempool_configs = match &common.mempool_config_path { Some(path) => { get_json_config::>(path, &common.aws_region).await? @@ -199,6 +200,14 @@ impl BuilderArgs { }; Ok(BuilderTaskArgs { + // TODO: support multiple entry points + entry_points: vec![EntryPointBuilderSettings { + address: chain_spec.entry_point_address, + version: EntryPointVersion::V0_6, + num_bundle_builders: common.num_builders, + bundle_builder_index_offset: self.builder_index_offset, + mempool_configs, + }], chain_spec, rpc_url, private_key: self.private_key.clone(), @@ -217,14 +226,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/crates/builder/src/lib.rs b/crates/builder/src/lib.rs index 66107d3c5..3c4173f08 100644 --- a/crates/builder/src/lib.rs +++ b/crates/builder/src/lib.rs @@ -34,6 +34,6 @@ 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/task.rs b/crates/builder/src/task.rs index 80389ea2c..03b8b011e 100644 --- a/crates/builder/src/task.rs +++ b/crates/builder/src/task.rs @@ -16,22 +16,24 @@ 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_provider::EthersEntryPointV0_6; +use rundler_provider::{EntryPointProvider, EthersEntryPointV0_6, Provider}; use rundler_sim::{ simulation::v0_6::{ SimulateValidationTracerImpl as SimulateValidationTracerImplV0_6, Simulator as SimulatorV0_6, }, - MempoolConfig, PriorityFeeMode, SimulationSettings, + MempoolConfig, PriorityFeeMode, SimulationSettings, Simulator, }; use rundler_task::Task; -use rundler_types::{chain::ChainSpec, pool::Pool}; +use rundler_types::{ + chain::ChainSpec, pool::Pool, v0_6, EntryPointVersion, UserOperation, UserOperationVariant, +}; use rundler_utils::{emit::WithEntryPoint, handle}; use rusoto_core::Region; use tokio::{ @@ -87,8 +89,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 @@ -103,10 +103,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 @@ -124,23 +137,43 @@ where 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, + 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 { + // TODO entry point v0.7: needs 0.7 EP and simulator + if ep.version != EntryPointVersion::V0_6 { + bail!("Unsupported entry point version: {:?}", ep.version); + } + + info!("Mempool config for ep v0.6: {:?}", ep.mempool_configs); + + for i in 0..ep.num_bundle_builders { + let (spawn_guard, bundle_sender_action) = 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); + } } + // flatten the senders handles to one handle, short-circuit on errors let sender_handle = tokio::spawn( future::try_join_all(sender_handles) @@ -211,14 +244,23 @@ where Box::new(self) } - async fn create_bundle_builder( + 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 { @@ -265,20 +307,6 @@ where bundle_priority_fee_overhead_percent: self.args.bundle_priority_fee_overhead_percent, }; - let ep = EthersEntryPointV0_6::new( - self.args.chain_spec.entry_point_address, - Arc::clone(&provider), - ); - let simulate_validation_tracer = - SimulateValidationTracerImplV0_6::new(Arc::clone(&provider), ep.clone()); - let simulator = SimulatorV0_6::new( - Arc::clone(&provider), - ep.clone(), - 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), @@ -314,7 +342,7 @@ where index, self.pool.clone(), simulator, - ep.clone(), + entry_point.clone(), Arc::clone(&provider), proposer_settings, self.event_sender.clone(), @@ -325,7 +353,7 @@ where self.args.chain_spec.clone(), beneficiary, proposer, - ep, + entry_point, transaction_tracker, self.pool.clone(), builder_settings, @@ -335,4 +363,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/provider/src/ethers/entry_point/v0_6.rs b/crates/provider/src/ethers/entry_point/v0_6.rs index 9bc7ad76e..e84ed769a 100644 --- a/crates/provider/src/ethers/entry_point/v0_6.rs +++ b/crates/provider/src/ethers/entry_point/v0_6.rs @@ -45,7 +45,8 @@ use rundler_utils::eth::{self, ContractRevertError}; use crate::{ traits::HandleOpsOut, AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, - ExecutionResult, L1GasProvider, Provider, SignatureAggregator, SimulationProvider, + EntryPointProvider, ExecutionResult, L1GasProvider, Provider, SignatureAggregator, + SimulationProvider, }; const ARBITRUM_NITRO_NODE_INTERFACE_ADDRESS: Address = H160([ @@ -388,6 +389,11 @@ where } } +impl

EntryPointProvider for EntryPoint

where + P: Provider + Middleware + Send + Sync + 'static +{ +} + fn get_handle_ops_call( entry_point: &IEntryPoint, ops_per_aggregator: Vec>, diff --git a/crates/provider/src/lib.rs b/crates/provider/src/lib.rs index e05a69cb1..a56c92bc0 100644 --- a/crates/provider/src/lib.rs +++ b/crates/provider/src/lib.rs @@ -30,7 +30,7 @@ pub use traits::test_utils::*; #[cfg(any(test, feature = "test-utils"))] pub use traits::MockProvider; pub use traits::{ - AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint, ExecutionResult, - HandleOpsOut, L1GasProvider, Provider, ProviderError, ProviderResult, SignatureAggregator, - SimulationProvider, + AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint, EntryPointProvider, + ExecutionResult, HandleOpsOut, L1GasProvider, Provider, ProviderError, ProviderResult, + SignatureAggregator, SimulationProvider, }; diff --git a/crates/provider/src/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index 8e8678b17..2c205b99b 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -212,3 +212,13 @@ pub trait SimulationProvider: Send + Sync + 'static { revert_data: Bytes, ) -> 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 42aa230d4..be618c09e 100644 --- a/crates/provider/src/traits/mod.rs +++ b/crates/provider/src/traits/mod.rs @@ -18,8 +18,8 @@ pub use error::ProviderError; mod entry_point; pub use entry_point::{ - AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint, ExecutionResult, - HandleOpsOut, L1GasProvider, SignatureAggregator, SimulationProvider, + AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint, EntryPointProvider, + ExecutionResult, HandleOpsOut, L1GasProvider, SignatureAggregator, SimulationProvider, }; mod provider; From 60ae74cd616c1b864e33033e5951232bae7a7f39 Mon Sep 17 00:00:00 2001 From: dancoombs Date: Mon, 25 Mar 2024 16:20:10 -0400 Subject: [PATCH 08/14] feat(provider): add an entry point v0.7 provider --- crates/dev/src/lib.rs | 8 +- crates/pool/src/chain.rs | 18 +- crates/pool/src/mempool/paymaster.rs | 5 +- crates/pool/src/mempool/uo_pool.rs | 5 +- crates/provider/src/ethers/entry_point/mod.rs | 44 ++ .../provider/src/ethers/entry_point/v0_6.rs | 38 +- .../provider/src/ethers/entry_point/v0_7.rs | 436 ++++++++++++++++++ crates/provider/src/ethers/mod.rs | 2 +- crates/provider/src/lib.rs | 5 +- crates/provider/src/traits/entry_point.rs | 4 +- crates/provider/src/traits/test_utils.rs | 4 +- crates/rpc/src/eth/router.rs | 2 +- crates/sim/src/simulation/v0_6/simulator.rs | 8 +- crates/sim/src/simulation/v0_6/tracer.rs | 7 +- crates/types/build.rs | 4 +- crates/types/contracts/src/v0_6/imports.sol | 2 +- .../types/contracts/src/v0_7/GetBalances.sol | 23 + crates/types/contracts/src/v0_7/imports.sol | 3 +- crates/types/src/lib.rs | 4 +- crates/types/src/timestamp.rs | 17 + crates/types/src/user_operation/v0_6.rs | 2 +- crates/types/src/validation_results.rs | 224 ++++++++- 22 files changed, 782 insertions(+), 83 deletions(-) create mode 100644 crates/provider/src/ethers/entry_point/v0_7.rs create mode 100644 crates/types/contracts/src/v0_7/GetBalances.sol diff --git a/crates/dev/src/lib.rs b/crates/dev/src/lib.rs index 9206fa63e..0d70c9724 100644 --- a/crates/dev/src/lib.rs +++ b/crates/dev/src/lib.rs @@ -43,7 +43,7 @@ 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, }, v0_6, UserOperation, @@ -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 @@ -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)); diff --git a/crates/pool/src/chain.rs b/crates/pool/src/chain.rs index c62754866..2d885bd58 100644 --- a/crates/pool/src/chain.rs +++ b/crates/pool/src/chain.rs @@ -27,7 +27,7 @@ use futures::future; use rundler_provider::Provider; use rundler_task::block_watcher; use rundler_types::{ - contracts::{v0_6::entry_point as entry_point_v0_6, v0_7::entry_point as entry_point_v0_7}, + 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::{ @@ -459,9 +459,9 @@ impl Chain

{ 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::EntryPointEvents::decode_log(&log.into()) { + if let Ok(event) = entry_point_v0_6::IEntryPointEvents::decode_log(&log.into()) { match event { - entry_point_v0_6::EntryPointEvents::UserOperationEventFilter(event) => { + entry_point_v0_6::IEntryPointEvents::UserOperationEventFilter(event) => { let paymaster = if event.paymaster.is_zero() { None } else { @@ -477,7 +477,7 @@ impl Chain

{ }; mined_ops.push(mined); } - entry_point_v0_6::EntryPointEvents::DepositedFilter(event) => { + entry_point_v0_6::IEntryPointEvents::DepositedFilter(event) => { let info = BalanceUpdate { entrypoint: address, address: event.account, @@ -486,7 +486,7 @@ impl Chain

{ }; balance_updates.push(info); } - entry_point_v0_6::EntryPointEvents::WithdrawnFilter(event) => { + entry_point_v0_6::IEntryPointEvents::WithdrawnFilter(event) => { let info = BalanceUpdate { entrypoint: address, address: event.account, @@ -502,9 +502,9 @@ impl Chain

{ 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::EntryPointEvents::decode_log(&log.into()) { + if let Ok(event) = entry_point_v0_7::IEntryPointEvents::decode_log(&log.into()) { match event { - entry_point_v0_7::EntryPointEvents::UserOperationEventFilter(event) => { + entry_point_v0_7::IEntryPointEvents::UserOperationEventFilter(event) => { let paymaster = if event.paymaster.is_zero() { None } else { @@ -520,7 +520,7 @@ impl Chain

{ }; mined_ops.push(mined); } - entry_point_v0_7::EntryPointEvents::DepositedFilter(event) => { + entry_point_v0_7::IEntryPointEvents::DepositedFilter(event) => { let info = BalanceUpdate { entrypoint: address, address: event.account, @@ -529,7 +529,7 @@ impl Chain

{ }; balance_updates.push(info); } - entry_point_v0_7::EntryPointEvents::WithdrawnFilter(event) => { + entry_point_v0_7::IEntryPointEvents::WithdrawnFilter(event) => { let info = BalanceUpdate { entrypoint: address, address: event.account, diff --git a/crates/pool/src/mempool/paymaster.rs b/crates/pool/src/mempool/paymaster.rs index a9280188d..28599459d 100644 --- a/crates/pool/src/mempool/paymaster.rs +++ b/crates/pool/src/mempool/paymaster.rs @@ -524,9 +524,10 @@ impl PaymasterBalance { mod tests { use ethers::types::{Address, H256, U256}; use rundler_provider::{DepositInfo, MockEntryPointV0_6}; - use rundler_sim::EntityInfos; use rundler_types::{ - v0_6::UserOperation, UserOperation as UserOperationTrait, UserOperationId, ValidTimeRange, + pool::{PaymasterMetadata, PoolOperation}, + v0_6::UserOperation, + EntityInfos, UserOperation as UserOperationTrait, UserOperationId, ValidTimeRange, }; use super::*; diff --git a/crates/pool/src/mempool/uo_pool.rs b/crates/pool/src/mempool/uo_pool.rs index 9af9c3794..23622ceb6 100644 --- a/crates/pool/src/mempool/uo_pool.rs +++ b/crates/pool/src/mempool/uo_pool.rs @@ -681,13 +681,12 @@ mod tests { use std::collections::HashMap; use ethers::types::{Bytes, H160}; - use rundler_provider::MockEntryPointV0_6; + use rundler_provider::{DepositInfo, MockEntryPointV0_6}; use rundler_sim::{ MockPrechecker, MockSimulator, PrecheckError, PrecheckSettings, SimulationError, SimulationResult, SimulationSettings, ViolationError, }; use rundler_types::{ - contracts::v0_6::verifying_paymaster::DepositInfo, pool::{PrecheckViolation, SimulationViolation}, v0_6::UserOperation, EntityInfo, EntityInfos, EntityType, EntryPointVersion, GasFees, @@ -1428,7 +1427,7 @@ mod tests { 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, diff --git a/crates/provider/src/ethers/entry_point/mod.rs b/crates/provider/src/ethers/entry_point/mod.rs index b93f2790a..d90460131 100644 --- a/crates/provider/src/ethers/entry_point/mod.rs +++ b/crates/provider/src/ethers/entry_point/mod.rs @@ -11,4 +11,48 @@ // 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 index e84ed769a..3bf96678b 100644 --- a/crates/provider/src/ethers/entry_point/v0_6.rs +++ b/crates/provider/src/ethers/entry_point/v0_6.rs @@ -20,7 +20,7 @@ use ethers::{ providers::{spoof, Middleware, RawCall}, types::{ transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, Eip1559TransactionRequest, - H160, H256, U256, U64, + H160, H256, U256, }, utils::hex, }; @@ -34,8 +34,8 @@ use rundler_types::{ i_entry_point::{ DepositInfo as DepositInfoV0_6, ExecutionResult as ExecutionResultV0_6, FailedOp, IEntryPoint, SignatureValidationFailed, + UserOpsPerAggregator as UserOpsPerAggregatorV0_6, }, - shared_types::UserOpsPerAggregator as UserOpsPerAggregatorV0_6, }, }, v0_6::UserOperation, @@ -270,12 +270,7 @@ where .calldata() .context("should get calldata for entry point handle ops")?; - let gas = self - .arb_node - .gas_estimate_l1_component(entry_point_address, false, data) - .call() - .await?; - Ok(U256::from(gas.0)) + super::estimate_arbitrum_l1_gas(&self.arb_node, entry_point_address, data).await } async fn calc_optimism_l1_gas( @@ -290,21 +285,8 @@ where .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 l1_fee = self.opt_gas_oracle.get_l1_fee(tx).call().await?; - Ok(l1_fee.checked_div(gas_price).unwrap_or(U256::MAX)) + super::estimate_optimism_l1_gas(&self.opt_gas_oracle, entry_point_address, data, gas_price) + .await } } @@ -315,18 +297,18 @@ where { type UO = UserOperation; - async fn get_simulate_validation_call( + fn get_tracer_simulate_validation_call( &self, user_op: UserOperation, max_validation_gas: u64, - ) -> anyhow::Result { + ) -> (TypedTransaction, spoof::State) { let pvg = user_op.pre_verification_gas; - let tx = self + let call = self .i_entry_point .simulate_validation(user_op) .gas(U256::from(max_validation_gas) + pvg) .tx; - Ok(tx) + (call, spoof::State::default()) } async fn call_simulate_validation( @@ -343,7 +325,7 @@ where .await { Ok(()) => anyhow::bail!("simulateValidation should always revert"), - Err(ContractError::Revert(revert_data)) => ValidationOutput::decode(revert_data) + Err(ContractError::Revert(revert_data)) => ValidationOutput::decode_v0_6(revert_data) .context("entry point should return validation output"), Err(error) => Err(error).context("call simulation RPC failed")?, } diff --git a/crates/provider/src/ethers/entry_point/v0_7.rs b/crates/provider/src/ethers/entry_point/v0_7.rs new file mode 100644 index 000000000..ef08cc44f --- /dev/null +++ b/crates/provider/src/ethers/entry_point/v0_7.rs @@ -0,0 +1,436 @@ +// 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_BYTECODE, + }, + get_balances::{GetBalancesResult, GETBALANCES_BYTECODE}, + i_aggregator::IAggregator, + i_entry_point::{ + DepositInfo as DepositInfoV0_7, FailedOp, IEntryPoint, SignatureValidationFailed, + UserOpsPerAggregator as UserOpsPerAggregatorV0_7, + }, + }, + }, + v0_7::UserOperation, + GasFees, UserOpsPerAggregator, ValidationOutput, +}; +use rundler_utils::eth::{self, ContractRevertError}; + +use crate::{ + AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, 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, Clone)] +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), + } + } +} + +#[async_trait::async_trait] +impl

crate::traits::EntryPoint 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_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, + ) -> anyhow::Result { + let addr = self.i_entry_point.address(); + let pvg = user_op.pre_verification_gas; + let mut spoof_ep = spoof::State::default(); + spoof_ep + .account(addr) + .code(ENTRYPOINTSIMULATIONS_BYTECODE.clone()); + + let ep_simulations = EntryPointSimulations::new(addr, Arc::clone(&self.provider)); + let result = ep_simulations + .simulate_validation(user_op.pack()) + .gas(U256::from(max_validation_gas) + pvg) + .call_raw() + .state(&spoof_ep) + .await + .context("should simulate validation")?; + Ok(result.into()) + } + + 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_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 +{ +} + +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 8ddcb7d21..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::v0_6::EntryPoint as EntryPointV0_6; +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/lib.rs b/crates/provider/src/lib.rs index a56c92bc0..9501c6dd7 100644 --- a/crates/provider/src/lib.rs +++ b/crates/provider/src/lib.rs @@ -22,7 +22,10 @@ //! A provider is a type that provides access to blockchain data and functions mod ethers; -pub use ethers::{provider::new_provider, EntryPointV0_6 as EthersEntryPointV0_6}; +pub use ethers::{ + provider::new_provider, EntryPointV0_6 as EthersEntryPointV0_6, + EntryPointV0_7 as EthersEntryPointV0_7, +}; mod traits; #[cfg(any(test, feature = "test-utils"))] diff --git a/crates/provider/src/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index 2c205b99b..e2554cf89 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -181,11 +181,11 @@ pub trait SimulationProvider: Send + Sync + 'static { 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: Self::UO, max_validation_gas: u64, - ) -> anyhow::Result; + ) -> (TypedTransaction, spoof::State); /// Call the entry point contract's `simulateValidation` function. async fn call_simulate_validation( diff --git a/crates/provider/src/traits/test_utils.rs b/crates/provider/src/traits/test_utils.rs index 62cf78815..a49ae96cd 100644 --- a/crates/provider/src/traits/test_utils.rs +++ b/crates/provider/src/traits/test_utils.rs @@ -52,11 +52,11 @@ mockall::mock! { #[async_trait::async_trait] impl SimulationProvider for EntryPointV0_6 { type UO = v0_6::UserOperation; - async fn get_simulate_validation_call( + fn get_tracer_simulate_validation_call( &self, user_op: v0_6::UserOperation, max_validation_gas: u64, - ) -> anyhow::Result; + ) -> (TypedTransaction, spoof::State); async fn call_simulate_validation( &self, user_op: v0_6::UserOperation, diff --git a/crates/rpc/src/eth/router.rs b/crates/rpc/src/eth/router.rs index 285c44608..b4959907c 100644 --- a/crates/rpc/src/eth/router.rs +++ b/crates/rpc/src/eth/router.rs @@ -289,7 +289,7 @@ where .call_simulate_validation(uo.into(), max_verification_gas) .await?; - Ok(!output.return_info.sig_failed) + Ok(!output.return_info.account_sig_failed) } } diff --git a/crates/sim/src/simulation/v0_6/simulator.rs b/crates/sim/src/simulation/v0_6/simulator.rs index bab4818cf..d0a30c2b8 100644 --- a/crates/sim/src/simulation/v0_6/simulator.rs +++ b/crates/sim/src/simulation/v0_6/simulator.rs @@ -175,7 +175,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, @@ -256,7 +256,11 @@ where 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); } diff --git a/crates/sim/src/simulation/v0_6/tracer.rs b/crates/sim/src/simulation/v0_6/tracer.rs index 15854408a..27af6b1e3 100644 --- a/crates/sim/src/simulation/v0_6/tracer.rs +++ b/crates/sim/src/simulation/v0_6/tracer.rs @@ -101,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 @@ -118,7 +117,7 @@ where )), ..Default::default() }, - ..Default::default() + state_overrides: Some(state_override), }, ) .await?, 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/types/src/lib.rs b/crates/types/src/lib.rs index b9faf6d97..febe53299 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -48,4 +48,6 @@ 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, ValidationOutput, ValidationReturnInfo, +}; 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/v0_6.rs b/crates/types/src/user_operation/v0_6.rs index e7ec3bbdf..d13757346 100644 --- a/crates/types/src/user_operation/v0_6.rs +++ b/crates/types/src/user_operation/v0_6.rs @@ -23,7 +23,7 @@ use strum::IntoEnumIterator; use super::{ GasOverheads, UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant, }; -pub use crate::contracts::v0_6::shared_types::{UserOperation, UserOpsPerAggregator}; +pub use crate::contracts::v0_6::i_entry_point::{UserOperation, UserOpsPerAggregator}; use crate::{ entity::{Entity, EntityType}, EntryPointVersion, diff --git a/crates/types/src/validation_results.rs b/crates/types/src/validation_results.rs index dad3659be..4317b9a4d 100644 --- a/crates/types/src/validation_results.rs +++ b/crates/types/src/validation_results.rs @@ -12,16 +12,29 @@ // 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]); + /// Equivalent to the generated `ValidationResult` or /// `ValidationResultWithAggregation` from `EntryPoint`, but with named structs /// instead of tuples and with a helper for deserializing. @@ -39,21 +52,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) = ValidationResultWithAggregationV0_6::decode(bytes) { return Ok(result.into()); } - if let Ok(result) = ValidationResultWithAggregation::decode(bytes) { + 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 +103,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 +122,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 +165,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 +176,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 +188,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 +284,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 +314,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); + } +} From d0a03704fdd6b994bbd0562b18d09e0276ac4253 Mon Sep 17 00:00:00 2001 From: Dan Coombs Date: Fri, 29 Mar 2024 16:48:46 -0500 Subject: [PATCH 09/14] feat: unsafe mode for EP v0.6 (#648) --- bin/rundler/src/cli/builder.rs | 1 + bin/rundler/src/cli/mod.rs | 4 + bin/rundler/src/cli/pool.rs | 1 + bin/rundler/src/cli/rpc.rs | 1 + crates/builder/src/task.rs | 31 +++- crates/pool/src/task.rs | 72 +++++--- .../provider/src/ethers/entry_point/v0_6.rs | 14 +- .../provider/src/ethers/entry_point/v0_7.rs | 11 +- crates/provider/src/traits/entry_point.rs | 1 + crates/provider/src/traits/test_utils.rs | 1 + crates/rpc/src/eth/router.rs | 2 +- crates/rpc/src/task.rs | 2 + crates/sim/src/simulation/v0_6/mod.rs | 3 + crates/sim/src/simulation/v0_6/simulator.rs | 5 - crates/sim/src/simulation/v0_6/unsafe_sim.rs | 173 ++++++++++++++++++ 15 files changed, 275 insertions(+), 47 deletions(-) create mode 100644 crates/sim/src/simulation/v0_6/unsafe_sim.rs diff --git a/bin/rundler/src/cli/builder.rs b/bin/rundler/src/cli/builder.rs index afe26cc3b..f103bb7b1 100644 --- a/bin/rundler/src/cli/builder.rs +++ b/bin/rundler/src/cli/builder.rs @@ -209,6 +209,7 @@ impl BuilderArgs { mempool_configs, }], 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(), diff --git a/bin/rundler/src/cli/mod.rs b/bin/rundler/src/cli/mod.rs index ae5816926..df060bd69 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", diff --git a/bin/rundler/src/cli/pool.rs b/bin/rundler/src/cli/pool.rs index 639103f2c..5e4169690 100644 --- a/bin/rundler/src/cli/pool.rs +++ b/bin/rundler/src/cli/pool.rs @@ -206,6 +206,7 @@ impl PoolArgs { Ok(PoolTaskArgs { chain_spec, + unsafe_mode: common.unsafe_mode, http_url: common .node_http .clone() diff --git a/bin/rundler/src/cli/rpc.rs b/bin/rundler/src/cli/rpc.rs index 475c6bc2d..64d5da482 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 diff --git a/crates/builder/src/task.rs b/crates/builder/src/task.rs index 03b8b011e..4c2853249 100644 --- a/crates/builder/src/task.rs +++ b/crates/builder/src/task.rs @@ -26,7 +26,7 @@ use rundler_provider::{EntryPointProvider, EthersEntryPointV0_6, Provider}; use rundler_sim::{ simulation::v0_6::{ SimulateValidationTracerImpl as SimulateValidationTracerImplV0_6, - Simulator as SimulatorV0_6, + Simulator as SimulatorV0_6, UnsafeSimulator as UnsafeSimulatorV0_6, }, MempoolConfig, PriorityFeeMode, SimulationSettings, Simulator, }; @@ -61,6 +61,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, @@ -157,8 +159,16 @@ where info!("Mempool config for ep v0.6: {:?}", ep.mempool_configs); for i in 0..ep.num_bundle_builders { - let (spawn_guard, bundle_sender_action) = self - .create_bundle_builder( + let (spawn_guard, bundle_sender_action) = if self.args.unsafe_mode { + self.create_bundle_builder( + i + ep.bundle_builder_index_offset, + Arc::clone(&provider), + ep_v0_6.clone(), + self.create_unsafe_simulator_v0_6(Arc::clone(&provider), ep_v0_6.clone()), + ) + .await? + } else { + self.create_bundle_builder( i + ep.bundle_builder_index_offset, Arc::clone(&provider), ep_v0_6.clone(), @@ -168,7 +178,8 @@ where ep.mempool_configs.clone(), ), ) - .await?; + .await? + }; sender_handles.push(spawn_guard); bundle_sender_actions.push(bundle_sender_action); } @@ -384,4 +395,16 @@ where mempool_configs, ) } + + fn create_unsafe_simulator_v0_6( + &self, + provider: Arc, + ep: E, + ) -> UnsafeSimulatorV0_6 + where + C: Provider, + E: EntryPointProvider + Clone, + { + UnsafeSimulatorV0_6::new(Arc::clone(&provider), ep, self.args.sim_settings) + } } diff --git a/crates/pool/src/task.rs b/crates/pool/src/task.rs index 7dc39540d..ed8a13de4 100644 --- a/crates/pool/src/task.rs +++ b/crates/pool/src/task.rs @@ -39,6 +39,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. @@ -94,6 +96,7 @@ impl Task for PoolTask { let pool = PoolTask::create_mempool_v0_6( self.args.chain_spec.clone(), pool_config, + self.args.unsafe_mode, self.event_sender.clone(), provider.clone(), ) @@ -121,17 +124,6 @@ impl Task for PoolTask { ); } } - - let pool = PoolTask::create_mempool_v0_6( - self.args.chain_spec.clone(), - pool_config, - self.event_sender.clone(), - provider.clone(), - ) - .await - .context("should have created mempool")?; - - mempools.insert(pool_config.entry_point, pool); } let pool_handle = self.pool_builder.get_handle(); @@ -204,6 +196,7 @@ impl PoolTask { async fn create_mempool_v0_6( chain_spec: ChainSpec, pool_config: &PoolConfig, + unsafe_mode: bool, event_sender: broadcast::Sender>, provider: Arc

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

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

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

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

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

{ i_entry_point: IEntryPoint

, provider: Arc

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

Clone for EntryPoint

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

crate::traits::EntryPoint for EntryPoint

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

EntryPointProvider for EntryPoint

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

{ - chain_spec: ChainSpec, + pub(crate) chain_spec: ChainSpec, pool: P, router: EntryPointRouter, } diff --git a/crates/rpc/src/eth/error.rs b/crates/rpc/src/eth/error.rs index 3a6447897..2a980f3bb 100644 --- a/crates/rpc/src/eth/error.rs +++ b/crates/rpc/src/eth/error.rs @@ -11,6 +11,8 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. +use std::fmt::Display; + use ethers::types::{Address, Bytes, Opcode, U256}; use jsonrpsee::types::{ error::{CALL_EXECUTION_FAILED_CODE, INTERNAL_ERROR_CODE, INVALID_PARAMS_CODE}, @@ -20,7 +22,7 @@ use rundler_provider::ProviderError; use rundler_sim::GasEstimationError; use rundler_types::{ pool::{MempoolError, PoolError, PrecheckViolation, SimulationViolation}, - Entity, EntityType, Timestamp, + Entity, EntityType, Timestamp, ValidationRevert, }; use serde::Serialize; @@ -117,6 +119,8 @@ pub enum EthRpcError { PrecheckFailed(PrecheckViolation), #[error("validation simulation failed: {0}")] SimulationFailed(SimulationViolation), + #[error("validation reverted: {0}")] + ValidationRevert(ValidationRevertData), #[error("{0}")] ExecutionReverted(String), #[error("execution reverted")] @@ -174,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 { @@ -306,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), } } @@ -355,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), } } diff --git a/crates/rpc/src/eth/events/common.rs b/crates/rpc/src/eth/events/common.rs new file mode 100644 index 000000000..f0486b3ed --- /dev/null +++ b/crates/rpc/src/eth/events/common.rs @@ -0,0 +1,263 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use std::{collections::VecDeque, sync::Arc}; + +use anyhow::Context; +use ethers::{ + prelude::EthEvent, + types::{ + Address, Bytes, Filter, GethDebugBuiltInTracerType, GethDebugTracerType, + GethDebugTracingOptions, GethTrace, GethTraceFrame, Log, TransactionReceipt, H256, U256, + }, +}; +use rundler_provider::Provider; +use rundler_types::{UserOperation, UserOperationVariant}; +use rundler_utils::{eth, log::LogOnError}; + +use super::UserOperationEventProvider; +use crate::types::{RpcUserOperationByHash, RpcUserOperationReceipt}; + +#[derive(Debug)] +pub(crate) struct UserOperationEventProviderImpl { + chain_id: u64, + address: Address, + provider: Arc

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

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

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

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

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

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

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

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

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

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

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

, + ) -> Self { + Self { + chain_spec, + entry_point, + _settings: settings, + fee_estimator, + } + } + + async fn estimate_pre_verification_gas( + &self, + op: &UserOperationOptionalGas, + gas_price: U256, + ) -> Result { + Ok(gas::estimate_pre_verification_gas( + &self.chain_spec, + &self.entry_point, + &op.max_fill(self.entry_point.address(), self.chain_spec.id), + &op.random_fill(self.entry_point.address(), self.chain_spec.id), + gas_price, + ) + .await?) } } diff --git a/crates/sim/src/gas/gas.rs b/crates/sim/src/gas/gas.rs index 8652a2a2a..b57d164e6 100644 --- a/crates/sim/src/gas/gas.rs +++ b/crates/sim/src/gas/gas.rs @@ -15,7 +15,7 @@ use std::{cmp, fmt::Debug, sync::Arc}; use anyhow::Context; use ethers::types::U256; -use rundler_provider::{L1GasProvider, Provider}; +use rundler_provider::{EntryPoint, L1GasProvider, Provider}; use rundler_types::{ chain::{self, ChainSpec, L1GasOracleContractType}, GasFees, UserOperation, @@ -40,9 +40,12 @@ use super::oracle::{ /// /// Networks that require dynamic pre_verification_gas are typically those that charge extra calldata fees /// that can scale based on dynamic gas prices. -pub async fn estimate_pre_verification_gas>( +pub async fn estimate_pre_verification_gas< + UO: UserOperation, + E: EntryPoint + L1GasProvider, +>( chain_spec: &ChainSpec, - enty_point: &E, + entry_point: &E, full_op: &UO, random_op: &UO, gas_price: U256, @@ -55,13 +58,13 @@ pub async fn estimate_pre_verification_gas panic!("Chain spec requires calldata pre_verification_gas but no l1_gas_oracle_contract_type is set"), L1GasOracleContractType::ArbitrumNitro => { - enty_point - .calc_arbitrum_l1_gas(chain_spec.entry_point_address, random_op.clone()) + entry_point + .calc_arbitrum_l1_gas(entry_point.address(), random_op.clone()) .await? }, L1GasOracleContractType::OptimismBedrock => { - enty_point - .calc_optimism_l1_gas(chain_spec.entry_point_address, random_op.clone(), gas_price) + entry_point + .calc_optimism_l1_gas(entry_point.address(), random_op.clone(), gas_price) .await? }, }; @@ -72,7 +75,10 @@ pub async fn estimate_pre_verification_gas>( +pub async fn calc_required_pre_verification_gas< + UO: UserOperation, + E: EntryPoint + L1GasProvider, +>( chain_spec: &ChainSpec, entry_point: &E, op: &UO, @@ -87,14 +93,14 @@ pub async fn calc_required_pre_verification_gas panic!("Chain spec requires calldata pre_verification_gas but no l1_gas_oracle_contract_type is set"), L1GasOracleContractType::ArbitrumNitro => { entry_point - .calc_arbitrum_l1_gas(chain_spec.entry_point_address, op.clone()) + .calc_arbitrum_l1_gas(entry_point.address(), op.clone()) .await? }, L1GasOracleContractType::OptimismBedrock => { let gas_price = cmp::min(base_fee + op.max_priority_fee_per_gas(), op.max_fee_per_gas()); entry_point - .calc_optimism_l1_gas(chain_spec.entry_point_address, op.clone(), gas_price) + .calc_optimism_l1_gas(entry_point.address(), op.clone(), gas_price) .await? }, }; diff --git a/crates/sim/src/simulation/mempool.rs b/crates/sim/src/simulation/mempool.rs index 44c1dfd1a..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 { @@ -468,6 +478,7 @@ mod tests { ( H256::random(), MempoolConfig { + entry_point: Address::random(), allowlist: vec![AllowlistEntry::new( AllowEntity::Type(EntityType::Account), AllowRule::ForbiddenOpcode { @@ -500,6 +511,7 @@ mod tests { ( H256::random(), MempoolConfig { + entry_point: Address::random(), allowlist: vec![AllowlistEntry::new( AllowEntity::Type(EntityType::Account), AllowRule::ForbiddenOpcode { @@ -544,6 +556,7 @@ mod tests { ( mempool1, MempoolConfig { + entry_point: Address::random(), allowlist: vec![AllowlistEntry::new( AllowEntity::Type(EntityType::Account), AllowRule::ForbiddenOpcode { @@ -579,6 +592,7 @@ mod tests { ( mempool1, MempoolConfig { + entry_point: Address::random(), allowlist: vec![ AllowlistEntry::new( AllowEntity::Type(EntityType::Account), @@ -600,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 70ab685cf..9a955e2b9 100644 --- a/crates/sim/src/simulation/mod.rs +++ b/crates/sim/src/simulation/mod.rs @@ -31,6 +31,9 @@ pub mod v0_6; mod mempool; pub use mempool::MempoolConfig; +mod unsafe_sim; +pub use unsafe_sim::UnsafeSimulator; + use crate::{ExpectedStorage, ViolationError}; /// The result of a successful simulation diff --git a/crates/sim/src/simulation/v0_6/unsafe_sim.rs b/crates/sim/src/simulation/unsafe_sim.rs similarity index 77% rename from crates/sim/src/simulation/v0_6/unsafe_sim.rs rename to crates/sim/src/simulation/unsafe_sim.rs index 032519e02..03f7169f0 100644 --- a/crates/sim/src/simulation/v0_6/unsafe_sim.rs +++ b/crates/sim/src/simulation/unsafe_sim.rs @@ -18,8 +18,8 @@ use rundler_provider::{ AggregatorOut, EntryPoint, Provider, SignatureAggregator, SimulationProvider, }; use rundler_types::{ - pool::SimulationViolation, v0_6::UserOperation, EntityInfo, EntityInfos, - UserOperation as UserOperationTrait, ValidTimeRange, + pool::SimulationViolation, EntityInfo, EntityInfos, UserOperation, ValidTimeRange, + ValidationError, }; use crate::{ @@ -32,40 +32,40 @@ use crate::{ /// /// WARNING: This is "unsafe" for a reason. None of the ERC-7562 checks are /// performed. -pub struct UnsafeSimulator { +pub struct UnsafeSimulator { provider: Arc

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

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

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

{ "Log with unknown entry point address: {:?}. Ignoring.", log.address ); - continue; } } } diff --git a/crates/pool/src/task.rs b/crates/pool/src/task.rs index 1313cf823..1a610acdf 100644 --- a/crates/pool/src/task.rs +++ b/crates/pool/src/task.rs @@ -120,10 +120,7 @@ impl Task for PoolTask { mempools.insert(pool_config.entry_point, pool); } EntryPointVersion::Unspecified => { - bail!( - "Unsupported entry point version: {:?}", - pool_config.entry_point_version - ); + bail!("Unsupported entry point version"); } } } From 43dd8ce0c756d73a894f5b16a583beace5b57e5b Mon Sep 17 00:00:00 2001 From: dancoombs Date: Sat, 30 Mar 2024 13:18:03 -0500 Subject: [PATCH 12/14] fix: remove extra box from dyn objects --- crates/pool/src/server/local.rs | 24 ++++++++++++------------ crates/pool/src/task.rs | 8 ++++---- crates/rpc/src/eth/router.rs | 16 ++++++++-------- crates/sim/src/gas/gas.rs | 12 ++++++------ 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/crates/pool/src/server/local.rs b/crates/pool/src/server/local.rs index 2b41a79c3..0733f9a78 100644 --- a/crates/pool/src/server/local.rs +++ b/crates/pool/src/server/local.rs @@ -68,7 +68,7 @@ impl LocalPoolBuilder { /// Run the local pool server, consumes the builder pub fn run( self, - mempools: HashMap>>, + mempools: HashMap>, chain_updates: broadcast::Receiver>, shutdown_token: CancellationToken, ) -> JoinHandle> { @@ -93,7 +93,7 @@ pub struct LocalPoolHandle { struct LocalPoolServerRunner { req_receiver: mpsc::Receiver, block_sender: broadcast::Sender, - mempools: HashMap>>, + mempools: HashMap>, chain_updates: broadcast::Receiver>, } @@ -357,7 +357,7 @@ impl LocalPoolServerRunner { fn new( req_receiver: mpsc::Receiver, block_sender: broadcast::Sender, - mempools: HashMap>>, + mempools: HashMap>, chain_updates: broadcast::Receiver>, ) -> Self { Self { @@ -368,7 +368,7 @@ impl LocalPoolServerRunner { } } - fn get_pool(&self, entry_point: Address) -> PoolResult<&Arc>> { + fn get_pool(&self, entry_point: Address) -> PoolResult<&Arc> { self.mempools .get(&entry_point) .ok_or_else(|| PoolError::MempoolError(MempoolError::UnknownEntryPoint(entry_point))) @@ -496,7 +496,7 @@ impl LocalPoolServerRunner { 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) { @@ -546,7 +546,7 @@ impl LocalPoolServerRunner { // 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 fut = |mempool: Arc, response: oneshot::Sender>| async move { let resp = 'resp: { match mempool.entry_point_version() { EntryPointVersion::V0_6 => { @@ -579,7 +579,7 @@ impl LocalPoolServerRunner { 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()), @@ -818,7 +818,7 @@ mod tests { .returning(move |_, _| Ok(hash0)); let ep = Address::random(); - let pool: Arc> = Arc::new(Box::new(mock_pool)); + let pool: Arc = Arc::new(mock_pool); let state = setup(HashMap::from([(ep, pool)])); let hash1 = state.handle.add_op(ep, mock_op()).await.unwrap(); @@ -831,7 +831,7 @@ mod tests { mock_pool.expect_on_chain_update().returning(|_| ()); let ep = Address::random(); - let pool: Arc> = Arc::new(Box::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(); @@ -859,7 +859,7 @@ mod tests { let state = setup( eps0.iter() .map(|ep| { - let pool: Arc> = Arc::new(Box::new(MockMempool::new())); + let pool: Arc = Arc::new(MockMempool::new()); (*ep, pool) }) .collect(), @@ -902,7 +902,7 @@ mod tests { let state = setup( zip(eps.iter(), pools.into_iter()) .map(|(ep, pool)| { - let pool: Arc> = Arc::new(Box::new(pool)); + let pool: Arc = Arc::new(pool); (*ep, pool) }) .collect(), @@ -919,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); diff --git a/crates/pool/src/task.rs b/crates/pool/src/task.rs index 1a610acdf..d6cea391f 100644 --- a/crates/pool/src/task.rs +++ b/crates/pool/src/task.rs @@ -188,7 +188,7 @@ impl PoolTask { unsafe_mode: bool, event_sender: broadcast::Sender>, provider: Arc

, - ) -> anyhow::Result>> { + ) -> anyhow::Result> { let ep = EthersEntryPointV0_6::new(pool_config.entry_point, Arc::clone(&provider)); if unsafe_mode { @@ -229,7 +229,7 @@ impl PoolTask { unsafe_mode: bool, event_sender: broadcast::Sender>, provider: Arc

, - ) -> anyhow::Result>> { + ) -> anyhow::Result> { let ep = EthersEntryPointV0_7::new(pool_config.entry_point, Arc::clone(&provider)); if unsafe_mode { @@ -255,7 +255,7 @@ impl PoolTask { provider: Arc

, ep: E, simulator: S, - ) -> anyhow::Result>> + ) -> anyhow::Result> where UO: UserOperation + From + Into, UserOperationVariant: From, @@ -299,6 +299,6 @@ impl PoolTask { reputation, ); - Ok(Arc::new(Box::new(uo_pool))) + Ok(Arc::new(uo_pool)) } } diff --git a/crates/rpc/src/eth/router.rs b/crates/rpc/src/eth/router.rs index 2743762cc..9a0746db4 100644 --- a/crates/rpc/src/eth/router.rs +++ b/crates/rpc/src/eth/router.rs @@ -29,8 +29,8 @@ use crate::{ #[derive(Default)] pub(crate) struct EntryPointRouterBuilder { entry_points: Vec

, - v0_6: Option<(Address, Arc>)>, - v0_7: Option<(Address, Arc>)>, + v0_6: Option<(Address, Arc)>, + v0_7: Option<(Address, Arc)>, } impl EntryPointRouterBuilder { @@ -46,7 +46,7 @@ impl EntryPointRouterBuilder { } self.entry_points.push(route.address()); - self.v0_6 = Some((route.address(), Arc::new(Box::new(route)))); + self.v0_6 = Some((route.address(), Arc::new(route))); self } @@ -62,7 +62,7 @@ impl EntryPointRouterBuilder { } self.entry_points.push(route.address()); - self.v0_7 = Some((route.address(), Arc::new(Box::new(route)))); + self.v0_7 = Some((route.address(), Arc::new(route))); self } @@ -78,8 +78,8 @@ impl EntryPointRouterBuilder { #[derive(Clone)] pub(crate) struct EntryPointRouter { entry_points: Vec
, - v0_6: Option<(Address, Arc>)>, - v0_7: Option<(Address, Arc>)>, + v0_6: Option<(Address, Arc)>, + v0_7: Option<(Address, Arc)>, } impl EntryPointRouter { @@ -91,7 +91,7 @@ impl EntryPointRouter { &self, entry_point: &Address, uo: &UserOperationVariant, - ) -> EthResult<&Arc>> { + ) -> EthResult<&Arc> { match self.get_ep_version(entry_point)? { EntryPointVersion::V0_6 => { if !matches!(uo, UserOperationVariant::V0_6(_)) { @@ -199,7 +199,7 @@ impl EntryPointRouter { ))) } - fn get_route(&self, entry_point: &Address) -> EthResult<&Arc>> { + fn get_route(&self, entry_point: &Address) -> EthResult<&Arc> { let ep = self.get_ep_version(entry_point)?; match ep { diff --git a/crates/sim/src/gas/gas.rs b/crates/sim/src/gas/gas.rs index b57d164e6..1051b2d84 100644 --- a/crates/sim/src/gas/gas.rs +++ b/crates/sim/src/gas/gas.rs @@ -257,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

{ @@ -333,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)) } } } From 07c8cae7eecd8e88ac33304647cb4c0ad0205776 Mon Sep 17 00:00:00 2001 From: dancoombs Date: Sat, 30 Mar 2024 13:26:17 -0500 Subject: [PATCH 13/14] chore: fixing PR comments --- crates/builder/src/bundle_proposer.rs | 14 ++--- crates/rpc/src/eth/api.rs | 61 +++++++++++---------- crates/sim/src/simulation/v0_6/simulator.rs | 4 +- crates/types/src/pool/types.rs | 11 +--- 4 files changed, 42 insertions(+), 48 deletions(-) diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index 6ec830050..49452b457 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -105,9 +105,9 @@ pub(crate) trait BundleProposer: Send + Sync + 'static { } #[derive(Debug)] -pub(crate) struct BundleProposerImpl { +pub(crate) struct BundleProposerImpl { builder_index: u64, - pool: C, + pool: M, simulator: S, entry_point: E, provider: Arc

, @@ -128,14 +128,14 @@ pub(crate) struct Settings { } #[async_trait] -impl BundleProposer for BundleProposerImpl +impl BundleProposer for BundleProposerImpl where UO: UserOperation + From, UserOperationVariant: AsRef, S: Simulator, E: EntryPoint + SignatureAggregator + BundleHandler + L1GasProvider, P: Provider, - C: Pool, + M: Pool, { type UO = UO; @@ -234,18 +234,18 @@ where } } -impl BundleProposerImpl +impl BundleProposerImpl where UO: UserOperation + From, UserOperationVariant: AsRef, S: Simulator, E: EntryPoint + SignatureAggregator + BundleHandler + L1GasProvider, P: Provider, - C: Pool, + M: Pool, { pub(crate) fn new( builder_index: u64, - pool: C, + pool: M, simulator: S, entry_point: E, provider: Arc

, diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 47c3cf56e..1f12e0e7b 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -172,9 +172,8 @@ mod tests { types::{Bytes, Log, Transaction}, }; use mockall::predicate::eq; - use rundler_pool::{IntoPoolOperationVariant, MockPoolServer, PoolOperation}; - use rundler_provider::{MockEntryPoint, MockEntryPointV0_6, MockProvider}; - use rundler_sim::{EntityInfos, PriorityFeeMode}; + 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}, @@ -319,38 +318,42 @@ mod tests { 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(), - 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, - ), - UserOperationEventProviderV0_6::new( - chain_spec.id, - ep.address(), - provider.clone(), - None, - ), + + 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), + 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 { diff --git a/crates/sim/src/simulation/v0_6/simulator.rs b/crates/sim/src/simulation/v0_6/simulator.rs index b553deb8d..2176ead0b 100644 --- a/crates/sim/src/simulation/v0_6/simulator.rs +++ b/crates/sim/src/simulation/v0_6/simulator.rs @@ -1009,7 +1009,7 @@ mod tests { 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()), @@ -1126,7 +1126,7 @@ mod tests { 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, diff --git a/crates/types/src/pool/types.rs b/crates/types/src/pool/types.rs index d2753c159..694f29f02 100644 --- a/crates/types/src/pool/types.rs +++ b/crates/types/src/pool/types.rs @@ -20,7 +20,7 @@ use crate::{ }; /// The new head of the chain, as viewed by the pool -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct NewHead { /// The hash of the new head pub block_hash: H256, @@ -28,15 +28,6 @@ pub struct NewHead { pub block_number: u64, } -impl Default for NewHead { - fn default() -> NewHead { - NewHead { - block_hash: H256::zero(), - block_number: 0, - } - } -} - /// The reputation of an entity #[derive(Debug, Clone)] pub struct Reputation { From 73f6d62b6e87805ae2b6b7e8e27c60eb23464241 Mon Sep 17 00:00:00 2001 From: dancoombs Date: Sun, 31 Mar 2024 09:36:25 -0500 Subject: [PATCH 14/14] chore: fixing PR comments --- crates/builder/src/bundle_proposer.rs | 5 +++-- crates/builder/src/bundle_sender.rs | 6 +++--- crates/pool/src/mempool/uo_pool.rs | 6 +++--- crates/provider/src/ethers/entry_point/v0_6.rs | 6 +++--- crates/provider/src/ethers/entry_point/v0_7.rs | 8 ++++---- crates/rpc/src/eth/events/common.rs | 6 +++--- crates/rpc/src/eth/router.rs | 6 +++--- crates/sim/src/estimation/v0_6.rs | 7 +++++-- crates/sim/src/precheck.rs | 9 ++++++--- crates/sim/src/simulation/unsafe_sim.rs | 6 +++--- crates/types/src/user_operation/mod.rs | 15 +++------------ 11 files changed, 39 insertions(+), 41 deletions(-) diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index 49452b457..14c1a9eca 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -14,6 +14,7 @@ use std::{ collections::{BTreeMap, HashMap, HashSet}, future::Future, + marker::PhantomData, mem, pin::Pin, sync::Arc, @@ -114,7 +115,7 @@ pub(crate) struct BundleProposerImpl { settings: Settings, fee_estimator: FeeEstimator

, event_sender: broadcast::Sender>, - _uo_type: std::marker::PhantomData, + _uo_type: PhantomData, } #[derive(Debug)] @@ -266,7 +267,7 @@ where ), settings, event_sender, - _uo_type: std::marker::PhantomData, + _uo_type: PhantomData, } } diff --git a/crates/builder/src/bundle_sender.rs b/crates/builder/src/bundle_sender.rs index 3814ae648..3fa54e665 100644 --- a/crates/builder/src/bundle_sender.rs +++ b/crates/builder/src/bundle_sender.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::{sync::Arc, time::Duration}; +use std::{marker::PhantomData, sync::Arc, time::Duration}; use anyhow::{bail, Context}; use async_trait::async_trait; @@ -58,7 +58,7 @@ pub(crate) struct BundleSenderImpl { pool: C, settings: Settings, event_sender: broadcast::Sender>, - _uo_type: std::marker::PhantomData, + _uo_type: PhantomData, } #[derive(Debug)] @@ -275,7 +275,7 @@ where pool, settings, event_sender, - _uo_type: std::marker::PhantomData, + _uo_type: PhantomData, } } diff --git a/crates/pool/src/mempool/uo_pool.rs b/crates/pool/src/mempool/uo_pool.rs index 23622ceb6..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}, @@ -55,7 +55,7 @@ pub(crate) struct UoPool>, prechecker: P, simulator: S, - _uo_type: std::marker::PhantomData, + _uo_type: PhantomData, } struct UoPoolState { @@ -91,7 +91,7 @@ where prechecker, simulator, config, - _uo_type: std::marker::PhantomData, + _uo_type: PhantomData, } } diff --git a/crates/provider/src/ethers/entry_point/v0_6.rs b/crates/provider/src/ethers/entry_point/v0_6.rs index b2fd860fb..b4dc549a4 100644 --- a/crates/provider/src/ethers/entry_point/v0_6.rs +++ b/crates/provider/src/ethers/entry_point/v0_6.rs @@ -45,8 +45,8 @@ use rundler_utils::eth::{self, ContractRevertError}; use crate::{ traits::HandleOpsOut, AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, - EntryPointProvider, ExecutionResult, L1GasProvider, Provider, SignatureAggregator, - SimulationProvider, + EntryPoint as EntryPointTrait, EntryPointProvider, ExecutionResult, L1GasProvider, Provider, + SignatureAggregator, SimulationProvider, }; const ARBITRUM_NITRO_NODE_INTERFACE_ADDRESS: Address = H160([ @@ -101,7 +101,7 @@ where } #[async_trait::async_trait] -impl

crate::traits::EntryPoint for EntryPoint

+impl

EntryPointTrait for EntryPoint

where P: Provider + Middleware + Send + Sync + 'static, { diff --git a/crates/provider/src/ethers/entry_point/v0_7.rs b/crates/provider/src/ethers/entry_point/v0_7.rs index ecb9c4e94..3e9be7000 100644 --- a/crates/provider/src/ethers/entry_point/v0_7.rs +++ b/crates/provider/src/ethers/entry_point/v0_7.rs @@ -47,9 +47,9 @@ use rundler_types::{ use rundler_utils::eth::{self, ContractRevertError}; use crate::{ - AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPointProvider, - ExecutionResult, HandleOpsOut, L1GasProvider, Provider, SignatureAggregator, - SimulationProvider, + AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint as EntryPointTrait, + EntryPointProvider, ExecutionResult, HandleOpsOut, L1GasProvider, Provider, + SignatureAggregator, SimulationProvider, }; // From v0.7 EP contract @@ -105,7 +105,7 @@ where } #[async_trait::async_trait] -impl

crate::traits::EntryPoint for EntryPoint

+impl

EntryPointTrait for EntryPoint

where P: Provider + Middleware + Send + Sync + 'static, { diff --git a/crates/rpc/src/eth/events/common.rs b/crates/rpc/src/eth/events/common.rs index f0486b3ed..29c2edc84 100644 --- a/crates/rpc/src/eth/events/common.rs +++ b/crates/rpc/src/eth/events/common.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::VecDeque, sync::Arc}; +use std::{collections::VecDeque, marker::PhantomData, sync::Arc}; use anyhow::Context; use ethers::{ @@ -34,7 +34,7 @@ pub(crate) struct UserOperationEventProviderImpl { address: Address, provider: Arc

, event_block_distance: Option, - _f_type: std::marker::PhantomData, + _f_type: PhantomData, } pub(crate) trait EntryPointFilters: Send + Sync + 'static { @@ -177,7 +177,7 @@ where address, provider, event_block_distance, - _f_type: std::marker::PhantomData, + _f_type: PhantomData, } } diff --git a/crates/rpc/src/eth/router.rs b/crates/rpc/src/eth/router.rs index 9a0746db4..b6a223ffc 100644 --- a/crates/rpc/src/eth/router.rs +++ b/crates/rpc/src/eth/router.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::{fmt::Debug, sync::Arc}; +use std::{fmt::Debug, marker::PhantomData, sync::Arc}; use ethers::types::{spoof, Address, H256}; use rundler_provider::{EntryPoint, SimulationProvider}; @@ -239,7 +239,7 @@ pub(crate) struct EntryPointRouteImpl { entry_point: E, gas_estimator: G, event_provider: EV, - _uo_type: std::marker::PhantomData, + _uo_type: PhantomData, } #[async_trait::async_trait] @@ -300,7 +300,7 @@ impl EntryPointRouteImpl { entry_point, gas_estimator, event_provider, - _uo_type: std::marker::PhantomData, + _uo_type: PhantomData, } } } diff --git a/crates/sim/src/estimation/v0_6.rs b/crates/sim/src/estimation/v0_6.rs index ab0a11308..8c8cc26c0 100644 --- a/crates/sim/src/estimation/v0_6.rs +++ b/crates/sim/src/estimation/v0_6.rs @@ -43,7 +43,10 @@ use rundler_utils::{eth, math}; use tokio::join; use super::{GasEstimationError, Settings}; -use crate::{gas, precheck::MIN_CALL_GAS_LIMIT, simulation, utils, FeeEstimator}; +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 @@ -79,7 +82,7 @@ pub struct GasEstimator { } #[async_trait::async_trait] -impl crate::estimation::GasEstimator for GasEstimator +impl GasEstimatorTrait for GasEstimator where P: Provider, E: EntryPoint + SimulationProvider + L1GasProvider, diff --git a/crates/sim/src/precheck.rs b/crates/sim/src/precheck.rs index 95391febe..cdf83791b 100644 --- a/crates/sim/src/precheck.rs +++ b/crates/sim/src/precheck.rs @@ -11,7 +11,10 @@ // 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; @@ -75,7 +78,7 @@ pub struct PrecheckerImpl { settings: Settings, fee_estimator: gas::FeeEstimator

, cache: RwLock, - _uo_type: std::marker::PhantomData, + _uo_type: PhantomData, } /// Precheck settings @@ -192,7 +195,7 @@ where settings, fee_estimator, cache: RwLock::new(AsyncDataCache { fees: None }), - _uo_type: std::marker::PhantomData, + _uo_type: PhantomData, } } diff --git a/crates/sim/src/simulation/unsafe_sim.rs b/crates/sim/src/simulation/unsafe_sim.rs index 03f7169f0..cb2c7b675 100644 --- a/crates/sim/src/simulation/unsafe_sim.rs +++ b/crates/sim/src/simulation/unsafe_sim.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::sync::Arc; +use std::{marker::PhantomData, sync::Arc}; use ethers::types::H256; use rundler_provider::{ @@ -36,7 +36,7 @@ pub struct UnsafeSimulator { provider: Arc

, entry_point: E, sim_settings: Settings, - _uo_type: std::marker::PhantomData, + _uo_type: PhantomData, } impl UnsafeSimulator { @@ -46,7 +46,7 @@ impl UnsafeSimulator { provider, entry_point, sim_settings, - _uo_type: std::marker::PhantomData, + _uo_type: PhantomData, } } } diff --git a/crates/types/src/user_operation/mod.rs b/crates/types/src/user_operation/mod.rs index e925a538e..246950e7b 100644 --- a/crates/types/src/user_operation/mod.rs +++ b/crates/types/src/user_operation/mod.rs @@ -130,15 +130,6 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { fn clear_signature(&mut self); } -/// User operation type enum -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum UserOperationType { - /// User operation type for EntryPoint v0.6 - V0_6, - /// User operation type for EntryPoint v0.7 - V0_7, -} - /// User operation enum #[derive(Debug, Clone, Eq, PartialEq)] pub enum UserOperationVariant { @@ -309,10 +300,10 @@ impl UserOperationVariant { } /// Returns the user operation type - pub fn uo_type(&self) -> UserOperationType { + pub fn uo_type(&self) -> EntryPointVersion { match self { - UserOperationVariant::V0_6(_) => UserOperationType::V0_6, - UserOperationVariant::V0_7(_) => UserOperationType::V0_7, + UserOperationVariant::V0_6(_) => EntryPointVersion::V0_6, + UserOperationVariant::V0_7(_) => EntryPointVersion::V0_7, } } }