diff --git a/Cargo.lock b/Cargo.lock index 324504f08..a4ab8fb05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4331,6 +4331,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 78431a142..92ec8b7be 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -29,7 +29,7 @@ use futures_util::TryFutureExt; use linked_hash_map::LinkedHashMap; #[cfg(test)] use mockall::automock; -use rundler_pool::{PoolOperation, PoolServer}; +use rundler_pool::{FromPoolOperationVariant, PoolOperation, PoolServer}; use rundler_provider::{EntryPoint, HandleOpsOut, Provider}; use rundler_sim::{ gas, EntityInfo, EntityInfos, ExpectedStorage, FeeEstimator, PriorityFeeMode, SimulationError, @@ -37,7 +37,7 @@ use rundler_sim::{ }; use rundler_types::{ chain::ChainSpec, Entity, EntityType, EntityUpdate, EntityUpdateType, GasFees, GasOverheads, - Timestamp, UserOperation, UserOpsPerAggregator, + Timestamp, UserOperation, UserOperationVariant, UserOpsPerAggregator, }; use rundler_utils::{emit::WithEntryPoint, math}; use tokio::{sync::broadcast, try_join}; @@ -50,17 +50,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() @@ -72,26 +85,29 @@ 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::UserOperationV0_6;))] #[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 +pub(crate) struct BundleProposerImpl where - S: Simulator, - E: EntryPoint, + UO: UserOperation, + S: Simulator, + E: EntryPoint, P: Provider, C: PoolServer, { @@ -103,6 +119,7 @@ where settings: Settings, fee_estimator: FeeEstimator

