diff --git a/.github/workflows/unit.yaml b/.github/workflows/unit.yaml index 5dcf3aa62..b709cd4b5 100644 --- a/.github/workflows/unit.yaml +++ b/.github/workflows/unit.yaml @@ -17,6 +17,8 @@ jobs: steps: - name: Checkout sources uses: actions/checkout@v3 + with: + submodules: 'recursive' - name: Install toolchain uses: dtolnay/rust-toolchain@stable diff --git a/.gitmodules b/.gitmodules index b19f910ea..649faf2ad 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,15 +1,23 @@ -[submodule "crates/types/contracts/lib/account-abstraction"] - path = crates/types/contracts/lib/account-abstraction +[submodule "crates/types/contracts/lib/account-abstraction-versions/v0_7"] + path = crates/types/contracts/lib/account-abstraction-versions/v0_7 url = https://github.com/eth-infinitism/account-abstraction - branch = v0.5 + branch = releases/v0.7 +[submodule "crates/types/contracts/lib/account-abstraction-versions/v0_6"] + path = crates/types/contracts/lib/account-abstraction-versions/v0_6 + url = https://github.com/eth-infinitism/account-abstraction + branch = releases/v0.6 [submodule "crates/types/contracts/lib/forge-std"] path = crates/types/contracts/lib/forge-std url = https://github.com/foundry-rs/forge-std branch = chore/v1.5.0 -[submodule "crates/types/contracts/lib/openzeppelin-contracts"] - path = crates/types/contracts/lib/openzeppelin-contracts +[submodule "crates/types/contracts/lib/openzeppelin-contracts-versions/v5_0"] + path = crates/types/contracts/lib/openzeppelin-contracts-versions/v5_0 + url = https://github.com/OpenZeppelin/openzeppelin-contracts + branch = release-v5.0 +[submodule "crates/types/contracts/lib/openzeppelin-contracts-versions/v4_9"] + path = crates/types/contracts/lib/openzeppelin-contracts-versions/v4_9 url = https://github.com/OpenZeppelin/openzeppelin-contracts - branch = release-v4.8 + branch = release-v4.9 [submodule "test/spec-tests/bundler-spec-tests"] path = test/spec-tests/bundler-spec-tests url = https://github.com/alchemyplatform/bundler-spec-tests.git diff --git a/bin/tools/src/bin/send_ops.rs b/bin/tools/src/bin/send_ops.rs index b8b06b597..ff0c66057 100644 --- a/bin/tools/src/bin/send_ops.rs +++ b/bin/tools/src/bin/send_ops.rs @@ -28,7 +28,7 @@ async fn main() -> anyhow::Result<()> { // simply call the nonce method multiple times for i in 0..10 { println!("Sending op {i}"); - let op = clients.new_wallet_op(wallet.nonce(), 0.into()).await?; + let op = clients.new_wallet_op(wallet.get_nonce(), 0.into()).await?; let call = entry_point.handle_ops(vec![op], bundler_client.address()); rundler_dev::await_mined_tx(call.send(), "send user operation").await?; } diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index 10a6f5215..78431a142 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -32,13 +32,12 @@ use mockall::automock; use rundler_pool::{PoolOperation, PoolServer}; use rundler_provider::{EntryPoint, HandleOpsOut, Provider}; use rundler_sim::{ - gas::{self, GasOverheads}, - EntityInfo, EntityInfos, ExpectedStorage, FeeEstimator, PriorityFeeMode, SimulationError, + gas, EntityInfo, EntityInfos, ExpectedStorage, FeeEstimator, PriorityFeeMode, SimulationError, SimulationResult, SimulationViolation, Simulator, ViolationError, }; use rundler_types::{ - chain::ChainSpec, Entity, EntityType, EntityUpdate, EntityUpdateType, GasFees, Timestamp, - UserOperation, UserOpsPerAggregator, + chain::ChainSpec, Entity, EntityType, EntityUpdate, EntityUpdateType, GasFees, GasOverheads, + Timestamp, UserOperation, UserOpsPerAggregator, }; use rundler_utils::{emit::WithEntryPoint, math}; use tokio::{sync::broadcast, try_join}; @@ -99,7 +98,7 @@ where builder_index: u64, pool: C, simulator: S, - entry_point: E, + entry_point: Arc, provider: Arc

, settings: Settings, fee_estimator: FeeEstimator

, @@ -230,7 +229,7 @@ where builder_index: u64, pool: C, simulator: S, - entry_point: E, + entry_point: Arc, provider: Arc

, settings: Settings, event_sender: broadcast::Sender>, @@ -266,8 +265,8 @@ where required_op_fees: GasFees, ) -> Option<(PoolOperation, Result)> { // filter by fees - if op.uo.max_fee_per_gas < required_op_fees.max_fee_per_gas - || op.uo.max_priority_fee_per_gas < required_op_fees.max_priority_fee_per_gas + if op.uo.max_fee_per_gas() < required_op_fees.max_fee_per_gas + || op.uo.max_priority_fee_per_gas() < required_op_fees.max_priority_fee_per_gas { self.emit(BuilderEvent::skipped_op( self.builder_index, @@ -275,8 +274,8 @@ where SkipReason::InsufficientFees { required_fees: required_op_fees, actual_fees: GasFees { - max_fee_per_gas: op.uo.max_fee_per_gas, - max_priority_fee_per_gas: op.uo.max_priority_fee_per_gas, + max_fee_per_gas: op.uo.max_fee_per_gas(), + max_priority_fee_per_gas: op.uo.max_priority_fee_per_gas(), }, }, )); @@ -286,7 +285,7 @@ where // Check if the pvg is enough let required_pvg = gas::calc_required_pre_verification_gas( &self.settings.chain_spec, - self.provider.clone(), + self.entry_point.clone(), &op.uo, base_fee, ) @@ -305,18 +304,18 @@ where }) .ok()?; - if op.uo.pre_verification_gas < required_pvg { + if op.uo.pre_verification_gas() < required_pvg { self.emit(BuilderEvent::skipped_op( self.builder_index, self.op_hash(&op.uo), SkipReason::InsufficientPreVerificationGas { base_fee, op_fees: GasFees { - max_fee_per_gas: op.uo.max_fee_per_gas, - max_priority_fee_per_gas: op.uo.max_priority_fee_per_gas, + max_fee_per_gas: op.uo.max_fee_per_gas(), + max_priority_fee_per_gas: op.uo.max_priority_fee_per_gas(), }, required_pvg, - actual_pvg: op.uo.pre_verification_gas, + actual_pvg: op.uo.pre_verification_gas(), }, )); return None; @@ -325,7 +324,11 @@ where // Simulate let result = self .simulator - .simulate_validation(op.uo.clone(), Some(block_hash), Some(op.expected_code_hash)) + .simulate_validation( + op.uo.clone().into(), + Some(block_hash), + Some(op.expected_code_hash), + ) .await; let result = match result { Ok(success) => (op, Ok(success)), @@ -360,7 +363,7 @@ where ) -> ProposalContext { let all_sender_addresses: HashSet

= ops_with_simulations .iter() - .map(|(op, _)| op.uo.sender) + .map(|(op, _)| op.uo.sender()) .collect(); let mut context = ProposalContext::new(); let mut paymasters_to_reject = Vec::::new(); @@ -410,13 +413,7 @@ where } // Skip this op if the bundle does not have enough remaining gas to execute it. - let required_gas = get_gas_required_for_op( - &self.settings.chain_spec, - gas_spent, - ov, - &op, - simulation.requires_post_op, - ); + let required_gas = get_gas_required_for_op(&self.settings.chain_spec, gas_spent, &op); if required_gas > self.settings.max_bundle_gas.into() { continue; } @@ -424,11 +421,11 @@ where if let Some(&other_sender) = simulation .accessed_addresses .iter() - .find(|&address| *address != op.sender && all_sender_addresses.contains(address)) + .find(|&address| *address != op.sender() && all_sender_addresses.contains(address)) { // Exclude ops that access the sender of another op in the // batch, but don't reject them (remove them from pool). - info!("Excluding op from {:?} because it accessed the address of another sender in the bundle.", op.sender); + info!("Excluding op from {:?} because it accessed the address of another sender in the bundle.", op.sender()); self.emit(BuilderEvent::skipped_op( self.builder_index, self.op_hash(&op), @@ -610,7 +607,7 @@ where .iter() .map(|op_with_simulation| op_with_simulation.op.clone()) .collect(); - let result = Arc::clone(&self.provider) + let result = Arc::clone(&self.entry_point) .aggregate_signatures(aggregator, ops) .await .map_err(anyhow::Error::from); @@ -842,7 +839,7 @@ where } fn op_hash(&self, op: &UserOperation) -> H256 { - op.op_hash(self.entry_point.address(), self.settings.chain_spec.id) + op.hash(self.entry_point.address(), self.settings.chain_spec.id) } } @@ -855,8 +852,9 @@ struct OpWithSimulation { impl OpWithSimulation { fn op_with_replaced_sig(&self) -> UserOperation { let mut op = self.op.clone(); - if let Some(aggregator) = &self.simulation.aggregator { - op.signature = aggregator.signature.clone(); + if self.simulation.aggregator.is_some() { + // if using an aggregator, clear out the user op signature + op.clear_signature(); } op } @@ -1039,13 +1037,7 @@ impl ProposalContext { let mut max_gas = U256::zero(); for op_with_sim in self.iter_ops_with_simulations() { let op = &op_with_sim.op; - let required_gas = get_gas_required_for_op( - chain_spec, - gas_spent, - ov, - op, - op_with_sim.simulation.requires_post_op, - ); + let required_gas = get_gas_required_for_op(chain_spec, gas_spent, op); max_gas = cmp::max(max_gas, required_gas); gas_spent += gas::user_operation_gas_limit( chain_spec, @@ -1226,24 +1218,12 @@ impl ProposalContext { } } -fn get_gas_required_for_op( - chain_spec: &ChainSpec, - gas_spent: U256, - ov: GasOverheads, - op: &UserOperation, - requires_post_op: bool, -) -> U256 { - let post_exec_req_gas = if requires_post_op { - cmp::max(op.verification_gas_limit, ov.bundle_transaction_gas_buffer) - } else { - ov.bundle_transaction_gas_buffer - }; - +fn get_gas_required_for_op(chain_spec: &ChainSpec, gas_spent: U256, op: &UserOperation) -> U256 { gas_spent + gas::user_operation_pre_verification_gas_limit(chain_spec, op, false) - + op.verification_gas_limit * 2 - + op.call_gas_limit - + post_exec_req_gas + + op.total_verification_gas_limit() + + op.required_pre_execution_buffer() + + op.call_gas_limit() } #[cfg(test)] diff --git a/crates/builder/src/bundle_sender.rs b/crates/builder/src/bundle_sender.rs index db95f1bf8..83ad563e4 100644 --- a/crates/builder/src/bundle_sender.rs +++ b/crates/builder/src/bundle_sender.rs @@ -543,7 +543,7 @@ where .remove_ops( self.entry_point.address(), ops.iter() - .map(|op| op.op_hash(self.entry_point.address(), self.chain_spec.id)) + .map(|op| op.hash(self.entry_point.address(), self.chain_spec.id)) .collect(), ) .await @@ -565,7 +565,7 @@ where } fn op_hash(&self, op: &UserOperation) -> H256 { - op.op_hash(self.entry_point.address(), self.chain_spec.id) + op.hash(self.entry_point.address(), self.chain_spec.id) } } diff --git a/crates/builder/src/task.rs b/crates/builder/src/task.rs index a69dc3b73..76ee05199 100644 --- a/crates/builder/src/task.rs +++ b/crates/builder/src/task.rs @@ -23,9 +23,9 @@ use ethers_signers::Signer; use futures::future; use futures_util::TryFutureExt; use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, EthersEntryPoint}; +use rundler_provider::{EntryPoint, EthersEntryPointV0_6}; use rundler_sim::{ - MempoolConfig, PriorityFeeMode, SimulateValidationTracerImpl, SimulationSettings, SimulatorImpl, + MempoolConfig, PriorityFeeMode, SimulateValidationTracerImpl, SimulationSettings, SimulatorV0_6, }; use rundler_task::Task; use rundler_types::chain::ChainSpec; @@ -262,15 +262,15 @@ where bundle_priority_fee_overhead_percent: self.args.bundle_priority_fee_overhead_percent, }; - let ep = EthersEntryPoint::new( + let ep = EthersEntryPointV0_6::new( self.args.chain_spec.entry_point_address, Arc::clone(&provider), ); let simulate_validation_tracer = SimulateValidationTracerImpl::new(Arc::clone(&provider), ep.clone()); - let simulator = SimulatorImpl::new( + let simulator = SimulatorV0_6::new( Arc::clone(&provider), - ep.address(), + Arc::new(ep.clone()), simulate_validation_tracer, self.args.sim_settings, self.args.mempool_configs.clone(), @@ -311,7 +311,7 @@ where index, self.pool.clone(), simulator, - ep.clone(), + Arc::new(ep.clone()), Arc::clone(&provider), proposer_settings, self.event_sender.clone(), diff --git a/crates/dev/src/lib.rs b/crates/dev/src/lib.rs index 25b08ab11..4132c49fd 100644 --- a/crates/dev/src/lib.rs +++ b/crates/dev/src/lib.rs @@ -42,11 +42,11 @@ use ethers::{ utils::{self, hex, keccak256}, }; use rundler_types::{ - contracts::{ + contracts::v0_6::{ entry_point::EntryPoint, simple_account::SimpleAccount, simple_account_factory::SimpleAccountFactory, verifying_paymaster::VerifyingPaymaster, }, - UserOperation, + UserOperationV0_6 as UserOperation, }; /// Chain ID used by Geth in --dev mode. @@ -320,7 +320,7 @@ pub async fn deploy_dev_contracts(entry_point_bytecode: &str) -> anyhow::Result< init_code, ..base_user_op() }; - let op_hash = op.op_hash(entry_point.address(), DEV_CHAIN_ID); + let op_hash = op.hash(entry_point.address(), DEV_CHAIN_ID); let signature = wallet_owner_eoa .sign_message(op_hash) .await @@ -426,7 +426,7 @@ impl DevClients { paymaster_and_data.extend(paymaster_signature.to_vec()); op.paymaster_and_data = paymaster_and_data.into() } - let op_hash = op.op_hash(self.entry_point.address(), DEV_CHAIN_ID); + let op_hash = op.hash(self.entry_point.address(), DEV_CHAIN_ID); let signature = self .wallet_owner_signer .sign_message(op_hash) @@ -470,7 +470,7 @@ impl DevClients { .context("call executed by wallet should have to address")?; let nonce = self .wallet - .nonce() + .get_nonce() .await .context("should read nonce from wallet")?; let call_data = Bytes::clone( diff --git a/crates/pool/proto/op_pool/op_pool.proto b/crates/pool/proto/op_pool/op_pool.proto index 6135a7da1..8f5056800 100644 --- a/crates/pool/proto/op_pool/op_pool.proto +++ b/crates/pool/proto/op_pool/op_pool.proto @@ -17,9 +17,15 @@ syntax = "proto3"; package op_pool; +message UserOperation { + oneof uo { + UserOperationV06 v06 = 1; + } +} + // Protocol Buffer representation of an ERC-4337 UserOperation. See the official // specification at https://eips.ethereum.org/EIPS/eip-4337#definitions -message UserOperation { +message UserOperationV06 { // The account making the operation bytes sender = 1; // Anti-replay parameter (see “Semi-abstracted Nonce Support” ) @@ -502,20 +508,18 @@ message OperationDropTooSoon { // PRECHECK VIOLATIONS message PrecheckViolationError { oneof violation { - InitCodeTooShort init_code_too_short = 1; - SenderIsNotContractAndNoInitCode sender_is_not_contract_and_no_init_code = 2; - ExistingSenderWithInitCode existing_sender_with_init_code = 3; - FactoryIsNotContract factory_is_not_contract = 4; - TotalGasLimitTooHigh total_gas_limit_too_high = 5; - VerificationGasLimitTooHigh verification_gas_limit_too_high = 6; - PreVerificationGasTooLow pre_verification_gas_too_low = 7; - PaymasterTooShort paymaster_too_short = 8; - PaymasterIsNotContract paymaster_is_not_contract = 9; - PaymasterDepositTooLow paymaster_deposit_too_low = 10; - SenderFundsTooLow sender_funds_too_low = 11; - MaxFeePerGasTooLow max_fee_per_gas_too_low = 12; - MaxPriorityFeePerGasTooLow max_priority_fee_per_gas_too_low = 13; - CallGasLimitTooLow call_gas_limit_too_low = 14; + SenderIsNotContractAndNoInitCode sender_is_not_contract_and_no_init_code = 1; + ExistingSenderWithInitCode existing_sender_with_init_code = 2; + FactoryIsNotContract factory_is_not_contract = 3; + TotalGasLimitTooHigh total_gas_limit_too_high = 4; + VerificationGasLimitTooHigh verification_gas_limit_too_high = 5; + PreVerificationGasTooLow pre_verification_gas_too_low = 6; + PaymasterIsNotContract paymaster_is_not_contract = 7; + PaymasterDepositTooLow paymaster_deposit_too_low = 8; + SenderFundsTooLow sender_funds_too_low = 9; + MaxFeePerGasTooLow max_fee_per_gas_too_low = 10; + MaxPriorityFeePerGasTooLow max_priority_fee_per_gas_too_low = 11; + CallGasLimitTooLow call_gas_limit_too_low = 12; } } diff --git a/crates/pool/src/chain.rs b/crates/pool/src/chain.rs index 04f015720..b35bfcb3c 100644 --- a/crates/pool/src/chain.rs +++ b/crates/pool/src/chain.rs @@ -27,7 +27,7 @@ use futures::future; use rundler_provider::Provider; use rundler_task::block_watcher; use rundler_types::{ - contracts::{ + contracts::v0_6::{ entry_point::{DepositedFilter, WithdrawnFilter}, i_entry_point::UserOperationEventFilter, }, diff --git a/crates/pool/src/emit.rs b/crates/pool/src/emit.rs index 520a1d4b3..e902ab551 100644 --- a/crates/pool/src/emit.rs +++ b/crates/pool/src/emit.rs @@ -157,8 +157,8 @@ impl Display for OpPoolEvent { format_entity_status("Factory", entities.factory.as_ref()), format_entity_status("Paymaster", entities.paymaster.as_ref()), format_entity_status("Aggregator", entities.aggregator.as_ref()), - op.max_fee_per_gas, - op.max_priority_fee_per_gas, + op.max_fee_per_gas(), + op.max_priority_fee_per_gas(), ) } OpPoolEvent::RemovedOp { op_hash, reason } => { diff --git a/crates/pool/src/mempool/error.rs b/crates/pool/src/mempool/error.rs index 3d3370bbc..69500459c 100644 --- a/crates/pool/src/mempool/error.rs +++ b/crates/pool/src/mempool/error.rs @@ -108,7 +108,7 @@ impl From for MempoolError { // extract violation and replace with dummy Self::PrecheckViolation(mem::replace( violation, - PrecheckViolation::InitCodeTooShort(0), + PrecheckViolation::SenderIsNotContractAndNoInitCode(Address::zero()), )) } } diff --git a/crates/pool/src/mempool/mod.rs b/crates/pool/src/mempool/mod.rs index c9c22fd78..90f72be1e 100644 --- a/crates/pool/src/mempool/mod.rs +++ b/crates/pool/src/mempool/mod.rs @@ -192,7 +192,7 @@ pub enum OperationOrigin { } /// A user operation with additional metadata from validation. -#[derive(Debug, Default, Clone, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct PoolOperation { /// The user operation stored in the pool pub uo: UserOperation, diff --git a/crates/pool/src/mempool/pool.rs b/crates/pool/src/mempool/pool.rs index d836eb446..63713962c 100644 --- a/crates/pool/src/mempool/pool.rs +++ b/crates/pool/src/mempool/pool.rs @@ -106,7 +106,7 @@ impl PoolInner { // Check if operation already known if self .by_hash - .contains_key(&op.op_hash(self.config.entry_point, self.config.chain_id)) + .contains_key(&op.hash(self.config.entry_point, self.config.chain_id)) { return Err(MempoolError::OperationAlreadyKnown); } @@ -115,19 +115,19 @@ impl PoolInner { let (replacement_priority_fee, replacement_fee) = self.get_min_replacement_fees(pool_op.uo()); - if op.max_priority_fee_per_gas < replacement_priority_fee - || op.max_fee_per_gas < replacement_fee + if op.max_priority_fee_per_gas() < replacement_priority_fee + || op.max_fee_per_gas() < replacement_fee { return Err(MempoolError::ReplacementUnderpriced( - pool_op.uo().max_priority_fee_per_gas, - pool_op.uo().max_fee_per_gas, + pool_op.uo().max_priority_fee_per_gas(), + pool_op.uo().max_fee_per_gas(), )); } Ok(Some( pool_op .uo() - .op_hash(self.config.entry_point, self.config.chain_id), + .hash(self.config.entry_point, self.config.chain_id), )) } else { Ok(None) @@ -187,9 +187,11 @@ impl PoolInner { // STO-040 pub(crate) fn check_multiple_roles_violation(&self, uo: &UserOperation) -> MempoolResult<()> { - if let Some(ec) = self.count_by_address.get(&uo.sender) { + if let Some(ec) = self.count_by_address.get(&uo.sender()) { if ec.includes_non_sender() { - return Err(MempoolError::SenderAddressUsedAsAlternateEntity(uo.sender)); + return Err(MempoolError::SenderAddressUsedAsAlternateEntity( + uo.sender(), + )); } } @@ -217,7 +219,7 @@ impl PoolInner { ) -> MempoolResult<()> { for storage_address in accessed_storage { if let Some(ec) = self.count_by_address.get(storage_address) { - if ec.sender().gt(&0) && storage_address.ne(&uo.sender) { + if ec.sender().gt(&0) && storage_address.ne(&uo.sender()) { // Reject UO if the sender is also an entity in another UO in the mempool for entity in uo.entities() { if storage_address.eq(&entity.address) { @@ -240,7 +242,7 @@ impl PoolInner { let hash = tx_in_pool .uo() - .op_hash(mined_op.entry_point, self.config.chain_id); + .hash(mined_op.entry_point, self.config.chain_id); let ret = self.remove_operation_internal(hash, Some(block_number)); @@ -285,10 +287,7 @@ impl PoolInner { } false }) - .map(|o| { - o.po.uo - .op_hash(self.config.entry_point, self.config.chain_id) - }) + .map(|o| o.po.uo.hash(self.config.entry_point, self.config.chain_id)) .collect::>(); for &hash in &to_remove { self.remove_operation_internal(hash, None); @@ -346,7 +345,7 @@ impl PoolInner { if let Some(worst) = self.best.pop_last() { let hash = worst .uo() - .op_hash(self.config.entry_point, self.config.chain_id); + .hash(self.config.entry_point, self.config.chain_id); let _ = self .remove_operation_internal(hash, None) @@ -390,7 +389,7 @@ impl PoolInner { // create and insert ordered operation let hash = pool_op .uo() - .op_hash(self.config.entry_point, self.config.chain_id); + .hash(self.config.entry_point, self.config.chain_id); self.pool_size += pool_op.mem_size(); self.by_hash.insert(hash, pool_op.clone()); self.by_id.insert(pool_op.uo().id(), pool_op.clone()); @@ -451,11 +450,11 @@ impl PoolInner { fn get_min_replacement_fees(&self, op: &UserOperation) -> (U256, U256) { let replacement_priority_fee = math::increase_by_percent( - op.max_priority_fee_per_gas, + op.max_priority_fee_per_gas(), self.config.min_replacement_fee_increase_percentage, ); let replacement_fee = math::increase_by_percent( - op.max_fee_per_gas, + op.max_fee_per_gas(), self.config.min_replacement_fee_increase_percentage, ); (replacement_priority_fee, replacement_fee) @@ -500,8 +499,8 @@ impl Ord for OrderedPoolOperation { // Sort by gas price descending then by id ascending other .uo() - .max_fee_per_gas - .cmp(&self.uo().max_fee_per_gas) + .max_fee_per_gas() + .cmp(&self.uo().max_fee_per_gas()) .then_with(|| self.submission_id.cmp(&other.submission_id)) } } diff --git a/crates/pool/src/mempool/uo_pool.rs b/crates/pool/src/mempool/uo_pool.rs index 6e9e4054c..892b5bbe1 100644 --- a/crates/pool/src/mempool/uo_pool.rs +++ b/crates/pool/src/mempool/uo_pool.rs @@ -380,7 +380,7 @@ where // Only let ops with successful simulations through let sim_result = self .simulator - .simulate_validation(op.clone(), None, None) + .simulate_validation(op.clone().into(), None, None) .await?; // No aggregators supported for now @@ -412,12 +412,12 @@ where { let state = self.state.read(); if !pool_op.account_is_staked - && state.pool.address_count(&pool_op.uo.sender) + && state.pool.address_count(&pool_op.uo.sender()) >= self.config.same_sender_mempool_count { return Err(MempoolError::MaxOperationsReached( self.config.same_sender_mempool_count, - pool_op.uo.sender, + pool_op.uo.sender(), )); } @@ -464,7 +464,7 @@ where } let op_hash = pool_op .uo - .op_hash(self.config.entry_point, self.config.chain_id); + .hash(self.config.entry_point, self.config.chain_id); let valid_after = pool_op.valid_time_range.valid_after; let valid_until = pool_op.valid_time_range.valid_until; self.emit(OpPoolEvent::ReceivedOp { @@ -522,7 +522,7 @@ where } }; - let hash = po.uo.op_hash(self.config.entry_point, self.config.chain_id); + let hash = po.uo.hash(self.config.entry_point, self.config.chain_id); // This can return none if the operation was removed by another thread if self @@ -577,12 +577,12 @@ where .filter(|op| { // short-circuit the mod if there is only 1 shard ((self.config.num_shards == 1) || - (U256::from_little_endian(op.uo.sender.as_bytes()) + (U256::from_little_endian(op.uo.sender().as_bytes()) .div_mod(self.config.num_shards.into()) .1 == shard_index.into())) && // filter out ops from senders we've already seen - senders.insert(op.uo.sender) + senders.insert(op.uo.sender()) }) .take(max) .collect()) diff --git a/crates/pool/src/server/remote/error.rs b/crates/pool/src/server/remote/error.rs index 0a57e9e4f..2e85a0143 100644 --- a/crates/pool/src/server/remote/error.rs +++ b/crates/pool/src/server/remote/error.rs @@ -22,11 +22,11 @@ use super::protos::{ AccessedUndeployedContract, AggregatorValidationFailed, AssociatedStorageIsAlternateSender, CallGasLimitTooLow, CallHadValue, CalledBannedEntryPointMethod, CodeHashChanged, DidNotRevert, DiscardedOnInsertError, Entity, EntityThrottledError, EntityType, ExistingSenderWithInitCode, - FactoryCalledCreate2Twice, FactoryIsNotContract, InitCodeTooShort, InvalidSignature, - InvalidStorageAccess, MaxFeePerGasTooLow, MaxOperationsReachedError, - MaxPriorityFeePerGasTooLow, MempoolError as ProtoMempoolError, MultipleRolesViolation, - NotStaked, OperationAlreadyKnownError, OperationDropTooSoon, OutOfGas, PaymasterBalanceTooLow, - PaymasterDepositTooLow, PaymasterIsNotContract, PaymasterTooShort, PreVerificationGasTooLow, + FactoryCalledCreate2Twice, FactoryIsNotContract, InvalidSignature, InvalidStorageAccess, + MaxFeePerGasTooLow, MaxOperationsReachedError, MaxPriorityFeePerGasTooLow, + MempoolError as ProtoMempoolError, MultipleRolesViolation, NotStaked, + OperationAlreadyKnownError, OperationDropTooSoon, OutOfGas, PaymasterBalanceTooLow, + PaymasterDepositTooLow, PaymasterIsNotContract, PreVerificationGasTooLow, PrecheckViolationError as ProtoPrecheckViolationError, ReplacementUnderpricedError, SenderAddressUsedAsAlternateEntity, SenderFundsTooLow, SenderIsNotContractAndNoInitCode, SimulationViolationError as ProtoSimulationViolationError, TotalGasLimitTooHigh, @@ -244,13 +244,6 @@ impl From for ProtoMempoolError { impl From for ProtoPrecheckViolationError { fn from(value: PrecheckViolation) -> Self { match value { - PrecheckViolation::InitCodeTooShort(length) => ProtoPrecheckViolationError { - violation: Some(precheck_violation_error::Violation::InitCodeTooShort( - InitCodeTooShort { - length: length as u64, - }, - )), - }, PrecheckViolation::SenderIsNotContractAndNoInitCode(addr) => { ProtoPrecheckViolationError { violation: Some( @@ -310,13 +303,6 @@ impl From for ProtoPrecheckViolationError { ), } } - PrecheckViolation::PaymasterTooShort(length) => ProtoPrecheckViolationError { - violation: Some(precheck_violation_error::Violation::PaymasterTooShort( - PaymasterTooShort { - length: length as u64, - }, - )), - }, PrecheckViolation::PaymasterIsNotContract(addr) => ProtoPrecheckViolationError { violation: Some(precheck_violation_error::Violation::PaymasterIsNotContract( PaymasterIsNotContract { @@ -377,9 +363,6 @@ impl TryFrom for PrecheckViolation { fn try_from(value: ProtoPrecheckViolationError) -> Result { Ok(match value.violation { - Some(precheck_violation_error::Violation::InitCodeTooShort(e)) => { - PrecheckViolation::InitCodeTooShort(e.length as usize) - } Some(precheck_violation_error::Violation::SenderIsNotContractAndNoInitCode(e)) => { PrecheckViolation::SenderIsNotContractAndNoInitCode(from_bytes(&e.sender_address)?) } @@ -407,9 +390,6 @@ impl TryFrom for PrecheckViolation { from_bytes(&e.min_gas)?, ) } - Some(precheck_violation_error::Violation::PaymasterTooShort(e)) => { - PrecheckViolation::PaymasterTooShort(e.length as usize) - } Some(precheck_violation_error::Violation::PaymasterIsNotContract(e)) => { PrecheckViolation::PaymasterIsNotContract(from_bytes(&e.paymaster_address)?) } @@ -780,12 +760,16 @@ mod tests { #[test] fn test_precheck_error() { - let error = MempoolError::PrecheckViolation(PrecheckViolation::InitCodeTooShort(0)); + let error = MempoolError::PrecheckViolation(PrecheckViolation::SenderFundsTooLow( + 0.into(), + 0.into(), + )); let proto_error: ProtoMempoolError = error.into(); let error2 = proto_error.try_into().unwrap(); match error2 { - MempoolError::PrecheckViolation(PrecheckViolation::InitCodeTooShort(v)) => { - assert_eq!(v, 0) + MempoolError::PrecheckViolation(PrecheckViolation::SenderFundsTooLow(x, y)) => { + assert_eq!(x, 0); + assert_eq!(y, 0); } _ => panic!("wrong error type"), } diff --git a/crates/pool/src/server/remote/protos.rs b/crates/pool/src/server/remote/protos.rs index a19ad9f73..3419a2b63 100644 --- a/crates/pool/src/server/remote/protos.rs +++ b/crates/pool/src/server/remote/protos.rs @@ -17,7 +17,7 @@ use rundler_task::grpc::protos::{from_bytes, to_le_bytes, ConversionError}; use rundler_types::{ Entity as RundlerEntity, EntityType as RundlerEntityType, EntityUpdate as RundlerEntityUpdate, EntityUpdateType as RundlerEntityUpdateType, UserOperation as RundlerUserOperation, - ValidTimeRange, + UserOperationV0_6 as RundlerUserOperationV0_6, ValidTimeRange, }; use crate::{ @@ -36,7 +36,18 @@ pub const OP_POOL_FILE_DESCRIPTOR_SET: &[u8] = impl From<&RundlerUserOperation> for UserOperation { fn from(op: &RundlerUserOperation) -> Self { - UserOperation { + match op { + RundlerUserOperation::V0_6(op) => op.into(), + RundlerUserOperation::V0_7(_) => { + unimplemented!("V0_7 user operation is not supported") + } + } + } +} + +impl From<&RundlerUserOperationV0_6> for UserOperation { + fn from(op: &RundlerUserOperationV0_6) -> Self { + let op = UserOperationV06 { sender: op.sender.0.to_vec(), nonce: to_le_bytes(op.nonce), init_code: op.init_code.to_vec(), @@ -48,15 +59,18 @@ impl From<&RundlerUserOperation> for UserOperation { max_priority_fee_per_gas: to_le_bytes(op.max_priority_fee_per_gas), paymaster_and_data: op.paymaster_and_data.to_vec(), signature: op.signature.to_vec(), + }; + UserOperation { + uo: Some(user_operation::Uo::V06(op)), } } } -impl TryFrom for RundlerUserOperation { +impl TryFrom for RundlerUserOperationV0_6 { type Error = ConversionError; - fn try_from(op: UserOperation) -> Result { - Ok(RundlerUserOperation { + fn try_from(op: UserOperationV06) -> Result { + Ok(RundlerUserOperationV0_6 { sender: from_bytes(&op.sender)?, nonce: from_bytes(&op.nonce)?, init_code: op.init_code.into(), @@ -72,6 +86,20 @@ impl TryFrom for RundlerUserOperation { } } +impl TryFrom for RundlerUserOperation { + type Error = ConversionError; + + fn try_from(op: UserOperation) -> Result { + let op = op + .uo + .expect("User operation should contain user operation oneof"); + + match op { + user_operation::Uo::V06(op) => Ok(RundlerUserOperation::V0_6(op.try_into()?)), + } + } +} + impl TryFrom for RundlerEntityType { type Error = ConversionError; diff --git a/crates/pool/src/task.rs b/crates/pool/src/task.rs index 7a556d13c..d46687ee9 100644 --- a/crates/pool/src/task.rs +++ b/crates/pool/src/task.rs @@ -16,9 +16,9 @@ use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; use anyhow::{bail, Context}; use async_trait::async_trait; use ethers::providers::Middleware; -use rundler_provider::{EntryPoint, EthersEntryPoint, Provider}; +use rundler_provider::{EntryPoint, EthersEntryPointV0_6, Provider}; use rundler_sim::{ - Prechecker, PrecheckerImpl, SimulateValidationTracerImpl, Simulator, SimulatorImpl, + Prechecker, PrecheckerImpl, SimulateValidationTracerImpl, Simulator, SimulatorV0_6, }; use rundler_task::Task; use rundler_types::chain::ChainSpec; @@ -163,20 +163,20 @@ impl PoolTask { event_sender: broadcast::Sender>, provider: Arc

