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 10a6f5215..de306f12f 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 6135a7da1..8f5056800 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 0a57e9e4f..9cff4cf79 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)?) } @@ -780,12 +760,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 f58b2d110..6d46e90c9 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..dadcc065e 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 validation 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 validation_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/types.rs b/crates/sim/src/estimation/types.rs deleted file mode 100644 index 664d55bfa..000000000 --- a/crates/sim/src/estimation/types.rs +++ /dev/null @@ -1,230 +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::types::{Address, Bytes, U256}; -use rand::RngCore; -use rundler_types::UserOperation; -use serde::{Deserialize, Serialize}; - -use crate::precheck::MIN_CALL_GAS_LIMIT; - -/// 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 validation 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 validation_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 - } -} - -/// 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) - /// - /// This is typically a "dummy" value with the following constraints: - /// - The first 20 bytes are the paymaster address - /// - The rest of the paymaster and data must be at least as long as the - /// longest possible data field for this user operation - /// - The data must also cause the paymaster validation call to use - /// the maximum amount of gas possible - /// - /// This is required in order to get an accurate gas estimation for the validation phase. - pub paymaster_and_data: Bytes, - /// Signature (required, dummy value) - /// - /// This is typically a "dummy" value with the following constraints: - /// - The signature must be at least as long as the longs possible signature for this user operation - /// - The data must also cause the account validation call to use - /// the maximum amount of gas possible, - /// - /// This is required in order to get an accurate gas estimation for the validation phase. - 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. - /// - /// When estimating pre-verification gas, callers take the results and plug them - /// into their user operation. Doing so changes the - /// pre-verification gas. To make sure the returned gas is enough to - /// cover the modified user op, calculate the gas needed for the worst - /// case scenario where the gas fields of the user operation are entirely - /// nonzero bytes. Likewise for the signature field. - pub fn max_fill(&self, settings: &Settings) -> 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(settings) - } - } - - /// 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, settings: &Settings) -> 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(settings) - } - } - - /// 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, settings: &Settings) -> 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_else(|| settings.max_verification_gas.into()) - .min(settings.max_verification_gas.into()), - call_gas_limit: self - .call_gas_limit - .unwrap_or_else(|| settings.max_call_gas.into()) - .min(settings.max_call_gas.into()), - // 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(), - } - } - - /// Convert into a full user operation with the provided gas estimates. - /// - /// Fee fields are left unchanged or are defaulted. - pub fn into_user_operation_with_estimates(self, estimates: GasEstimate) -> 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, - verification_gas_limit: estimates.verification_gas_limit, - call_gas_limit: estimates.call_gas_limit, - pre_verification_gas: estimates.pre_verification_gas, - 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(), - } - } - - /// Convert from a full user operation, keeping the gas fields set - pub fn from_user_operation_keeping_gas(op: UserOperation) -> Self { - Self::from_user_operation(op, true) - } - - /// Convert from a full user operation, ignoring the gas fields - pub fn from_user_operation_without_gas(op: UserOperation) -> Self { - Self::from_user_operation(op, false) - } - - fn from_user_operation(op: UserOperation, keep_gas: bool) -> Self { - let if_keep_gas = |x: U256| Some(x).filter(|_| keep_gas); - Self { - sender: op.sender, - nonce: op.nonce, - init_code: op.init_code, - call_data: op.call_data, - call_gas_limit: if_keep_gas(op.call_gas_limit), - verification_gas_limit: if_keep_gas(op.verification_gas_limit), - pre_verification_gas: if_keep_gas(op.pre_verification_gas), - max_fee_per_gas: if_keep_gas(op.max_fee_per_gas), - max_priority_fee_per_gas: if_keep_gas(op.max_priority_fee_per_gas), - paymaster_and_data: op.paymaster_and_data, - signature: op.signature, - } - } - - fn random_bytes(len: usize) -> Bytes { - let mut bytes = vec![0_u8; len]; - rand::thread_rng().fill_bytes(&mut bytes); - bytes.into() - } -} - -/// Gas estimate for a user operation -#[derive(Debug, Copy, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GasEstimate { - /// Pre verification gas estimate - pub pre_verification_gas: U256, - /// Verification gas limit estimate - pub verification_gas_limit: U256, - /// Call gas limit estimate - pub call_gas_limit: U256, -} 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 939b0250f..fdfa4f25d 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, }; 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, @@ -425,9 +413,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?) @@ -449,14 +443,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; @@ -467,8 +466,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) @@ -484,9 +483,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, @@ -494,7 +493,7 @@ mod tests { validation_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, @@ -561,7 +560,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; @@ -583,9 +585,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))); @@ -604,7 +606,7 @@ mod tests { ..Default::default() }; let provider = Arc::new(provider); - let estimator: GasEstimatorImpl = GasEstimatorImpl::new( + let estimator: GasEstimator = GasEstimator::new( cs, provider.clone(), entry, @@ -618,7 +620,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; @@ -641,10 +646,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))); @@ -663,7 +668,7 @@ mod tests { ..Default::default() }; let provider = Arc::new(provider); - let estimator: GasEstimatorImpl = GasEstimatorImpl::new( + let estimator: GasEstimator = GasEstimator::new( cs, provider.clone(), entry, @@ -677,7 +682,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; @@ -720,7 +728,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())); } @@ -1142,7 +1150,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())); } @@ -1210,7 +1218,7 @@ mod tests { estimation.verification_gas_limit, cmp::max( math::increase_by_percent(gas_usage, 10), - gas_usage + simulation::REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER + gas_usage + REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER ) ); @@ -1286,7 +1294,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 97c9626c1..628ccea76 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..e365d4f5f 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("Unstaked {0.entity} accessed {0.accessed_address} ({0.accessed_entity:?}) at slot {0.slot}")] + 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) + } +} + +#[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 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 + } + + 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 { + Allowed, + NeedsStake(Address, Option, U256), + Banned(U256), +} + +/// Information about a storage violation based on stake status +#[derive(Debug, PartialEq, Clone, PartialOrd, Eq, Ord)] +pub struct NeedsStakeInformation { + /// Entity of stake information + pub entity: Entity, + /// Address that was accessed while unstaked + pub accessed_address: Address, + /// Type of accessed entity if it is a known entity + pub accessed_entity: Option, + /// 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, + has_factory: bool, + entity: &'a Entity, + entity_infos: &'a EntityInfos, +} + +fn parse_storage_accesses(args: ParseStorageAccess<'_>) -> Result { + let ParseStorageAccess { + access_info, + address, + sender, + entrypoint, + entity_infos, + entity, + slots_by_address, + has_factory, + .. + } = args; + + if address.eq(&sender) || address.eq(&entrypoint) { + return Ok(StorageRestriction::Allowed); + } + + let mut required_stake_slot = None; + + 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); + + if is_sender_associated { + if has_factory + // special case: account.validateUserOp is allowed to use assoc storage if factory is staked. + // [STO-022], [STO-021] + && !(entity.address.eq(&sender) + && entity_infos + .factory + .expect("Factory needs to be present and staked") + .is_staked) + { + required_stake_slot = Some(slot); + } + } else if is_entity_associated || is_same_address || is_read_permission { + required_stake_slot = Some(slot); + } else { + return Ok(StorageRestriction::Banned(*slot)); + } + } + + if let Some(required_stake_slot) = required_stake_slot { + if let Some(entity_type) = entity_infos.type_from_address(address) { + return Ok(StorageRestriction::NeedsStake( + address, + Some(entity_type), + *required_stake_slot, + )); + } + + return Ok(StorageRestriction::NeedsStake( + address, + None, + *required_stake_slot, + )); + } + + Ok(StorageRestriction::Allowed) +} + +/// 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, + } + } +} + +#[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() + } +} 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 64% rename from crates/sim/src/simulation/simulation.rs rename to crates/sim/src/simulation/v0_6/simulator.rs index 9e0d5aef2..315edcc80 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, 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, + 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; @@ -357,7 +280,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, @@ -386,13 +309,13 @@ where let address = *addr; accessed_addresses.insert(address); - let violation = parse_storage_accesses(ParseStorageAccess { + let violation = 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, entity_infos, })?; @@ -435,7 +358,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) } } @@ -467,7 +390,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(), )); } } @@ -481,11 +404,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, )); } @@ -553,11 +476,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, @@ -618,7 +547,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, @@ -651,93 +580,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("Unstaked {0.entity} accessed {0.accessed_address} ({0.accessed_entity:?}) at slot {0.slot}")] - 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), @@ -756,309 +598,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 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 - } - - 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 { - Allowed, - NeedsStake(Address, Option, U256), - Banned(U256), -} - -/// Information about a storage violation based on stake status -#[derive(Debug, PartialEq, Clone, PartialOrd, Eq, Ord)] -pub struct NeedsStakeInformation { - /// Entity of stake information - pub entity: Entity, - /// Address that was accessed while unstaked - pub accessed_address: Address, - /// Type of accessed entity if it is a known entity - pub accessed_entity: Option, - /// 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, - entity_infos: &'a EntityInfos, -} - -fn parse_storage_accesses(args: ParseStorageAccess<'_>) -> Result { - let ParseStorageAccess { - access_info, - address, - sender, - entrypoint, - entity_infos, - entity, - slots_by_address, - initcode_length, - .. - } = args; - - if address.eq(&sender) || address.eq(&entrypoint) { - return Ok(StorageRestriction::Allowed); - } - - let mut required_stake_slot = None; - - 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); - - if is_sender_associated { - if initcode_length > 2 - // special case: account.validateUserOp is allowed to use assoc storage if factory is staked. - // [STO-022], [STO-021] - && !(entity.address.eq(&sender) - && entity_infos - .factory - .expect("Factory needs to be present and staked") - .is_staked) - { - required_stake_slot = Some(slot); - } - } else if is_entity_associated || is_same_address || is_read_permission { - required_stake_slot = Some(slot); - } else { - return Ok(StorageRestriction::Banned(*slot)); - } - } - - if let Some(required_stake_slot) = required_stake_slot { - if let Some(entity_type) = entity_infos.type_from_address(address) { - return Ok(StorageRestriction::NeedsStake( - address, - Some(entity_type), - *required_stake_slot, - )); - } - - return Ok(StorageRestriction::NeedsStake( - address, - None, - *required_stake_slot, - )); - } - - Ok(StorageRestriction::Allowed) -} - -/// 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 { @@ -1131,8 +702,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(); @@ -1140,21 +712,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() @@ -1185,7 +760,7 @@ mod tests { }) }); - provider + entry_point .expect_validate_user_op_signature() .returning(|_, _, _| Ok(AggregatorOut::NotNeeded)); @@ -1203,7 +778,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; @@ -1212,7 +787,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() @@ -1242,7 +817,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; @@ -1262,7 +837,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(); @@ -1300,7 +878,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( @@ -1342,7 +920,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!( 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()); + } +}