, event_sender: broadcast::Sender>, + _uo_type: std::marker::PhantomData, } #[derive(Debug)] @@ -116,18 +133,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, 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 @@ -218,10 +238,11 @@ where } } -impl BundleProposerImpl +impl BundleProposerImpl where - S: Simulator, - E: EntryPoint, + UO: UserOperation + From, + S: Simulator, + E: EntryPoint, P: Provider, C: PoolServer, { @@ -248,6 +269,7 @@ where ), settings, event_sender, + _uo_type: std::marker::PhantomData, } } @@ -259,11 +281,11 @@ 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 @@ -324,11 +346,7 @@ where // Simulate let result = self .simulator - .simulate_validation( - op.uo.clone().into(), - Some(block_hash), - Some(op.expected_code_hash), - ) + .simulate_validation(op.uo.clone(), Some(block_hash), Some(op.expected_code_hash)) .await; let result = match result { Ok(success) => (op, Ok(success)), @@ -358,9 +376,9 @@ 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()) @@ -472,19 +490,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() @@ -497,7 +520,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| { @@ -517,7 +540,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 @@ -563,20 +586,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( @@ -600,7 +627,7 @@ where async fn aggregate_signatures( &self, aggregator: Address, - group: &AggregatorGroup, + group: &AggregatorGroup, ) -> (Address, anyhow::Result>) { let ops = group .ops_with_simulations @@ -616,7 +643,7 @@ where async fn process_failed_op( &self, - context: &mut ProposalContext, + context: &mut ProposalContext, index: usize, message: String, ) -> anyhow::Result<()> { @@ -676,7 +703,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(); @@ -729,7 +756,7 @@ where async fn check_for_post_op_revert_single_op( &self, - op: UserOperation, + op: UO, gas: U256, op_index: usize, ) -> Vec { @@ -765,7 +792,7 @@ where async fn check_for_post_op_revert_agg_ops( &self, - group: UserOpsPerAggregator, + group: UserOpsPerAggregator, gas: U256, start_index: usize, ) -> Vec { @@ -798,8 +825,8 @@ where fn limit_user_operations_for_simulation( &self, - ops: Vec, - ) -> (Vec, u64) { + ops: Vec>, + ) -> (Vec>, u64) { // Make the bundle gas limit 10% higher here so that we simulate more UOs than we need in case that we end up dropping some UOs later so we can still pack a full bundle let mut gas_left = math::increase_by_percent(U256::from(self.settings.max_bundle_gas), 10); let mut ops_in_bundle = Vec::new(); @@ -838,19 +865,19 @@ where }); } - fn op_hash(&self, op: &UserOperation) -> H256 { + 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 self.simulation.aggregator.is_some() { // if using an aggregator, clear out the user op signature @@ -865,24 +892,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(), } } @@ -906,7 +942,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() { @@ -990,7 +1026,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 { @@ -1016,7 +1052,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 { @@ -1049,13 +1085,13 @@ impl ProposalContext { max_gas } - 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) } @@ -1161,7 +1197,7 @@ impl ProposalContext { fn add_entity_update(&mut self, entity: Entity, entity_infos: EntityInfos) { let entity_update = EntityUpdate { entity, - update_type: ProposalContext::get_entity_update_type(entity.kind, entity_infos), + update_type: ProposalContext::::get_entity_update_type(entity.kind, entity_infos), }; self.entity_updates.insert(entity.address, entity_update); } @@ -1218,7 +1254,11 @@ impl ProposalContext { } } -fn get_gas_required_for_op(chain_spec: &ChainSpec, gas_spent: U256, op: &UserOperation) -> U256 { +fn get_gas_required_for_op( + chain_spec: &ChainSpec, + gas_spent: U256, + op: &UO, +) -> U256 { gas_spent + gas::user_operation_pre_verification_gas_limit(chain_spec, op, false) + op.total_verification_gas_limit() diff --git a/crates/builder/src/bundle_sender.rs b/crates/builder/src/bundle_sender.rs index 83ad563e4..6fb5e0c82 100644 --- a/crates/builder/src/bundle_sender.rs +++ b/crates/builder/src/bundle_sender.rs @@ -47,10 +47,11 @@ pub(crate) struct Settings { } #[derive(Debug)] -pub(crate) struct BundleSenderImpl +pub(crate) struct BundleSenderImpl where - P: BundleProposer, - E: EntryPoint, + UO: UserOperation, + P: BundleProposer, + E: EntryPoint, T: TransactionTracker, C: PoolServer, { @@ -64,6 +65,7 @@ where pool: C, settings: Settings, event_sender: broadcast::Sender>, + _uo_type: std::marker::PhantomData, } #[derive(Debug)] @@ -99,10 +101,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, T: TransactionTracker, C: PoolServer, { @@ -247,10 +250,11 @@ where } } -impl BundleSenderImpl +impl BundleSenderImpl where - P: BundleProposer, - E: EntryPoint, + UO: UserOperation, + P: BundleProposer, + E: EntryPoint, T: TransactionTracker, C: PoolServer, { @@ -278,6 +282,7 @@ where pool, settings, event_sender, + _uo_type: std::marker::PhantomData, } } @@ -538,7 +543,7 @@ 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(), @@ -564,7 +569,7 @@ where }); } - fn op_hash(&self, op: &UserOperation) -> H256 { + fn op_hash(&self, op: &UO) -> H256 { op.hash(self.entry_point.address(), self.chain_spec.id) } } diff --git a/crates/dev/src/lib.rs b/crates/dev/src/lib.rs index 4132c49fd..7c8dc316e 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, }, - UserOperationV0_6 as UserOperation, + UserOperation, UserOperationV0_6, }; /// 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() -> UserOperationV0_6 { + UserOperationV0_6 { 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() + ..UserOperationV0_6::default() } } @@ -315,7 +315,7 @@ 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 = UserOperationV0_6 { sender: wallet_address, init_code, ..base_user_op() @@ -400,7 +400,7 @@ impl DevClients { /// Adds a signature to a user operation. pub async fn add_signature( &self, - op: &mut UserOperation, + op: &mut UserOperationV0_6, use_paymaster: bool, ) -> anyhow::Result<()> { if use_paymaster { @@ -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 = UserOperationV0_6 { sender: self.wallet.address(), call_data, nonce, diff --git a/crates/pool/src/emit.rs b/crates/pool/src/emit.rs index e902ab551..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 diff --git a/crates/pool/src/lib.rs b/crates/pool/src/lib.rs index aab69baf5..0ce1c89b2 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, MempoolError, PoolConfig, PoolOperation, Reputation, + ReputationStatus, StakeStatus, }; mod server; diff --git a/crates/pool/src/mempool/mod.rs b/crates/pool/src/mempool/mod.rs index 90f72be1e..029028e75 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, UserOperationV0_6, + 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::UserOperationV0_6;))] #[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 @@ -193,9 +193,9 @@ pub enum OperationOrigin { /// A user operation with additional metadata from validation. #[derive(Debug, Clone, Eq, PartialEq)] -pub struct PoolOperation { +pub struct PoolOperation { /// The user operation stored in the pool - pub uo: 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,6 +291,49 @@ impl PoolOperation { } } +// TODO had to hardcode these because trying to use the generic conflicts with the core `impl From for T` impl + +pub trait FromPoolOperationVariant { + fn from_variant(op: PoolOperation) -> Self; +} + +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 From> for PoolOperation { + fn from(op: PoolOperation) -> PoolOperation { + 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, + } + } +} + #[cfg(test)] mod tests { use rundler_sim::EntityInfo; diff --git a/crates/pool/src/mempool/paymaster.rs b/crates/pool/src/mempool/paymaster.rs index 5b77c718f..f54c5d329 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,9 +60,10 @@ impl PaymasterConfig { } } -impl PaymasterTracker +impl PaymasterTracker where - E: EntryPoint, + UO: UserOperation, + E: EntryPoint, { pub(crate) fn new(entry_point: E, config: PaymasterConfig) -> Self { 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(); diff --git a/crates/pool/src/mempool/pool.rs b/crates/pool/src/mempool/pool.rs index 63713962c..bd8229881 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,7 +102,7 @@ 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 @@ -134,13 +134,13 @@ impl PoolInner { } } - pub(crate) fn add_operation(&mut self, op: PoolOperation) -> MempoolResult { + pub(crate) fn add_operation(&mut self, op: PoolOperation) -> MempoolResult { let ret = self.add_operation_internal(Arc::new(op), None); self.update_metrics(); ret } - pub(crate) fn best_operations(&self) -> impl Iterator> { + pub(crate) fn best_operations(&self) -> impl Iterator>> { self.best.clone().into_iter().map(|v| v.po) } @@ -171,22 +171,28 @@ 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<()> { + 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( @@ -215,7 +221,7 @@ 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) { @@ -237,7 +243,7 @@ impl PoolInner { &mut self, mined_op: &MinedOp, block_number: u64, - ) -> Option> { + ) -> Option>> { let tx_in_pool = self.by_id.get(&mined_op.id())?; let hash = tx_in_pool @@ -250,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 @@ -358,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 @@ -411,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); @@ -448,7 +457,7 @@ 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(), self.config.min_replacement_fee_increase_percentage, @@ -477,24 +486,24 @@ 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 @@ -505,13 +514,13 @@ impl Ord for OrderedPoolOperation { } } -impl PartialOrd for OrderedPoolOperation { +impl PartialOrd for OrderedPoolOperation { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl PartialEq for OrderedPoolOperation { +impl PartialEq for OrderedPoolOperation { fn eq(&self, other: &Self) -> bool { self.cmp(other) == Ordering::Equal } diff --git a/crates/pool/src/mempool/uo_pool.rs b/crates/pool/src/mempool/uo_pool.rs index 892b5bbe1..2d2e57e48 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,34 +47,35 @@ 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, - E: EntryPoint, + UO: UserOperation, + P: Prechecker, + S: Simulator, + E: EntryPoint, { pub(crate) fn new( config: PoolConfig, 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, - E: EntryPoint, + 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 @@ -380,7 +382,7 @@ where // Only let ops with successful simulations through let sim_result = self .simulator - .simulate_validation(op.clone().into(), None, None) + .simulate_validation(op.clone(), None, None) .await?; // No aggregators supported for now @@ -469,7 +471,7 @@ where 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, @@ -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"))?; } @@ -588,11 +590,11 @@ where .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) } diff --git a/crates/pool/src/server/local.rs b/crates/pool/src/server/local.rs index 873f99341..3ea984351 100644 --- a/crates/pool/src/server/local.rs +++ b/crates/pool/src/server/local.rs @@ -19,7 +19,9 @@ 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::{ + EntityUpdate, UserOperation, UserOperationId, UserOperationV0_6, UserOperationVariant, +}; use tokio::{ sync::{broadcast, mpsc, oneshot}, task::JoinHandle, @@ -65,12 +67,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 +127,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 +145,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 +158,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 +244,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 +365,7 @@ impl HealthCheck for LocalPoolHandle { impl LocalPoolServerRunner where - M: Mempool, + M: Mempool, { fn new( req_receiver: mpsc::Receiver, @@ -381,19 +392,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()) .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())); } } Ok(None) @@ -449,12 +463,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()) .collect()) } @@ -549,7 +566,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 +697,7 @@ enum ServerRequestKind { GetSupportedEntryPoints, AddOp { entry_point: Address, - op: UserOperation, + op: UserOperationVariant, origin: OperationOrigin, }, GetOps { @@ -746,10 +763,10 @@ enum ServerResponse { hash: H256, }, GetOps { - ops: Vec, + ops: Vec>, }, GetOpByHash { - op: Option, + op: Option>, }, RemoveOps, RemoveOpById { @@ -759,7 +776,7 @@ enum ServerResponse { DebugClearState, AdminSetTracking, DebugDumpMempool { - ops: Vec, + ops: Vec>, }, DebugSetReputations, DebugDumpReputation { diff --git a/crates/pool/src/server/mod.rs b/crates/pool/src/server/mod.rs index 311317b49..15e8667a0 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, UserOperation, 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/protos.rs b/crates/pool/src/server/remote/protos.rs index 3419a2b63..468700697 100644 --- a/crates/pool/src/server/remote/protos.rs +++ b/crates/pool/src/server/remote/protos.rs @@ -16,8 +16,8 @@ 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, - UserOperationV0_6 as RundlerUserOperationV0_6, ValidTimeRange, + EntityUpdateType as RundlerEntityUpdateType, UserOperationV0_6, UserOperationVariant, + ValidTimeRange, }; use crate::{ @@ -34,19 +34,19 @@ tonic::include_proto!("op_pool"); pub const OP_POOL_FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("op_pool_descriptor"); -impl From<&RundlerUserOperation> for UserOperation { - fn from(op: &RundlerUserOperation) -> Self { +impl From<&UserOperationVariant> for UserOperation { + fn from(op: &UserOperationVariant) -> Self { match op { - RundlerUserOperation::V0_6(op) => op.into(), - RundlerUserOperation::V0_7(_) => { + UserOperationVariant::V0_6(op) => op.into(), + UserOperationVariant::V0_7(_) => { unimplemented!("V0_7 user operation is not supported") } } } } -impl From<&RundlerUserOperationV0_6> for UserOperation { - fn from(op: &RundlerUserOperationV0_6) -> Self { +impl From<&UserOperationV0_6> for UserOperation { + fn from(op: &UserOperationV0_6) -> Self { let op = UserOperationV06 { sender: op.sender.0.to_vec(), nonce: to_le_bytes(op.nonce), @@ -66,11 +66,11 @@ impl From<&RundlerUserOperationV0_6> for UserOperation { } } -impl TryFrom for RundlerUserOperationV0_6 { +impl TryFrom for UserOperationV0_6 { type Error = ConversionError; fn try_from(op: UserOperationV06) -> Result { - Ok(RundlerUserOperationV0_6 { + Ok(UserOperationV0_6 { sender: from_bytes(&op.sender)?, nonce: from_bytes(&op.nonce)?, init_code: op.init_code.into(), @@ -86,7 +86,7 @@ impl TryFrom for RundlerUserOperationV0_6 { } } -impl TryFrom for RundlerUserOperation { +impl TryFrom for UserOperationVariant { type Error = ConversionError; fn try_from(op: UserOperation) -> Result { @@ -95,7 +95,7 @@ impl TryFrom for RundlerUserOperation { .expect("User operation should contain user operation oneof"); match op { - user_operation::Uo::V06(op) => Ok(RundlerUserOperation::V0_6(op.try_into()?)), + user_operation::Uo::V06(op) => Ok(UserOperationVariant::V0_6(op.try_into()?)), } } } @@ -274,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(), @@ -295,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 d46687ee9..03f679c68 100644 --- a/crates/pool/src/task.rs +++ b/crates/pool/src/task.rs @@ -21,7 +21,7 @@ use rundler_sim::{ Prechecker, PrecheckerImpl, SimulateValidationTracerImpl, Simulator, SimulatorV0_6, }; use rundler_task::Task; -use rundler_types::chain::ChainSpec; +use rundler_types::{chain::ChainSpec, UserOperationV0_6}; use rundler_utils::{emit::WithEntryPoint, handle}; use tokio::{sync::broadcast, try_join}; use tokio_util::sync::CancellationToken; @@ -89,7 +89,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,12 +157,19 @@ 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> { + ) -> anyhow::Result< + UoPool< + UserOperationV0_6, + impl Prechecker, + impl Simulator, + impl EntryPoint, + >, + > { let ep = EthersEntryPointV0_6::new(pool_config.entry_point, Arc::clone(&provider)); let prechecker = PrecheckerImpl::new( diff --git a/crates/provider/src/ethers/entry_point_v0_6.rs b/crates/provider/src/ethers/entry_point_v0_6.rs index 99b89c46b..b2797bcae 100644 --- a/crates/provider/src/ethers/entry_point_v0_6.rs +++ b/crates/provider/src/ethers/entry_point_v0_6.rs @@ -35,7 +35,7 @@ use rundler_types::{ shared_types::UserOpsPerAggregator as UserOpsPerAggregatorV0_6, }, }, - DepositInfoV0_6, GasFees, UserOperation, UserOpsPerAggregator, ValidationOutput, + DepositInfoV0_6, GasFees, UserOperationV0_6, UserOpsPerAggregator, ValidationOutput, }; use rundler_utils::eth::{self, ContractRevertError}; @@ -54,13 +54,6 @@ const OPTIMISM_BEDROCK_GAS_ORACLE_ADDRESS: Address = H160([ const REVERT_REASON_MAX_LEN: usize = 2048; -// TODO(danc): -// - Modify this interface to take only `UserOperationV0_6` and remove the `into_v0_6` calls -// - Places that are already in a V0_6 context can use this directly -// - Implement a wrapper that takes `UserOperation` and dispatches to the correct entry point version -// based on a constructor from ChainSpec. -// - Use either version depending on the abstraction of the caller. - /// Implementation of the `EntryPoint` trait for the v0.6 version of the entry point contract using ethers #[derive(Debug)] pub struct EntryPointV0_6 { @@ -107,19 +100,17 @@ impl