, ) -> anyhow::Result> { - let ep = EthersEntryPoint::new(pool_config.entry_point, Arc::clone(&provider)); + let ep = EthersEntryPointV0_6::new(pool_config.entry_point, Arc::clone(&provider)); let prechecker = PrecheckerImpl::new( chain_spec, Arc::clone(&provider), - ep.clone(), + Arc::new(ep.clone()), pool_config.precheck_settings, ); let simulate_validation_tracer = SimulateValidationTracerImpl::new(Arc::clone(&provider), ep.clone()); - let simulator = SimulatorImpl::new( + let simulator = SimulatorV0_6::new( Arc::clone(&provider), - ep.address(), + Arc::new(ep.clone()), simulate_validation_tracer, pool_config.sim_settings, pool_config.mempool_channel_configs.clone(), diff --git a/crates/provider/src/ethers/entry_point.rs b/crates/provider/src/ethers/entry_point_v0_6.rs similarity index 57% rename from crates/provider/src/ethers/entry_point.rs rename to crates/provider/src/ethers/entry_point_v0_6.rs index 4cfebc4ee..99b89c46b 100644 --- a/crates/provider/src/ethers/entry_point.rs +++ b/crates/provider/src/ethers/entry_point_v0_6.rs @@ -20,35 +20,57 @@ use ethers::{ providers::{spoof, Middleware, RawCall}, types::{ transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, Eip1559TransactionRequest, - H256, U256, + H160, H256, U256, U64, }, utils::hex, }; use rundler_types::{ contracts::{ - get_balances::{GetBalancesResult, GETBALANCES_BYTECODE}, - i_entry_point::{ExecutionResult, FailedOp, IEntryPoint, SignatureValidationFailed}, - shared_types::UserOpsPerAggregator, + arbitrum::node_interface::NodeInterface, + optimism::gas_price_oracle::GasPriceOracle, + v0_6::{ + get_balances::{GetBalancesResult, GETBALANCES_BYTECODE}, + i_aggregator::IAggregator, + i_entry_point::{ExecutionResult, FailedOp, IEntryPoint, SignatureValidationFailed}, + shared_types::UserOpsPerAggregator as UserOpsPerAggregatorV0_6, + }, }, - DepositInfo, GasFees, UserOperation, ValidationOutput, + DepositInfoV0_6, GasFees, UserOperation, UserOpsPerAggregator, ValidationOutput, }; use rundler_utils::eth::{self, ContractRevertError}; use crate::{ traits::{EntryPoint, HandleOpsOut}, - Provider, + AggregatorOut, AggregatorSimOut, Provider, }; +const ARBITRUM_NITRO_NODE_INTERFACE_ADDRESS: Address = H160([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xc8, +]); + +const OPTIMISM_BEDROCK_GAS_ORACLE_ADDRESS: Address = H160([ + 0x42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0F, +]); + const REVERT_REASON_MAX_LEN: usize = 2048; +// 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 EntryPointImpl { +pub struct EntryPointV0_6 { i_entry_point: IEntryPoint

, provider: Arc

, + arb_node: NodeInterface

, + opt_gas_oracle: GasPriceOracle

, } -impl

Clone for EntryPointImpl

+impl

Clone for EntryPointV0_6

where P: Provider + Middleware, { @@ -56,25 +78,32 @@ where Self { i_entry_point: self.i_entry_point.clone(), provider: self.provider.clone(), + arb_node: self.arb_node.clone(), + opt_gas_oracle: self.opt_gas_oracle.clone(), } } } -impl

EntryPointImpl

+impl

EntryPointV0_6

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

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

EntryPoint for EntryPointImpl

+impl

EntryPoint for EntryPointV0_6

where P: Provider + Middleware + Send + Sync + 'static, { @@ -87,6 +116,10 @@ where user_op: UserOperation, 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 @@ -101,6 +134,10 @@ where user_op: UserOperation, 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 @@ -166,16 +203,20 @@ where async fn call_spoofed_simulate_op( &self, - op: UserOperation, + user_op: UserOperation, 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(op, target, target_call_data) + .simulate_handle_op(user_op, target, target_call_data) .block(block_hash) .gas(gas) .call_raw() @@ -219,7 +260,7 @@ where } } - async fn get_deposit_info(&self, address: Address) -> anyhow::Result { + async fn get_deposit_info(&self, address: Address) -> anyhow::Result { Ok(self .i_entry_point .get_deposit_info(address) @@ -240,14 +281,119 @@ where .context("should compute balances")?; Ok(out.balances) } + + async fn aggregate_signatures( + self: Arc, + aggregator_address: Address, + 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; + match result { + Ok(bytes) => Ok(Some(bytes)), + Err(ContractError::Revert(_)) => Ok(None), + Err(error) => Err(error).context("aggregator contract should aggregate signatures")?, + } + } + + async fn validate_user_op_signature( + self: Arc, + aggregator_address: Address, + user_op: UserOperation, + gas_cap: u64, + ) -> 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) + .gas(gas_cap) + .call() + .await; + + match result { + Ok(sig) => Ok(AggregatorOut::SuccessWithInfo(AggregatorSimOut { + address: aggregator_address, + signature: sig, + })), + Err(ContractError::Revert(_)) => Ok(AggregatorOut::ValidationReverted), + Err(error) => Err(error).context("should call aggregator to validate signature")?, + } + } + + async fn calc_arbitrum_l1_gas( + self: Arc, + entry_point_address: Address, + user_op: UserOperation, + ) -> 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()) + .calldata() + .context("should get calldata for entry point handle ops")?; + + let gas = self + .arb_node + .gas_estimate_l1_component(entry_point_address, false, data) + .call() + .await?; + Ok(U256::from(gas.0)) + } + + async fn calc_optimism_l1_gas( + self: Arc, + entry_point_address: Address, + user_op: UserOperation, + 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()) + .calldata() + .context("should get calldata for entry point handle ops")?; + + // construct an unsigned transaction with default values just for L1 gas estimation + let tx = Eip1559TransactionRequest::new() + .from(Address::random()) + .to(entry_point_address) + .gas(U256::from(1_000_000)) + .max_priority_fee_per_gas(U256::from(100_000_000)) + .max_fee_per_gas(U256::from(100_000_000)) + .value(U256::from(0)) + .data(data) + .nonce(U256::from(100_000)) + .chain_id(U64::from(100_000)) + .rlp(); + + let l1_fee = self.opt_gas_oracle.get_l1_fee(tx).call().await?; + Ok(l1_fee.checked_div(gas_price).unwrap_or(U256::MAX)) + } } fn get_handle_ops_call( entry_point: &IEntryPoint, - mut ops_per_aggregator: Vec, + ops_per_aggregator: Vec, beneficiary: Address, gas: U256, ) -> FunctionCall, M, ()> { + let mut ops_per_aggregator: Vec = + ops_per_aggregator.into_iter().map(|x| x.into()).collect(); let call = if ops_per_aggregator.len() == 1 && ops_per_aggregator[0].aggregator == Address::zero() { entry_point.handle_ops(ops_per_aggregator.swap_remove(0).user_ops, beneficiary) diff --git a/crates/provider/src/ethers/mod.rs b/crates/provider/src/ethers/mod.rs index 072d4bc8f..c336ac027 100644 --- a/crates/provider/src/ethers/mod.rs +++ b/crates/provider/src/ethers/mod.rs @@ -13,7 +13,7 @@ //! Provider implementations using [ethers-rs](https://github.com/gakonst/ethers-rs) -mod entry_point; -pub use entry_point::EntryPointImpl as EthersEntryPoint; +mod entry_point_v0_6; +pub use entry_point_v0_6::EntryPointV0_6 as EthersEntryPointV0_6; mod metrics_middleware; pub(crate) mod provider; diff --git a/crates/provider/src/ethers/provider.rs b/crates/provider/src/ethers/provider.rs index 3b2c3acc9..8e700aeab 100644 --- a/crates/provider/src/ethers/provider.rs +++ b/crates/provider/src/ethers/provider.rs @@ -16,7 +16,6 @@ use std::{fmt::Debug, sync::Arc, time::Duration}; use anyhow::Context; use ethers::{ abi::{AbiDecode, AbiEncode}, - contract::ContractError, prelude::ContractError as EthersContractError, providers::{ Http, HttpRateLimitRetryPolicy, JsonRpcClient, Middleware, Provider as EthersProvider, @@ -25,30 +24,15 @@ use ethers::{ types::{ spoof, transaction::eip2718::TypedTransaction, Address, Block, BlockId, BlockNumber, Bytes, Eip1559TransactionRequest, FeeHistory, Filter, GethDebugTracingCallOptions, - GethDebugTracingOptions, GethTrace, Log, Transaction, TransactionReceipt, TxHash, H160, - H256, U256, U64, + GethDebugTracingOptions, GethTrace, Log, Transaction, TransactionReceipt, TxHash, H256, + U256, U64, }, }; use reqwest::Url; -use rundler_types::{ - contracts::{ - gas_price_oracle::GasPriceOracle, i_aggregator::IAggregator, i_entry_point::IEntryPoint, - node_interface::NodeInterface, - }, - UserOperation, -}; use serde::{de::DeserializeOwned, Serialize}; use super::metrics_middleware::MetricsMiddleware; -use crate::{AggregatorOut, AggregatorSimOut, Provider, ProviderError, ProviderResult}; - -const ARBITRUM_NITRO_NODE_INTERFACE_ADDRESS: Address = H160([ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xc8, -]); - -const OPTIMISM_BEDROCK_GAS_ORACLE_ADDRESS: Address = H160([ - 0x42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0F, -]); +use crate::{Provider, ProviderError, ProviderResult}; #[async_trait::async_trait] impl Provider for EthersProvider { @@ -191,44 +175,6 @@ impl Provider for EthersProvider { Ok(Middleware::get_logs(self, filter).await?) } - async fn aggregate_signatures( - self: Arc, - aggregator_address: Address, - ops: Vec, - ) -> ProviderResult> { - let aggregator = IAggregator::new(aggregator_address, self); - // TODO: Cap the gas here. - let result = aggregator.aggregate_signatures(ops).call().await; - match result { - Ok(bytes) => Ok(Some(bytes)), - Err(ContractError::Revert(_)) => Ok(None), - Err(error) => Err(error).context("aggregator contract should aggregate signatures")?, - } - } - - async fn validate_user_op_signature( - self: Arc, - aggregator_address: Address, - user_op: UserOperation, - gas_cap: u64, - ) -> ProviderResult { - let aggregator = IAggregator::new(aggregator_address, self); - let result = aggregator - .validate_user_op_signature(user_op) - .gas(gas_cap) - .call() - .await; - - match result { - Ok(sig) => Ok(AggregatorOut::SuccessWithInfo(AggregatorSimOut { - address: aggregator_address, - signature: sig, - })), - Err(ContractError::Revert(_)) => Ok(AggregatorOut::ValidationReverted), - Err(error) => Err(error).context("should call aggregator to validate signature")?, - } - } - async fn get_code(&self, address: Address, block_hash: Option) -> ProviderResult { Ok(Middleware::get_code(self, address, block_hash.map(|b| b.into())).await?) } @@ -236,57 +182,6 @@ impl Provider for EthersProvider { async fn get_transaction_count(&self, address: Address) -> ProviderResult { Ok(Middleware::get_transaction_count(self, address, None).await?) } - - async fn calc_arbitrum_l1_gas( - self: Arc, - entry_point_address: Address, - op: UserOperation, - ) -> ProviderResult { - let entry_point = IEntryPoint::new(entry_point_address, Arc::clone(&self)); - let data = entry_point - .handle_ops(vec![op], Address::random()) - .calldata() - .context("should get calldata for entry point handle ops")?; - - let arb_node = NodeInterface::new(ARBITRUM_NITRO_NODE_INTERFACE_ADDRESS, self); - let gas = arb_node - .gas_estimate_l1_component(entry_point_address, false, data) - .call() - .await?; - Ok(U256::from(gas.0)) - } - - async fn calc_optimism_l1_gas( - self: Arc, - entry_point_address: Address, - op: UserOperation, - gas_price: U256, - ) -> ProviderResult { - let entry_point = IEntryPoint::new(entry_point_address, Arc::clone(&self)); - let data = entry_point - .handle_ops(vec![op], Address::random()) - .calldata() - .context("should get calldata for entry point handle ops")?; - - // construct an unsigned transaction with default values just for L1 gas estimation - let tx = Eip1559TransactionRequest::new() - .from(Address::random()) - .to(entry_point_address) - .gas(U256::from(1_000_000)) - .max_priority_fee_per_gas(U256::from(100_000_000)) - .max_fee_per_gas(U256::from(100_000_000)) - .value(U256::from(0)) - .data(data) - .nonce(U256::from(100_000)) - .chain_id(U64::from(100_000)) - .rlp(); - - let gas_oracle = - GasPriceOracle::new(OPTIMISM_BEDROCK_GAS_ORACLE_ADDRESS, Arc::clone(&self)); - - let l1_fee = gas_oracle.get_l1_fee(tx).call().await?; - Ok(l1_fee.checked_div(gas_price).unwrap_or(U256::MAX)) - } } impl From for ProviderError { diff --git a/crates/provider/src/lib.rs b/crates/provider/src/lib.rs index 1f2415d8f..e3138ea4d 100644 --- a/crates/provider/src/lib.rs +++ b/crates/provider/src/lib.rs @@ -22,7 +22,7 @@ //! A provider is a type that provides access to blockchain data and functions mod ethers; -pub use ethers::{provider::new_provider, EthersEntryPoint}; +pub use ethers::{provider::new_provider, EthersEntryPointV0_6}; mod traits; pub use traits::{ diff --git a/crates/provider/src/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index 93c1adf3f..5c374aba1 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -11,16 +11,38 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. +use std::sync::Arc; + use ethers::types::{ spoof, transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, H256, U256, }; #[cfg(feature = "test-utils")] use mockall::automock; use rundler_types::{ - contracts::{i_entry_point::ExecutionResult, shared_types::UserOpsPerAggregator}, - DepositInfo, GasFees, UserOperation, ValidationOutput, + contracts::v0_6::i_entry_point::ExecutionResult, DepositInfoV0_6, GasFees, UserOperation, + UserOpsPerAggregator, ValidationOutput, }; +/// Output of a successful signature aggregator simulation call +#[derive(Clone, Debug, Default)] +pub struct AggregatorSimOut { + /// Address of the aggregator contract + pub address: Address, + /// Aggregated signature + pub signature: Bytes, +} + +/// Result of a signature aggregator call +#[derive(Debug)] +pub enum AggregatorOut { + /// No aggregator used + NotNeeded, + /// Successful call + SuccessWithInfo(AggregatorSimOut), + /// Aggregator validation function reverted + ValidationReverted, +} + /// Result of an entry point handle ops call #[derive(Clone, Debug)] pub enum HandleOpsOut { @@ -98,8 +120,38 @@ pub trait EntryPoint: Send + Sync + 'static { ) -> Result; /// Get the deposit info for an address - async fn get_deposit_info(&self, address: Address) -> anyhow::Result; + async fn get_deposit_info(&self, address: Address) -> anyhow::Result; /// Get the balances of a list of addresses in order async fn get_balances(&self, addresses: Vec

) -> anyhow::Result>; + + /// Call an aggregator to aggregate signatures for a set of operations + async fn aggregate_signatures( + self: Arc, + aggregator_address: Address, + 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, + gas_cap: u64, + ) -> anyhow::Result; + + /// Calculate the L1 portion of the gas for a user operation on Arbitrum + async fn calc_arbitrum_l1_gas( + self: Arc, + entry_point_address: Address, + op: UserOperation, + ) -> 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, + gas_price: U256, + ) -> anyhow::Result; } diff --git a/crates/provider/src/traits/mod.rs b/crates/provider/src/traits/mod.rs index 87bde2210..f62d25418 100644 --- a/crates/provider/src/traits/mod.rs +++ b/crates/provider/src/traits/mod.rs @@ -19,9 +19,9 @@ pub use error::ProviderError; mod entry_point; #[cfg(feature = "test-utils")] pub use entry_point::MockEntryPoint; -pub use entry_point::{EntryPoint, HandleOpsOut}; +pub use entry_point::{AggregatorOut, AggregatorSimOut, EntryPoint, HandleOpsOut}; mod provider; #[cfg(feature = "test-utils")] pub use provider::MockProvider; -pub use provider::{AggregatorOut, AggregatorSimOut, Provider, ProviderResult}; +pub use provider::{Provider, ProviderResult}; diff --git a/crates/provider/src/traits/provider.rs b/crates/provider/src/traits/provider.rs index 0c0fbec76..4578194c4 100644 --- a/crates/provider/src/traits/provider.rs +++ b/crates/provider/src/traits/provider.rs @@ -13,7 +13,7 @@ //! Trait for interacting with chain data and contracts. -use std::{fmt::Debug, sync::Arc}; +use std::fmt::Debug; use ethers::{ abi::{AbiDecode, AbiEncode}, @@ -25,31 +25,10 @@ use ethers::{ }; #[cfg(feature = "test-utils")] use mockall::automock; -use rundler_types::UserOperation; use serde::{de::DeserializeOwned, Serialize}; use super::error::ProviderError; -/// Output of a successful signature aggregator simulation call -#[derive(Clone, Debug, Default)] -pub struct AggregatorSimOut { - /// Address of the aggregator contract - pub address: Address, - /// Aggregated signature - pub signature: Bytes, -} - -/// Result of a signature aggregator call -#[derive(Debug)] -pub enum AggregatorOut { - /// No aggregator used - NotNeeded, - /// Successful call - SuccessWithInfo(AggregatorSimOut), - /// Aggregator validation function reverted - ValidationReverted, -} - /// Result of a provider method call pub type ProviderResult = Result; @@ -148,34 +127,4 @@ pub trait Provider: Send + Sync + Debug + 'static { /// Get the logs matching a filter async fn get_logs(&self, filter: &Filter) -> ProviderResult>; - - /// Call an aggregator to aggregate signatures for a set of operations - async fn aggregate_signatures( - self: Arc, - aggregator_address: Address, - ops: Vec, - ) -> ProviderResult>; - - /// Validate a user operation signature using an aggregator - async fn validate_user_op_signature( - self: Arc, - aggregator_address: Address, - user_op: UserOperation, - gas_cap: u64, - ) -> ProviderResult; - - /// Calculate the L1 portion of the gas for a user operation on Arbitrum - async fn calc_arbitrum_l1_gas( - self: Arc, - entry_point_address: Address, - op: UserOperation, - ) -> ProviderResult; - - /// Calculate the L1 portion of the gas for a user operation on optimism - async fn calc_optimism_l1_gas( - self: Arc, - entry_point_address: Address, - op: UserOperation, - gas_price: U256, - ) -> ProviderResult; } diff --git a/crates/rpc/src/debug.rs b/crates/rpc/src/debug.rs index a7b3b20bd..1310b5731 100644 --- a/crates/rpc/src/debug.rs +++ b/crates/rpc/src/debug.rs @@ -17,6 +17,7 @@ use futures_util::StreamExt; use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::error::INTERNAL_ERROR_CODE}; use rundler_builder::{BuilderServer, BundlingMode}; use rundler_pool::PoolServer; +use rundler_types::UserOperationV0_6; use crate::{ error::rpc_err, @@ -126,7 +127,7 @@ where .await .map_err(|e| rpc_err(INTERNAL_ERROR_CODE, e.to_string()))? .into_iter() - .map(|pop| pop.uo.into()) + .map(|pop| UserOperationV0_6::from(pop.uo).into()) .collect::>()) } diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 4ec65b905..05ab9fc1b 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, - GasEstimatorImpl, PrecheckSettings, UserOperationOptionalGas, + GasEstimatorV0_6, PrecheckSettings, UserOperationOptionalGasV0_6, }; use rundler_types::{ chain::ChainSpec, - contracts::i_entry_point::{ + contracts::v0_6::i_entry_point::{ IEntryPointCalls, UserOperationEventFilter, UserOperationRevertReasonFilter, }, - UserOperation, + UserOperation, UserOperationV0_6, }; use rundler_utils::{eth::log_to_raw_log, log::LogOnError}; use tracing::Level; @@ -64,7 +64,7 @@ impl Settings { #[derive(Debug)] struct EntryPointContext { - gas_estimator: GasEstimatorImpl, + gas_estimator: GasEstimatorV0_6, } impl EntryPointContext @@ -75,11 +75,11 @@ where fn new( chain_spec: ChainSpec, provider: Arc

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