EntryPoint for EntryPointV0_6

where P: Provider + Middleware + Send + Sync + 'static, { + type UO = UserOperationV0_6; + fn address(&self) -> Address { self.i_entry_point.address() } async fn get_simulate_validation_call( &self, - user_op: UserOperation, + user_op: UserOperationV0_6, max_validation_gas: u64, ) -> anyhow::Result { - let user_op = user_op - .into_v0_6() - .expect("V0_6 EP called with non-V0_6 op"); - let pvg = user_op.pre_verification_gas; let tx = self .i_entry_point @@ -131,13 +122,9 @@ where async fn call_simulate_validation( &self, - user_op: UserOperation, + user_op: UserOperationV0_6, max_validation_gas: u64, ) -> anyhow::Result { - let user_op = user_op - .into_v0_6() - .expect("V0_6 EP called with non-V0_6 op"); - let pvg = user_op.pre_verification_gas; match self .i_entry_point @@ -155,7 +142,7 @@ where async fn call_handle_ops( &self, - ops_per_aggregator: Vec, + ops_per_aggregator: Vec>, beneficiary: Address, gas: U256, ) -> anyhow::Result { @@ -203,17 +190,13 @@ where async fn call_spoofed_simulate_op( &self, - user_op: UserOperation, + user_op: UserOperationV0_6, target: Address, target_call_data: Bytes, block_hash: H256, gas: U256, spoofed_state: &spoof::State, ) -> anyhow::Result> { - let user_op = user_op - .into_v0_6() - .expect("V0_6 EP called with non-V0_6 op"); - let contract_error = self .i_entry_point .simulate_handle_op(user_op, target, target_call_data) @@ -231,7 +214,7 @@ where fn get_send_bundle_transaction( &self, - ops_per_aggregator: Vec, + ops_per_aggregator: Vec>, beneficiary: Address, gas: U256, gas_fees: GasFees, @@ -285,13 +268,8 @@ where async fn aggregate_signatures( self: Arc, aggregator_address: Address, - ops: Vec, + ops: Vec, ) -> anyhow::Result> { - let ops = ops - .into_iter() - .map(|op| op.into_v0_6().expect("V0_6 EP called with non-V0_6 op")) - .collect(); - let aggregator = IAggregator::new(aggregator_address, Arc::clone(&self.provider)); // TODO: Cap the gas here. let result = aggregator.aggregate_signatures(ops).call().await; @@ -305,13 +283,10 @@ where async fn validate_user_op_signature( self: Arc, aggregator_address: Address, - user_op: UserOperation, + user_op: UserOperationV0_6, gas_cap: u64, ) -> anyhow::Result { let aggregator = IAggregator::new(aggregator_address, Arc::clone(&self.provider)); - let user_op = user_op - .into_v0_6() - .expect("V0_6 EP called with non-V0_6 op"); let result = aggregator .validate_user_op_signature(user_op) @@ -332,12 +307,8 @@ where async fn calc_arbitrum_l1_gas( self: Arc, entry_point_address: Address, - user_op: UserOperation, + user_op: UserOperationV0_6, ) -> anyhow::Result { - let user_op = user_op - .into_v0_6() - .expect("V0_6 EP called with non-V0_6 op"); - let data = self .i_entry_point .handle_ops(vec![user_op], Address::random()) @@ -355,13 +326,9 @@ where async fn calc_optimism_l1_gas( self: Arc, entry_point_address: Address, - user_op: UserOperation, + user_op: UserOperationV0_6, gas_price: U256, ) -> anyhow::Result { - let user_op = user_op - .into_v0_6() - .expect("V0_6 EP called with non-V0_6 op"); - let data = self .i_entry_point .handle_ops(vec![user_op], Address::random()) @@ -386,14 +353,21 @@ where } } +// TODO: pack for 0.7 fn get_handle_ops_call( entry_point: &IEntryPoint, - 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(|x| x.into()).collect(); + 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/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index 5c374aba1..20b9c4915 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -60,16 +60,19 @@ 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)] +#[cfg_attr(feature = "test-utils", automock(type UO = rundler_types::UserOperationV0_6;))] #[async_trait::async_trait] pub trait EntryPoint: Send + Sync + 'static { + /// The type of user operation used by this entry point + type UO: UserOperation; + /// Get the address of the entry point contract fn address(&self) -> Address; /// 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; @@ -81,14 +84,14 @@ pub trait EntryPoint: Send + Sync + 'static { /// 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; @@ -96,7 +99,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, @@ -107,7 +110,7 @@ pub trait EntryPoint: Send + Sync + 'static { /// Construct the transaction to send a bundle of operations to the entry point contract fn get_send_bundle_transaction( &self, - ops_per_aggregator: Vec, + ops_per_aggregator: Vec>, beneficiary: Address, gas: U256, gas_fees: GasFees, @@ -129,14 +132,14 @@ pub trait EntryPoint: Send + Sync + 'static { async fn aggregate_signatures( self: Arc, aggregator_address: Address, - ops: Vec, + ops: Vec, ) -> anyhow::Result>; /// Validate a user operation signature using an aggregator async fn validate_user_op_signature( self: Arc, aggregator_address: Address, - user_op: UserOperation, + user_op: Self::UO, gas_cap: u64, ) -> anyhow::Result; @@ -144,14 +147,14 @@ pub trait EntryPoint: Send + Sync + 'static { async fn calc_arbitrum_l1_gas( self: Arc, entry_point_address: Address, - op: UserOperation, + 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: Arc, entry_point_address: Address, - op: UserOperation, + op: Self::UO, gas_price: U256, ) -> anyhow::Result; } diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 05ab9fc1b..97a67cb15 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -31,14 +31,14 @@ use rundler_pool::PoolServer; use rundler_provider::{EntryPoint, Provider}; use rundler_sim::{ EstimationSettings, FeeEstimator, GasEstimate, GasEstimationError, GasEstimator, - GasEstimatorV0_6, PrecheckSettings, UserOperationOptionalGasV0_6, + GasEstimatorV0_6, PrecheckSettings, }; use rundler_types::{ chain::ChainSpec, contracts::v0_6::i_entry_point::{ IEntryPointCalls, UserOperationEventFilter, UserOperationRevertReasonFilter, }, - UserOperation, UserOperationV0_6, + UserOperation, UserOperationOptionalGasV0_6, UserOperationV0_6, }; use rundler_utils::{eth::log_to_raw_log, log::LogOnError}; use tracing::Level; @@ -70,7 +70,7 @@ struct EntryPointContext { impl EntryPointContext where P: Provider, - E: EntryPoint, + E: EntryPoint, { fn new( chain_spec: ChainSpec, @@ -102,7 +102,7 @@ pub(crate) struct EthApi { impl EthApi where P: Provider, - E: EntryPoint, + E: EntryPoint, PS: PoolServer, { pub(crate) fn new( diff --git a/crates/rpc/src/eth/mod.rs b/crates/rpc/src/eth/mod.rs index 59d220243..fced6a6ea 100644 --- a/crates/rpc/src/eth/mod.rs +++ b/crates/rpc/src/eth/mod.rs @@ -21,7 +21,8 @@ mod server; use ethers::types::{spoof, Address, H256, U64}; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; -use rundler_sim::{GasEstimate, UserOperationOptionalGasV0_6}; +use rundler_sim::GasEstimate; +use rundler_types::UserOperationOptionalGasV0_6; use crate::types::{RichUserOperation, RpcUserOperation, UserOperationReceipt}; diff --git a/crates/rpc/src/eth/server.rs b/crates/rpc/src/eth/server.rs index 45fb92b22..5eadd7251 100644 --- a/crates/rpc/src/eth/server.rs +++ b/crates/rpc/src/eth/server.rs @@ -16,7 +16,8 @@ use ethers::types::{spoof, Address, H256, U64}; use jsonrpsee::core::RpcResult; use rundler_pool::PoolServer; use rundler_provider::{EntryPoint, Provider}; -use rundler_sim::{GasEstimate, UserOperationOptionalGasV0_6}; +use rundler_sim::GasEstimate; +use rundler_types::{UserOperationOptionalGasV0_6, UserOperationV0_6}; use super::{api::EthApi, EthApiServer}; use crate::types::{RichUserOperation, RpcUserOperation, UserOperationReceipt}; @@ -25,7 +26,7 @@ use crate::types::{RichUserOperation, RpcUserOperation, UserOperationReceipt}; impl EthApiServer for EthApi where P: Provider, - E: EntryPoint, + E: EntryPoint, PS: PoolServer, { async fn send_user_operation( diff --git a/crates/rpc/src/rundler.rs b/crates/rpc/src/rundler.rs index ddb648aa8..96eea7c74 100644 --- a/crates/rpc/src/rundler.rs +++ b/crates/rpc/src/rundler.rs @@ -71,7 +71,7 @@ pub(crate) struct RundlerApi { impl RundlerApi where P: Provider, - E: EntryPoint, + E: EntryPoint, PS: PoolServer, { pub(crate) fn new( @@ -99,7 +99,7 @@ where impl RundlerApiServer for RundlerApi where P: Provider, - E: EntryPoint, + E: EntryPoint, PS: PoolServer, { async fn max_priority_fee_per_gas(&self) -> RpcResult { diff --git a/crates/rpc/src/task.rs b/crates/rpc/src/task.rs index 492d28eff..2d49d802d 100644 --- a/crates/rpc/src/task.rs +++ b/crates/rpc/src/task.rs @@ -28,7 +28,7 @@ use rundler_task::{ server::{format_socket_addr, HealthCheck}, Task, }; -use rundler_types::chain::ChainSpec; +use rundler_types::{chain::ChainSpec, UserOperationV0_6}; use tokio_util::sync::CancellationToken; use tracing::info; @@ -148,12 +148,16 @@ 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 + Clone, + C: JsonRpcClient + 'static, + { for api in &self.args.api_namespaces { match api { ApiNamespace::Eth => module.merge( diff --git a/crates/sim/src/estimation/mod.rs b/crates/sim/src/estimation/mod.rs index d2f85cdd4..96f817caa 100644 --- a/crates/sim/src/estimation/mod.rs +++ b/crates/sim/src/estimation/mod.rs @@ -20,7 +20,7 @@ use serde::{Deserialize, Serialize}; use crate::precheck::MIN_CALL_GAS_LIMIT; mod v0_6; -pub use v0_6::{GasEstimatorV0_6, UserOperationOptionalGasV0_6}; +pub use v0_6::GasEstimatorV0_6; /// Error type for gas estimation #[derive(Debug, thiserror::Error)] @@ -52,7 +52,7 @@ pub struct GasEstimate { } /// Gas estimator trait -#[cfg_attr(feature = "test-utils", automock(type UserOperationOptionalGas = UserOperation;))] +#[cfg_attr(feature = "test-utils", automock(type UserOperationOptionalGas = rundler_types::UserOperationOptionalGasV0_6;))] #[async_trait::async_trait] pub trait GasEstimator: Send + Sync + 'static { type UserOperationOptionalGas; diff --git a/crates/sim/src/estimation/v0_6/estimator_v0_6.rs b/crates/sim/src/estimation/v0_6/estimator_v0_6.rs index 37c291ee8..9b22f4487 100644 --- a/crates/sim/src/estimation/v0_6/estimator_v0_6.rs +++ b/crates/sim/src/estimation/v0_6/estimator_v0_6.rs @@ -36,15 +36,12 @@ use rundler_types::{ }, i_entry_point, }, - UserOperationV0_6, + UserOperationOptionalGasV0_6, UserOperationV0_6, }; use rundler_utils::{eth, math}; use tokio::join; -use super::{ - super::{GasEstimate, GasEstimationError, GasEstimator, Settings}, - types::UserOperationOptionalGas, -}; +use super::super::{GasEstimate, GasEstimationError, GasEstimator, Settings}; use crate::{gas, precheck::MIN_CALL_GAS_LIMIT, simulation, utils, FeeEstimator}; /// Gas estimates will be rounded up to the next multiple of this. Increasing @@ -81,12 +78,16 @@ pub struct GasEstimatorV0_6 { } #[async_trait::async_trait] -impl GasEstimator for GasEstimatorV0_6 { - type UserOperationOptionalGas = UserOperationOptionalGas; +impl GasEstimator for GasEstimatorV0_6 +where + P: Provider, + E: EntryPoint, +{ + type UserOperationOptionalGas = UserOperationOptionalGasV0_6; async fn estimate_op_gas( &self, - op: UserOperationOptionalGas, + op: UserOperationOptionalGasV0_6, state_override: spoof::State, ) -> Result { let Self { @@ -112,7 +113,10 @@ impl GasEstimator for GasEstimatorV0_6 { let op = UserOperationV0_6 { 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 = @@ -153,7 +157,11 @@ impl GasEstimator for GasEstimatorV0_6 { } } -impl GasEstimatorV0_6 { +impl GasEstimatorV0_6 +where + P: Provider, + E: EntryPoint, +{ /// Create a new gas estimator pub fn new( chain_spec: ChainSpec, @@ -230,7 +238,7 @@ impl GasEstimatorV0_6 { let error_message = self .entry_point .call_spoofed_simulate_op( - op.into(), + op, Address::zero(), Bytes::new(), block_hash, @@ -342,7 +350,7 @@ impl GasEstimatorV0_6 { let target_revert_data = self .entry_point .call_spoofed_simulate_op( - callless_op.clone().into(), + callless_op.clone(), self.entry_point.address(), target_call_data, block_hash, @@ -393,14 +401,20 @@ impl GasEstimatorV0_6 { async fn estimate_pre_verification_gas( &self, - op: &UserOperationOptionalGas, + op: &UserOperationOptionalGasV0_6, gas_price: U256, ) -> Result { Ok(gas::estimate_pre_verification_gas( &self.chain_spec, self.entry_point.clone(), - &op.max_fill(&self.settings).into(), - &op.random_fill(&self.settings).into(), + &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?) diff --git a/crates/sim/src/estimation/v0_6/mod.rs b/crates/sim/src/estimation/v0_6/mod.rs index cdf09a658..ea67d1b32 100644 --- a/crates/sim/src/estimation/v0_6/mod.rs +++ b/crates/sim/src/estimation/v0_6/mod.rs @@ -13,5 +13,3 @@ mod estimator_v0_6; pub use estimator_v0_6::GasEstimatorV0_6; -mod types; -pub use types::UserOperationOptionalGas as UserOperationOptionalGasV0_6; diff --git a/crates/sim/src/estimation/v0_6/types.rs b/crates/sim/src/estimation/v0_6/types.rs deleted file mode 100644 index bdc3f969c..000000000 --- a/crates/sim/src/estimation/v0_6/types.rs +++ /dev/null @@ -1,188 +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, UserOperationV0_6}; -use serde::{Deserialize, Serialize}; - -use super::super::{GasEstimate, Settings}; - -/// 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) -> UserOperationV0_6 { - UserOperationV0_6 { - 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) -> UserOperationV0_6 { - UserOperationV0_6 { - 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) -> UserOperationV0_6 { - UserOperationV0_6 { - 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) -> UserOperationV0_6 { - UserOperationV0_6 { - 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: UserOperationV0_6) -> 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: UserOperationV0_6) -> Self { - Self::from_user_operation(op, false) - } - - fn from_user_operation(op: UserOperationV0_6, 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() - } -} diff --git a/crates/sim/src/gas/gas.rs b/crates/sim/src/gas/gas.rs index d49e05318..428b74942 100644 --- a/crates/sim/src/gas/gas.rs +++ b/crates/sim/src/gas/gas.rs @@ -40,11 +40,11 @@ use super::oracle::{ /// /// Networks that require dynamic pre_verification_gas are typically those that charge extra calldata fees /// that can scale based on dynamic gas prices. -pub async fn estimate_pre_verification_gas( +pub async fn estimate_pre_verification_gas>( chain_spec: &ChainSpec, enty_point: Arc, - full_op: &UserOperation, - random_op: &UserOperation, + full_op: &UO, + random_op: &UO, gas_price: U256, ) -> anyhow::Result { let static_gas = full_op.calc_static_pre_verification_gas(true); @@ -74,10 +74,10 @@ 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, entry_point: Arc, - op: &UserOperation, + op: &UO, base_fee: U256, ) -> anyhow::Result { let static_gas = op.calc_static_pre_verification_gas(true); @@ -123,9 +123,9 @@ 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( +pub fn user_operation_gas_limit( chain_spec: &ChainSpec, - uo: &UserOperation, + uo: &UO, assume_single_op_bundle: bool, paymaster_post_op: bool, ) -> U256 { @@ -136,9 +136,9 @@ pub fn user_operation_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( +pub fn user_operation_execution_gas_limit( chain_spec: &ChainSpec, - uo: &UserOperation, + uo: &UO, assume_single_op_bundle: bool, paymaster_post_op: bool, ) -> U256 { @@ -149,9 +149,9 @@ pub fn user_operation_execution_gas_limit( } /// Returns the static pre-verification gas cost of a user operation -pub fn user_operation_pre_verification_execution_gas_limit( +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 @@ -165,9 +165,9 @@ pub fn user_operation_pre_verification_execution_gas_limit( } /// Returns the gas limit for the user operation that applies to bundle transaction's limit -pub fn user_operation_pre_verification_gas_limit( +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 diff --git a/crates/sim/src/lib.rs b/crates/sim/src/lib.rs index 28f05fa40..bc9810d42 100644 --- a/crates/sim/src/lib.rs +++ b/crates/sim/src/lib.rs @@ -32,8 +32,7 @@ mod estimation; pub use estimation::{ - GasEstimate, GasEstimationError, GasEstimator, GasEstimatorV0_6, - Settings as EstimationSettings, UserOperationOptionalGasV0_6, + GasEstimate, GasEstimationError, GasEstimator, GasEstimatorV0_6, Settings as EstimationSettings, }; pub mod gas; diff --git a/crates/sim/src/precheck.rs b/crates/sim/src/precheck.rs index 404f9577f..5493a3f18 100644 --- a/crates/sim/src/precheck.rs +++ b/crates/sim/src/precheck.rs @@ -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::UserOperationV0_6;))] #[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: Arc, 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, + 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, + UO: UserOperation, +{ /// Create a new prechecker pub fn new( chain_spec: ChainSpec, @@ -155,14 +170,11 @@ 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, @@ -188,11 +200,7 @@ impl PrecheckerImpl { 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, @@ -267,7 +275,7 @@ impl PrecheckerImpl { 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, @@ -295,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 ( @@ -333,13 +341,13 @@ 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(), @@ -350,7 +358,7 @@ impl PrecheckerImpl { .context("precheck should get payer balance") } - async fn get_payer_balance(&self, op: &UserOperation) -> anyhow::Result { + 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()); @@ -370,7 +378,7 @@ 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( diff --git a/crates/sim/src/simulation/mod.rs b/crates/sim/src/simulation/mod.rs index 3285aa97a..25ebb4f96 100644 --- a/crates/sim/src/simulation/mod.rs +++ b/crates/sim/src/simulation/mod.rs @@ -100,17 +100,17 @@ impl From for SimulationError { } /// Simulator trait for running user operation simulations -#[cfg_attr(feature = "test-utils", automock(type UserOperationType = UserOperation;))] +#[cfg_attr(feature = "test-utils", automock(type UO = rundler_types::UserOperationV0_6;))] #[async_trait::async_trait] pub trait Simulator: Send + Sync + 'static { /// The type of user operation that this simulator can handle - type UserOperationType: From; + type UO: UserOperation; /// Simulate a user operation, returning simulation information /// upon success, or simulation violations. async fn simulate_validation( &self, - op: Self::UserOperationType, + op: Self::UO, block_hash: Option, expected_code_hash: Option, ) -> Result; diff --git a/crates/sim/src/simulation/simulator_v0_6.rs b/crates/sim/src/simulation/simulator_v0_6.rs index 9d9473e29..84924f36b 100644 --- a/crates/sim/src/simulation/simulator_v0_6.rs +++ b/crates/sim/src/simulation/simulator_v0_6.rs @@ -26,8 +26,8 @@ use ethers::{ use indexmap::IndexSet; use rundler_provider::{AggregatorOut, AggregatorSimOut, EntryPoint, Provider}; use rundler_types::{ - contracts::v0_6::i_entry_point::FailedOp, Entity, EntityType, StorageSlot, UserOperationV0_6, - ValidTimeRange, ValidationOutput, ValidationReturnInfo, + contracts::v0_6::i_entry_point::FailedOp, Entity, EntityType, StorageSlot, UserOperation, + UserOperationV0_6, ValidTimeRange, ValidationOutput, ValidationReturnInfo, }; use super::{ @@ -69,7 +69,7 @@ pub struct SimulatorV0_6 SimulatorV0_6 where P: Provider, - E: EntryPoint, + E: EntryPoint, T: SimulateValidationTracer, { /// Create a new simulator @@ -123,11 +123,7 @@ where let paymaster_address = op.paymaster(); let tracer_out = self .simulate_validation_tracer - .trace_simulate_validation( - op.clone().into(), - block_id, - self.sim_settings.max_verification_gas, - ) + .trace_simulate_validation(op.clone(), block_id, self.sim_settings.max_verification_gas) .await?; let num_phases = tracer_out.phases.len() as u32; // Check if there are too many phases here, then check too few at the @@ -229,7 +225,7 @@ where self.entry_point .clone() - .validate_user_op_signature(aggregator_address, op.into(), gas_cap) + .validate_user_op_signature(aggregator_address, op, gas_cap) .await } @@ -478,10 +474,10 @@ where impl Simulator for SimulatorV0_6 where P: Provider, - E: EntryPoint, + E: EntryPoint, T: SimulateValidationTracer, { - type UserOperationType = UserOperationV0_6; + type UO = UserOperationV0_6; async fn simulate_validation( &self, diff --git a/crates/sim/src/simulation/tracer.rs b/crates/sim/src/simulation/tracer.rs index bbe93eb25..92157bc34 100644 --- a/crates/sim/src/simulation/tracer.rs +++ b/crates/sim/src/simulation/tracer.rs @@ -26,7 +26,7 @@ use ethers::types::{ #[cfg(test)] use mockall::automock; use rundler_provider::{EntryPoint, Provider}; -use rundler_types::UserOperation; +use rundler_types::UserOperationV0_6; use serde::{Deserialize, Serialize}; use crate::ExpectedStorage; @@ -106,7 +106,7 @@ pub trait SimulateValidationTracer: Send + Sync + 'static { /// Traces the simulation of a user operation. async fn trace_simulate_validation( &self, - op: UserOperation, + op: UserOperationV0_6, block_id: BlockId, max_validation_gas: u64, ) -> anyhow::Result; @@ -130,11 +130,11 @@ where impl SimulateValidationTracer for SimulateValidationTracerImpl where P: Provider, - E: EntryPoint, + E: EntryPoint, { async fn trace_simulate_validation( &self, - op: UserOperation, + op: UserOperationV0_6, block_id: BlockId, max_validation_gas: u64, ) -> anyhow::Result { 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 aaadd4ba8..b8bee6c28 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -37,8 +37,8 @@ pub use timestamp::{Timestamp, ValidTimeRange}; mod user_operation; pub use user_operation::{ - GasOverheads, UserOperation, UserOperationId, UserOperationV0_6, UserOperationV0_7, - UserOpsPerAggregator, UserOpsPerAggregatorV0_6, + GasOverheads, UserOperation, UserOperationId, UserOperationOptionalGasV0_6, UserOperationV0_6, + UserOperationV0_7, UserOperationVariant, UserOpsPerAggregator, UserOpsPerAggregatorV0_6, }; mod storage; diff --git a/crates/types/src/user_operation/mod.rs b/crates/types/src/user_operation/mod.rs index 68b072057..1e81f0368 100644 --- a/crates/types/src/user_operation/mod.rs +++ b/crates/types/src/user_operation/mod.rs @@ -15,7 +15,8 @@ use ethers::types::{Address, Bytes, H256, U256}; mod v0_6; pub use v0_6::{ - UserOperation as UserOperationV0_6, UserOpsPerAggregator as UserOpsPerAggregatorV0_6, + UserOperation as UserOperationV0_6, UserOperationOptionalGas as UserOperationOptionalGasV0_6, + UserOpsPerAggregator as UserOpsPerAggregatorV0_6, }; mod v0_7; pub use v0_7::UserOperation as UserOperationV0_7; @@ -31,6 +32,68 @@ pub struct UserOperationId { pub nonce: U256, } +/// User operation trait +pub trait UserOperation: Clone + Send + Sync + 'static { + /// 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 + fn required_pre_execution_buffer(&self) -> U256; + + /// Returns the post-op required gas + fn post_op_required_gas(&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 { @@ -40,195 +103,160 @@ pub enum UserOperationType { /// User operation enum #[derive(Debug, Clone, Eq, PartialEq)] -pub enum UserOperation { +pub enum UserOperationVariant { /// User operation version 0.6 V0_6(UserOperationV0_6), /// User operation version 0.7 V0_7(UserOperationV0_7), } -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 hash(&self, entry_point: Address, chain_id: u64) -> H256 { +impl UserOperation for UserOperationVariant { + fn hash(&self, entry_point: Address, chain_id: u64) -> H256 { match self { - UserOperation::V0_6(op) => op.hash(entry_point, chain_id), - UserOperation::V0_7(op) => op.hash(entry_point, chain_id), + UserOperationVariant::V0_6(op) => op.hash(entry_point, chain_id), + UserOperationVariant::V0_7(op) => op.hash(entry_point, chain_id), } } - /// Get the user operation id - pub fn id(&self) -> UserOperationId { + fn id(&self) -> UserOperationId { match self { - UserOperation::V0_6(op) => op.id(), - UserOperation::V0_7(op) => op.id(), + UserOperationVariant::V0_6(op) => op.id(), + UserOperationVariant::V0_7(op) => op.id(), } } - /// Get the user operation sender address - pub fn sender(&self) -> Address { + fn sender(&self) -> Address { match self { - UserOperation::V0_6(op) => op.sender, - UserOperation::V0_7(op) => op.sender, + UserOperationVariant::V0_6(op) => op.sender(), + UserOperationVariant::V0_7(op) => op.sender(), } } - /// Get the user operation paymaster address, if any - pub fn paymaster(&self) -> Option
{ + fn paymaster(&self) -> Option
{ match self { - UserOperation::V0_6(op) => op.paymaster(), - UserOperation::V0_7(op) => op.paymaster(), + UserOperationVariant::V0_6(op) => op.paymaster(), + UserOperationVariant::V0_7(op) => op.paymaster(), } } - /// Get the user operation factory address, if any - pub fn factory(&self) -> Option
{ + fn factory(&self) -> Option
{ match self { - UserOperation::V0_6(op) => op.factory(), - UserOperation::V0_7(op) => op.factory(), + UserOperationVariant::V0_6(op) => op.factory(), + UserOperationVariant::V0_7(op) => op.factory(), } } - /// Returns the maximum cost, in wei, of this user operation - pub fn max_gas_cost(&self) -> U256 { + fn max_gas_cost(&self) -> U256 { match self { - UserOperation::V0_6(op) => op.max_gas_cost(), - UserOperation::V0_7(op) => op.max_gas_cost(), + UserOperationVariant::V0_6(op) => op.max_gas_cost(), + UserOperationVariant::V0_7(op) => op.max_gas_cost(), } } - /// Gets an iterator on all entities associated with this user operation - pub fn entities(&'_ self) -> Vec { + fn entities(&'_ self) -> Vec { match self { - UserOperation::V0_6(op) => op.entities(), - UserOperation::V0_7(op) => op.entities(), + UserOperationVariant::V0_6(op) => op.entities(), + UserOperationVariant::V0_7(op) => op.entities(), } } - /// Returns the heap size of the user operation - pub fn heap_size(&self) -> usize { + fn heap_size(&self) -> usize { match self { - UserOperation::V0_6(op) => op.heap_size(), - UserOperation::V0_7(op) => op.heap_size(), + UserOperationVariant::V0_6(op) => op.heap_size(), + UserOperationVariant::V0_7(op) => op.heap_size(), } } - /// Returns the call gas limit - pub fn call_gas_limit(&self) -> U256 { + fn call_gas_limit(&self) -> U256 { match self { - UserOperation::V0_6(op) => op.call_gas_limit, - UserOperation::V0_7(op) => op.call_gas_limit.into(), + UserOperationVariant::V0_6(op) => op.call_gas_limit(), + UserOperationVariant::V0_7(op) => op.call_gas_limit(), } } - pub fn verification_gas_limit(&self) -> U256 { + fn verification_gas_limit(&self) -> U256 { match self { - UserOperation::V0_6(op) => op.verification_gas_limit, - UserOperation::V0_7(op) => op.verification_gas_limit.into(), + UserOperationVariant::V0_6(op) => op.verification_gas_limit(), + UserOperationVariant::V0_7(op) => op.verification_gas_limit(), } } - /// Returns the total verification gas limit - pub fn total_verification_gas_limit(&self) -> U256 { + fn total_verification_gas_limit(&self) -> U256 { match self { - UserOperation::V0_6(op) => op.total_verification_gas_limit(), - UserOperation::V0_7(op) => op.total_verification_gas_limit(), + UserOperationVariant::V0_6(op) => op.total_verification_gas_limit(), + UserOperationVariant::V0_7(op) => op.total_verification_gas_limit(), } } - // TODO(danc) - pub fn required_pre_execution_buffer(&self) -> U256 { + fn required_pre_execution_buffer(&self) -> U256 { match self { - UserOperation::V0_6(op) => op.verification_gas_limit + U256::from(5_000), - UserOperation::V0_7(op) => { - U256::from(op.paymaster_post_op_gas_limit) + U256::from(10_000) - } + UserOperationVariant::V0_6(op) => op.required_pre_execution_buffer(), + UserOperationVariant::V0_7(op) => op.required_pre_execution_buffer(), } } - pub fn post_op_required_gas(&self) -> U256 { + fn post_op_required_gas(&self) -> U256 { match self { - UserOperation::V0_6(op) => { - if op.paymaster().is_some() { - op.verification_gas_limit - } else { - U256::zero() - } - } - UserOperation::V0_7(op) => op.paymaster_post_op_gas_limit.into(), + UserOperationVariant::V0_6(op) => op.post_op_required_gas(), + UserOperationVariant::V0_7(op) => op.post_op_required_gas(), } } - /// Returns the pre-verification gas - pub fn pre_verification_gas(&self) -> U256 { + fn pre_verification_gas(&self) -> U256 { match self { - UserOperation::V0_6(op) => op.pre_verification_gas, - UserOperation::V0_7(op) => op.pre_verification_gas, + UserOperationVariant::V0_6(op) => op.pre_verification_gas(), + UserOperationVariant::V0_7(op) => op.pre_verification_gas(), } } - /// Calculate the static portion of the pre-verification gas for this user operation - pub fn calc_static_pre_verification_gas(&self, include_fixed_gas_overhead: bool) -> U256 { + fn calc_static_pre_verification_gas(&self, include_fixed_gas_overhead: bool) -> U256 { match self { - UserOperation::V0_6(op) => { + UserOperationVariant::V0_6(op) => { op.calc_static_pre_verification_gas(include_fixed_gas_overhead) } - UserOperation::V0_7(op) => { + UserOperationVariant::V0_7(op) => { op.calc_static_pre_verification_gas(include_fixed_gas_overhead) } } } - /// Returns the max fee per gas - pub fn max_fee_per_gas(&self) -> U256 { - match self { - UserOperation::V0_6(op) => op.max_fee_per_gas, - UserOperation::V0_7(op) => op.max_fee_per_gas.into(), - } - } - - /// Returns the max priority fee per gas - pub fn max_priority_fee_per_gas(&self) -> U256 { + fn max_fee_per_gas(&self) -> U256 { match self { - UserOperation::V0_6(op) => op.max_priority_fee_per_gas, - UserOperation::V0_7(op) => op.max_priority_fee_per_gas.into(), + UserOperationVariant::V0_6(op) => op.max_fee_per_gas(), + UserOperationVariant::V0_7(op) => op.max_fee_per_gas(), } } - /// Clear the signature field of the user op - /// - /// Used when a user op is using a signature aggregator prior to being submitted - pub fn clear_signature(&mut self) { + fn max_priority_fee_per_gas(&self) -> U256 { match self { - UserOperation::V0_6(op) => op.signature = Bytes::new(), - UserOperation::V0_7(op) => op.signature = Bytes::new(), + UserOperationVariant::V0_6(op) => op.max_priority_fee_per_gas(), + UserOperationVariant::V0_7(op) => op.max_priority_fee_per_gas(), } } - /// Returns the user operation type - pub fn uo_type(&self) -> UserOperationType { + fn clear_signature(&mut self) { match self { - UserOperation::V0_6(_) => UserOperationType::V0_6, - UserOperation::V0_7(_) => UserOperationType::V0_7, + UserOperationVariant::V0_6(op) => op.clear_signature(), + UserOperationVariant::V0_7(op) => op.clear_signature(), } } +} +impl UserOperationVariant { /// Returns true if the user operation is of type [UserOperationV0_6] pub fn is_v0_6(&self) -> bool { - matches!(self, UserOperation::V0_6(_)) + matches!(self, UserOperationVariant::V0_6(_)) } /// Returns true if the user operation is of type [UserOperationV0_7] pub fn is_v0_7(&self) -> bool { - matches!(self, UserOperation::V0_7(_)) + matches!(self, UserOperationVariant::V0_7(_)) } /// Returns a reference to the [UserOperationV0_6] variant if the user operation is of that type pub fn as_v0_6(&self) -> Option<&UserOperationV0_6> { match self { - UserOperation::V0_6(op) => Some(op), + UserOperationVariant::V0_6(op) => Some(op), _ => None, } } @@ -236,7 +264,7 @@ impl UserOperation { /// Returns a reference to the [UserOperationV0_7] variant if the user operation is of that type pub fn as_v0_7(&self) -> Option<&UserOperationV0_7> { match self { - UserOperation::V0_7(op) => Some(op), + UserOperationVariant::V0_7(op) => Some(op), _ => None, } } @@ -244,7 +272,7 @@ impl UserOperation { /// Returns the [UserOperationV0_6] variant if the user operation is of that type pub fn into_v0_6(self) -> Option { match self { - UserOperation::V0_6(op) => Some(op), + UserOperationVariant::V0_6(op) => Some(op), _ => None, } } @@ -252,21 +280,17 @@ impl UserOperation { /// Returns the [UserOperationV0_7] variant if the user operation is of that type pub fn into_v0_7(self) -> Option { match self { - UserOperation::V0_7(op) => Some(op), + UserOperationVariant::V0_7(op) => Some(op), _ => None, } } -} - -impl From for UserOperationV0_6 { - fn from(value: UserOperation) -> Self { - value.into_v0_6().expect("Expected UserOperationV0_6") - } -} -impl From for UserOperationV0_7 { - fn from(value: UserOperation) -> Self { - value.into_v0_7().expect("Expected UserOperationV0_7") + /// 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, + } } } @@ -297,18 +321,8 @@ impl Default for GasOverheads { } #[derive(Debug)] -pub struct UserOpsPerAggregator { - pub user_ops: Vec, +pub struct UserOpsPerAggregator { + pub user_ops: Vec, pub aggregator: Address, pub signature: Bytes, } - -impl From for UserOpsPerAggregatorV0_6 { - fn from(value: UserOpsPerAggregator) -> Self { - Self { - user_ops: value.user_ops.into_iter().map(Into::into).collect(), - aggregator: value.aggregator, - signature: value.signature, - } - } -} diff --git a/crates/types/src/user_operation/v0_6.rs b/crates/types/src/user_operation/v0_6.rs index 597047ff8..9db0812cd 100644 --- a/crates/types/src/user_operation/v0_6.rs +++ b/crates/types/src/user_operation/v0_6.rs @@ -16,15 +16,18 @@ use ethers::{ types::{Address, Bytes, H256, U256}, utils::keccak256, }; +use rand::{self, RngCore}; +use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; -use super::{GasOverheads, UserOperationId}; +use super::{ + GasOverheads, UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant, +}; pub use crate::contracts::v0_6::shared_types::{UserOperation, UserOpsPerAggregator}; use crate::entity::{Entity, EntityType}; -impl UserOperation { - /// asdf - pub fn hash(&self, entry_point: Address, chain_id: u64) -> H256 { +impl UserOperationTrait for UserOperation { + 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), @@ -33,41 +36,39 @@ impl UserOperation { .into() } - /// asdf - pub fn id(&self) -> UserOperationId { + fn id(&self) -> UserOperationId { UserOperationId { sender: self.sender, nonce: self.nonce, } } - /// asdf - pub fn factory(&self) -> Option
{ + fn sender(&self) -> Address { + self.sender + } + + fn factory(&self) -> Option
{ Self::get_address_from_field(&self.init_code) } - /// asdf - pub fn paymaster(&self) -> Option
{ + fn paymaster(&self) -> Option
{ Self::get_address_from_field(&self.paymaster_and_data) } - /// asdf - pub fn max_gas_cost(&self) -> U256 { + 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) } - /// asdf - pub fn heap_size(&self) -> usize { + fn heap_size(&self) -> usize { self.init_code.len() + self.call_data.len() + self.paymaster_and_data.len() + self.signature.len() } - /// asdf - pub fn entities(&'_ self) -> Vec { + fn entities(&self) -> Vec { EntityType::iter() .filter_map(|entity| { self.entity_address(entity) @@ -76,14 +77,40 @@ impl UserOperation { .collect() } - /// asdf - pub fn total_verification_gas_limit(&self) -> U256 { + 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 } - /// asdf - pub fn calc_static_pre_verification_gas(&self, include_fixed_gas_overhead: bool) -> U256 { + fn post_op_required_gas(&self) -> U256 { + self.verification_gas_limit + } + + 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(); let encoded_op = self.clone().encode(); let length_in_words = encoded_op.len() / 32; // size of packed user op is always a multiple of 32 bytes @@ -109,6 +136,12 @@ impl UserOperation { }) } + fn clear_signature(&mut self) { + self.signature = Bytes::default(); + } +} + +impl UserOperation { fn get_address_from_field(data: &Bytes) -> Option
{ if data.len() < 20 { None @@ -147,9 +180,173 @@ impl UserOperation { } } -impl From for super::UserOperation { +impl From for UserOperation { + fn from(value: UserOperationVariant) -> Self { + value.into_v0_6().expect("Expected UserOperationV0_6") + } +} + +impl From for super::UserOperationVariant { fn from(op: UserOperation) -> Self { - super::UserOperation::V0_6(op) + 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) + /// + /// 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, 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(), + } + } + + /// 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() } } diff --git a/crates/types/src/user_operation/v0_7.rs b/crates/types/src/user_operation/v0_7.rs index 31ef79088..42ee8822a 100644 --- a/crates/types/src/user_operation/v0_7.rs +++ b/crates/types/src/user_operation/v0_7.rs @@ -17,7 +17,7 @@ use ethers::{ utils::keccak256, }; -use super::UserOperationId; +use super::{UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant}; use crate::{contracts::v0_7::shared_types::PackedUserOperation, Entity}; /// User Operation @@ -44,27 +44,31 @@ pub struct UserOperation { packed: PackedUserOperation, } -impl UserOperation { - pub fn hash(&self, _entry_point: Address, _chain_id: u64) -> H256 { +impl UserOperationTrait for UserOperation { + fn hash(&self, _entry_point: Address, _chain_id: u64) -> H256 { self.hash } - pub fn id(&self) -> UserOperationId { + fn id(&self) -> UserOperationId { UserOperationId { sender: self.sender, nonce: self.nonce, } } - pub fn paymaster(&self) -> Option
{ + fn sender(&self) -> Address { + self.sender + } + + fn paymaster(&self) -> Option
{ self.paymaster } - pub fn factory(&self) -> Option
{ + fn factory(&self) -> Option
{ self.factory } - pub fn max_gas_cost(&self) -> U256 { + fn max_gas_cost(&self) -> U256 { U256::from(self.max_fee_per_gas) * (self.pre_verification_gas + self.call_gas_limit @@ -73,7 +77,7 @@ impl UserOperation { + self.paymaster_post_op_gas_limit) } - pub fn entities(&self) -> Vec { + fn entities(&self) -> Vec { let mut ret = vec![Entity::account(self.sender)]; if let Some(factory) = self.factory { ret.push(Entity::factory(factory)); @@ -84,27 +88,66 @@ impl UserOperation { ret } - pub fn heap_size(&self) -> usize { + fn heap_size(&self) -> usize { unimplemented!() } - pub fn total_verification_gas_limit(&self) -> U256 { + 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) } - /// asdf - pub fn calc_static_pre_verification_gas(&self, include_fixed_gas_overhead: bool) -> U256 { - todo!() + fn calc_static_pre_verification_gas(&self, include_fixed_gas_overhead: bool) -> U256 { + unimplemented!() + } + + fn required_pre_execution_buffer(&self) -> U256 { + unimplemented!() + } + + fn post_op_required_gas(&self) -> U256 { + unimplemented!() } + fn clear_signature(&mut self) { + self.signature = Bytes::new(); + } +} + +impl UserOperation { pub fn pack(self) -> PackedUserOperation { self.packed } } -impl From for super::UserOperation { +impl From for UserOperation { + fn from(value: UserOperationVariant) -> Self { + value.into_v0_7().expect("Expected UserOperationV0_7") + } +} + +impl From for super::UserOperationVariant { fn from(op: UserOperation) -> Self { - super::UserOperation::V0_7(op) + super::UserOperationVariant::V0_7(op) } }