, ) -> Self { - let gas_estimator = GasEstimatorImpl::new( + let gas_estimator = GasEstimatorV0_6::new( chain_spec, provider, entry_point, @@ -108,7 +108,7 @@ where pub(crate) fn new( chain_spec: ChainSpec, provider: Arc

, - entry_points: Vec, + entry_points: Vec>, pool: PS, settings: Settings, estimation_settings: EstimationSettings, @@ -157,6 +157,8 @@ where "supplied entry point addr is not a known entry point".to_string(), )); } + let op: UserOperationV0_6 = op.into(); + self.pool .add_op(entry_point, op.into()) .await @@ -166,7 +168,7 @@ where pub(crate) async fn estimate_user_operation_gas( &self, - op: UserOperationOptionalGas, + op: UserOperationOptionalGasV0_6, entry_point: Address, state_override: Option, ) -> EthResult { @@ -346,7 +348,7 @@ where let user_operation = if self.contexts_by_entry_point.contains_key(&to) { self.get_user_operations_from_tx_data(tx.input) .into_iter() - .find(|op| op.op_hash(to, self.chain_spec.id) == hash) + .find(|op| op.hash(to, self.chain_spec.id) == hash) .context("matching user operation should be found in tx data")? } else { self.trace_find_user_operation(transaction_hash, hash) @@ -378,7 +380,7 @@ where .await .map_err(EthRpcError::from)?; Ok(res.map(|op| RichUserOperation { - user_operation: op.uo.into(), + user_operation: UserOperationV0_6::from(op.uo).into(), entry_point: op.entry_point.into(), block_number: None, block_hash: None, @@ -410,7 +412,7 @@ where Ok(logs.into_iter().next()) } - fn get_user_operations_from_tx_data(&self, tx_data: Bytes) -> Vec { + fn get_user_operations_from_tx_data(&self, tx_data: Bytes) -> Vec { let entry_point_calls = match IEntryPointCalls::decode(tx_data) { Ok(entry_point_calls) => entry_point_calls, Err(_) => return vec![], @@ -510,7 +512,7 @@ where &self, tx_hash: H256, user_op_hash: H256, - ) -> EthResult> { + ) -> EthResult> { // initial call wasn't to an entrypoint, so we need to trace the transaction to find the user operation let trace_options = GethDebugTracingOptions { tracer: Some(GethDebugTracerType::BuiltInTracer( @@ -543,7 +545,7 @@ where if let Some(uo) = self .get_user_operations_from_tx_data(call_frame.input) .into_iter() - .find(|op| op.op_hash(*to, self.chain_spec.id) == user_op_hash) + .find(|op| op.hash(*to, self.chain_spec.id) == user_op_hash) { return Ok(Some(uo)); } @@ -567,7 +569,7 @@ mod tests { use rundler_pool::{MockPoolServer, PoolOperation}; use rundler_provider::{MockEntryPoint, MockProvider}; use rundler_sim::PriorityFeeMode; - use rundler_types::contracts::i_entry_point::HandleOpsCall; + use rundler_types::contracts::v0_6::i_entry_point::HandleOpsCall; use super::*; diff --git a/crates/rpc/src/eth/mod.rs b/crates/rpc/src/eth/mod.rs index 3cb50e52b..59d220243 100644 --- a/crates/rpc/src/eth/mod.rs +++ b/crates/rpc/src/eth/mod.rs @@ -21,7 +21,7 @@ mod server; use ethers::types::{spoof, Address, H256, U64}; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; -use rundler_sim::{GasEstimate, UserOperationOptionalGas}; +use rundler_sim::{GasEstimate, UserOperationOptionalGasV0_6}; use crate::types::{RichUserOperation, RpcUserOperation, UserOperationReceipt}; @@ -41,7 +41,7 @@ pub trait EthApi { #[method(name = "estimateUserOperationGas")] async fn estimate_user_operation_gas( &self, - op: UserOperationOptionalGas, + op: UserOperationOptionalGasV0_6, entry_point: Address, state_override: Option, ) -> RpcResult; diff --git a/crates/rpc/src/eth/server.rs b/crates/rpc/src/eth/server.rs index 58a0d290d..45fb92b22 100644 --- a/crates/rpc/src/eth/server.rs +++ b/crates/rpc/src/eth/server.rs @@ -16,7 +16,7 @@ use ethers::types::{spoof, Address, H256, U64}; use jsonrpsee::core::RpcResult; use rundler_pool::PoolServer; use rundler_provider::{EntryPoint, Provider}; -use rundler_sim::{GasEstimate, UserOperationOptionalGas}; +use rundler_sim::{GasEstimate, UserOperationOptionalGasV0_6}; use super::{api::EthApi, EthApiServer}; use crate::types::{RichUserOperation, RpcUserOperation, UserOperationReceipt}; @@ -38,7 +38,7 @@ where async fn estimate_user_operation_gas( &self, - op: UserOperationOptionalGas, + op: UserOperationOptionalGasV0_6, entry_point: Address, state_override: Option, ) -> RpcResult { diff --git a/crates/rpc/src/rundler.rs b/crates/rpc/src/rundler.rs index 5f11815bf..ddb648aa8 100644 --- a/crates/rpc/src/rundler.rs +++ b/crates/rpc/src/rundler.rs @@ -23,7 +23,7 @@ use jsonrpsee::{ use rundler_pool::PoolServer; use rundler_provider::{EntryPoint, Provider}; use rundler_sim::{gas, FeeEstimator}; -use rundler_types::{chain::ChainSpec, UserOperation, UserOperationId}; +use rundler_types::{chain::ChainSpec, UserOperation, UserOperationId, UserOperationV0_6}; use crate::{error::rpc_err, eth::EthRpcError, RpcUserOperation}; @@ -126,7 +126,7 @@ where )); } - let uo: UserOperation = user_op.into(); + let uo: UserOperationV0_6 = user_op.into(); let id = UserOperationId { sender: uo.sender, nonce: uo.nonce, @@ -145,7 +145,7 @@ where let output = self .entry_point - .call_simulate_validation(uo, self.settings.max_verification_gas) + .call_simulate_validation(uo.into(), self.settings.max_verification_gas) .await .map_err(|e| rpc_err(INTERNAL_ERROR_CODE, e.to_string()))?; diff --git a/crates/rpc/src/task.rs b/crates/rpc/src/task.rs index 54b05e6d4..492d28eff 100644 --- a/crates/rpc/src/task.rs +++ b/crates/rpc/src/task.rs @@ -22,7 +22,7 @@ use jsonrpsee::{ }; use rundler_builder::BuilderServer; use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, EthersEntryPoint}; +use rundler_provider::{EntryPoint, EthersEntryPointV0_6}; use rundler_sim::{EstimationSettings, PrecheckSettings}; use rundler_task::{ server::{format_socket_addr, HealthCheck}, @@ -88,7 +88,8 @@ where tracing::info!("Starting rpc server on {}", addr); let provider = rundler_provider::new_provider(&self.args.rpc_url, None)?; - let ep = EthersEntryPoint::new(self.args.chain_spec.entry_point_address, provider.clone()); + let ep = + EthersEntryPointV0_6::new(self.args.chain_spec.entry_point_address, provider.clone()); let mut module = RpcModule::new(()); self.attach_namespaces(provider, ep, &mut module)?; @@ -160,7 +161,7 @@ where self.args.chain_spec.clone(), provider.clone(), // TODO: support multiple entry points - vec![entry_point.clone()], + vec![Arc::new(entry_point.clone())], self.pool.clone(), self.args.eth_api_settings, self.args.estimation_settings, diff --git a/crates/rpc/src/types.rs b/crates/rpc/src/types.rs index 30c182858..b75db3a41 100644 --- a/crates/rpc/src/types.rs +++ b/crates/rpc/src/types.rs @@ -16,7 +16,7 @@ use ethers::{ utils::to_checksum, }; use rundler_pool::{Reputation, ReputationStatus}; -use rundler_types::UserOperation; +use rundler_types::UserOperationV0_6; use serde::{Deserialize, Deserializer, Serialize, Serializer}; /// API namespace @@ -96,8 +96,8 @@ pub struct RpcUserOperation { signature: Bytes, } -impl From for RpcUserOperation { - fn from(op: UserOperation) -> Self { +impl From for RpcUserOperation { + fn from(op: UserOperationV0_6) -> Self { RpcUserOperation { sender: op.sender.into(), nonce: op.nonce, @@ -114,9 +114,9 @@ impl From for RpcUserOperation { } } -impl From for UserOperation { +impl From for UserOperationV0_6 { fn from(def: RpcUserOperation) -> Self { - UserOperation { + UserOperationV0_6 { sender: def.sender.into(), nonce: def.nonce, init_code: def.init_code, diff --git a/crates/sim/src/estimation/mod.rs b/crates/sim/src/estimation/mod.rs index ce3a96c45..d2f85cdd4 100644 --- a/crates/sim/src/estimation/mod.rs +++ b/crates/sim/src/estimation/mod.rs @@ -11,9 +11,87 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -#[allow(clippy::module_inception)] -mod estimation; -pub use estimation::*; +use ethers::types::{Bytes, U256}; +#[cfg(feature = "test-utils")] +use mockall::automock; +use rundler_types::UserOperation; +use serde::{Deserialize, Serialize}; -mod types; -pub use types::{GasEstimate, Settings, UserOperationOptionalGas}; +use crate::precheck::MIN_CALL_GAS_LIMIT; + +mod v0_6; +pub use v0_6::{GasEstimatorV0_6, UserOperationOptionalGasV0_6}; + +/// Error type for gas estimation +#[derive(Debug, thiserror::Error)] +pub enum GasEstimationError { + /// Validation reverted + #[error("{0}")] + RevertInValidation(String), + /// Call reverted with a string message + #[error("user operation's call reverted: {0}")] + RevertInCallWithMessage(String), + /// Call reverted with bytes + #[error("user operation's call reverted: {0:#x}")] + RevertInCallWithBytes(Bytes), + /// Other error + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +/// Gas estimate for a user operation +#[derive(Debug, Copy, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GasEstimate { + /// Pre verification gas estimate + pub pre_verification_gas: U256, + /// Verification gas limit estimate + pub verification_gas_limit: U256, + /// Call gas limit estimate + pub call_gas_limit: U256, +} + +/// Gas estimator trait +#[cfg_attr(feature = "test-utils", automock(type UserOperationOptionalGas = UserOperation;))] +#[async_trait::async_trait] +pub trait GasEstimator: Send + Sync + 'static { + type UserOperationOptionalGas; + + /// Returns a gas estimate or a revert message, or an anyhow error on any + /// other error. + async fn estimate_op_gas( + &self, + op: Self::UserOperationOptionalGas, + state_override: ethers::types::spoof::State, + ) -> Result; +} + +/// Settings for gas estimation +#[derive(Clone, Copy, Debug)] +pub struct Settings { + /// The maximum amount of gas that can be used for the verification step of a user operation + pub max_verification_gas: u64, + /// The maximum amount of gas that can be used for the call step of a user operation + pub max_call_gas: u64, + /// The maximum amount of gas that can be used in a call to `simulateHandleOps` + pub max_simulate_handle_ops_gas: u64, + /// The gas fee to use during validation gas estimation, required to be held by the fee-payer + /// during estimation. If using a paymaster, the fee-payer must have 3x this value. + /// As the gas limit is varied during estimation, the fee is held constant by varied the + /// gas price. + /// Clients can use state overrides to set the balance of the fee-payer to at least this value. + pub validation_estimation_gas_fee: u64, +} + +impl Settings { + /// Check if the settings are valid + pub fn validate(&self) -> Option { + if U256::from(self.max_call_gas) + .cmp(&MIN_CALL_GAS_LIMIT) + .is_lt() + { + return Some("max_call_gas field cannot be lower than MIN_CALL_GAS_LIMIT".to_string()); + } + None + } +} diff --git a/crates/sim/src/estimation/estimation.rs b/crates/sim/src/estimation/v0_6/estimator_v0_6.rs similarity index 94% rename from crates/sim/src/estimation/estimation.rs rename to crates/sim/src/estimation/v0_6/estimator_v0_6.rs index 2daac5c33..37c291ee8 100644 --- a/crates/sim/src/estimation/estimation.rs +++ b/crates/sim/src/estimation/v0_6/estimator_v0_6.rs @@ -24,13 +24,11 @@ use ethers::{ providers::spoof, types::{Address, Bytes, H256, U256}, }; -#[cfg(feature = "test-utils")] -use mockall::automock; use rand::Rng; use rundler_provider::{EntryPoint, Provider}; use rundler_types::{ chain::ChainSpec, - contracts::{ + contracts::v0_6::{ call_gas_estimation_proxy::{ EstimateCallGasArgs, EstimateCallGasCall, EstimateCallGasContinuation, EstimateCallGasResult, EstimateCallGasRevertAtMax, @@ -38,12 +36,15 @@ use rundler_types::{ }, i_entry_point, }, - UserOperation, + UserOperationV0_6, }; use rundler_utils::{eth, math}; use tokio::join; -use super::types::{GasEstimate, Settings, UserOperationOptionalGas}; +use super::{ + super::{GasEstimate, GasEstimationError, GasEstimator, Settings}, + types::UserOperationOptionalGas, +}; use crate::{gas, precheck::MIN_CALL_GAS_LIMIT, simulation, utils, FeeEstimator}; /// Gas estimates will be rounded up to the next multiple of this. Increasing @@ -69,48 +70,20 @@ const CALL_GAS_BUFFER_VALUE: U256 = U256([3000, 0, 0, 0]); /// failure will tell you the new value. const PROXY_TARGET_OFFSET: usize = 137; -/// Error type for gas estimation -#[derive(Debug, thiserror::Error)] -pub enum GasEstimationError { - /// Validation reverted - #[error("{0}")] - RevertInValidation(String), - /// Call reverted with a string message - #[error("user operation's call reverted: {0}")] - RevertInCallWithMessage(String), - /// Call reverted with bytes - #[error("user operation's call reverted: {0:#x}")] - RevertInCallWithBytes(Bytes), - /// Other error - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -/// Gas estimator trait -#[cfg_attr(feature = "test-utils", automock)] -#[async_trait::async_trait] -pub trait GasEstimator: Send + Sync + 'static { - /// Returns a gas estimate or a revert message, or an anyhow error on any - /// other error. - async fn estimate_op_gas( - &self, - op: UserOperationOptionalGas, - state_override: spoof::State, - ) -> Result; -} - /// Gas estimator implementation #[derive(Debug)] -pub struct GasEstimatorImpl { +pub struct GasEstimatorV0_6 { chain_spec: ChainSpec, provider: Arc

, - entry_point: E, + entry_point: Arc, settings: Settings, fee_estimator: FeeEstimator

, } #[async_trait::async_trait] -impl GasEstimator for GasEstimatorImpl { +impl GasEstimator for GasEstimatorV0_6 { + type UserOperationOptionalGas = UserOperationOptionalGas; + async fn estimate_op_gas( &self, op: UserOperationOptionalGas, @@ -137,7 +110,7 @@ impl GasEstimator for GasEstimatorImpl { }; let pre_verification_gas = self.estimate_pre_verification_gas(&op, gas_price).await?; - let op = UserOperation { + let op = UserOperationV0_6 { pre_verification_gas, ..op.into_user_operation(settings) }; @@ -163,7 +136,7 @@ impl GasEstimator for GasEstimatorImpl { // to ensure we get at least a 2000 gas buffer. Cap at the max verification gas. let verification_gas_limit = cmp::max( math::increase_by_percent(verification_gas_limit, VERIFICATION_GAS_BUFFER_PERCENT), - verification_gas_limit + simulation::REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER, + verification_gas_limit + simulation::REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER_V0_6, ) .min(settings.max_verification_gas.into()); @@ -180,12 +153,12 @@ impl GasEstimator for GasEstimatorImpl { } } -impl GasEstimatorImpl { +impl GasEstimatorV0_6 { /// Create a new gas estimator pub fn new( chain_spec: ChainSpec, provider: Arc

, - entry_point: E, + entry_point: Arc, settings: Settings, fee_estimator: FeeEstimator

, ) -> Self { @@ -200,7 +173,7 @@ impl GasEstimatorImpl { async fn binary_search_verification_gas( &self, - op: &UserOperation, + op: &UserOperationV0_6, block_hash: H256, state_override: &spoof::State, ) -> Result { @@ -211,7 +184,7 @@ impl GasEstimatorImpl { // Make one attempt at max gas, to see if success is possible. // Capture the gas usage of this attempt and use as the initial guess in the binary search - let initial_op = UserOperation { + let initial_op = UserOperationV0_6 { verification_gas_limit: simulation_gas, max_fee_per_gas: gas_fee .checked_div(simulation_gas + op.pre_verification_gas) @@ -248,7 +221,7 @@ impl GasEstimatorImpl { let max_fee_per_gas = gas_fee .checked_div(U256::from(gas) + op.pre_verification_gas) .unwrap_or(U256::MAX); - let op = UserOperation { + let op = UserOperationV0_6 { max_fee_per_gas, verification_gas_limit: gas.into(), call_gas_limit: 0.into(), @@ -257,7 +230,7 @@ impl GasEstimatorImpl { let error_message = self .entry_point .call_spoofed_simulate_op( - op, + op.into(), Address::zero(), Bytes::new(), block_hash, @@ -318,7 +291,7 @@ impl GasEstimatorImpl { async fn estimate_call_gas( &self, - op: &UserOperation, + op: &UserOperationV0_6, block_hash: H256, mut state_override: spoof::State, ) -> Result { @@ -343,7 +316,7 @@ impl GasEstimatorImpl { .account(self.entry_point.address()) .code(estimation_proxy_bytecode); - let callless_op = UserOperation { + let callless_op = UserOperationV0_6 { call_gas_limit: 0.into(), max_fee_per_gas: 0.into(), verification_gas_limit: self.settings.max_verification_gas.into(), @@ -369,7 +342,7 @@ impl GasEstimatorImpl { let target_revert_data = self .entry_point .call_spoofed_simulate_op( - callless_op.clone(), + callless_op.clone().into(), self.entry_point.address(), target_call_data, block_hash, @@ -425,9 +398,9 @@ impl GasEstimatorImpl { ) -> Result { Ok(gas::estimate_pre_verification_gas( &self.chain_spec, - self.provider.clone(), - &op.max_fill(&self.settings), - &op.random_fill(&self.settings), + self.entry_point.clone(), + &op.max_fill(&self.settings).into(), + &op.random_fill(&self.settings).into(), gas_price, ) .await?) @@ -452,7 +425,7 @@ mod tests { use rundler_provider::{MockEntryPoint, MockProvider}; use rundler_types::{ chain::L1GasOracleContractType, - contracts::{get_gas_used::GasUsedResult, i_entry_point::ExecutionResult}, + contracts::{utils::get_gas_used::GasUsedResult, v0_6::i_entry_point::ExecutionResult}, }; use super::*; @@ -486,7 +459,7 @@ mod tests { fn create_estimator( entry: MockEntryPoint, provider: MockProvider, - ) -> (GasEstimatorImpl, Settings) { + ) -> (GasEstimatorV0_6, Settings) { let settings = Settings { max_verification_gas: 10000000000, max_call_gas: 10000000000, @@ -494,7 +467,8 @@ mod tests { validation_estimation_gas_fee: 1_000_000_000_000, }; let provider = Arc::new(provider); - let estimator: GasEstimatorImpl = GasEstimatorImpl::new( + let entry = Arc::new(entry); + let estimator: GasEstimatorV0_6 = GasEstimatorV0_6::new( ChainSpec::default(), provider.clone(), entry, @@ -521,8 +495,8 @@ mod tests { } } - fn demo_user_op() -> UserOperation { - UserOperation { + fn demo_user_op() -> UserOperationV0_6 { + UserOperationV0_6 { sender: Address::zero(), nonce: U256::zero(), init_code: Bytes::new(), @@ -585,7 +559,7 @@ mod tests { async fn test_calc_pre_verification_input_arbitrum() { let (mut entry, mut provider) = create_base_config(); entry.expect_address().return_const(Address::zero()); - provider + entry .expect_calc_arbitrum_l1_gas() .returning(|_a, _b| Ok(U256::from(1000))); @@ -604,7 +578,8 @@ mod tests { ..Default::default() }; let provider = Arc::new(provider); - let estimator: GasEstimatorImpl = GasEstimatorImpl::new( + let entry = Arc::new(entry); + let estimator: GasEstimatorV0_6 = GasEstimatorV0_6::new( cs, provider.clone(), entry, @@ -644,7 +619,7 @@ mod tests { let (mut entry, mut provider) = create_base_config(); entry.expect_address().return_const(Address::zero()); - provider + entry .expect_calc_optimism_l1_gas() .returning(|_a, _b, _c| Ok(U256::from(1000))); @@ -663,7 +638,8 @@ mod tests { ..Default::default() }; let provider = Arc::new(provider); - let estimator: GasEstimatorImpl = GasEstimatorImpl::new( + let entry = Arc::new(entry); + let estimator: GasEstimatorV0_6 = GasEstimatorV0_6::new( cs, provider.clone(), entry, @@ -720,7 +696,7 @@ mod tests { entry .expect_call_spoofed_simulate_op() .returning(move |op, _b, _c, _d, _e, _f| { - if op.verification_gas_limit < gas_usage { + if op.total_verification_gas_limit() < gas_usage { return Ok(Err("AA23".to_string())); } @@ -1142,7 +1118,7 @@ mod tests { entry .expect_call_spoofed_simulate_op() .returning(move |op, _b, _c, _d, _e, _f| { - if op.verification_gas_limit < gas_usage { + if op.total_verification_gas_limit() < gas_usage { return Ok(Err("AA23".to_string())); } @@ -1210,7 +1186,7 @@ mod tests { estimation.verification_gas_limit, cmp::max( math::increase_by_percent(gas_usage, 10), - gas_usage + simulation::REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER + gas_usage + simulation::REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER_V0_6 ) ); @@ -1286,7 +1262,8 @@ mod tests { }; let provider = Arc::new(provider); - let estimator: GasEstimatorImpl = GasEstimatorImpl::new( + let entry = Arc::new(entry); + let estimator: GasEstimatorV0_6 = GasEstimatorV0_6::new( ChainSpec::default(), provider.clone(), entry, diff --git a/crates/sim/src/estimation/v0_6/mod.rs b/crates/sim/src/estimation/v0_6/mod.rs new file mode 100644 index 000000000..cdf09a658 --- /dev/null +++ b/crates/sim/src/estimation/v0_6/mod.rs @@ -0,0 +1,17 @@ +// 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/. + +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/types.rs b/crates/sim/src/estimation/v0_6/types.rs similarity index 80% rename from crates/sim/src/estimation/types.rs rename to crates/sim/src/estimation/v0_6/types.rs index 664d55bfa..bdc3f969c 100644 --- a/crates/sim/src/estimation/types.rs +++ b/crates/sim/src/estimation/v0_6/types.rs @@ -13,40 +13,10 @@ use ethers::types::{Address, Bytes, U256}; use rand::RngCore; -use rundler_types::UserOperation; +use rundler_types::{UserOperation, UserOperationV0_6}; use serde::{Deserialize, Serialize}; -use crate::precheck::MIN_CALL_GAS_LIMIT; - -/// Settings for gas estimation -#[derive(Clone, Copy, Debug)] -pub struct Settings { - /// The maximum amount of gas that can be used for the verification step of a user operation - pub max_verification_gas: u64, - /// The maximum amount of gas that can be used for the call step of a user operation - pub max_call_gas: u64, - /// The maximum amount of gas that can be used in a call to `simulateHandleOps` - pub max_simulate_handle_ops_gas: u64, - /// The gas fee to use during validation gas estimation, required to be held by the fee-payer - /// during estimation. If using a paymaster, the fee-payer must have 3x this value. - /// As the gas limit is varied during estimation, the fee is held constant by varied the - /// gas price. - /// Clients can use state overrides to set the balance of the fee-payer to at least this value. - pub validation_estimation_gas_fee: u64, -} - -impl Settings { - /// Check if the settings are valid - pub fn validate(&self) -> Option { - if U256::from(self.max_call_gas) - .cmp(&MIN_CALL_GAS_LIMIT) - .is_lt() - { - return Some("max_call_gas field cannot be lower than MIN_CALL_GAS_LIMIT".to_string()); - } - None - } -} +use super::super::{GasEstimate, Settings}; /// User operation with optional gas fields for gas estimation #[derive(Serialize, Deserialize, Clone, Debug)] @@ -102,8 +72,8 @@ impl UserOperationOptionalGas { /// cover the modified user op, calculate the gas needed for the worst /// case scenario where the gas fields of the user operation are entirely /// nonzero bytes. Likewise for the signature field. - pub fn max_fill(&self, settings: &Settings) -> UserOperation { - UserOperation { + 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, @@ -124,8 +94,8 @@ impl UserOperationOptionalGas { // /// Note that this will slightly overestimate the calldata gas needed as it uses /// the worst case scenario for the unknown gas values and paymaster_and_data. - pub fn random_fill(&self, settings: &Settings) -> UserOperation { - UserOperation { + 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 @@ -139,8 +109,8 @@ impl UserOperationOptionalGas { /// Convert into a full user operation. /// Fill in the optional fields of the user operation with default values if unset - pub fn into_user_operation(self, settings: &Settings) -> UserOperation { - UserOperation { + pub fn into_user_operation(self, settings: &Settings) -> UserOperationV0_6 { + UserOperationV0_6 { sender: self.sender, nonce: self.nonce, init_code: self.init_code, @@ -167,8 +137,8 @@ impl UserOperationOptionalGas { /// Convert into a full user operation with the provided gas estimates. /// /// Fee fields are left unchanged or are defaulted. - pub fn into_user_operation_with_estimates(self, estimates: GasEstimate) -> UserOperation { - UserOperation { + 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, @@ -184,16 +154,16 @@ impl UserOperationOptionalGas { } /// Convert from a full user operation, keeping the gas fields set - pub fn from_user_operation_keeping_gas(op: UserOperation) -> Self { + 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: UserOperation) -> Self { + pub fn from_user_operation_without_gas(op: UserOperationV0_6) -> Self { Self::from_user_operation(op, false) } - fn from_user_operation(op: UserOperation, keep_gas: bool) -> Self { + 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, @@ -216,15 +186,3 @@ impl UserOperationOptionalGas { bytes.into() } } - -/// Gas estimate for a user operation -#[derive(Debug, Copy, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GasEstimate { - /// Pre verification gas estimate - pub pre_verification_gas: U256, - /// Verification gas limit estimate - pub verification_gas_limit: U256, - /// Call gas limit estimate - pub call_gas_limit: U256, -} diff --git a/crates/sim/src/gas/gas.rs b/crates/sim/src/gas/gas.rs index 002c1a3ce..d49e05318 100644 --- a/crates/sim/src/gas/gas.rs +++ b/crates/sim/src/gas/gas.rs @@ -14,8 +14,8 @@ use std::{cmp, fmt::Debug, sync::Arc}; use anyhow::Context; -use ethers::{abi::AbiEncode, types::U256}; -use rundler_provider::Provider; +use ethers::types::U256; +use rundler_provider::{EntryPoint, Provider}; use rundler_types::{ chain::{self, ChainSpec, L1GasOracleContractType}, GasFees, UserOperation, @@ -27,32 +27,6 @@ use super::oracle::{ ConstantOracle, FeeOracle, ProviderOracle, UsageBasedFeeOracle, UsageBasedFeeOracleConfig, }; -/// Gas overheads for user operations used in calculating the pre-verification gas. See: https://github.com/eth-infinitism/bundler/blob/main/packages/sdk/src/calcPreVerificationGas.ts -#[derive(Clone, Copy, Debug)] -pub struct GasOverheads { - /// The Entrypoint requires a gas buffer for the bundle to account for the gas spent outside of the major steps in the processing of UOs - pub bundle_transaction_gas_buffer: U256, - /// The fixed gas overhead for any EVM transaction - pub transaction_gas_overhead: U256, - per_user_op: U256, - per_user_op_word: U256, - zero_byte: U256, - non_zero_byte: U256, -} - -impl Default for GasOverheads { - fn default() -> Self { - Self { - bundle_transaction_gas_buffer: 5_000.into(), - transaction_gas_overhead: 21_000.into(), - per_user_op: 18_300.into(), - per_user_op_word: 4.into(), - zero_byte: 4.into(), - non_zero_byte: 16.into(), - } - } -} - /// Returns the required pre_verification_gas for the given user operation /// /// `full_op` is either the user operation submitted via `sendUserOperation` @@ -66,14 +40,14 @@ impl Default for GasOverheads { /// /// Networks that require dynamic pre_verification_gas are typically those that charge extra calldata fees /// that can scale based on dynamic gas prices. -pub async fn estimate_pre_verification_gas( +pub async fn estimate_pre_verification_gas( chain_spec: &ChainSpec, - provider: Arc

, + enty_point: Arc, full_op: &UserOperation, random_op: &UserOperation, gas_price: U256, ) -> anyhow::Result { - let static_gas = calc_static_pre_verification_gas(full_op, true); + let static_gas = full_op.calc_static_pre_verification_gas(true); if !chain_spec.calldata_pre_verification_gas { return Ok(static_gas); } @@ -81,13 +55,13 @@ pub async fn estimate_pre_verification_gas( let dynamic_gas = match chain_spec.l1_gas_oracle_contract_type { L1GasOracleContractType::None => panic!("Chain spec requires calldata pre_verification_gas but no l1_gas_oracle_contract_type is set"), L1GasOracleContractType::ArbitrumNitro => { - provider + enty_point .clone() .calc_arbitrum_l1_gas(chain_spec.entry_point_address, random_op.clone()) .await? }, L1GasOracleContractType::OptimismBedrock => { - provider + enty_point .clone() .calc_optimism_l1_gas(chain_spec.entry_point_address, random_op.clone(), gas_price) .await? @@ -100,13 +74,13 @@ pub async fn estimate_pre_verification_gas( /// Calculate the required pre_verification_gas for the given user operation and the provided base fee. /// /// The effective gas price is calculated as min(base_fee + max_priority_fee_per_gas, max_fee_per_gas) -pub async fn calc_required_pre_verification_gas( +pub async fn calc_required_pre_verification_gas( chain_spec: &ChainSpec, - provider: Arc

, + entry_point: Arc, op: &UserOperation, base_fee: U256, ) -> anyhow::Result { - let static_gas = calc_static_pre_verification_gas(op, true); + let static_gas = op.calc_static_pre_verification_gas(true); if !chain_spec.calldata_pre_verification_gas { return Ok(static_gas); } @@ -114,15 +88,15 @@ pub async fn calc_required_pre_verification_gas( let dynamic_gas = match chain_spec.l1_gas_oracle_contract_type { L1GasOracleContractType::None => panic!("Chain spec requires calldata pre_verification_gas but no l1_gas_oracle_contract_type is set"), L1GasOracleContractType::ArbitrumNitro => { - provider + entry_point .clone() .calc_arbitrum_l1_gas(chain_spec.entry_point_address, op.clone()) .await? }, L1GasOracleContractType::OptimismBedrock => { - let gas_price = cmp::min(base_fee + op.max_priority_fee_per_gas, op.max_fee_per_gas); + let gas_price = cmp::min(base_fee + op.max_priority_fee_per_gas(), op.max_fee_per_gas()); - provider + entry_point .clone() .calc_optimism_l1_gas(chain_spec.entry_point_address, op.clone(), gas_price) .await? @@ -156,9 +130,9 @@ pub fn user_operation_gas_limit( paymaster_post_op: bool, ) -> U256 { user_operation_pre_verification_gas_limit(chain_spec, uo, assume_single_op_bundle) - + uo.call_gas_limit - + uo.verification_gas_limit - * verification_gas_limit_multiplier(assume_single_op_bundle, paymaster_post_op) + + uo.total_verification_gas_limit() + + uo.required_pre_execution_buffer() + + uo.call_gas_limit() } /// Returns the gas limit for the user operation that applies to bundle transaction's execution limit @@ -169,9 +143,9 @@ pub fn user_operation_execution_gas_limit( paymaster_post_op: bool, ) -> U256 { user_operation_pre_verification_execution_gas_limit(chain_spec, uo, assume_single_op_bundle) - + uo.call_gas_limit - + uo.verification_gas_limit - * verification_gas_limit_multiplier(assume_single_op_bundle, paymaster_post_op) + + uo.call_gas_limit() + + uo.required_pre_execution_buffer() + + uo.total_verification_gas_limit() } /// Returns the static pre-verification gas cost of a user operation @@ -184,9 +158,9 @@ pub fn user_operation_pre_verification_execution_gas_limit( // but this not part of the EXECUTION gas limit of the transaction. // In such cases we only consider the static portion of the pre_verification_gas in the gas limit. if chain_spec.calldata_pre_verification_gas { - calc_static_pre_verification_gas(uo, include_fixed_gas_overhead) + uo.calc_static_pre_verification_gas(include_fixed_gas_overhead) } else { - uo.pre_verification_gas + uo.pre_verification_gas() } } @@ -200,51 +174,9 @@ pub fn user_operation_pre_verification_gas_limit( // but this not part of the execution TOTAL limit of the transaction. // In such cases we only consider the static portion of the pre_verification_gas in the gas limit. if chain_spec.calldata_pre_verification_gas && !chain_spec.include_l1_gas_in_gas_limit { - calc_static_pre_verification_gas(uo, include_fixed_gas_overhead) - } else { - uo.pre_verification_gas - } -} - -fn calc_static_pre_verification_gas(op: &UserOperation, include_fixed_gas_overhead: bool) -> U256 { - let ov = GasOverheads::default(); - let encoded_op = op.clone().encode(); - let length_in_words = encoded_op.len() / 32; // size of packed user op is always a multiple of 32 bytes - let call_data_cost: U256 = encoded_op - .iter() - .map(|&x| { - if x == 0 { - ov.zero_byte - } else { - ov.non_zero_byte - } - }) - .reduce(|a, b| a + b) - .unwrap_or_default(); - - call_data_cost - + ov.per_user_op - + ov.per_user_op_word * length_in_words - + (if include_fixed_gas_overhead { - ov.transaction_gas_overhead - } else { - 0.into() - }) -} - -fn verification_gas_limit_multiplier( - assume_single_op_bundle: bool, - paymaster_post_op: bool, -) -> u64 { - // If using a paymaster that has a postOp, we need to account for potentially 2 postOp calls which can each use up to verification_gas_limit gas. - // otherwise the entrypoint expects the gas for 1 postOp call that uses verification_gas_limit plus the actual verification call - // we only add the additional verification_gas_limit only if we know for sure that this is a single op bundle, which what we do to get a worst-case upper bound - if paymaster_post_op { - 3 - } else if assume_single_op_bundle { - 2 + uo.calc_static_pre_verification_gas(include_fixed_gas_overhead) } else { - 1 + uo.pre_verification_gas() } } diff --git a/crates/sim/src/lib.rs b/crates/sim/src/lib.rs index db94aeb37..28f05fa40 100644 --- a/crates/sim/src/lib.rs +++ b/crates/sim/src/lib.rs @@ -32,8 +32,8 @@ mod estimation; pub use estimation::{ - GasEstimate, GasEstimationError, GasEstimator, GasEstimatorImpl, - Settings as EstimationSettings, UserOperationOptionalGas, + GasEstimate, GasEstimationError, GasEstimator, GasEstimatorV0_6, + Settings as EstimationSettings, UserOperationOptionalGasV0_6, }; pub mod gas; @@ -53,7 +53,7 @@ pub use simulation::MockSimulator; pub use simulation::{ EntityInfo, EntityInfos, MempoolConfig, NeedsStakeInformation, Settings as SimulationSettings, SimulateValidationTracer, SimulateValidationTracerImpl, SimulationError, SimulationResult, - SimulationViolation, Simulator, SimulatorImpl, ViolationOpCode, + SimulationViolation, Simulator, SimulatorV0_6, ViolationOpCode, }; mod types; diff --git a/crates/sim/src/precheck.rs b/crates/sim/src/precheck.rs index 0558b0d8d..404f9577f 100644 --- a/crates/sim/src/precheck.rs +++ b/crates/sim/src/precheck.rs @@ -46,7 +46,7 @@ pub type PrecheckError = ViolationError; pub struct PrecheckerImpl { chain_spec: ChainSpec, provider: Arc

, - entry_point: E, + entry_point: Arc, settings: Settings, fee_estimator: gas::FeeEstimator

, @@ -138,7 +138,7 @@ impl PrecheckerImpl { pub fn new( chain_spec: ChainSpec, provider: Arc

, - entry_point: E, + entry_point: Arc, settings: Settings, ) -> Self { let fee_estimator = gas::FeeEstimator::new( @@ -169,23 +169,20 @@ impl PrecheckerImpl { .. } = async_data; let mut violations = ArrayVec::new(); - let len = op.init_code.len(); - if len == 0 { + if op.factory().is_none() { if !sender_exists { violations.push(PrecheckViolation::SenderIsNotContractAndNoInitCode( - op.sender, + op.sender(), )); } } else { - if len < 20 { - violations.push(PrecheckViolation::InitCodeTooShort(len)); - } else if !factory_exists { + if !factory_exists { violations.push(PrecheckViolation::FactoryIsNotContract( op.factory().unwrap(), )) } if sender_exists { - violations.push(PrecheckViolation::ExistingSenderWithInitCode(op.sender)); + violations.push(PrecheckViolation::ExistingSenderWithInitCode(op.sender())); } } violations @@ -208,9 +205,9 @@ impl PrecheckerImpl { } = async_data; let mut violations = ArrayVec::new(); - if op.verification_gas_limit > max_verification_gas { + if op.verification_gas_limit() > max_verification_gas { violations.push(PrecheckViolation::VerificationGasLimitTooHigh( - op.verification_gas_limit, + op.verification_gas_limit(), max_verification_gas, )); } @@ -231,9 +228,9 @@ impl PrecheckerImpl { min_pre_verification_gas, self.settings.pre_verification_gas_accept_percent, ); - if op.pre_verification_gas < min_pre_verification_gas { + if op.pre_verification_gas() < min_pre_verification_gas { violations.push(PrecheckViolation::PreVerificationGasTooLow( - op.pre_verification_gas, + op.pre_verification_gas(), min_pre_verification_gas, )); } @@ -248,22 +245,22 @@ impl PrecheckerImpl { let min_max_fee = min_base_fee + min_priority_fee; // check priority fee first, since once ruled out we can check max fee - if op.max_priority_fee_per_gas < min_priority_fee { + if op.max_priority_fee_per_gas() < min_priority_fee { violations.push(PrecheckViolation::MaxPriorityFeePerGasTooLow( - op.max_priority_fee_per_gas, + op.max_priority_fee_per_gas(), min_priority_fee, )); } - if op.max_fee_per_gas < min_max_fee { + if op.max_fee_per_gas() < min_max_fee { violations.push(PrecheckViolation::MaxFeePerGasTooLow( - op.max_fee_per_gas, + op.max_fee_per_gas(), min_max_fee, )); } - if op.call_gas_limit < MIN_CALL_GAS_LIMIT { + if op.call_gas_limit() < MIN_CALL_GAS_LIMIT { violations.push(PrecheckViolation::CallGasLimitTooLow( - op.call_gas_limit, + op.call_gas_limit(), MIN_CALL_GAS_LIMIT, )); } @@ -276,19 +273,14 @@ impl PrecheckerImpl { payer_funds, .. } = async_data; - if !op.paymaster_and_data.is_empty() { - let Some(paymaster) = op.paymaster() else { - return Some(PrecheckViolation::PaymasterTooShort( - op.paymaster_and_data.len(), - )); - }; + if let Some(paymaster) = op.paymaster() { if !paymaster_exists { return Some(PrecheckViolation::PaymasterIsNotContract(paymaster)); } } let max_gas_cost = op.max_gas_cost(); if payer_funds < max_gas_cost { - if op.paymaster_and_data.is_empty() { + if op.paymaster().is_none() { return Some(PrecheckViolation::SenderFundsTooLow( payer_funds, max_gas_cost, @@ -314,7 +306,7 @@ impl PrecheckerImpl { min_pre_verification_gas, ) = tokio::try_join!( self.is_contract(op.factory()), - self.is_contract(Some(op.sender)), + self.is_contract(Some(op.sender())), self.is_contract(op.paymaster()), self.get_payer_funds(op), self.get_required_pre_verification_gas(op.clone(), base_fee) @@ -350,7 +342,7 @@ impl PrecheckerImpl { async fn get_payer_deposit(&self, op: &UserOperation) -> anyhow::Result { let payer = match op.paymaster() { Some(paymaster) => paymaster, - None => op.sender, + None => op.sender(), }; self.entry_point .balance_of(payer, None) @@ -359,12 +351,12 @@ impl PrecheckerImpl { } async fn get_payer_balance(&self, op: &UserOperation) -> anyhow::Result { - if !op.paymaster_and_data.is_empty() { + if op.paymaster().is_some() { // Paymasters must deposit eth, and cannot pay with their own. return Ok(0.into()); } self.provider - .get_balance(op.sender, None) + .get_balance(op.sender(), None) .await .context("precheck should get sender balance") } @@ -383,7 +375,7 @@ impl PrecheckerImpl { ) -> anyhow::Result { gas::calc_required_pre_verification_gas( &self.chain_spec, - self.provider.clone(), + self.entry_point.clone(), &op, base_fee, ) @@ -397,9 +389,6 @@ impl PrecheckerImpl { /// All possible errors that can be returned from a precheck. #[derive(Clone, Debug, parse_display::Display, Eq, PartialEq, Ord, PartialOrd)] pub enum PrecheckViolation { - /// The init code is too short to contain a factory address. - #[display("initCode must start with a 20-byte factory address, but was only {0} bytes")] - InitCodeTooShort(usize), /// The sender is not deployed, and no init code is provided. #[display("sender {0:?} is not a contract and initCode is empty")] SenderIsNotContractAndNoInitCode(Address), @@ -419,9 +408,6 @@ pub enum PrecheckViolation { /// The pre-verification gas of the user operation is too low. #[display("preVerificationGas is {0} but must be at least {1}")] PreVerificationGasTooLow(U256, U256), - /// The paymaster and data is too short to contain a paymaster address. - #[display("paymasterAndData must start a 20-byte paymaster address, but was only {0} bytes")] - PaymasterTooShort(usize), /// A paymaster is provided, but the address is not deployed. #[display("paymasterAndData indicates paymaster with no code: {0:?}")] PaymasterIsNotContract(Address), @@ -449,6 +435,7 @@ mod tests { use ethers::types::Bytes; use rundler_provider::{MockEntryPoint, MockProvider}; + use rundler_types::UserOperationV0_6; use super::*; @@ -474,9 +461,13 @@ mod tests { #[tokio::test] async fn test_check_init_code() { let (cs, provider, entry_point) = create_base_config(); - let prechecker = - PrecheckerImpl::new(cs, Arc::new(provider), entry_point, Settings::default()); - let op = UserOperation { + let prechecker = PrecheckerImpl::new( + cs, + Arc::new(provider), + Arc::new(entry_point), + Settings::default(), + ); + let op = UserOperation::V0_6(UserOperationV0_6 { sender: Address::from_str("0x3f8a2b6c4d5e1079286fa1b3c0d4e5f6902b7c8d").unwrap(), nonce: 100.into(), init_code: Bytes::from_str("0x1000").unwrap(), @@ -488,18 +479,14 @@ mod tests { max_priority_fee_per_gas: 2_000.into(), paymaster_and_data: Bytes::default(), signature: Bytes::default(), - }; + }); let res = prechecker.check_init_code(&op, get_test_async_data()); - assert_eq!( - res, - ArrayVec::::from([ - PrecheckViolation::InitCodeTooShort(2), - PrecheckViolation::ExistingSenderWithInitCode( - Address::from_str("0x3f8a2b6c4d5e1079286fa1b3c0d4e5f6902b7c8d").unwrap() - ) - ]) - ); + let mut expected = ArrayVec::new(); + expected.push(PrecheckViolation::ExistingSenderWithInitCode( + Address::from_str("0x3f8a2b6c4d5e1079286fa1b3c0d4e5f6902b7c8d").unwrap(), + )); + assert_eq!(res, expected); } #[tokio::test] @@ -513,8 +500,9 @@ mod tests { base_fee_accept_percent: 100, pre_verification_gas_accept_percent: 100, }; - let prechecker = PrecheckerImpl::new(cs, Arc::new(provider), entry_point, test_settings); - let op = UserOperation { + let prechecker = + PrecheckerImpl::new(cs, Arc::new(provider), Arc::new(entry_point), test_settings); + let op = UserOperation::V0_6(UserOperationV0_6 { sender: Address::from_str("0x3f8a2b6c4d5e1079286fa1b3c0d4e5f6902b7c8d").unwrap(), nonce: 100.into(), init_code: Bytes::from_str("0x1000000000000000000000000000000000000000").unwrap(), @@ -526,7 +514,7 @@ mod tests { max_priority_fee_per_gas: 2_000.into(), paymaster_and_data: Bytes::default(), signature: Bytes::default(), - }; + }); let res = prechecker.check_gas(&op, get_test_async_data()); @@ -546,9 +534,13 @@ mod tests { #[tokio::test] async fn test_check_payer_paymaster_deposit_too_low() { let (cs, provider, entry_point) = create_base_config(); - let prechecker = - PrecheckerImpl::new(cs, Arc::new(provider), entry_point, Settings::default()); - let op = UserOperation { + let prechecker = PrecheckerImpl::new( + cs, + Arc::new(provider), + Arc::new(entry_point), + Settings::default(), + ); + let op = UserOperation::V0_6(UserOperationV0_6 { sender: Address::from_str("0x3f8a2b6c4d5e1079286fa1b3c0d4e5f6902b7c8d").unwrap(), nonce: 100.into(), init_code: Bytes::default(), @@ -563,7 +555,7 @@ mod tests { ) .unwrap(), signature: Bytes::default(), - }; + }); let res = prechecker.check_payer(&op, get_test_async_data()); assert_eq!( @@ -585,13 +577,14 @@ mod tests { let (mut cs, provider, entry_point) = create_base_config(); cs.id = 10; let mintip = cs.min_max_priority_fee_per_gas; - let prechecker = PrecheckerImpl::new(cs, Arc::new(provider), entry_point, settings); + let prechecker = + PrecheckerImpl::new(cs, Arc::new(provider), Arc::new(entry_point), settings); let mut async_data = get_test_async_data(); async_data.base_fee = 5_000.into(); async_data.min_pre_verification_gas = 1_000.into(); - let op = UserOperation { + let op = UserOperation::V0_6(UserOperationV0_6 { max_fee_per_gas: U256::from(math::percent(5000, settings.base_fee_accept_percent)) + mintip, max_priority_fee_per_gas: mintip, @@ -602,7 +595,7 @@ mod tests { .into(), call_gas_limit: MIN_CALL_GAS_LIMIT, ..Default::default() - }; + }); let res = prechecker.check_gas(&op, async_data); assert!(res.is_empty()); @@ -616,19 +609,20 @@ mod tests { ..Default::default() }; let (cs, provider, entry_point) = create_base_config(); - let prechecker = PrecheckerImpl::new(cs, Arc::new(provider), entry_point, settings); + let prechecker = + PrecheckerImpl::new(cs, Arc::new(provider), Arc::new(entry_point), settings); let mut async_data = get_test_async_data(); async_data.base_fee = 5_000.into(); async_data.min_pre_verification_gas = 1_000.into(); - let op = UserOperation { + let op = UserOperation::V0_6(UserOperationV0_6 { max_fee_per_gas: math::percent(5000, settings.base_fee_accept_percent - 10).into(), max_priority_fee_per_gas: 0.into(), pre_verification_gas: 1_000.into(), call_gas_limit: MIN_CALL_GAS_LIMIT, ..Default::default() - }; + }); let res = prechecker.check_gas(&op, async_data); let mut expected = ArrayVec::::new(); @@ -651,7 +645,8 @@ mod tests { cs.id = 10; cs.min_max_priority_fee_per_gas = 100_000.into(); let mintip = cs.min_max_priority_fee_per_gas; - let prechecker = PrecheckerImpl::new(cs, Arc::new(provider), entry_point, settings); + let prechecker = + PrecheckerImpl::new(cs, Arc::new(provider), Arc::new(entry_point), settings); let mut async_data = get_test_async_data(); async_data.base_fee = 5_000.into(); @@ -659,13 +654,13 @@ mod tests { let undertip = mintip - U256::from(1); - let op = UserOperation { + let op = UserOperation::V0_6(UserOperationV0_6 { max_fee_per_gas: U256::from(5_000) + mintip, max_priority_fee_per_gas: undertip, pre_verification_gas: 1_000.into(), call_gas_limit: MIN_CALL_GAS_LIMIT, ..Default::default() - }; + }); let res = prechecker.check_gas(&op, async_data); let mut expected = ArrayVec::::new(); @@ -685,13 +680,14 @@ mod tests { ..Default::default() }; let (cs, provider, entry_point) = create_base_config(); - let prechecker = PrecheckerImpl::new(cs, Arc::new(provider), entry_point, settings); + let prechecker = + PrecheckerImpl::new(cs, Arc::new(provider), Arc::new(entry_point), settings); let mut async_data = get_test_async_data(); async_data.base_fee = 5_000.into(); async_data.min_pre_verification_gas = 1_000.into(); - let op = UserOperation { + let op = UserOperation::V0_6(UserOperationV0_6 { max_fee_per_gas: 5000.into(), max_priority_fee_per_gas: 0.into(), pre_verification_gas: math::percent( @@ -701,7 +697,7 @@ mod tests { .into(), call_gas_limit: MIN_CALL_GAS_LIMIT, ..Default::default() - }; + }); let res = prechecker.check_gas(&op, async_data); let mut expected = ArrayVec::::new(); diff --git a/crates/sim/src/simulation/mempool.rs b/crates/sim/src/simulation/mempool.rs index 97c9626c1..628ccea76 100644 --- a/crates/sim/src/simulation/mempool.rs +++ b/crates/sim/src/simulation/mempool.rs @@ -204,7 +204,7 @@ mod tests { use rundler_types::StorageSlot; use super::*; - use crate::simulation::{simulation::NeedsStakeInformation, ViolationOpCode}; + use crate::simulation::{NeedsStakeInformation, ViolationOpCode}; #[test] fn test_allow_entity_any() { diff --git a/crates/sim/src/simulation/mod.rs b/crates/sim/src/simulation/mod.rs index 5b6a99b76..3285aa97a 100644 --- a/crates/sim/src/simulation/mod.rs +++ b/crates/sim/src/simulation/mod.rs @@ -11,18 +11,476 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -#[allow(clippy::module_inception)] -mod simulation; +use std::collections::HashSet; + +use anyhow::Error; +use ethers::types::{Address, Opcode, H256, U256}; #[cfg(feature = "test-utils")] -pub use simulation::MockSimulator; -pub(crate) use simulation::REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER; -pub use simulation::{ - EntityInfo, EntityInfos, NeedsStakeInformation, Settings, SimulationError, SimulationResult, - SimulationViolation, Simulator, SimulatorImpl, ViolationOpCode, +use mockall::automock; +use rundler_provider::AggregatorSimOut; +use rundler_types::{ + Entity, EntityType, StakeInfo, StorageSlot, UserOperation, ValidTimeRange, ValidationOutput, }; +mod simulator_v0_6; +pub use simulator_v0_6::SimulatorV0_6; +pub(crate) use simulator_v0_6::REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER_V0_6; + mod mempool; pub use mempool::MempoolConfig; mod tracer; +use strum::IntoEnumIterator; pub use tracer::{SimulateValidationTracer, SimulateValidationTracerImpl}; + +use self::tracer::{AccessInfo, AssociatedSlotsByAddress}; +use crate::{ExpectedStorage, ViolationError}; + +/// The result of a successful simulation +#[derive(Clone, Debug, Default)] +pub struct SimulationResult { + /// The mempool IDs that support this operation + pub mempools: Vec, + /// Block hash this operation was simulated against + pub block_hash: H256, + /// Block number this operation was simulated against + pub block_number: Option, + /// Gas used in the pre-op phase of simulation measured + /// by the entry point + pub pre_op_gas: U256, + /// The time range for which this operation is valid + pub valid_time_range: ValidTimeRange, + /// If using an aggregator, the result of the aggregation + /// simulation + pub aggregator: Option, + /// Code hash of all accessed contracts + pub code_hash: H256, + /// List of used entities that need to be staked for this operation + /// to be valid + pub entities_needing_stake: Vec, + /// Whether the sender account is staked + pub account_is_staked: bool, + /// List of all addresses accessed during validation + pub accessed_addresses: HashSet

, + /// List of addresses that have associated storage slots + /// accessed within the simulation + pub associated_addresses: HashSet
, + /// Expected storage values for all accessed slots during validation + pub expected_storage: ExpectedStorage, + /// Whether the operation requires a post-op + pub requires_post_op: bool, + /// All the entities used in this operation and their staking state + pub entity_infos: EntityInfos, +} + +impl SimulationResult { + /// Get the aggregator address if one was used + pub fn aggregator_address(&self) -> Option
{ + self.aggregator.as_ref().map(|agg| agg.address) + } +} + +/// The result of a failed simulation. We return a list of the violations that ocurred during the failed simulation +/// and also information about all the entities used in the op to handle entity penalties +#[derive(Clone, Debug)] +pub struct SimulationError { + /// A list of violations that occurred during simulation, or some other error that occurred not directly related to simulation rules + pub violation_error: ViolationError, + /// The addresses and staking states of all the entities involved in an op. This value is None when simulation fails at a point where we are no + pub entity_infos: Option, +} + +impl From for SimulationError { + fn from(error: Error) -> Self { + SimulationError { + violation_error: ViolationError::Other(error), + entity_infos: None, + } + } +} + +/// Simulator trait for running user operation simulations +#[cfg_attr(feature = "test-utils", automock(type UserOperationType = UserOperation;))] +#[async_trait::async_trait] +pub trait Simulator: Send + Sync + 'static { + /// The type of user operation that this simulator can handle + type UserOperationType: From; + + /// Simulate a user operation, returning simulation information + /// upon success, or simulation violations. + async fn simulate_validation( + &self, + op: Self::UserOperationType, + block_hash: Option, + expected_code_hash: Option, + ) -> Result; +} + +/// All possible simulation violations +#[derive(Clone, Debug, parse_display::Display, Ord, Eq, PartialOrd, PartialEq)] +pub enum SimulationViolation { + // Make sure to maintain the order here based on the importance + // of the violation for converting to an JSON RPC error + /// The user operation signature is invalid + #[display("invalid signature")] + InvalidSignature, + /// The user operation used an opcode that is not allowed + #[display("{0.kind} uses banned opcode: {2} in contract {1:?}")] + UsedForbiddenOpcode(Entity, Address, ViolationOpCode), + /// The user operation used a precompile that is not allowed + #[display("{0.kind} uses banned precompile: {2:?} in contract {1:?}")] + UsedForbiddenPrecompile(Entity, Address, Address), + /// The user operation accessed a contract that has not been deployed + #[display( + "{0.kind} tried to access code at {1} during validation, but that address is not a contract" + )] + AccessedUndeployedContract(Entity, Address), + /// The user operation factory entity called CREATE2 more than once during initialization + #[display("factory may only call CREATE2 once during initialization")] + FactoryCalledCreate2Twice(Address), + /// The user operation accessed a storage slot that is not allowed + #[display("{0.kind} accessed forbidden storage at address {1:?} during validation")] + InvalidStorageAccess(Entity, StorageSlot), + /// The user operation called an entry point method that is not allowed + #[display("{0.kind} called entry point method other than depositTo")] + CalledBannedEntryPointMethod(Entity), + /// The user operation made a call that contained value to a contract other than the entrypoint + /// during validation + #[display("{0.kind} must not send ETH during validation (except from account to entry point)")] + CallHadValue(Entity), + /// The code hash of accessed contracts changed on the second simulation + #[display("code accessed by validation has changed since the last time validation was run")] + CodeHashChanged, + /// The user operation contained an entity that accessed storage without being staked + #[display("Unstaked {0.entity} accessed {0.accessed_address} ({0.accessed_entity:?}) at slot {0.slot}")] + NotStaked(Box), + /// The user operation uses a paymaster that returns a context while being unstaked + #[display("Unstaked paymaster must not return context")] + UnstakedPaymasterContext, + /// The user operation uses an aggregator entity and it is not staked + #[display("An aggregator must be staked, regardless of storager usage")] + UnstakedAggregator, + /// Simulation reverted with an unintended reason, containing a message + #[display("reverted while simulating {0} validation: {1}")] + UnintendedRevertWithMessage(EntityType, String, Option
), + /// Simulation reverted with an unintended reason + #[display("reverted while simulating {0} validation")] + UnintendedRevert(EntityType, Option
), + /// Simulation did not revert, a revert is always expected + #[display("simulateValidation did not revert. Make sure your EntryPoint is valid")] + DidNotRevert, + /// Simulation had the wrong number of phases + #[display("simulateValidation should have 3 parts but had {0} instead. Make sure your EntryPoint is valid")] + WrongNumberOfPhases(u32), + /// The user operation ran out of gas during validation + #[display("ran out of gas during {0.kind} validation")] + OutOfGas(Entity), + /// The user operation aggregator signature validation failed + #[display("aggregator signature validation failed")] + AggregatorValidationFailed, + /// Verification gas limit doesn't have the required buffer on the measured gas + #[display("verification gas limit doesn't have the required buffer on the measured gas, limit: {0}, needed: {1}")] + VerificationGasLimitBufferTooLow(U256, U256), +} + +/// A wrapper around Opcode that implements extra traits +#[derive(Debug, PartialEq, Clone, parse_display::Display, Eq)] +#[display("{0:?}")] +pub struct ViolationOpCode(pub Opcode); + +impl PartialOrd for ViolationOpCode { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ViolationOpCode { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let left = self.0 as i32; + let right = other.0 as i32; + + left.cmp(&right) + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +/// additional context about an entity +pub struct EntityInfo { + /// The address of an entity + pub address: Address, + /// Whether the entity is staked or not + pub is_staked: bool, +} + +impl EntityInfo { + fn override_is_staked(&mut self, allow_unstaked_addresses: &HashSet
) { + self.is_staked = allow_unstaked_addresses.contains(&self.address) || self.is_staked; + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +/// additional context for all the entities used in an op +pub struct EntityInfos { + /// The entity info for the factory + pub factory: Option, + /// The entity info for the op sender + pub sender: EntityInfo, + /// The entity info for the paymaster + pub paymaster: Option, + /// The entity info for the aggregator + pub aggregator: Option, +} + +impl EntityInfos { + fn new( + factory_address: Option
, + sender_address: Address, + paymaster_address: Option
, + entry_point_out: &ValidationOutput, + sim_settings: Settings, + ) -> Self { + let factory = factory_address.map(|address| EntityInfo { + address, + is_staked: is_staked(entry_point_out.factory_info, sim_settings), + }); + let sender = EntityInfo { + address: sender_address, + is_staked: is_staked(entry_point_out.sender_info, sim_settings), + }; + let paymaster = paymaster_address.map(|address| EntityInfo { + address, + is_staked: is_staked(entry_point_out.paymaster_info, sim_settings), + }); + let aggregator = entry_point_out + .aggregator_info + .map(|aggregator_info| EntityInfo { + address: aggregator_info.address, + is_staked: is_staked(aggregator_info.stake_info, sim_settings), + }); + + Self { + factory, + sender, + paymaster, + aggregator, + } + } + + /// Get iterator over the entities + pub fn entities(&'_ self) -> impl Iterator + '_ { + EntityType::iter().filter_map(|t| self.get(t).map(|info| (t, info))) + } + + fn override_is_staked(&mut self, allow_unstaked_addresses: &HashSet
) { + if let Some(mut factory) = self.factory { + factory.override_is_staked(allow_unstaked_addresses) + } + self.sender.override_is_staked(allow_unstaked_addresses); + if let Some(mut paymaster) = self.paymaster { + paymaster.override_is_staked(allow_unstaked_addresses) + } + if let Some(mut aggregator) = self.aggregator { + aggregator.override_is_staked(allow_unstaked_addresses) + } + } + + /// Get the EntityInfo of a specific entity + pub fn get(self, entity: EntityType) -> Option { + match entity { + EntityType::Factory => self.factory, + EntityType::Account => Some(self.sender), + EntityType::Paymaster => self.paymaster, + EntityType::Aggregator => self.aggregator, + } + } + + fn type_from_address(self, address: Address) -> Option { + if address.eq(&self.sender.address) { + return Some(EntityType::Account); + } + + if let Some(factory) = self.factory { + if address.eq(&factory.address) { + return Some(EntityType::Factory); + } + } + + if let Some(paymaster) = self.paymaster { + if address.eq(&paymaster.address) { + return Some(EntityType::Paymaster); + } + } + + if let Some(aggregator) = self.aggregator { + if address.eq(&aggregator.address) { + return Some(EntityType::Aggregator); + } + } + + None + } + + fn sender_address(self) -> Address { + self.sender.address + } +} + +fn is_staked(info: StakeInfo, sim_settings: Settings) -> bool { + info.stake >= sim_settings.min_stake_value.into() + && info.unstake_delay_sec >= sim_settings.min_unstake_delay.into() +} + +#[derive(Clone, Debug, Eq, PartialEq)] +enum StorageRestriction { + Allowed, + NeedsStake(Address, Option, U256), + Banned(U256), +} + +/// Information about a storage violation based on stake status +#[derive(Debug, PartialEq, Clone, PartialOrd, Eq, Ord)] +pub struct NeedsStakeInformation { + /// Entity of stake information + pub entity: Entity, + /// Address that was accessed while unstaked + pub accessed_address: Address, + /// Type of accessed entity if it is a known entity + pub accessed_entity: Option, + /// The accessed slot number + pub slot: U256, + /// Minumum stake + pub min_stake: U256, + /// Minumum delay after an unstake event + pub min_unstake_delay: U256, +} + +#[derive(Clone, Debug)] +struct ParseStorageAccess<'a> { + access_info: &'a AccessInfo, + slots_by_address: &'a AssociatedSlotsByAddress, + address: Address, + sender: Address, + entrypoint: Address, + has_factory: bool, + entity: &'a Entity, + entity_infos: &'a EntityInfos, +} + +fn parse_storage_accesses(args: ParseStorageAccess<'_>) -> Result { + let ParseStorageAccess { + access_info, + address, + sender, + entrypoint, + entity_infos, + entity, + slots_by_address, + has_factory, + .. + } = args; + + if address.eq(&sender) || address.eq(&entrypoint) { + return Ok(StorageRestriction::Allowed); + } + + let mut required_stake_slot = None; + + let slots: Vec<&U256> = access_info + .reads + .keys() + .chain(access_info.writes.keys()) + .collect(); + + for slot in slots { + let is_sender_associated = slots_by_address.is_associated_slot(sender, *slot); + // [STO-032] + let is_entity_associated = slots_by_address.is_associated_slot(entity.address, *slot); + // [STO-031] + let is_same_address = address.eq(&entity.address); + // [STO-033] + let is_read_permission = !access_info.writes.contains_key(slot); + + if is_sender_associated { + if has_factory + // special case: account.validateUserOp is allowed to use assoc storage if factory is staked. + // [STO-022], [STO-021] + && !(entity.address.eq(&sender) + && entity_infos + .factory + .expect("Factory needs to be present and staked") + .is_staked) + { + required_stake_slot = Some(slot); + } + } else if is_entity_associated || is_same_address || is_read_permission { + required_stake_slot = Some(slot); + } else { + return Ok(StorageRestriction::Banned(*slot)); + } + } + + if let Some(required_stake_slot) = required_stake_slot { + if let Some(entity_type) = entity_infos.type_from_address(address) { + return Ok(StorageRestriction::NeedsStake( + address, + Some(entity_type), + *required_stake_slot, + )); + } + + return Ok(StorageRestriction::NeedsStake( + address, + None, + *required_stake_slot, + )); + } + + Ok(StorageRestriction::Allowed) +} + +/// Simulation Settings +#[derive(Debug, Copy, Clone)] +pub struct Settings { + /// The minimum amount of time that a staked entity must have configured as + /// their unstake delay on the entry point contract in order to be considered staked. + pub min_unstake_delay: u32, + /// The minimum amount of stake that a staked entity must have on the entry point + /// contract in order to be considered staked. + pub min_stake_value: u128, + /// The maximum amount of gas that can be used during the simulation call + pub max_simulate_handle_ops_gas: u64, + /// The maximum amount of verification gas that can be used during the simulation call + pub max_verification_gas: u64, +} + +impl Settings { + /// Create new settings + pub fn new( + min_unstake_delay: u32, + min_stake_value: u128, + max_simulate_handle_ops_gas: u64, + max_verification_gas: u64, + ) -> Self { + Self { + min_unstake_delay, + min_stake_value, + max_simulate_handle_ops_gas, + max_verification_gas, + } + } +} + +#[cfg(any(test, feature = "test-utils"))] +impl Default for Settings { + fn default() -> Self { + Self { + // one day in seconds: defined in the ERC-4337 spec + min_unstake_delay: 84600, + // 10^18 wei = 1 eth + min_stake_value: 1_000_000_000_000_000_000, + // 550 million gas: currently the defaults for Alchemy eth_call + max_simulate_handle_ops_gas: 550_000_000, + max_verification_gas: 5_000_000, + } + } +} diff --git a/crates/sim/src/simulation/simulation.rs b/crates/sim/src/simulation/simulator_v0_6.rs similarity index 64% rename from crates/sim/src/simulation/simulation.rs rename to crates/sim/src/simulation/simulator_v0_6.rs index 602e82335..9d9473e29 100644 --- a/crates/sim/src/simulation/simulation.rs +++ b/crates/sim/src/simulation/simulator_v0_6.rs @@ -18,113 +18,32 @@ use std::{ sync::Arc, }; -use anyhow::Error; use async_trait::async_trait; use ethers::{ abi::AbiDecode, - types::{Address, BlockId, Opcode, H256, U256}, + types::{Address, BlockId, H256, U256}, }; use indexmap::IndexSet; -#[cfg(feature = "test-utils")] -use mockall::automock; -use rundler_provider::{AggregatorOut, AggregatorSimOut, Provider}; +use rundler_provider::{AggregatorOut, AggregatorSimOut, EntryPoint, Provider}; use rundler_types::{ - contracts::i_entry_point::FailedOp, Entity, EntityType, StakeInfo, StorageSlot, UserOperation, + contracts::v0_6::i_entry_point::FailedOp, Entity, EntityType, StorageSlot, UserOperationV0_6, ValidTimeRange, ValidationOutput, ValidationReturnInfo, }; -use strum::IntoEnumIterator; use super::{ + is_staked, mempool::{match_mempools, AllowEntity, AllowRule, MempoolConfig, MempoolMatchResult}, - tracer::{ - parse_combined_tracer_str, AccessInfo, AssociatedSlotsByAddress, SimulateValidationTracer, - SimulationTracerOutput, - }, + parse_storage_accesses, + tracer::{parse_combined_tracer_str, SimulateValidationTracer, SimulationTracerOutput}, + ParseStorageAccess, Settings, Simulator, StorageRestriction, }; use crate::{ - types::{ExpectedStorage, ViolationError}, - utils, + types::ViolationError, utils, EntityInfos, NeedsStakeInformation, SimulationError, + SimulationResult, SimulationViolation, ViolationOpCode, }; /// Required buffer for verification gas limit when targeting the 0.6 entrypoint contract -pub(crate) const REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER: U256 = U256([2000, 0, 0, 0]); - -/// The result of a successful simulation -#[derive(Clone, Debug, Default)] -pub struct SimulationResult { - /// The mempool IDs that support this operation - pub mempools: Vec, - /// Block hash this operation was simulated against - pub block_hash: H256, - /// Block number this operation was simulated against - pub block_number: Option, - /// Gas used in the pre-op phase of simulation measured - /// by the entry point - pub pre_op_gas: U256, - /// The time range for which this operation is valid - pub valid_time_range: ValidTimeRange, - /// If using an aggregator, the result of the aggregation - /// simulation - pub aggregator: Option, - /// Code hash of all accessed contracts - pub code_hash: H256, - /// List of used entities that need to be staked for this operation - /// to be valid - pub entities_needing_stake: Vec, - /// Whether the sender account is staked - pub account_is_staked: bool, - /// List of all addresses accessed during validation - pub accessed_addresses: HashSet
, - /// List of addresses that have associated storage slots - /// accessed within the simulation - pub associated_addresses: HashSet
, - /// Expected storage values for all accessed slots during validation - pub expected_storage: ExpectedStorage, - /// Whether the operation requires a post-op - pub requires_post_op: bool, - /// All the entities used in this operation and their staking state - pub entity_infos: EntityInfos, -} - -impl SimulationResult { - /// Get the aggregator address if one was used - pub fn aggregator_address(&self) -> Option
{ - self.aggregator.as_ref().map(|agg| agg.address) - } -} - -/// The result of a failed simulation. We return a list of the violations that ocurred during the failed simulation -/// and also information about all the entities used in the op to handle entity penalties -#[derive(Clone, Debug)] -pub struct SimulationError { - /// A list of violations that occurred during simulation, or some other error that occurred not directly related to simulation rules - pub violation_error: ViolationError, - /// The addresses and staking states of all the entities involved in an op. This value is None when simulation fails at a point where we are no - pub entity_infos: Option, -} - -impl From for SimulationError { - fn from(error: Error) -> Self { - SimulationError { - violation_error: ViolationError::Other(error), - entity_infos: None, - } - } -} - -/// Simulator trait for running user operation simulations -#[cfg_attr(feature = "test-utils", automock)] -#[async_trait::async_trait] -pub trait Simulator: Send + Sync + 'static { - /// Simulate a user operation, returning simulation information - /// upon success, or simulation violations. - async fn simulate_validation( - &self, - op: UserOperation, - block_hash: Option, - expected_code_hash: Option, - ) -> Result; -} +pub(crate) const REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER_V0_6: U256 = U256([2000, 0, 0, 0]); /// Simulator implementation. /// @@ -138,18 +57,19 @@ pub trait Simulator: Send + Sync + 'static { /// If no mempools are found, the simulator will return an error containing /// the violations. #[derive(Debug)] -pub struct SimulatorImpl { +pub struct SimulatorV0_6 { provider: Arc

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

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

, - entry_point_address: Address, + entry_point: Arc, simulate_validation_tracer: T, sim_settings: Settings, mempool_configs: HashMap, @@ -178,7 +98,7 @@ where Self { provider, - entry_point_address, + entry_point, simulate_validation_tracer, sim_settings, mempool_configs, @@ -195,7 +115,7 @@ where // Any violations during this stage are errors. async fn create_context( &self, - op: UserOperation, + op: UserOperationV0_6, block_id: BlockId, ) -> Result { let factory_address = op.factory(); @@ -203,7 +123,11 @@ where let paymaster_address = op.paymaster(); let tracer_out = self .simulate_validation_tracer - .trace_simulate_validation(op.clone(), block_id, self.sim_settings.max_verification_gas) + .trace_simulate_validation( + op.clone().into(), + 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 @@ -279,8 +203,7 @@ where }; let associated_addresses = tracer_out.associated_slots_by_address.addresses(); - - let initcode_length = op.init_code.len(); + let has_factory = op.factory().is_some(); Ok(ValidationContext { op, block_id, @@ -290,13 +213,13 @@ where associated_addresses, entities_needing_stake: vec![], accessed_addresses: HashSet::new(), - initcode_length, + has_factory, }) } async fn validate_aggregator_signature( &self, - op: UserOperation, + op: UserOperationV0_6, aggregator_address: Option

, gas_cap: u64, ) -> anyhow::Result { @@ -304,11 +227,10 @@ where return Ok(AggregatorOut::NotNeeded); }; - Ok(self - .provider + self.entry_point .clone() - .validate_user_op_signature(aggregator_address, op, gas_cap) - .await?) + .validate_user_op_signature(aggregator_address, op.into(), gas_cap) + .await } // Parse the output from tracing and return a list of violations. @@ -325,7 +247,7 @@ where ref entry_point_out, ref mut entities_needing_stake, ref mut accessed_addresses, - initcode_length, + has_factory, .. } = context; @@ -357,7 +279,7 @@ where } for (addr, opcode) in &phase.ext_code_access_info { - if *addr == self.entry_point_address { + if *addr == self.entry_point.address() { violations.push(SimulationViolation::UsedForbiddenOpcode( entity, *addr, @@ -391,8 +313,8 @@ where slots_by_address: &tracer_out.associated_slots_by_address, address, sender: sender_address, - entrypoint: self.entry_point_address, - initcode_length, + entrypoint: self.entry_point.address(), + has_factory, entity: &entity, entity_infos, })?; @@ -467,7 +389,7 @@ where // weird case where CREATE2 is called > 1, but there isn't a factory // defined. This should never happen, blame the violation on the entry point. violations.push(SimulationViolation::FactoryCalledCreate2Twice( - self.entry_point_address, + self.entry_point.address(), )); } } @@ -481,12 +403,12 @@ where .pre_op_gas .saturating_sub(op.pre_verification_gas); let verification_buffer = op - .verification_gas_limit + .total_verification_gas_limit() .saturating_sub(verification_gas_used); - if verification_buffer < REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER { + if verification_buffer < REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER_V0_6 { violations.push(SimulationViolation::VerificationGasLimitBufferTooLow( - op.verification_gas_limit, - verification_gas_used + REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER, + op.total_verification_gas_limit(), + verification_gas_used + REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER_V0_6, )); } @@ -498,7 +420,7 @@ where // Violations during this stage are always errors. async fn check_contracts( &self, - op: UserOperation, + op: UserOperationV0_6, context: &mut ValidationContext, expected_code_hash: Option, ) -> Result<(H256, Option), SimulationError> { @@ -553,14 +475,17 @@ where } #[async_trait] -impl Simulator for SimulatorImpl +impl Simulator for SimulatorV0_6 where P: Provider, + E: EntryPoint, T: SimulateValidationTracer, { + type UserOperationType = UserOperationV0_6; + async fn simulate_validation( &self, - op: UserOperation, + op: UserOperationV0_6, block_hash: Option, expected_code_hash: Option, ) -> Result { @@ -651,93 +576,6 @@ where } } -/// All possible simulation violations -#[derive(Clone, Debug, parse_display::Display, Ord, Eq, PartialOrd, PartialEq)] -pub enum SimulationViolation { - // Make sure to maintain the order here based on the importance - // of the violation for converting to an JSON RPC error - /// The user operation signature is invalid - #[display("invalid signature")] - InvalidSignature, - /// The user operation used an opcode that is not allowed - #[display("{0.kind} uses banned opcode: {2} in contract {1:?}")] - UsedForbiddenOpcode(Entity, Address, ViolationOpCode), - /// The user operation used a precompile that is not allowed - #[display("{0.kind} uses banned precompile: {2:?} in contract {1:?}")] - UsedForbiddenPrecompile(Entity, Address, Address), - /// The user operation accessed a contract that has not been deployed - #[display( - "{0.kind} tried to access code at {1} during validation, but that address is not a contract" - )] - AccessedUndeployedContract(Entity, Address), - /// The user operation factory entity called CREATE2 more than once during initialization - #[display("factory may only call CREATE2 once during initialization")] - FactoryCalledCreate2Twice(Address), - /// The user operation accessed a storage slot that is not allowed - #[display("{0.kind} accessed forbidden storage at address {1:?} during validation")] - InvalidStorageAccess(Entity, StorageSlot), - /// The user operation called an entry point method that is not allowed - #[display("{0.kind} called entry point method other than depositTo")] - CalledBannedEntryPointMethod(Entity), - /// The user operation made a call that contained value to a contract other than the entrypoint - /// during validation - #[display("{0.kind} must not send ETH during validation (except from account to entry point)")] - CallHadValue(Entity), - /// The code hash of accessed contracts changed on the second simulation - #[display("code accessed by validation has changed since the last time validation was run")] - CodeHashChanged, - /// The user operation contained an entity that accessed storage without being staked - #[display("Unstaked {0.entity} accessed {0.accessed_address} ({0.accessed_entity:?}) at slot {0.slot}")] - NotStaked(Box), - /// The user operation uses a paymaster that returns a context while being unstaked - #[display("Unstaked paymaster must not return context")] - UnstakedPaymasterContext, - /// The user operation uses an aggregator entity and it is not staked - #[display("An aggregator must be staked, regardless of storager usage")] - UnstakedAggregator, - /// Simulation reverted with an unintended reason, containing a message - #[display("reverted while simulating {0} validation: {1}")] - UnintendedRevertWithMessage(EntityType, String, Option
), - /// Simulation reverted with an unintended reason - #[display("reverted while simulating {0} validation")] - UnintendedRevert(EntityType, Option
), - /// Simulation did not revert, a revert is always expected - #[display("simulateValidation did not revert. Make sure your EntryPoint is valid")] - DidNotRevert, - /// Simulation had the wrong number of phases - #[display("simulateValidation should have 3 parts but had {0} instead. Make sure your EntryPoint is valid")] - WrongNumberOfPhases(u32), - /// The user operation ran out of gas during validation - #[display("ran out of gas during {0.kind} validation")] - OutOfGas(Entity), - /// The user operation aggregator signature validation failed - #[display("aggregator signature validation failed")] - AggregatorValidationFailed, - /// Verification gas limit doesn't have the required buffer on the measured gas - #[display("verification gas limit doesn't have the required buffer on the measured gas, limit: {0}, needed: {1}")] - VerificationGasLimitBufferTooLow(U256, U256), -} - -/// A wrapper around Opcode that implements extra traits -#[derive(Debug, PartialEq, Clone, parse_display::Display, Eq)] -#[display("{0:?}")] -pub struct ViolationOpCode(pub Opcode); - -impl PartialOrd for ViolationOpCode { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for ViolationOpCode { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - let left = self.0 as i32; - let right = other.0 as i32; - - left.cmp(&right) - } -} - fn entity_type_from_simulation_phase(i: usize) -> Option { match i { 0 => Some(EntityType::Factory), @@ -749,316 +587,38 @@ fn entity_type_from_simulation_phase(i: usize) -> Option { #[derive(Debug)] struct ValidationContext { - op: UserOperation, + op: UserOperationV0_6, block_id: BlockId, entity_infos: EntityInfos, tracer_out: SimulationTracerOutput, entry_point_out: ValidationOutput, entities_needing_stake: Vec, accessed_addresses: HashSet
, - initcode_length: usize, + has_factory: bool, associated_addresses: HashSet
, } -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -/// additional context about an entity -pub struct EntityInfo { - /// The address of an entity - pub address: Address, - /// Whether the entity is staked or not - pub is_staked: bool, -} - -impl EntityInfo { - fn override_is_staked(&mut self, allow_unstaked_addresses: &HashSet
) { - self.is_staked = allow_unstaked_addresses.contains(&self.address) || self.is_staked; - } -} - -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -/// additional context for all the entities used in an op -pub struct EntityInfos { - /// The entity info for the factory - pub factory: Option, - /// The entity info for the op sender - pub sender: EntityInfo, - /// The entity info for the paymaster - pub paymaster: Option, - /// The entity info for the aggregator - pub aggregator: Option, -} - -impl EntityInfos { - fn new( - factory_address: Option
, - sender_address: Address, - paymaster_address: Option
, - entry_point_out: &ValidationOutput, - sim_settings: Settings, - ) -> Self { - let factory = factory_address.map(|address| EntityInfo { - address, - is_staked: is_staked(entry_point_out.factory_info, sim_settings), - }); - let sender = EntityInfo { - address: sender_address, - is_staked: is_staked(entry_point_out.sender_info, sim_settings), - }; - let paymaster = paymaster_address.map(|address| EntityInfo { - address, - is_staked: is_staked(entry_point_out.paymaster_info, sim_settings), - }); - let aggregator = entry_point_out - .aggregator_info - .map(|aggregator_info| EntityInfo { - address: aggregator_info.address, - is_staked: is_staked(aggregator_info.stake_info, sim_settings), - }); - - Self { - factory, - sender, - paymaster, - aggregator, - } - } - - /// Get iterator over the entities - pub fn entities(&'_ self) -> impl Iterator + '_ { - EntityType::iter().filter_map(|t| self.get(t).map(|info| (t, info))) - } - - fn override_is_staked(&mut self, allow_unstaked_addresses: &HashSet
) { - if let Some(mut factory) = self.factory { - factory.override_is_staked(allow_unstaked_addresses) - } - self.sender.override_is_staked(allow_unstaked_addresses); - if let Some(mut paymaster) = self.paymaster { - paymaster.override_is_staked(allow_unstaked_addresses) - } - if let Some(mut aggregator) = self.aggregator { - aggregator.override_is_staked(allow_unstaked_addresses) - } - } - - /// Get the EntityInfo of a specific entity - pub fn get(self, entity: EntityType) -> Option { - match entity { - EntityType::Factory => self.factory, - EntityType::Account => Some(self.sender), - EntityType::Paymaster => self.paymaster, - EntityType::Aggregator => self.aggregator, - } - } - - fn type_from_address(self, address: Address) -> Option { - if address.eq(&self.sender.address) { - return Some(EntityType::Account); - } - - if let Some(factory) = self.factory { - if address.eq(&factory.address) { - return Some(EntityType::Factory); - } - } - - if let Some(paymaster) = self.paymaster { - if address.eq(&paymaster.address) { - return Some(EntityType::Paymaster); - } - } - - if let Some(aggregator) = self.aggregator { - if address.eq(&aggregator.address) { - return Some(EntityType::Aggregator); - } - } - - None - } - - fn sender_address(self) -> Address { - self.sender.address - } -} - -fn is_staked(info: StakeInfo, sim_settings: Settings) -> bool { - info.stake >= sim_settings.min_stake_value.into() - && info.unstake_delay_sec >= sim_settings.min_unstake_delay.into() -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum StorageRestriction { - Allowed, - NeedsStake(Address, Option, U256), - Banned(U256), -} - -/// Information about a storage violation based on stake status -#[derive(Debug, PartialEq, Clone, PartialOrd, Eq, Ord)] -pub struct NeedsStakeInformation { - /// Entity of stake information - pub entity: Entity, - /// Address that was accessed while unstaked - pub accessed_address: Address, - /// Type of accessed entity if it is a known entity - pub accessed_entity: Option, - /// The accessed slot number - pub slot: U256, - /// Minumum stake - pub min_stake: U256, - /// Minumum delay after an unstake event - pub min_unstake_delay: U256, -} - -#[derive(Clone, Debug)] -struct ParseStorageAccess<'a> { - access_info: &'a AccessInfo, - slots_by_address: &'a AssociatedSlotsByAddress, - address: Address, - sender: Address, - entrypoint: Address, - initcode_length: usize, - entity: &'a Entity, - entity_infos: &'a EntityInfos, -} - -fn parse_storage_accesses(args: ParseStorageAccess<'_>) -> Result { - let ParseStorageAccess { - access_info, - address, - sender, - entrypoint, - entity_infos, - entity, - slots_by_address, - initcode_length, - .. - } = args; - - if address.eq(&sender) || address.eq(&entrypoint) { - return Ok(StorageRestriction::Allowed); - } - - let mut required_stake_slot = None; - - let slots: Vec<&U256> = access_info - .reads - .keys() - .chain(access_info.writes.keys()) - .collect(); - - for slot in slots { - let is_sender_associated = slots_by_address.is_associated_slot(sender, *slot); - // [STO-032] - let is_entity_associated = slots_by_address.is_associated_slot(entity.address, *slot); - // [STO-031] - let is_same_address = address.eq(&entity.address); - // [STO-033] - let is_read_permission = !access_info.writes.contains_key(slot); - - if is_sender_associated { - if initcode_length > 2 - // special case: account.validateUserOp is allowed to use assoc storage if factory is staked. - // [STO-022], [STO-021] - && !(entity.address.eq(&sender) - && entity_infos - .factory - .expect("Factory needs to be present and staked") - .is_staked) - { - required_stake_slot = Some(slot); - } - } else if is_entity_associated || is_same_address || is_read_permission { - required_stake_slot = Some(slot); - } else { - return Ok(StorageRestriction::Banned(*slot)); - } - } - - if let Some(required_stake_slot) = required_stake_slot { - if let Some(entity_type) = entity_infos.type_from_address(address) { - return Ok(StorageRestriction::NeedsStake( - address, - Some(entity_type), - *required_stake_slot, - )); - } - - return Ok(StorageRestriction::NeedsStake( - address, - None, - *required_stake_slot, - )); - } - - Ok(StorageRestriction::Allowed) -} - -/// Simulation Settings -#[derive(Debug, Copy, Clone)] -pub struct Settings { - /// The minimum amount of time that a staked entity must have configured as - /// their unstake delay on the entry point contract in order to be considered staked. - pub min_unstake_delay: u32, - /// The minimum amount of stake that a staked entity must have on the entry point - /// contract in order to be considered staked. - pub min_stake_value: u128, - /// The maximum amount of gas that can be used during the simulation call - pub max_simulate_handle_ops_gas: u64, - /// The maximum amount of verification gas that can be used during the simulation call - pub max_verification_gas: u64, -} - -impl Settings { - /// Create new settings - pub fn new( - min_unstake_delay: u32, - min_stake_value: u128, - max_simulate_handle_ops_gas: u64, - max_verification_gas: u64, - ) -> Self { - Self { - min_unstake_delay, - min_stake_value, - max_simulate_handle_ops_gas, - max_verification_gas, - } - } -} - -#[cfg(any(test, feature = "test-utils"))] -impl Default for Settings { - fn default() -> Self { - Self { - // one day in seconds: defined in the ERC-4337 spec - min_unstake_delay: 84600, - // 10^18 wei = 1 eth - min_stake_value: 1_000_000_000_000_000_000, - // 550 million gas: currently the defaults for Alchemy eth_call - max_simulate_handle_ops_gas: 550_000_000, - max_verification_gas: 5_000_000, - } - } -} - #[cfg(test)] mod tests { use std::str::FromStr; use ethers::{ abi::AbiEncode, - types::{Address, BlockNumber, Bytes, U64}, + types::{Address, BlockNumber, Bytes, Opcode, U64}, utils::hex, }; - use rundler_provider::{AggregatorOut, MockProvider}; - use rundler_types::contracts::get_code_hashes::CodeHashesResult; + use rundler_provider::{AggregatorOut, MockEntryPoint, MockProvider}; + use rundler_types::{contracts::utils::get_code_hashes::CodeHashesResult, StakeInfo}; use super::*; - use crate::simulation::tracer::{MockSimulateValidationTracer, Phase}; - - fn create_base_config() -> (MockProvider, MockSimulateValidationTracer) { - (MockProvider::new(), MockSimulateValidationTracer::new()) + use crate::simulation::tracer::{AccessInfo, MockSimulateValidationTracer, Phase}; + + fn create_base_config() -> (MockProvider, MockEntryPoint, MockSimulateValidationTracer) { + ( + MockProvider::new(), + MockEntryPoint::new(), + MockSimulateValidationTracer::new(), + ) } fn get_test_tracer_output() -> SimulationTracerOutput { @@ -1131,8 +691,9 @@ mod tests { fn create_simulator( provider: MockProvider, + entry_point: MockEntryPoint, simulate_validation_tracer: MockSimulateValidationTracer, - ) -> SimulatorImpl { + ) -> SimulatorV0_6 { let settings = Settings::default(); let mut mempool_configs = HashMap::new(); @@ -1140,10 +701,10 @@ mod tests { let provider = Arc::new(provider); - let simulator: SimulatorImpl = - SimulatorImpl::new( + let simulator: SimulatorV0_6 = + SimulatorV0_6::new( Arc::clone(&provider), - Address::from_str("0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789").unwrap(), + Arc::new(entry_point), simulate_validation_tracer, settings, mempool_configs, @@ -1154,7 +715,7 @@ mod tests { #[tokio::test] async fn test_simulate_validation() { - let (mut provider, mut tracer) = create_base_config(); + let (mut provider, mut entry_point, mut tracer) = create_base_config(); provider .expect_get_latest_block_hash_and_number() @@ -1185,11 +746,11 @@ mod tests { }) }); - provider + entry_point .expect_validate_user_op_signature() .returning(|_, _, _| Ok(AggregatorOut::NotNeeded)); - let user_operation = UserOperation { + let user_operation = UserOperationV0_6 { sender: Address::from_str("b856dbd4fa1a79a46d426f537455e7d3e79ab7c4").unwrap(), nonce: U256::from(264), init_code: Bytes::from_str("0x").unwrap(), @@ -1203,7 +764,7 @@ mod tests { signature: Bytes::from_str("0x98f89993ce573172635b44ef3b0741bd0c19dd06909d3539159f6d66bef8c0945550cc858b1cf5921dfce0986605097ba34c2cf3fc279154dd25e161ea7b3d0f1c").unwrap(), }; - let simulator = create_simulator(provider, tracer); + let simulator = create_simulator(provider, entry_point, tracer); let res = simulator .simulate_validation(user_operation, None, None) .await; @@ -1212,7 +773,7 @@ mod tests { #[tokio::test] async fn test_create_context_two_phases_unintended_revert() { - let (provider, mut tracer) = create_base_config(); + let (provider, mut entry_point, mut tracer) = create_base_config(); tracer .expect_trace_simulate_validation() @@ -1228,7 +789,7 @@ mod tests { Ok(tracer_output) }); - let user_operation = UserOperation { + let user_operation = UserOperationV0_6 { sender: Address::from_str("b856dbd4fa1a79a46d426f537455e7d3e79ab7c4").unwrap(), nonce: U256::from(264), init_code: Bytes::from_str("0x").unwrap(), @@ -1242,7 +803,7 @@ mod tests { signature: Bytes::from_str("0x98f89993ce573172635b44ef3b0741bd0c19dd06909d3539159f6d66bef8c0945550cc858b1cf5921dfce0986605097ba34c2cf3fc279154dd25e161ea7b3d0f1c").unwrap(), }; - let simulator = create_simulator(provider, tracer); + let simulator = create_simulator(provider, entry_point, tracer); let res = simulator .create_context(user_operation, BlockId::Number(BlockNumber::Latest)) .await; @@ -1262,7 +823,7 @@ mod tests { #[tokio::test] async fn test_gather_context_violations() { - let (provider, tracer) = create_base_config(); + let (provider, entry_point, tracer) = create_base_config(); let mut tracer_output = get_test_tracer_output(); @@ -1295,12 +856,12 @@ mod tests { ); let mut validation_context = ValidationContext { - op: UserOperation { + op: UserOperationV0_6 { verification_gas_limit: U256::from(2000), pre_verification_gas: U256::from(1000), ..Default::default() }, - initcode_length: 10, + has_factory: true, associated_addresses: HashSet::new(), block_id: BlockId::Number(BlockNumber::Latest), entity_infos: EntityInfos::new( @@ -1342,7 +903,7 @@ mod tests { accessed_addresses: HashSet::new(), }; - let simulator = create_simulator(provider, tracer); + let simulator = create_simulator(provider, entry_point, tracer); let res = simulator.gather_context_violations(&mut validation_context); assert_eq!( diff --git a/crates/sim/src/utils.rs b/crates/sim/src/utils.rs index ac26b522a..f60d12d68 100644 --- a/crates/sim/src/utils.rs +++ b/crates/sim/src/utils.rs @@ -17,7 +17,7 @@ use ethers::{ types::{spoof, Address, BlockId, Bytes, Selector, H256, U256}, }; use rundler_provider::Provider; -use rundler_types::contracts::{ +use rundler_types::contracts::utils::{ get_code_hashes::{CodeHashesResult, GETCODEHASHES_BYTECODE}, get_gas_used::{GasUsedResult, GETGASUSED_BYTECODE}, }; diff --git a/crates/types/.gitignore b/crates/types/.gitignore index 1eb781682..8e68a7764 100644 --- a/crates/types/.gitignore +++ b/crates/types/.gitignore @@ -1,2 +1,6 @@ # Generated code -/src/contracts +/src/contracts/v0_6 +/src/contracts/v0_7 +/src/contracts/arbitrum +/src/contracts/optimism +/src/contracts/utils diff --git a/crates/types/build.rs b/crates/types/build.rs index 9a43ee637..e91b29a36 100644 --- a/crates/types/build.rs +++ b/crates/types/build.rs @@ -19,48 +19,146 @@ fn main() -> Result<(), Box> { println!("cargo:rerun-if-changed=contracts/lib"); println!("cargo:rerun-if-changed=contracts/src"); println!("cargo:rerun-if-changed=contracts/foundry.toml"); - generate_contract_bindings()?; + generate_v0_6_bindings()?; + generate_v0_7_bindings()?; + generate_utils_bindings()?; + generate_arbitrum_bindings()?; + generate_optimism_bindings()?; Ok(()) } -fn generate_contract_bindings() -> Result<(), Box> { - generate_abis()?; +fn generate_v0_6_bindings() -> Result<(), Box> { + run_command( + Command::new("forge") + .arg("build") + .arg("--root") + .arg("./contracts") + .arg("--contracts") + .arg("src/v0_6") + .arg("--out") + .arg("out/v0_6") + .arg("--remappings") + .arg("@openzeppelin/=lib/openzeppelin-contracts-versions/v4_9"), + "https://getfoundry.sh/", + "generate ABIs", + )?; + MultiAbigen::from_abigens([ - abigen_of("IEntryPoint")?, - abigen_of("EntryPoint")?, - abigen_of("IAggregator")?, - abigen_of("IStakeManager")?, - abigen_of("GetCodeHashes")?, - abigen_of("GetBalances")?, - abigen_of("GetGasUsed")?, - abigen_of("CallGasEstimationProxy")?, - abigen_of("SimpleAccount")?, - abigen_of("SimpleAccountFactory")?, - abigen_of("VerifyingPaymaster")?, - abigen_of("NodeInterface")?, - abigen_of("GasPriceOracle")?, + abigen_of("v0_6", "IEntryPoint")?, + abigen_of("v0_6", "EntryPoint")?, + abigen_of("v0_6", "IAggregator")?, + abigen_of("v0_6", "IStakeManager")?, + abigen_of("v0_6", "GetBalances")?, + abigen_of("v0_6", "CallGasEstimationProxy")?, + abigen_of("v0_6", "SimpleAccount")?, + abigen_of("v0_6", "SimpleAccountFactory")?, + abigen_of("v0_6", "VerifyingPaymaster")?, ]) .build()? - .write_to_module("src/contracts", false)?; + .write_to_module("src/contracts/v0_6", false)?; + Ok(()) } -fn abigen_of(contract: &str) -> Result> { - Ok(Abigen::new( - contract, - format!("contracts/out/{contract}.sol/{contract}.json"), - )?) +fn generate_v0_7_bindings() -> Result<(), Box> { + run_command( + Command::new("forge") + .arg("build") + .arg("--root") + .arg("./contracts") + .arg("--contracts") + .arg("src/v0_7") + .arg("--out") + .arg("out/v0_7") + .arg("--remappings") + .arg("@openzeppelin/=lib/openzeppelin-contracts-versions/v5_0"), + "https://getfoundry.sh/", + "generate ABIs", + )?; + + MultiAbigen::from_abigens([ + abigen_of("v0_7", "IEntryPoint")?, + abigen_of("v0_7", "EntryPoint")?, + abigen_of("v0_7", "IAggregator")?, + abigen_of("v0_7", "IStakeManager")?, + ]) + .build()? + .write_to_module("src/contracts/v0_7", false)?; + + Ok(()) +} + +fn generate_utils_bindings() -> Result<(), Box> { + run_command( + Command::new("forge") + .arg("build") + .arg("--root") + .arg("./contracts") + .arg("--contracts") + .arg("src/utils") + .arg("--out") + .arg("out/utils"), + "https://getfoundry.sh/", + "generate ABIs", + )?; + + MultiAbigen::from_abigens([ + abigen_of("utils", "GetCodeHashes")?, + abigen_of("utils", "GetGasUsed")?, + ]) + .build()? + .write_to_module("src/contracts/utils", false)?; + + Ok(()) +} + +fn generate_arbitrum_bindings() -> Result<(), Box> { + run_command( + Command::new("forge") + .arg("build") + .arg("--root") + .arg("./contracts") + .arg("--contracts") + .arg("src/arbitrum") + .arg("--out") + .arg("out/arbitrum"), + "https://getfoundry.sh/", + "generate ABIs", + )?; + + MultiAbigen::from_abigens([abigen_of("arbitrum", "NodeInterface")?]) + .build()? + .write_to_module("src/contracts/arbitrum", false)?; + + Ok(()) } -fn generate_abis() -> Result<(), Box> { +fn generate_optimism_bindings() -> Result<(), Box> { run_command( Command::new("forge") .arg("build") .arg("--root") - .arg("./contracts"), + .arg("./contracts") + .arg("--contracts") + .arg("src/optimism") + .arg("--out") + .arg("out/optimism"), "https://getfoundry.sh/", "generate ABIs", - ) + )?; + + MultiAbigen::from_abigens([abigen_of("optimism", "GasPriceOracle")?]) + .build()? + .write_to_module("src/contracts/optimism", false)?; + + Ok(()) +} + +fn abigen_of(extra_path: &str, contract: &str) -> Result> { + Ok(Abigen::new( + contract, + format!("contracts/out/{extra_path}/{contract}.sol/{contract}.json"), + )?) } fn run_command( diff --git a/crates/types/contracts/foundry.toml b/crates/types/contracts/foundry.toml index d82db5af8..0705faad6 100644 --- a/crates/types/contracts/foundry.toml +++ b/crates/types/contracts/foundry.toml @@ -4,10 +4,11 @@ out = 'out' libs = ['lib'] test = 'test' cache_path = 'cache' +solc_version = '0.8.23' remappings = [ 'forge-std/=lib/forge-std/src', 'ds-test/=lib/forge-std/lib/ds-test/src/', - 'account-abstraction/=lib/account-abstraction/contracts/', - '@openzeppelin/=lib/openzeppelin-contracts/' + 'account-abstraction/v0_6=lib/account-abstraction-versions/v0_6/contracts/', + 'account-abstraction/v0_7=lib/account-abstraction-versions/v0_7/contracts/', ] diff --git a/crates/types/contracts/lib/account-abstraction b/crates/types/contracts/lib/account-abstraction deleted file mode 160000 index f3b5f7955..000000000 --- a/crates/types/contracts/lib/account-abstraction +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f3b5f795515ad8a7a7bf447575d6554854b820da diff --git a/crates/types/contracts/lib/account-abstraction-versions/v0_6 b/crates/types/contracts/lib/account-abstraction-versions/v0_6 new file mode 160000 index 000000000..fa61290d3 --- /dev/null +++ b/crates/types/contracts/lib/account-abstraction-versions/v0_6 @@ -0,0 +1 @@ +Subproject commit fa61290d37d079e928d92d53a122efcc63822214 diff --git a/crates/types/contracts/lib/account-abstraction-versions/v0_7 b/crates/types/contracts/lib/account-abstraction-versions/v0_7 new file mode 160000 index 000000000..7af70c899 --- /dev/null +++ b/crates/types/contracts/lib/account-abstraction-versions/v0_7 @@ -0,0 +1 @@ +Subproject commit 7af70c8993a6f42973f520ae0752386a5032abe7 diff --git a/crates/types/contracts/lib/openzeppelin-contracts b/crates/types/contracts/lib/openzeppelin-contracts deleted file mode 160000 index 0a25c1940..000000000 --- a/crates/types/contracts/lib/openzeppelin-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0a25c1940ca220686588c4af3ec526f725fe2582 diff --git a/crates/types/contracts/lib/openzeppelin-contracts-versions/v4_9 b/crates/types/contracts/lib/openzeppelin-contracts-versions/v4_9 new file mode 160000 index 000000000..dc44c9f1a --- /dev/null +++ b/crates/types/contracts/lib/openzeppelin-contracts-versions/v4_9 @@ -0,0 +1 @@ +Subproject commit dc44c9f1a4c3b10af99492eed84f83ed244203f6 diff --git a/crates/types/contracts/lib/openzeppelin-contracts-versions/v5_0 b/crates/types/contracts/lib/openzeppelin-contracts-versions/v5_0 new file mode 160000 index 000000000..dbb6104ce --- /dev/null +++ b/crates/types/contracts/lib/openzeppelin-contracts-versions/v5_0 @@ -0,0 +1 @@ +Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 diff --git a/crates/types/contracts/src/imports.sol b/crates/types/contracts/src/imports.sol deleted file mode 100644 index cc0cd33e3..000000000 --- a/crates/types/contracts/src/imports.sol +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -// Simply importing a dependency is enough for Forge to include it in builds. - -import "account-abstraction/samples/SimpleAccount.sol"; -import "account-abstraction/samples/SimpleAccountFactory.sol"; -import "account-abstraction/samples/VerifyingPaymaster.sol"; -import "account-abstraction/core/EntryPoint.sol"; -import "account-abstraction/interfaces/IAggregator.sol"; -import "account-abstraction/interfaces/IStakeManager.sol"; diff --git a/crates/types/contracts/src/GetCodeHashes.sol b/crates/types/contracts/src/utils/GetCodeHashes.sol similarity index 100% rename from crates/types/contracts/src/GetCodeHashes.sol rename to crates/types/contracts/src/utils/GetCodeHashes.sol diff --git a/crates/types/contracts/src/GetGasUsed.sol b/crates/types/contracts/src/utils/GetGasUsed.sol similarity index 100% rename from crates/types/contracts/src/GetGasUsed.sol rename to crates/types/contracts/src/utils/GetGasUsed.sol diff --git a/crates/types/contracts/src/CallGasEstimationProxy.sol b/crates/types/contracts/src/v0_6/CallGasEstimationProxy.sol similarity index 99% rename from crates/types/contracts/src/CallGasEstimationProxy.sol rename to crates/types/contracts/src/v0_6/CallGasEstimationProxy.sol index 331c31732..95143682c 100644 --- a/crates/types/contracts/src/CallGasEstimationProxy.sol +++ b/crates/types/contracts/src/v0_6/CallGasEstimationProxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; -import "account-abstraction/interfaces/IEntryPoint.sol"; +import "account-abstraction/v0_6/interfaces/IEntryPoint.sol"; import "@openzeppelin/contracts/proxy/Proxy.sol"; import "@openzeppelin/contracts/utils/math/Math.sol"; diff --git a/crates/types/contracts/src/GetBalances.sol b/crates/types/contracts/src/v0_6/GetBalances.sol similarity index 91% rename from crates/types/contracts/src/GetBalances.sol rename to crates/types/contracts/src/v0_6/GetBalances.sol index fa94d0f1d..e857bdb38 100644 --- a/crates/types/contracts/src/GetBalances.sol +++ b/crates/types/contracts/src/v0_6/GetBalances.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity ^0.8.12; -import "account-abstraction/interfaces/IStakeManager.sol"; +import "account-abstraction/v0_6/interfaces/IStakeManager.sol"; contract GetBalances { error GetBalancesResult(uint256[] balances); diff --git a/crates/types/contracts/src/PrecompileAccount.sol b/crates/types/contracts/src/v0_6/PrecompileAccount.sol similarity index 94% rename from crates/types/contracts/src/PrecompileAccount.sol rename to crates/types/contracts/src/v0_6/PrecompileAccount.sol index 311658507..3c0372048 100644 --- a/crates/types/contracts/src/PrecompileAccount.sol +++ b/crates/types/contracts/src/v0_6/PrecompileAccount.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.13; -import "account-abstraction/interfaces/IAccount.sol"; +import "account-abstraction/v0_6/interfaces/IAccount.sol"; contract PrecompileAccount is IAccount { address public precompile; diff --git a/crates/types/contracts/src/v0_6/imports.sol b/crates/types/contracts/src/v0_6/imports.sol new file mode 100644 index 000000000..fbb9f4ab1 --- /dev/null +++ b/crates/types/contracts/src/v0_6/imports.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +// Simply importing a dependency is enough for Forge to include it in builds. + +import "account-abstraction/v0_6/samples/SimpleAccount.sol"; +import "account-abstraction/v0_6/samples/SimpleAccountFactory.sol"; +import "account-abstraction/v0_6/samples/VerifyingPaymaster.sol"; +import "account-abstraction/v0_6/core/EntryPoint.sol"; +import "account-abstraction/v0_6/interfaces/IAggregator.sol"; +import "account-abstraction/v0_6/interfaces/IStakeManager.sol"; diff --git a/crates/types/contracts/src/v0_7/imports.sol b/crates/types/contracts/src/v0_7/imports.sol new file mode 100644 index 000000000..72be598ea --- /dev/null +++ b/crates/types/contracts/src/v0_7/imports.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +// Simply importing a dependency is enough for Forge to include it in builds. + +import "account-abstraction/v0_7/core/EntryPoint.sol"; +import "account-abstraction/v0_7/interfaces/IAggregator.sol"; +import "account-abstraction/v0_7/interfaces/IStakeManager.sol"; diff --git a/crates/types/contracts/test/PrecompileAccountTest.sol b/crates/types/contracts/test/PrecompileAccountTest.sol index 8b59023bd..7ed89f0d7 100644 --- a/crates/types/contracts/test/PrecompileAccountTest.sol +++ b/crates/types/contracts/test/PrecompileAccountTest.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.14; -import "../src/PrecompileAccount.sol"; -import "account-abstraction/interfaces/UserOperation.sol"; +import "../src/v0_6/PrecompileAccount.sol"; +import "account-abstraction/v0_6/interfaces/UserOperation.sol"; import "forge-std/Test.sol"; contract PrecompileAccountTest is Test { diff --git a/crates/types/src/contracts/mod.rs b/crates/types/src/contracts/mod.rs new file mode 100644 index 000000000..796006a37 --- /dev/null +++ b/crates/types/src/contracts/mod.rs @@ -0,0 +1,24 @@ +// 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/. + +//! Generated contract interfaces + +#![allow(non_snake_case)] +#![allow(clippy::all)] +#![allow(missing_docs)] + +pub mod arbitrum; +pub mod optimism; +pub mod utils; +pub mod v0_6; +pub mod v0_7; diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index fe9b7e8f1..aaadd4ba8 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -22,13 +22,9 @@ pub mod chain; -/// Generated contracts module -#[allow(non_snake_case)] #[rustfmt::skip] -#[allow(clippy::all)] -#[allow(missing_docs)] pub mod contracts; -pub use contracts::shared_types::{DepositInfo, UserOperation, UserOpsPerAggregator}; +pub use contracts::v0_6::shared_types::DepositInfo as DepositInfoV0_6; mod entity; pub use entity::{Entity, EntityType, EntityUpdate, EntityUpdateType}; @@ -40,7 +36,10 @@ mod timestamp; pub use timestamp::{Timestamp, ValidTimeRange}; mod user_operation; -pub use user_operation::UserOperationId; +pub use user_operation::{ + GasOverheads, UserOperation, UserOperationId, UserOperationV0_6, UserOperationV0_7, + UserOpsPerAggregator, UserOpsPerAggregatorV0_6, +}; mod storage; pub use storage::StorageSlot; diff --git a/crates/types/src/user_operation/mod.rs b/crates/types/src/user_operation/mod.rs new file mode 100644 index 000000000..68b072057 --- /dev/null +++ b/crates/types/src/user_operation/mod.rs @@ -0,0 +1,314 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use ethers::types::{Address, Bytes, H256, U256}; + +mod v0_6; +pub use v0_6::{ + UserOperation as UserOperationV0_6, UserOpsPerAggregator as UserOpsPerAggregatorV0_6, +}; +mod v0_7; +pub use v0_7::UserOperation as UserOperationV0_7; + +use crate::Entity; + +/// Unique identifier for a user operation from a given sender +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct UserOperationId { + /// sender of user operation + pub sender: Address, + /// nonce of user operation + pub nonce: U256, +} + +/// User operation type enum +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum UserOperationType { + V0_6, + V0_7, +} + +/// User operation enum +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum UserOperation { + /// 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 { + match self { + UserOperation::V0_6(op) => op.hash(entry_point, chain_id), + UserOperation::V0_7(op) => op.hash(entry_point, chain_id), + } + } + + /// Get the user operation id + pub fn id(&self) -> UserOperationId { + match self { + UserOperation::V0_6(op) => op.id(), + UserOperation::V0_7(op) => op.id(), + } + } + + /// Get the user operation sender address + pub fn sender(&self) -> Address { + match self { + UserOperation::V0_6(op) => op.sender, + UserOperation::V0_7(op) => op.sender, + } + } + + /// Get the user operation paymaster address, if any + pub fn paymaster(&self) -> Option
{ + match self { + UserOperation::V0_6(op) => op.paymaster(), + UserOperation::V0_7(op) => op.paymaster(), + } + } + + /// Get the user operation factory address, if any + pub fn factory(&self) -> Option
{ + match self { + UserOperation::V0_6(op) => op.factory(), + UserOperation::V0_7(op) => op.factory(), + } + } + + /// Returns the maximum cost, in wei, of this user operation + pub fn max_gas_cost(&self) -> U256 { + match self { + UserOperation::V0_6(op) => op.max_gas_cost(), + UserOperation::V0_7(op) => op.max_gas_cost(), + } + } + + /// Gets an iterator on all entities associated with this user operation + pub fn entities(&'_ self) -> Vec { + match self { + UserOperation::V0_6(op) => op.entities(), + UserOperation::V0_7(op) => op.entities(), + } + } + + /// Returns the heap size of the user operation + pub fn heap_size(&self) -> usize { + match self { + UserOperation::V0_6(op) => op.heap_size(), + UserOperation::V0_7(op) => op.heap_size(), + } + } + + /// Returns the call gas limit + pub 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(), + } + } + + pub 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(), + } + } + + /// Returns the total verification gas limit + pub 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(), + } + } + + // TODO(danc) + pub 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) + } + } + } + + pub 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(), + } + } + + /// Returns the pre-verification gas + pub fn pre_verification_gas(&self) -> U256 { + match self { + UserOperation::V0_6(op) => op.pre_verification_gas, + UserOperation::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 { + match self { + UserOperation::V0_6(op) => { + op.calc_static_pre_verification_gas(include_fixed_gas_overhead) + } + UserOperation::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 { + match self { + UserOperation::V0_6(op) => op.max_priority_fee_per_gas, + UserOperation::V0_7(op) => op.max_priority_fee_per_gas.into(), + } + } + + /// 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) { + match self { + UserOperation::V0_6(op) => op.signature = Bytes::new(), + UserOperation::V0_7(op) => op.signature = Bytes::new(), + } + } + + /// Returns the user operation type + pub fn uo_type(&self) -> UserOperationType { + match self { + UserOperation::V0_6(_) => UserOperationType::V0_6, + UserOperation::V0_7(_) => UserOperationType::V0_7, + } + } + + /// Returns true if the user operation is of type [UserOperationV0_6] + pub fn is_v0_6(&self) -> bool { + matches!(self, UserOperation::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(_)) + } + + /// 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), + _ => None, + } + } + + /// 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), + _ => None, + } + } + + /// 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), + _ => None, + } + } + + /// 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), + _ => 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") + } +} + +/// Gas overheads for user operations used in calculating the pre-verification gas. See: https://github.com/eth-infinitism/bundler/blob/main/packages/sdk/src/calcPreVerificationGas.ts +#[derive(Clone, Copy, Debug)] +pub struct GasOverheads { + /// The Entrypoint requires a gas buffer for the bundle to account for the gas spent outside of the major steps in the processing of UOs + pub bundle_transaction_gas_buffer: U256, + /// The fixed gas overhead for any EVM transaction + pub transaction_gas_overhead: U256, + per_user_op: U256, + per_user_op_word: U256, + zero_byte: U256, + non_zero_byte: U256, +} + +impl Default for GasOverheads { + fn default() -> Self { + Self { + bundle_transaction_gas_buffer: 5_000.into(), + transaction_gas_overhead: 21_000.into(), + per_user_op: 18_300.into(), + per_user_op_word: 4.into(), + zero_byte: 4.into(), + non_zero_byte: 16.into(), + } + } +} + +#[derive(Debug)] +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.rs b/crates/types/src/user_operation/v0_6.rs similarity index 65% rename from crates/types/src/user_operation.rs rename to crates/types/src/user_operation/v0_6.rs index fb3e633dc..597047ff8 100644 --- a/crates/types/src/user_operation.rs +++ b/crates/types/src/user_operation/v0_6.rs @@ -12,35 +12,19 @@ // If not, see https://www.gnu.org/licenses/. use ethers::{ - abi::{encode, Token}, + abi::{encode, AbiEncode, Token}, types::{Address, Bytes, H256, U256}, utils::keccak256, }; use strum::IntoEnumIterator; -use crate::{ - entity::{Entity, EntityType}, - UserOperation, -}; - -/// Number of bytes in the fixed size portion of an ABI encoded user operation -const PACKED_USER_OPERATION_FIXED_LEN: usize = 480; - -/// Unique identifier for a user operation from a given sender -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct UserOperationId { - /// sender of user operation - pub sender: Address, - /// nonce of user operation - pub nonce: U256, -} +use super::{GasOverheads, UserOperationId}; +pub use crate::contracts::v0_6::shared_types::{UserOperation, UserOpsPerAggregator}; +use crate::entity::{Entity, EntityType}; impl UserOperation { - /// Hash a user operation with the given entry point and chain ID. - /// - /// The hash is used to uniquely identify a user operation in the entry point. - /// It does not include the signature field. - pub fn op_hash(&self, entry_point: Address, chain_id: u64) -> H256 { + /// asdf + pub 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), @@ -49,7 +33,7 @@ impl UserOperation { .into() } - /// Get the unique identifier for this user operation from its sender + /// asdf pub fn id(&self) -> UserOperationId { UserOperationId { sender: self.sender, @@ -57,27 +41,75 @@ impl UserOperation { } } - /// Get the address of the factory entity associated with this user operation, if any + /// asdf pub fn factory(&self) -> Option
{ Self::get_address_from_field(&self.init_code) } - /// Returns the maximum cost, in wei, of this user operation + /// asdf + pub fn paymaster(&self) -> Option
{ + Self::get_address_from_field(&self.paymaster_and_data) + } + + /// asdf pub fn max_gas_cost(&self) -> U256 { let mul = if self.paymaster().is_some() { 3 } else { 1 }; self.max_fee_per_gas * (self.pre_verification_gas + self.call_gas_limit + self.verification_gas_limit * mul) } - /// Get the address of the paymaster entity associated with this user operation, if any - pub fn paymaster(&self) -> Option
{ - Self::get_address_from_field(&self.paymaster_and_data) + /// asdf + pub 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 { + EntityType::iter() + .filter_map(|entity| { + self.entity_address(entity) + .map(|address| Entity::new(entity, address)) + }) + .collect() + } + + /// asdf + pub 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 { + 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 + let call_data_cost: U256 = encoded_op + .iter() + .map(|&x| { + if x == 0 { + ov.zero_byte + } else { + ov.non_zero_byte + } + }) + .reduce(|a, b| a + b) + .unwrap_or_default(); + + call_data_cost + + ov.per_user_op + + ov.per_user_op_word * length_in_words + + (if include_fixed_gas_overhead { + ov.transaction_gas_overhead + } else { + 0.into() + }) } - /// Extracts an address from the beginning of a data field - /// Useful to extract the paymaster address from paymaster_and_data - /// and the factory address from init_code - pub fn get_address_from_field(data: &Bytes) -> Option
{ + fn get_address_from_field(data: &Bytes) -> Option
{ if data.len() < 20 { None } else { @@ -85,25 +117,7 @@ impl UserOperation { } } - /// Efficient calculation of the size of a packed user operation - pub fn abi_encoded_size(&self) -> usize { - PACKED_USER_OPERATION_FIXED_LEN - + pad_len(&self.init_code) - + pad_len(&self.call_data) - + pad_len(&self.paymaster_and_data) - + pad_len(&self.signature) - } - - /// Compute the amount of heap memory the UserOperation takes up. - pub fn heap_size(&self) -> usize { - self.init_code.len() - + self.call_data.len() - + self.paymaster_and_data.len() - + self.signature.len() - } - - /// Gets the byte array representation of the user operation to be used in the signature - pub fn pack_for_hash(&self) -> Bytes { + fn pack_for_hash(&self) -> Bytes { let hash_init_code = keccak256(self.init_code.clone()); let hash_call_data = keccak256(self.call_data.clone()); let hash_paymaster_and_data = keccak256(self.paymaster_and_data.clone()); @@ -123,15 +137,6 @@ impl UserOperation { .into() } - /// Gets an iterator on all entities associated with this user operation - pub fn entities(&'_ self) -> impl Iterator + '_ { - EntityType::iter().filter_map(|entity| { - self.entity_address(entity) - .map(|address| Entity::new(entity, address)) - }) - } - - /// Gets the address of the entity of the given type associated with this user operation, if any fn entity_address(&self, entity: EntityType) -> Option
{ match entity { EntityType::Account => Some(self.sender), @@ -142,19 +147,16 @@ impl UserOperation { } } -/// Calculates the size a byte array padded to the next largest multiple of 32 -fn pad_len(b: &Bytes) -> usize { - (b.len() + 31) & !31 +impl From for super::UserOperation { + fn from(op: UserOperation) -> Self { + super::UserOperation::V0_6(op) + } } #[cfg(test)] mod tests { - use std::str::FromStr; - use ethers::{ - abi::AbiEncode, - types::{Bytes, U256}, - }; + use ethers::types::{Bytes, U256}; use super::*; @@ -198,7 +200,7 @@ mod tests { .parse() .unwrap(); let chain_id = 1337; - let hash = operation.op_hash(entry_point, chain_id); + let hash = operation.hash(entry_point, chain_id); assert_eq!( hash, "0xdca97c3b49558ab360659f6ead939773be8bf26631e61bb17045bb70dc983b2d" @@ -258,7 +260,7 @@ mod tests { .parse() .unwrap(); let chain_id = 1337; - let hash = operation.op_hash(entry_point, chain_id); + let hash = operation.hash(entry_point, chain_id); assert_eq!( hash, "0x484add9e4d8c3172d11b5feb6a3cc712280e176d278027cfa02ee396eb28afa1" @@ -281,28 +283,4 @@ mod tests { .unwrap() ); } - - #[test] - fn test_abi_encoded_size() { - let user_operation = UserOperation { - sender: "0xe29a7223a7e040d70b5cd460ef2f4ac6a6ab304d" - .parse() - .unwrap(), - nonce: U256::from_dec_str("3937668929043450082210854285941660524781292117276598730779").unwrap(), - init_code: Bytes::default(), - call_data: Bytes::from_str("0x5194544700000000000000000000000058440a3e78b190e5bd07905a08a60e30bb78cb5b000000000000000000000000000000000000000000000000000009184e72a000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap(), - call_gas_limit: 40_960.into(), - verification_gas_limit: 75_099.into(), - pre_verification_gas: 46_330.into(), - max_fee_per_gas: 105_000_000.into(), - max_priority_fee_per_gas: 105_000_000.into(), - paymaster_and_data: Bytes::from_str("0xc03aac639bb21233e0139381970328db8bceeb6700006508996f000065089a9b0000000000000000000000000000000000000000ca7517be4e51ca2cde69bc44c4c3ce00ff7f501ce4ee1b3c6b2a742f579247292e4f9a672522b15abee8eaaf1e1487b8e3121d61d42ba07a47f5ccc927aa7eb61b").unwrap(), - signature: Bytes::from_str("0x00000000f8a0655423f2dfbb104e0ff906b7b4c64cfc12db0ac5ef0fb1944076650ce92a1a736518e5b6cd46c6ff6ece7041f2dae199fb4c8e7531704fbd629490b712dc1b").unwrap(), - }; - - assert_eq!( - user_operation.clone().encode().len(), - user_operation.abi_encoded_size() - ); - } } diff --git a/crates/types/src/user_operation/v0_7.rs b/crates/types/src/user_operation/v0_7.rs new file mode 100644 index 000000000..31ef79088 --- /dev/null +++ b/crates/types/src/user_operation/v0_7.rs @@ -0,0 +1,286 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use ethers::{ + abi::{encode, Token}, + types::{Address, Bytes, H256, U128, U256}, + utils::keccak256, +}; + +use super::UserOperationId; +use crate::{contracts::v0_7::shared_types::PackedUserOperation, Entity}; + +/// User Operation +/// +/// Offchain version, must be packed before sending onchain +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct UserOperation { + pub sender: Address, + pub nonce: U256, + pub factory: Option
, + pub factory_data: Bytes, + pub call_data: Bytes, + pub call_gas_limit: U128, + pub verification_gas_limit: U128, + pub pre_verification_gas: U256, + pub max_priority_fee_per_gas: U128, + pub max_fee_per_gas: U128, + pub paymaster: Option
, + pub paymaster_verification_gas_limit: U128, + pub paymaster_post_op_gas_limit: U128, + pub paymaster_data: Bytes, + pub signature: Bytes, + hash: H256, + packed: PackedUserOperation, +} + +impl UserOperation { + pub fn hash(&self, _entry_point: Address, _chain_id: u64) -> H256 { + self.hash + } + + pub fn id(&self) -> UserOperationId { + UserOperationId { + sender: self.sender, + nonce: self.nonce, + } + } + + pub fn paymaster(&self) -> Option
{ + self.paymaster + } + + pub fn factory(&self) -> Option
{ + self.factory + } + + pub fn max_gas_cost(&self) -> U256 { + U256::from(self.max_fee_per_gas) + * (self.pre_verification_gas + + self.call_gas_limit + + self.verification_gas_limit + + self.paymaster_verification_gas_limit + + self.paymaster_post_op_gas_limit) + } + + pub fn entities(&self) -> Vec { + let mut ret = vec![Entity::account(self.sender)]; + if let Some(factory) = self.factory { + ret.push(Entity::factory(factory)); + } + if let Some(paymaster) = self.paymaster { + ret.push(Entity::paymaster(paymaster)); + } + ret + } + + pub fn heap_size(&self) -> usize { + unimplemented!() + } + + pub 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!() + } + + pub fn pack(self) -> PackedUserOperation { + self.packed + } +} + +impl From for super::UserOperation { + fn from(op: UserOperation) -> Self { + super::UserOperation::V0_7(op) + } +} + +pub struct UserOperationBuilder { + // required fields for hash + entry_point: Address, + chain_id: u64, + + // required fields + required: UserOperationRequiredFields, + + // optional fields + factory: Option
, + factory_data: Bytes, + paymaster: Option
, + paymaster_verification_gas_limit: U128, + paymaster_post_op_gas_limit: U128, + paymaster_data: Bytes, +} + +pub struct UserOperationRequiredFields { + sender: Address, + nonce: U256, + call_data: Bytes, + call_gas_limit: U128, + verification_gas_limit: U128, + pre_verification_gas: U256, + max_priority_fee_per_gas: U128, + max_fee_per_gas: U128, + signature: Bytes, +} + +impl UserOperationBuilder { + pub fn new(entry_point: Address, chain_id: u64, required: UserOperationRequiredFields) -> Self { + Self { + entry_point, + chain_id, + required, + factory: None, + factory_data: Bytes::new(), + paymaster: None, + paymaster_verification_gas_limit: U128::zero(), + paymaster_post_op_gas_limit: U128::zero(), + paymaster_data: Bytes::new(), + } + } + + pub fn factory(mut self, factory: Address, factory_data: Bytes) -> Self { + self.factory = Some(factory); + self.factory_data = factory_data; + self + } + + pub fn paymaster( + mut self, + paymaster: Address, + paymaster_verification_gas_limit: U128, + paymaster_post_op_gas_limit: U128, + paymaster_data: Bytes, + ) -> Self { + self.paymaster = Some(paymaster); + self.paymaster_verification_gas_limit = paymaster_verification_gas_limit; + self.paymaster_post_op_gas_limit = paymaster_post_op_gas_limit; + self.paymaster_data = paymaster_data; + self + } + + pub fn build(self) -> UserOperation { + let uo = UserOperation { + sender: self.required.sender, + nonce: self.required.nonce, + factory: self.factory, + factory_data: self.factory_data, + call_data: self.required.call_data, + call_gas_limit: self.required.call_gas_limit, + verification_gas_limit: self.required.verification_gas_limit, + pre_verification_gas: self.required.pre_verification_gas, + max_priority_fee_per_gas: self.required.max_priority_fee_per_gas, + max_fee_per_gas: self.required.max_fee_per_gas, + paymaster: self.paymaster, + paymaster_verification_gas_limit: self.paymaster_verification_gas_limit, + paymaster_post_op_gas_limit: self.paymaster_post_op_gas_limit, + paymaster_data: self.paymaster_data, + signature: self.required.signature, + hash: H256::zero(), + packed: PackedUserOperation::default(), + }; + + let packed = pack_user_operation(uo.clone()); + let hash = hash_packed_user_operation(packed.clone(), self.entry_point, self.chain_id); + + UserOperation { hash, packed, ..uo } + } +} + +fn pack_user_operation(uo: UserOperation) -> PackedUserOperation { + let init_code = if let Some(factory) = uo.factory { + let mut init_code = factory.as_bytes().to_vec(); + init_code.extend_from_slice(&uo.factory_data); + Bytes::from(init_code) + } else { + Bytes::new() + }; + + let account_gas_limits = concat_128( + uo.verification_gas_limit.low_u128().to_le_bytes(), + uo.call_gas_limit.low_u128().to_le_bytes(), + ); + + let gas_fees = concat_128( + uo.max_priority_fee_per_gas.low_u128().to_le_bytes(), + uo.max_fee_per_gas.low_u128().to_le_bytes(), + ); + + let paymaster_and_data = if let Some(paymaster) = uo.paymaster { + let mut paymaster_and_data = paymaster.as_bytes().to_vec(); + paymaster_and_data + .extend_from_slice(&uo.paymaster_verification_gas_limit.low_u128().to_le_bytes()); + paymaster_and_data + .extend_from_slice(&uo.paymaster_post_op_gas_limit.low_u128().to_le_bytes()); + paymaster_and_data.extend_from_slice(&uo.paymaster_data); + Bytes::from(paymaster_and_data) + } else { + Bytes::new() + }; + + PackedUserOperation { + sender: uo.sender, + nonce: uo.nonce, + init_code, + call_data: uo.call_data, + account_gas_limits, + pre_verification_gas: uo.pre_verification_gas, + gas_fees, + paymaster_and_data, + signature: uo.signature, + } +} + +fn hash_packed_user_operation( + puo: PackedUserOperation, + entry_point: Address, + chain_id: u64, +) -> H256 { + let hash_init_code = keccak256(puo.init_code.clone()); + let hash_call_data = keccak256(puo.call_data.clone()); + let hash_paymaster_and_data = keccak256(puo.paymaster_and_data.clone()); + + let encoded: Bytes = encode(&[ + Token::Address(puo.sender), + Token::Uint(puo.nonce), + Token::FixedBytes(hash_init_code.to_vec()), + Token::FixedBytes(hash_call_data.to_vec()), + Token::FixedBytes(puo.account_gas_limits.to_vec()), + Token::Uint(puo.pre_verification_gas), + Token::FixedBytes(puo.gas_fees.to_vec()), + Token::FixedBytes(hash_paymaster_and_data.to_vec()), + ]) + .into(); + + let hashed = keccak256(encoded); + + keccak256(encode(&[ + Token::FixedBytes(hashed.to_vec()), + Token::Address(entry_point), + Token::Uint(chain_id.into()), + ])) + .into() +} + +fn concat_128(a: [u8; 16], b: [u8; 16]) -> [u8; 32] { + std::array::from_fn(|i| { + if let Some(i) = i.checked_sub(a.len()) { + b[i] + } else { + a[i] + } + }) +} diff --git a/crates/types/src/validation_results.rs b/crates/types/src/validation_results.rs index 695738210..dad3659be 100644 --- a/crates/types/src/validation_results.rs +++ b/crates/types/src/validation_results.rs @@ -18,7 +18,7 @@ use ethers::{ }; use crate::{ - contracts::entry_point::{ValidationResult, ValidationResultWithAggregation}, + contracts::v0_6::entry_point::{ValidationResult, ValidationResultWithAggregation}, Timestamp, };