From 784e36ad716c25a09d3a7b17f26b07f7929c420d Mon Sep 17 00:00:00 2001 From: dancoombs Date: Tue, 5 Mar 2024 17:01:43 -0500 Subject: [PATCH 01/12] feat(types): add entrypoint 0.7 contract types --- crates/pool/src/mempool/paymaster.rs | 7 ++- .../provider/src/ethers/entry_point/v0_6.rs | 45 +++++++++++++++---- crates/provider/src/lib.rs | 5 ++- crates/provider/src/traits/entry_point.rs | 39 +++++++++++++--- crates/provider/src/traits/mod.rs | 4 +- crates/provider/src/traits/test_utils.rs | 11 ++--- crates/rpc/src/eth/api.rs | 4 ++ crates/sim/src/estimation/v0_6.rs | 32 ++++++------- crates/types/src/lib.rs | 1 - 9 files changed, 103 insertions(+), 45 deletions(-) diff --git a/crates/pool/src/mempool/paymaster.rs b/crates/pool/src/mempool/paymaster.rs index 471a55ef2..f1e3fee97 100644 --- a/crates/pool/src/mempool/paymaster.rs +++ b/crates/pool/src/mempool/paymaster.rs @@ -523,11 +523,10 @@ impl PaymasterBalance { #[cfg(test)] mod tests { use ethers::types::{Address, H256, U256}; - use rundler_provider::MockEntryPointV0_6; + use rundler_provider::{DepositInfo, MockEntryPointV0_6}; use rundler_sim::EntityInfos; use rundler_types::{ - contracts::v0_6::verifying_paymaster::DepositInfo, v0_6::UserOperation, - UserOperation as UserOperationTrait, UserOperationId, ValidTimeRange, + v0_6::UserOperation, UserOperation as UserOperationTrait, UserOperationId, ValidTimeRange, }; use super::*; @@ -974,7 +973,7 @@ mod tests { entrypoint.expect_get_deposit_info().returning(|_| { Ok(DepositInfo { - deposit: 1000, + deposit: 1000.into(), staked: true, stake: 10000, unstake_delay_sec: 100, diff --git a/crates/provider/src/ethers/entry_point/v0_6.rs b/crates/provider/src/ethers/entry_point/v0_6.rs index a2223e039..9bc7ad76e 100644 --- a/crates/provider/src/ethers/entry_point/v0_6.rs +++ b/crates/provider/src/ethers/entry_point/v0_6.rs @@ -31,18 +31,21 @@ use rundler_types::{ v0_6::{ get_balances::{GetBalancesResult, GETBALANCES_BYTECODE}, i_aggregator::IAggregator, - i_entry_point::{ExecutionResult, FailedOp, IEntryPoint, SignatureValidationFailed}, + i_entry_point::{ + DepositInfo as DepositInfoV0_6, ExecutionResult as ExecutionResultV0_6, FailedOp, + IEntryPoint, SignatureValidationFailed, + }, shared_types::UserOpsPerAggregator as UserOpsPerAggregatorV0_6, }, }, v0_6::UserOperation, - DepositInfoV0_6, GasFees, UserOpsPerAggregator, ValidationOutput, + GasFees, UserOpsPerAggregator, ValidationOutput, }; use rundler_utils::eth::{self, ContractRevertError}; use crate::{ - traits::HandleOpsOut, AggregatorOut, AggregatorSimOut, BundleHandler, L1GasProvider, Provider, - SignatureAggregator, SimulationProvider, + traits::HandleOpsOut, AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, + ExecutionResult, L1GasProvider, Provider, SignatureAggregator, SimulationProvider, }; const ARBITRUM_NITRO_NODE_INTERFACE_ADDRESS: Address = H160([ @@ -119,12 +122,13 @@ where .context("entry point should return balance") } - async fn get_deposit_info(&self, address: Address) -> anyhow::Result { + async fn get_deposit_info(&self, address: Address) -> anyhow::Result { Ok(self .i_entry_point .get_deposit_info(address) .await - .context("should get deposit info")?) + .context("should get deposit info")? + .into()) } async fn get_balances(&self, addresses: Vec
) -> anyhow::Result> { @@ -348,8 +352,8 @@ where &self, revert_data: Bytes, ) -> Result { - if let Ok(result) = ExecutionResult::decode(&revert_data) { - Ok(result) + if let Ok(result) = ExecutionResultV0_6::decode(&revert_data) { + Ok(result.into()) } else if let Ok(failed_op) = FailedOp::decode(&revert_data) { Err(failed_op.reason) } else if let Ok(err) = ContractRevertError::decode(&revert_data) { @@ -406,3 +410,28 @@ fn get_handle_ops_call( }; call.gas(gas) } + +impl From for ExecutionResult { + fn from(result: ExecutionResultV0_6) -> Self { + ExecutionResult { + pre_op_gas: result.pre_op_gas, + paid: result.paid, + valid_after: result.valid_after.into(), + valid_until: result.valid_until.into(), + target_success: result.target_success, + target_result: result.target_result, + } + } +} + +impl From for DepositInfo { + fn from(info: DepositInfoV0_6) -> Self { + DepositInfo { + deposit: info.deposit.into(), + staked: info.staked, + stake: info.stake, + unstake_delay_sec: info.unstake_delay_sec, + withdraw_time: info.withdraw_time, + } + } +} diff --git a/crates/provider/src/lib.rs b/crates/provider/src/lib.rs index bf8a5efeb..e05a69cb1 100644 --- a/crates/provider/src/lib.rs +++ b/crates/provider/src/lib.rs @@ -30,6 +30,7 @@ pub use traits::test_utils::*; #[cfg(any(test, feature = "test-utils"))] pub use traits::MockProvider; pub use traits::{ - AggregatorOut, AggregatorSimOut, BundleHandler, EntryPoint, HandleOpsOut, L1GasProvider, - Provider, ProviderError, ProviderResult, SignatureAggregator, SimulationProvider, + AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint, ExecutionResult, + HandleOpsOut, L1GasProvider, Provider, ProviderError, ProviderResult, SignatureAggregator, + SimulationProvider, }; diff --git a/crates/provider/src/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index ea69fd015..8e8678b17 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -14,10 +14,7 @@ use ethers::types::{ spoof, transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, H256, U256, }; -use rundler_types::{ - contracts::v0_6::i_entry_point::ExecutionResult, DepositInfoV0_6, GasFees, UserOperation, - UserOpsPerAggregator, ValidationOutput, -}; +use rundler_types::{GasFees, Timestamp, UserOperation, UserOpsPerAggregator, ValidationOutput}; /// Output of a successful signature aggregator simulation call #[derive(Clone, Debug, Default)] @@ -53,6 +50,38 @@ pub enum HandleOpsOut { PostOpRevert, } +/// Deposit info for an address from the entry point contract +#[derive(Clone, Debug, Default)] +pub struct DepositInfo { + /// Amount deposited on the entry point + pub deposit: U256, + /// Whether the address has staked + pub staked: bool, + /// Amount staked on the entry point + pub stake: u128, + /// The amount of time in sections that must pass before the stake can be withdrawn + pub unstake_delay_sec: u32, + /// The time at which the stake can be withdrawn + pub withdraw_time: u64, +} + +/// Result of an execution +#[derive(Clone, Debug, Default)] +pub struct ExecutionResult { + /// Gas used before the operation execution + pub pre_op_gas: U256, + /// Amount paid by the operation + pub paid: U256, + /// Time which the operation is valid after + pub valid_after: Timestamp, + /// Time which the operation is valid until + pub valid_until: Timestamp, + /// True if the operation execution succeeded + pub target_success: bool, + /// Result of the operation execution + pub target_result: Bytes, +} + /// Trait for interacting with an entry point contract. #[async_trait::async_trait] #[auto_impl::auto_impl(&, Arc)] @@ -65,7 +94,7 @@ pub trait EntryPoint: Send + Sync + 'static { -> anyhow::Result; /// Get the deposit info for an address - async fn get_deposit_info(&self, address: Address) -> anyhow::Result; + async fn get_deposit_info(&self, address: Address) -> anyhow::Result; /// Get the balances of a list of addresses in order async fn get_balances(&self, addresses: Vec
) -> anyhow::Result>; diff --git a/crates/provider/src/traits/mod.rs b/crates/provider/src/traits/mod.rs index 24fb1b441..42aa230d4 100644 --- a/crates/provider/src/traits/mod.rs +++ b/crates/provider/src/traits/mod.rs @@ -18,8 +18,8 @@ pub use error::ProviderError; mod entry_point; pub use entry_point::{ - AggregatorOut, AggregatorSimOut, BundleHandler, EntryPoint, HandleOpsOut, L1GasProvider, - SignatureAggregator, SimulationProvider, + AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint, ExecutionResult, + HandleOpsOut, L1GasProvider, SignatureAggregator, SimulationProvider, }; mod provider; diff --git a/crates/provider/src/traits/test_utils.rs b/crates/provider/src/traits/test_utils.rs index 7b5492e01..62cf78815 100644 --- a/crates/provider/src/traits/test_utils.rs +++ b/crates/provider/src/traits/test_utils.rs @@ -14,14 +14,11 @@ use ethers::types::{ spoof, transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, H256, U256, }; -use rundler_types::{ - contracts::v0_6::i_entry_point::ExecutionResult, v0_6, DepositInfoV0_6, GasFees, - UserOpsPerAggregator, ValidationOutput, -}; +use rundler_types::{v0_6, GasFees, UserOpsPerAggregator, ValidationOutput}; use crate::{ - AggregatorOut, BundleHandler, EntryPoint, HandleOpsOut, L1GasProvider, SignatureAggregator, - SimulationProvider, + AggregatorOut, BundleHandler, DepositInfo, EntryPoint, ExecutionResult, HandleOpsOut, + L1GasProvider, SignatureAggregator, SimulationProvider, }; mockall::mock! { @@ -32,7 +29,7 @@ mockall::mock! { fn address(&self) -> Address; async fn balance_of(&self, address: Address, block_id: Option) -> anyhow::Result; - async fn get_deposit_info(&self, address: Address) -> anyhow::Result; + async fn get_deposit_info(&self, address: Address) -> anyhow::Result; async fn get_balances(&self, addresses: Vec
) -> anyhow::Result>; } diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 99fb4c49a..bbbac1b87 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -587,6 +587,10 @@ mod tests { contracts::v0_6::i_entry_point::HandleOpsCall, v0_6::UserOperation, UserOperation as UserOperationTrait, ValidTimeRange, }; + use rundler_pool::{MockPoolServer, PoolOperation}; + use rundler_provider::{MockEntryPoint, MockProvider}; + use rundler_sim::PriorityFeeMode; + use rundler_types::contracts::v0_6::i_entry_point::HandleOpsCall; use super::*; diff --git a/crates/sim/src/estimation/v0_6.rs b/crates/sim/src/estimation/v0_6.rs index 8342020fc..9f09b752e 100644 --- a/crates/sim/src/estimation/v0_6.rs +++ b/crates/sim/src/estimation/v0_6.rs @@ -470,10 +470,10 @@ mod tests { types::U64, utils::hex, }; - use rundler_provider::{MockEntryPointV0_6, MockProvider}; + use rundler_provider::{ExecutionResult, MockEntryPointV0_6, MockProvider}; use rundler_types::{ chain::L1GasOracleContractType, - contracts::{utils::get_gas_used::GasUsedResult, v0_6::i_entry_point::ExecutionResult}, + contracts::utils::get_gas_used::GasUsedResult, v0_6::{UserOperation, UserOperationOptionalGas}, UserOperation as UserOperationTrait, }; @@ -746,8 +746,8 @@ mod tests { Ok(ExecutionResult { pre_op_gas: U256::from(10000), paid: U256::from(100000), - valid_after: 100000000000, - valid_until: 100000000001, + valid_after: 100000000000.into(), + valid_until: 100000000001.into(), target_success: true, target_result: Bytes::new(), }) @@ -804,8 +804,8 @@ mod tests { Ok(ExecutionResult { pre_op_gas: U256::from(10000), paid: U256::from(100000), - valid_after: 100000000000, - valid_until: 100000000001, + valid_after: 100000000000.into(), + valid_until: 100000000001.into(), target_success: true, target_result: Bytes::new(), }) @@ -861,8 +861,8 @@ mod tests { Ok(ExecutionResult { pre_op_gas: U256::from(10000), paid: U256::from(100000), - valid_after: 100000000000, - valid_until: 100000000001, + valid_after: 100000000000.into(), + valid_until: 100000000001.into(), target_success: true, target_result: Bytes::new(), }) @@ -957,8 +957,8 @@ mod tests { Ok(ExecutionResult { pre_op_gas: U256::from(10000), paid: U256::from(100000), - valid_after: 100000000000, - valid_until: 100000000001, + valid_after: 100000000000.into(), + valid_until: 100000000001.into(), target_success: true, target_result: Bytes::new(), }) @@ -999,8 +999,8 @@ mod tests { Ok(ExecutionResult { pre_op_gas: U256::from(10000), paid: U256::from(100000), - valid_after: 100000000000, - valid_until: 100000000001, + valid_after: 100000000000.into(), + valid_until: 100000000001.into(), target_success: true, target_result: Bytes::new(), }) @@ -1199,8 +1199,8 @@ mod tests { Ok(ExecutionResult { pre_op_gas: U256::from(10000), paid: U256::from(100000), - valid_after: 100000000000, - valid_until: 100000000001, + valid_after: 100000000000.into(), + valid_until: 100000000001.into(), target_success: true, target_result: Bytes::new(), }) @@ -1283,8 +1283,8 @@ mod tests { Ok(ExecutionResult { pre_op_gas: U256::from(10000), paid: U256::from(100000), - valid_after: 100000000000, - valid_until: 100000000001, + valid_after: 100000000000.into(), + valid_until: 100000000001.into(), target_success: true, target_result: Bytes::new(), }) diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 7df0bde63..10366d99c 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -24,7 +24,6 @@ pub mod chain; #[rustfmt::skip] pub mod contracts; -pub use contracts::v0_6::shared_types::DepositInfo as DepositInfoV0_6; mod entity; pub use entity::{Entity, EntityType, EntityUpdate, EntityUpdateType}; From 5a5442059d675361285616ace7afc485cf90dbce Mon Sep 17 00:00:00 2001 From: dancoombs Date: Tue, 12 Mar 2024 06:59:12 -0400 Subject: [PATCH 02/12] feat: add and use a user operation trait --- crates/rpc/src/eth/api.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index bbbac1b87..01dc61509 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -581,16 +581,12 @@ mod tests { }; use mockall::predicate::eq; use rundler_pool::{IntoPoolOperationVariant, MockPoolServer, PoolOperation}; - use rundler_provider::{MockEntryPointV0_6, MockProvider}; + use rundler_provider::{MockEntryPoint, MockEntryPointV0_6, MockProvider}; use rundler_sim::{EntityInfos, PriorityFeeMode}; use rundler_types::{ contracts::v0_6::i_entry_point::HandleOpsCall, v0_6::UserOperation, UserOperation as UserOperationTrait, ValidTimeRange, }; - use rundler_pool::{MockPoolServer, PoolOperation}; - use rundler_provider::{MockEntryPoint, MockProvider}; - use rundler_sim::PriorityFeeMode; - use rundler_types::contracts::v0_6::i_entry_point::HandleOpsCall; use super::*; From 0646bf06f3f82ff36ab58d5225ec26dd3b011f9f Mon Sep 17 00:00:00 2001 From: dancoombs Date: Thu, 14 Mar 2024 13:53:42 -0400 Subject: [PATCH 03/12] feat(rpc): introduce entry point routing --- bin/tools/src/bin/get_example_ops.rs | 38 -- crates/rpc/src/debug.rs | 3 +- crates/rpc/src/eth/api.rs | 735 +++------------------- crates/rpc/src/eth/error.rs | 21 +- crates/rpc/src/eth/events/mod.rs | 219 +++++++ crates/rpc/src/eth/events/v0_6.rs | 287 +++++++++ crates/rpc/src/eth/events/v0_7.rs | 34 + crates/rpc/src/eth/mod.rs | 20 +- crates/rpc/src/eth/router.rs | 305 +++++++++ crates/rpc/src/eth/server.rs | 32 +- crates/rpc/src/lib.rs | 1 - crates/rpc/src/rundler.rs | 64 +- crates/rpc/src/task.rs | 109 ++-- crates/rpc/src/{types.rs => types/mod.rs} | 184 +++--- crates/rpc/src/types/v0_6.rs | 105 ++++ crates/rpc/src/types/v0_7.rs | 88 +++ crates/sim/src/estimation/mod.rs | 5 +- crates/sim/src/estimation/v0_6.rs | 8 +- crates/sim/src/estimation/v0_7.rs | 36 ++ crates/sim/src/lib.rs | 7 +- crates/types/src/user_operation/mod.rs | 65 +- crates/types/src/user_operation/v0_6.rs | 13 +- crates/types/src/user_operation/v0_7.rs | 12 +- 23 files changed, 1472 insertions(+), 919 deletions(-) delete mode 100644 bin/tools/src/bin/get_example_ops.rs create mode 100644 crates/rpc/src/eth/events/mod.rs create mode 100644 crates/rpc/src/eth/events/v0_6.rs create mode 100644 crates/rpc/src/eth/events/v0_7.rs create mode 100644 crates/rpc/src/eth/router.rs rename crates/rpc/src/{types.rs => types/mod.rs} (66%) create mode 100644 crates/rpc/src/types/v0_6.rs create mode 100644 crates/rpc/src/types/v0_7.rs create mode 100644 crates/sim/src/estimation/v0_7.rs diff --git a/bin/tools/src/bin/get_example_ops.rs b/bin/tools/src/bin/get_example_ops.rs deleted file mode 100644 index 0c967a826..000000000 --- a/bin/tools/src/bin/get_example_ops.rs +++ /dev/null @@ -1,38 +0,0 @@ -// This file is part of Rundler. -// -// Rundler is free software: you can redistribute it and/or modify it under the -// terms of the GNU Lesser General Public License as published by the Free Software -// Foundation, either version 3 of the License, or (at your option) any later version. -// -// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -// See the GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along with Rundler. -// If not, see https://www.gnu.org/licenses/. - -use dotenv::dotenv; -use rundler_dev::DevClients; -use rundler_rpc::RpcUserOperation; - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - dotenv()?; - let clients = DevClients::new_from_env()?; - // We'll make operations that call the entry point's addStake. - let op = clients - .new_wallet_op(clients.entry_point.add_stake(1), 1.into()) - .await?; - println!("User operation to make wallet call EntryPoint#addStake():"); - println!( - "{}", - serde_json::to_string(&RpcUserOperation::from(op.clone()))? - ); - let op = clients - .new_wallet_op_with_paymaster(clients.entry_point.add_stake(1), 1.into()) - .await?; - println!(); - println!("User operation to make wallet call EntryPoint#addStake() with paymaster:"); - println!("{}", serde_json::to_string(&RpcUserOperation::from(op))?); - Ok(()) -} diff --git a/crates/rpc/src/debug.rs b/crates/rpc/src/debug.rs index 982320eb7..a7b3b20bd 100644 --- a/crates/rpc/src/debug.rs +++ b/crates/rpc/src/debug.rs @@ -17,7 +17,6 @@ use futures_util::StreamExt; use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::error::INTERNAL_ERROR_CODE}; use rundler_builder::{BuilderServer, BundlingMode}; use rundler_pool::PoolServer; -use rundler_types::v0_6; use crate::{ error::rpc_err, @@ -127,7 +126,7 @@ where .await .map_err(|e| rpc_err(INTERNAL_ERROR_CODE, e.to_string()))? .into_iter() - .map(|pop| v0_6::UserOperation::from(pop.uo).into()) + .map(|pop| pop.uo.into()) .collect::>()) } diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 01dc61509..85304281b 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -11,40 +11,23 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use std::{ - collections::{HashMap, VecDeque}, - sync::Arc, -}; +use std::{future::Future, pin::Pin}; -use anyhow::Context; use ethers::{ - abi::{AbiDecode, RawLog}, - prelude::EthEvent, - types::{ - spoof, Address, Bytes, Filter, GethDebugBuiltInTracerType, GethDebugTracerType, - GethDebugTracingOptions, GethTrace, GethTraceFrame, Log, TransactionReceipt, H256, U256, - U64, - }, + types::{spoof, Address, H256, U64}, utils::to_checksum, }; +use futures_util::future; use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, L1GasProvider, Provider, SimulationProvider}; -use rundler_sim::{ - estimation::v0_6::GasEstimator as GasEstimatorV0_6, EstimationSettings, FeeEstimator, - GasEstimationError, GasEstimator, PrecheckSettings, -}; -use rundler_types::{ - chain::ChainSpec, - contracts::v0_6::i_entry_point::{ - IEntryPointCalls, UserOperationEventFilter, UserOperationRevertReasonFilter, - }, - v0_6, UserOperation, -}; -use rundler_utils::{eth::log_to_raw_log, log::LogOnError}; +use rundler_types::{chain::ChainSpec, UserOperationOptionalGas, UserOperationVariant}; +use rundler_utils::log::LogOnError; use tracing::Level; -use super::error::{EthResult, EthRpcError, ExecutionRevertedWithBytesData}; -use crate::types::{RichUserOperation, RpcGasEstimate, RpcUserOperation, UserOperationReceipt}; +use super::{ + error::{EthResult, EthRpcError}, + router::EntryPointRouter, +}; +use crate::types::{RpcGasEstimate, RpcUserOperationByHash, RpcUserOperationReceipt}; /// Settings for the `eth_` API #[derive(Copy, Clone, Debug)] @@ -62,109 +45,36 @@ impl Settings { } } -#[derive(Debug)] -struct EntryPointContext { - gas_estimator: GasEstimatorV0_6, -} - -impl EntryPointContext +pub(crate) struct EthApi where - P: Provider, - E: EntryPoint - + L1GasProvider - + SimulationProvider, + PS: PoolServer, { - fn new( - chain_spec: ChainSpec, - provider: Arc

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

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

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

, - entry_points: Vec, - pool: PS, - settings: Settings, - estimation_settings: EstimationSettings, - precheck_settings: PrecheckSettings, - ) -> Self - where - E: Clone, - { - let contexts_by_entry_point = entry_points - .into_iter() - .map(|entry_point| { - ( - entry_point.address(), - EntryPointContext::new( - chain_spec.clone(), - Arc::clone(&provider), - entry_point, - estimation_settings, - FeeEstimator::new( - &chain_spec, - Arc::clone(&provider), - precheck_settings.priority_fee_mode, - precheck_settings.bundle_priority_fee_overhead_percent, - ), - ), - ) - }) - .collect(); - + pub(crate) fn new(chain_spec: ChainSpec, router: EntryPointRouter, pool: PS) -> Self { Self { - settings, - contexts_by_entry_point, - provider, - chain_spec, + router, pool, + chain_spec, } } pub(crate) async fn send_user_operation( &self, - op: RpcUserOperation, + op: UserOperationVariant, entry_point: Address, ) -> EthResult { - if !self.contexts_by_entry_point.contains_key(&entry_point) { - return Err(EthRpcError::InvalidParams( - "supplied entry point addr is not a known entry point".to_string(), - )); - } - let op: v0_6::UserOperation = op.try_into()?; + self.router.check_and_get_route(&entry_point, &op)?; self.pool - .add_op(entry_point, op.into()) + .add_op(entry_point, op) .await .map_err(EthRpcError::from) .log_on_error_level(Level::DEBUG, "failed to add op to the mempool") @@ -172,152 +82,63 @@ where pub(crate) async fn estimate_user_operation_gas( &self, - op: v0_6::UserOperationOptionalGas, + uo: UserOperationOptionalGas, entry_point: Address, state_override: Option, ) -> EthResult { - let context = self - .contexts_by_entry_point - .get(&entry_point) - .ok_or_else(|| { - EthRpcError::InvalidParams( - "supplied entry_point address is not a known entry point".to_string(), - ) - })?; - - if op.init_code.len() > 0 && op.init_code.len() < 20 { - return Err(EthRpcError::InvalidParams( - "init_code must be empty or at least 20 bytes".to_string(), - )); - } else if op.paymaster_and_data.len() > 0 && op.paymaster_and_data.len() < 20 { - return Err(EthRpcError::InvalidParams( - "paymaster_and_data must be empty or at least 20 bytes".to_string(), - )); - } - - let result = context - .gas_estimator - .estimate_op_gas(op, state_override.unwrap_or_default()) - .await; - match result { - Ok(estimate) => Ok(estimate.into()), - Err(GasEstimationError::RevertInValidation(message)) => { - Err(EthRpcError::EntryPointValidationRejected(message))? - } - Err(GasEstimationError::RevertInCallWithMessage(message)) => { - Err(EthRpcError::ExecutionReverted(message))? - } - Err(GasEstimationError::RevertInCallWithBytes(b)) => { - Err(EthRpcError::ExecutionRevertedWithBytes( - ExecutionRevertedWithBytesData { revert_data: b }, - ))? - } - Err(GasEstimationError::Other(error)) => Err(error)?, - } + self.router + .estimate_gas(&entry_point, uo, state_override) + .await } pub(crate) async fn get_user_operation_by_hash( &self, hash: H256, - ) -> EthResult> { + ) -> EthResult> { if hash == H256::zero() { return Err(EthRpcError::InvalidParams( "Missing/invalid userOpHash".to_string(), )); } - // check for the user operation both in the pool and mined on chain - let mined_fut = self.get_mined_user_operation_by_hash(hash); - let pending_fut = self.get_pending_user_operation_by_hash(hash); - let (mined, pending) = tokio::join!(mined_fut, pending_fut); - - // mined takes precedence over pending - if let Ok(Some(mined)) = mined { - Ok(Some(mined)) - } else if let Ok(Some(pending)) = pending { - Ok(Some(pending)) - } else if mined.is_err() || pending.is_err() { - // if either futures errored, and the UO was not found, return the errors - Err(EthRpcError::Internal(anyhow::anyhow!( - "error fetching user operation by hash: mined: {:?}, pending: {:?}", - mined.err().map(|e| e.to_string()).unwrap_or_default(), - pending.err().map(|e| e.to_string()).unwrap_or_default(), - ))) - } else { - // not found in either pool or mined - Ok(None) + // check both entry points and pending for the user operation event + #[allow(clippy::type_complexity)] + let mut futs: Vec< + Pin>> + Send>>, + > = vec![]; + + for ep in self.router.entry_points() { + futs.push(Box::pin(self.router.get_mined_by_hash(ep, hash))); } + futs.push(Box::pin(self.get_pending_user_operation_by_hash(hash))); + + let results = future::try_join_all(futs).await?; + Ok(results.into_iter().find_map(|x| x)) } pub(crate) async fn get_user_operation_receipt( &self, hash: H256, - ) -> EthResult> { + ) -> EthResult> { if hash == H256::zero() { return Err(EthRpcError::InvalidParams( "Missing/invalid userOpHash".to_string(), )); } - // Get event associated with hash (need to check all entry point addresses associated with this API) - let log = self - .get_user_operation_event_by_hash(hash) - .await - .context("should have fetched user ops by hash")?; + let futs = self + .router + .entry_points() + .map(|ep| self.router.get_receipt(ep, hash)); - let Some(log) = log else { return Ok(None) }; - let entry_point = log.address; - - // If the event is found, get the TX receipt - let tx_hash = log.transaction_hash.context("tx_hash should be present")?; - let tx_receipt = self - .provider - .get_transaction_receipt(tx_hash) - .await - .context("should have fetched tx receipt")? - .context("Failed to fetch tx receipt")?; - - // Return null if the tx isn't included in the block yet - if tx_receipt.block_hash.is_none() && tx_receipt.block_number.is_none() { - return Ok(None); - } - - // Filter receipt logs to match just those belonging to the user op - let filtered_logs = - EthApi::::filter_receipt_logs_matching_user_op(&log, &tx_receipt) - .context("should have found receipt logs matching user op")?; - - // Decode log and find failure reason if not success - let uo_event = self - .decode_user_operation_event(log) - .context("should have decoded user operation event")?; - let reason: String = if uo_event.success { - "".to_owned() - } else { - EthApi::::get_user_operation_failure_reason(&tx_receipt.logs, hash) - .context("should have found revert reason if tx wasn't successful")? - .unwrap_or_default() - }; - - Ok(Some(UserOperationReceipt { - user_op_hash: hash, - entry_point: entry_point.into(), - sender: uo_event.sender.into(), - nonce: uo_event.nonce, - paymaster: uo_event.paymaster.into(), - actual_gas_cost: uo_event.actual_gas_cost, - actual_gas_used: uo_event.actual_gas_used, - success: uo_event.success, - logs: filtered_logs, - receipt: tx_receipt, - reason, - })) + let results = future::try_join_all(futs).await?; + Ok(results.into_iter().find_map(|x| x)) } pub(crate) async fn supported_entry_points(&self) -> EthResult> { Ok(self - .contexts_by_entry_point - .keys() + .router + .entry_points() .map(|ep| to_checksum(ep, None)) .collect()) } @@ -326,411 +147,48 @@ where Ok(self.chain_spec.id.into()) } - async fn get_mined_user_operation_by_hash( - &self, - hash: H256, - ) -> EthResult> { - // Get event associated with hash (need to check all entry point addresses associated with this API) - let event = self - .get_user_operation_event_by_hash(hash) - .await - .log_on_error("should have successfully queried for user op events by hash")?; - - let Some(event) = event else { return Ok(None) }; - - // If the event is found, get the TX and entry point - let transaction_hash = event - .transaction_hash - .context("tx_hash should be present")?; - - let tx = self - .provider - .get_transaction(transaction_hash) - .await - .context("should have fetched tx from provider")? - .context("should have found tx")?; - - // We should return null if the tx isn't included in the block yet - if tx.block_hash.is_none() && tx.block_number.is_none() { - return Ok(None); - } - let to = tx - .to - .context("tx.to should be present on transaction containing user operation event")?; - - // Find first op matching the hash - let user_operation = if self.contexts_by_entry_point.contains_key(&to) { - self.get_user_operations_from_tx_data(tx.input) - .into_iter() - .find(|op| op.hash(to, self.chain_spec.id) == hash) - .context("matching user operation should be found in tx data")? - } else { - self.trace_find_user_operation(transaction_hash, hash) - .await - .context("error running trace")? - .context("should have found user operation in trace")? - }; - - Ok(Some(RichUserOperation { - user_operation: user_operation.into(), - entry_point: event.address.into(), - block_number: Some( - tx.block_number - .map(|n| U256::from(n.as_u64())) - .unwrap_or_default(), - ), - block_hash: Some(tx.block_hash.unwrap_or_default()), - transaction_hash: Some(transaction_hash), - })) - } - async fn get_pending_user_operation_by_hash( &self, hash: H256, - ) -> EthResult> { + ) -> EthResult> { let res = self .pool .get_op_by_hash(hash) .await .map_err(EthRpcError::from)?; - Ok(res.map(|op| RichUserOperation { - user_operation: v0_6::UserOperation::from(op.uo).into(), + + Ok(res.map(|op| RpcUserOperationByHash { + user_operation: op.uo.into(), entry_point: op.entry_point.into(), block_number: None, block_hash: None, transaction_hash: None, })) } - - async fn get_user_operation_event_by_hash(&self, hash: H256) -> EthResult> { - let to_block = self.provider.get_block_number().await?; - - let from_block = match self.settings.user_operation_event_block_distance { - Some(distance) => to_block.saturating_sub(distance), - None => 0, - }; - - let filter = Filter::new() - .address::>( - self.contexts_by_entry_point - .iter() - .map(|ep| *ep.0) - .collect(), - ) - .event(&UserOperationEventFilter::abi_signature()) - .from_block(from_block) - .to_block(to_block) - .topic1(hash); - - let logs = self.provider.get_logs(&filter).await?; - Ok(logs.into_iter().next()) - } - - fn get_user_operations_from_tx_data(&self, tx_data: Bytes) -> Vec { - let entry_point_calls = match IEntryPointCalls::decode(tx_data) { - Ok(entry_point_calls) => entry_point_calls, - Err(_) => return vec![], - }; - - match entry_point_calls { - IEntryPointCalls::HandleOps(handle_ops_call) => handle_ops_call.ops, - IEntryPointCalls::HandleAggregatedOps(handle_aggregated_ops_call) => { - handle_aggregated_ops_call - .ops_per_aggregator - .into_iter() - .flat_map(|ops| ops.user_ops) - .collect() - } - _ => vec![], - } - } - - fn decode_user_operation_event(&self, log: Log) -> EthResult { - Ok(UserOperationEventFilter::decode_log(&log_to_raw_log(log)) - .context("log should be a user operation event")?) - } - - /// This method takes a user operation event and a transaction receipt and filters out all the logs - /// relevant to the user operation. Since there are potentially many user operations in a transaction, - /// we want to find all the logs (including the user operation event itself) that are sandwiched between - /// ours and the one before it that wasn't ours. - /// eg. reference_log: UserOp(hash_moldy) logs: \[...OtherLogs, UserOp(hash1), ...OtherLogs, UserOp(hash_moldy), ...OtherLogs\] - /// -> logs: logs\[(idx_of_UserOp(hash1) + 1)..=idx_of_UserOp(hash_moldy)\] - /// - /// topic\[0\] == event name - /// topic\[1\] == user operation hash - /// - /// NOTE: we can't convert just decode all the logs as user operations and filter because we still want all the other log types - /// - fn filter_receipt_logs_matching_user_op( - reference_log: &Log, - tx_receipt: &TransactionReceipt, - ) -> EthResult> { - let mut start_idx = 0; - let mut end_idx = tx_receipt.logs.len() - 1; - let logs = &tx_receipt.logs; - - let is_ref_user_op = |log: &Log| { - log.topics[0] == reference_log.topics[0] - && log.topics[1] == reference_log.topics[1] - && log.address == reference_log.address - }; - - let is_user_op_event = |log: &Log| log.topics[0] == reference_log.topics[0]; - - let mut i = 0; - while i < logs.len() { - if i < end_idx && is_user_op_event(&logs[i]) && !is_ref_user_op(&logs[i]) { - start_idx = i; - } else if is_ref_user_op(&logs[i]) { - end_idx = i; - } - - i += 1; - } - - if !is_ref_user_op(&logs[end_idx]) { - return Err(EthRpcError::Internal(anyhow::anyhow!( - "fatal: no user ops found in tx receipt ({start_idx},{end_idx})" - ))); - } - - let start_idx = if start_idx == 0 { 0 } else { start_idx + 1 }; - Ok(logs[start_idx..=end_idx].to_vec()) - } - - fn get_user_operation_failure_reason( - logs: &[Log], - user_op_hash: H256, - ) -> EthResult> { - let revert_reason_evt: Option = logs - .iter() - .filter(|l| l.topics.len() > 1 && l.topics[1] == user_op_hash) - .map_while(|l| { - UserOperationRevertReasonFilter::decode_log(&RawLog { - topics: l.topics.clone(), - data: l.data.to_vec(), - }) - .ok() - }) - .next(); - - Ok(revert_reason_evt.map(|r| r.revert_reason.to_string())) - } - - /// This method takes a transaction hash and a user operation hash and returns the full user operation if it exists. - /// This is meant to be used when a user operation event is found in the logs of a transaction, but the top level call - /// wasn't to an entrypoint, so we need to trace the transaction to find the user operation by inspecting each call frame - /// and returning the user operation that matches the hash. - async fn trace_find_user_operation( - &self, - tx_hash: H256, - user_op_hash: H256, - ) -> EthResult> { - // initial call wasn't to an entrypoint, so we need to trace the transaction to find the user operation - let trace_options = GethDebugTracingOptions { - tracer: Some(GethDebugTracerType::BuiltInTracer( - GethDebugBuiltInTracerType::CallTracer, - )), - ..Default::default() - }; - let trace = self - .provider - .debug_trace_transaction(tx_hash, trace_options) - .await - .context("should have fetched trace from provider")?; - - // breadth first search for the user operation in the trace - let mut frame_queue = VecDeque::new(); - - if let GethTrace::Known(GethTraceFrame::CallTracer(call_frame)) = trace { - frame_queue.push_back(call_frame); - } - - while let Some(call_frame) = frame_queue.pop_front() { - // check if the call is to an entrypoint, if not enqueue the child calls if any - if let Some(to) = call_frame - .to - .as_ref() - .and_then(|to| to.as_address()) - .filter(|to| self.contexts_by_entry_point.contains_key(to)) - { - // check if the user operation is in the call frame - if let Some(uo) = self - .get_user_operations_from_tx_data(call_frame.input) - .into_iter() - .find(|op| op.hash(*to, self.chain_spec.id) == user_op_hash) - { - return Ok(Some(uo)); - } - } else if let Some(calls) = call_frame.calls { - frame_queue.extend(calls) - } - } - - Ok(None) - } } #[cfg(test)] mod tests { + use std::sync::Arc; + use ethers::{ abi::AbiEncode, - types::{Log, Transaction, TransactionReceipt}, - utils::keccak256, + types::{Bytes, Log, Transaction}, }; use mockall::predicate::eq; use rundler_pool::{IntoPoolOperationVariant, MockPoolServer, PoolOperation}; use rundler_provider::{MockEntryPoint, MockEntryPointV0_6, MockProvider}; use rundler_sim::{EntityInfos, PriorityFeeMode}; use rundler_types::{ - contracts::v0_6::i_entry_point::HandleOpsCall, v0_6::UserOperation, + contracts::v0_6::i_entry_point::{HandleOpsCall, IEntryPointCalls}, + v0_6::UserOperation, UserOperation as UserOperationTrait, ValidTimeRange, }; use super::*; - - const UO_OP_TOPIC: &str = "user-op-event-topic"; - - #[test] - fn test_filter_receipt_logs_when_at_beginning_of_list() { - let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); - let receipt = given_receipt(vec![ - given_log("other-topic", "some-hash"), - reference_log.clone(), - given_log(UO_OP_TOPIC, "other-hash"), - given_log(UO_OP_TOPIC, "another-hash"), - ]); - - let result = - EthApi::::filter_receipt_logs_matching_user_op( - &reference_log, - &receipt, - ); - - assert!(result.is_ok(), "{}", result.unwrap_err()); - let result = result.unwrap(); - assert_eq!(result, receipt.logs[0..=1]); - } - - #[test] - fn test_filter_receipt_logs_when_in_middle_of_list() { - let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); - let receipt = given_receipt(vec![ - given_log("other-topic", "some-hash"), - given_log(UO_OP_TOPIC, "other-hash"), - given_log("another-topic", "some-hash"), - given_log("another-topic-2", "some-hash"), - reference_log.clone(), - given_log(UO_OP_TOPIC, "another-hash"), - ]); - - let result = - EthApi::::filter_receipt_logs_matching_user_op( - &reference_log, - &receipt, - ); - - assert!(result.is_ok(), "{}", result.unwrap_err()); - let result = result.unwrap(); - assert_eq!(result, receipt.logs[2..=4]); - } - - #[test] - fn test_filter_receipt_logs_when_at_end_of_list() { - let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); - let receipt = given_receipt(vec![ - given_log("other-topic", "some-hash"), - given_log(UO_OP_TOPIC, "other-hash"), - given_log(UO_OP_TOPIC, "another-hash"), - given_log("another-topic", "some-hash"), - given_log("another-topic-2", "some-hash"), - reference_log.clone(), - ]); - - let result = - EthApi::::filter_receipt_logs_matching_user_op( - &reference_log, - &receipt, - ); - - assert!(result.is_ok(), "{}", result.unwrap_err()); - let result = result.unwrap(); - assert_eq!(result, receipt.logs[3..=5]); - } - - #[test] - fn test_filter_receipt_logs_skips_event_from_different_address() { - let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); - let mut reference_log_w_different_address = reference_log.clone(); - reference_log_w_different_address.address = Address::from_low_u64_be(0x1234); - - let receipt = given_receipt(vec![ - given_log("other-topic", "some-hash"), - given_log(UO_OP_TOPIC, "other-hash"), - given_log(UO_OP_TOPIC, "another-hash"), - reference_log_w_different_address, - given_log("another-topic", "some-hash"), - given_log("another-topic-2", "some-hash"), - reference_log.clone(), - ]); - - let result = - EthApi::::filter_receipt_logs_matching_user_op( - &reference_log, - &receipt, - ); - - assert!(result.is_ok(), "{}", result.unwrap_err()); - let result = result.unwrap(); - assert_eq!(result, receipt.logs[4..=6]); - } - - #[test] - fn test_filter_receipt_logs_includes_multiple_sets_of_ref_uo() { - let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); - - let receipt = given_receipt(vec![ - given_log("other-topic", "some-hash"), - given_log(UO_OP_TOPIC, "other-hash"), - given_log("other-topic-2", "another-hash"), - reference_log.clone(), - given_log("another-topic", "some-hash"), - given_log("another-topic-2", "some-hash"), - reference_log.clone(), - given_log(UO_OP_TOPIC, "other-hash"), - ]); - - let result = - EthApi::::filter_receipt_logs_matching_user_op( - &reference_log, - &receipt, - ); - - assert!(result.is_ok(), "{}", result.unwrap_err()); - let result = result.unwrap(); - assert_eq!(result, receipt.logs[2..=6]); - } - - #[test] - fn test_filter_receipt_logs_when_not_found() { - let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); - let receipt = given_receipt(vec![ - given_log("other-topic", "some-hash"), - given_log(UO_OP_TOPIC, "other-hash"), - given_log(UO_OP_TOPIC, "another-hash"), - given_log("another-topic", "some-hash"), - given_log("another-topic-2", "some-hash"), - ]); - - let result = - EthApi::::filter_receipt_logs_matching_user_op( - &reference_log, - &receipt, - ); - - assert!(result.is_err(), "{:?}", result.unwrap()); - } + use crate::eth::{ + EntryPointRouteImpl, EntryPointRouterBuilder, UserOperationEventProviderV0_6, + }; #[tokio::test] async fn test_get_user_op_by_hash_pending() { @@ -766,8 +224,8 @@ mod tests { let api = create_api(provider, entry_point, pool); let res = api.get_user_operation_by_hash(hash).await.unwrap(); - let ro = RichUserOperation { - user_operation: uo.into(), + let ro = RpcUserOperationByHash { + user_operation: UserOperationVariant::from(uo).into(), entry_point: ep.into(), block_number: None, block_hash: None, @@ -825,8 +283,8 @@ mod tests { let api = create_api(provider, entry_point, pool); let res = api.get_user_operation_by_hash(hash).await.unwrap(); - let ro = RichUserOperation { - user_operation: uo.into(), + let ro = RpcUserOperationByHash { + user_operation: UserOperationVariant::from(uo).into(), entry_point: ep.into(), block_number: Some(block_number.into()), block_hash: Some(block_hash), @@ -859,60 +317,49 @@ mod tests { assert_eq!(res, None); } - fn given_log(topic_0: &str, topic_1: &str) -> Log { - Log { - topics: vec![ - keccak256(topic_0.as_bytes()).into(), - keccak256(topic_1.as_bytes()).into(), - ], - ..Default::default() - } - } - - fn given_receipt(logs: Vec) -> TransactionReceipt { - TransactionReceipt { - logs, - ..Default::default() - } - } - fn create_api( provider: MockProvider, ep: MockEntryPointV0_6, pool: MockPoolServer, - ) -> EthApi { - let mut contexts_by_entry_point = HashMap::new(); + ) -> EthApi { let provider = Arc::new(provider); let chain_spec = ChainSpec { id: 1, ..Default::default() }; - contexts_by_entry_point.insert( - ep.address(), - EntryPointContext::new( - chain_spec.clone(), - Arc::clone(&provider), - ep, - EstimationSettings { - max_verification_gas: 1_000_000, - max_call_gas: 1_000_000, - max_simulate_handle_ops_gas: 1_000_000, - verification_estimation_gas_fee: 1_000_000_000_000, - }, - FeeEstimator::new( - &chain_spec, + contexts_by_entry_point + .insert( + ep.address(), + EntryPointContext::new( + chain_spec.clone(), Arc::clone(&provider), - PriorityFeeMode::BaseFeePercent(0), - 0, + ep, + EstimationSettings { + max_verification_gas: 1_000_000, + max_call_gas: 1_000_000, + max_simulate_handle_ops_gas: 1_000_000, + verification_estimation_gas_fee: 1_000_000_000_000, + }, + FeeEstimator::new( + &chain_spec, + Arc::clone(&provider), + PriorityFeeMode::BaseFeePercent(0), + 0, + ), + UserOperationEventProviderV0_6::new( + chain_spec.id, + ep.address(), + provider.clone(), + None, + ), ), - ), - ); + ) + .build(); + EthApi { - contexts_by_entry_point, - provider, + router, chain_spec, pool, - settings: Settings::new(None), } } } diff --git a/crates/rpc/src/eth/error.rs b/crates/rpc/src/eth/error.rs index 25bd4e4a2..3b356c8c3 100644 --- a/crates/rpc/src/eth/error.rs +++ b/crates/rpc/src/eth/error.rs @@ -18,7 +18,7 @@ use jsonrpsee::types::{ }; use rundler_pool::{MempoolError, PoolServerError}; use rundler_provider::ProviderError; -use rundler_sim::{PrecheckViolation, SimulationViolation}; +use rundler_sim::{GasEstimationError, PrecheckViolation, SimulationViolation}; use rundler_types::{Entity, EntityType, Timestamp}; use serde::Serialize; @@ -373,3 +373,22 @@ impl From for EthRpcError { EthRpcError::Internal(anyhow::anyhow!("provider error: {e:?}")) } } + +impl From for EthRpcError { + fn from(e: GasEstimationError) -> Self { + match e { + GasEstimationError::RevertInValidation(message) => { + EthRpcError::EntryPointValidationRejected(message) + } + GasEstimationError::RevertInCallWithMessage(message) => { + EthRpcError::ExecutionReverted(message) + } + GasEstimationError::RevertInCallWithBytes(b) => { + EthRpcError::ExecutionRevertedWithBytes(ExecutionRevertedWithBytesData { + revert_data: b, + }) + } + GasEstimationError::Other(error) => EthRpcError::Internal(error), + } + } +} diff --git a/crates/rpc/src/eth/events/mod.rs b/crates/rpc/src/eth/events/mod.rs new file mode 100644 index 000000000..3ef20a9f5 --- /dev/null +++ b/crates/rpc/src/eth/events/mod.rs @@ -0,0 +1,219 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use anyhow::bail; +use ethers::types::{Log, TransactionReceipt, H256}; + +use crate::types::{RpcUserOperationByHash, RpcUserOperationReceipt}; + +mod v0_6; +pub(crate) use v0_6::UserOperationEventProviderV0_6; +mod v0_7; + +#[async_trait::async_trait] +pub(crate) trait UserOperationEventProvider: Send + Sync + 'static { + async fn get_mined_by_hash(&self, hash: H256) + -> anyhow::Result>; + + async fn get_receipt(&self, hash: H256) -> anyhow::Result>; +} + +// This method takes a user operation event and a transaction receipt and filters out all the logs +// relevant to the user operation. Since there are potentially many user operations in a transaction, +// we want to find all the logs (including the user operation event itself) that are sandwiched between +// ours and the one before it that wasn't ours. +// eg. reference_log: UserOp(hash_moldy) logs: \[...OtherLogs, UserOp(hash1), ...OtherLogs, UserOp(hash_moldy), ...OtherLogs\] +// -> logs: logs\[(idx_of_UserOp(hash1) + 1)..=idx_of_UserOp(hash_moldy)\] +// +// topic\[0\] == event name +// topic\[1\] == user operation hash +// +// NOTE: we can't convert just decode all the logs as user operations and filter because we still want all the other log types +// +fn filter_receipt_logs_matching_user_op( + reference_log: &Log, + tx_receipt: &TransactionReceipt, +) -> anyhow::Result> { + let mut start_idx = 0; + let mut end_idx = tx_receipt.logs.len() - 1; + let logs = &tx_receipt.logs; + + let is_ref_user_op = |log: &Log| { + log.topics[0] == reference_log.topics[0] + && log.topics[1] == reference_log.topics[1] + && log.address == reference_log.address + }; + + let is_user_op_event = |log: &Log| log.topics[0] == reference_log.topics[0]; + + let mut i = 0; + while i < logs.len() { + if i < end_idx && is_user_op_event(&logs[i]) && !is_ref_user_op(&logs[i]) { + start_idx = i; + } else if is_ref_user_op(&logs[i]) { + end_idx = i; + } + + i += 1; + } + + if !is_ref_user_op(&logs[end_idx]) { + bail!("fatal: no user ops found in tx receipt ({start_idx},{end_idx})"); + } + + let start_idx = if start_idx == 0 { 0 } else { start_idx + 1 }; + Ok(logs[start_idx..=end_idx].to_vec()) +} + +#[cfg(test)] +mod tests { + + use ethers::{types::Address, utils::keccak256}; + + use super::*; + + const UO_OP_TOPIC: &str = "user-op-event-topic"; + + #[test] + fn test_filter_receipt_logs_when_at_beginning_of_list() { + let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); + let receipt = given_receipt(vec![ + given_log("other-topic", "some-hash"), + reference_log.clone(), + given_log(UO_OP_TOPIC, "other-hash"), + given_log(UO_OP_TOPIC, "another-hash"), + ]); + + let result = filter_receipt_logs_matching_user_op(&reference_log, &receipt); + + assert!(result.is_ok(), "{}", result.unwrap_err()); + let result = result.unwrap(); + assert_eq!(result, receipt.logs[0..=1]); + } + + #[test] + fn test_filter_receipt_logs_when_in_middle_of_list() { + let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); + let receipt = given_receipt(vec![ + given_log("other-topic", "some-hash"), + given_log(UO_OP_TOPIC, "other-hash"), + given_log("another-topic", "some-hash"), + given_log("another-topic-2", "some-hash"), + reference_log.clone(), + given_log(UO_OP_TOPIC, "another-hash"), + ]); + + let result = filter_receipt_logs_matching_user_op(&reference_log, &receipt); + + assert!(result.is_ok(), "{}", result.unwrap_err()); + let result = result.unwrap(); + assert_eq!(result, receipt.logs[2..=4]); + } + + #[test] + fn test_filter_receipt_logs_when_at_end_of_list() { + let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); + let receipt = given_receipt(vec![ + given_log("other-topic", "some-hash"), + given_log(UO_OP_TOPIC, "other-hash"), + given_log(UO_OP_TOPIC, "another-hash"), + given_log("another-topic", "some-hash"), + given_log("another-topic-2", "some-hash"), + reference_log.clone(), + ]); + + let result = filter_receipt_logs_matching_user_op(&reference_log, &receipt); + + assert!(result.is_ok(), "{}", result.unwrap_err()); + let result = result.unwrap(); + assert_eq!(result, receipt.logs[3..=5]); + } + + #[test] + fn test_filter_receipt_logs_skips_event_from_different_address() { + let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); + let mut reference_log_w_different_address = reference_log.clone(); + reference_log_w_different_address.address = Address::from_low_u64_be(0x1234); + + let receipt = given_receipt(vec![ + given_log("other-topic", "some-hash"), + given_log(UO_OP_TOPIC, "other-hash"), + given_log(UO_OP_TOPIC, "another-hash"), + reference_log_w_different_address, + given_log("another-topic", "some-hash"), + given_log("another-topic-2", "some-hash"), + reference_log.clone(), + ]); + + let result = filter_receipt_logs_matching_user_op(&reference_log, &receipt); + + assert!(result.is_ok(), "{}", result.unwrap_err()); + let result = result.unwrap(); + assert_eq!(result, receipt.logs[4..=6]); + } + + #[test] + fn test_filter_receipt_logs_includes_multiple_sets_of_ref_uo() { + let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); + + let receipt = given_receipt(vec![ + given_log("other-topic", "some-hash"), + given_log(UO_OP_TOPIC, "other-hash"), + given_log("other-topic-2", "another-hash"), + reference_log.clone(), + given_log("another-topic", "some-hash"), + given_log("another-topic-2", "some-hash"), + reference_log.clone(), + given_log(UO_OP_TOPIC, "other-hash"), + ]); + + let result = filter_receipt_logs_matching_user_op(&reference_log, &receipt); + + assert!(result.is_ok(), "{}", result.unwrap_err()); + let result = result.unwrap(); + assert_eq!(result, receipt.logs[2..=6]); + } + + #[test] + fn test_filter_receipt_logs_when_not_found() { + let reference_log = given_log(UO_OP_TOPIC, "moldy-hash"); + let receipt = given_receipt(vec![ + given_log("other-topic", "some-hash"), + given_log(UO_OP_TOPIC, "other-hash"), + given_log(UO_OP_TOPIC, "another-hash"), + given_log("another-topic", "some-hash"), + given_log("another-topic-2", "some-hash"), + ]); + + let result = filter_receipt_logs_matching_user_op(&reference_log, &receipt); + + assert!(result.is_err(), "{:?}", result.unwrap()); + } + + fn given_log(topic_0: &str, topic_1: &str) -> Log { + Log { + topics: vec![ + keccak256(topic_0.as_bytes()).into(), + keccak256(topic_1.as_bytes()).into(), + ], + ..Default::default() + } + } + + fn given_receipt(logs: Vec) -> TransactionReceipt { + TransactionReceipt { + logs, + ..Default::default() + } + } +} diff --git a/crates/rpc/src/eth/events/v0_6.rs b/crates/rpc/src/eth/events/v0_6.rs new file mode 100644 index 000000000..d2efdacd8 --- /dev/null +++ b/crates/rpc/src/eth/events/v0_6.rs @@ -0,0 +1,287 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use std::{collections::VecDeque, sync::Arc}; + +use anyhow::Context; +use ethers::{ + abi::{AbiDecode, RawLog}, + prelude::EthEvent, + types::{ + Address, Bytes, Filter, GethDebugBuiltInTracerType, GethDebugTracerType, + GethDebugTracingOptions, GethTrace, GethTraceFrame, Log, H256, U256, + }, +}; +use rundler_provider::Provider; +use rundler_types::{ + contracts::v0_6::i_entry_point::{ + IEntryPointCalls, UserOperationEventFilter, UserOperationRevertReasonFilter, + }, + v0_6::UserOperation, + UserOperation as UserOperationTrait, UserOperationVariant, +}; +use rundler_utils::{eth, log::LogOnError}; + +use super::UserOperationEventProvider; +use crate::types::{RpcUserOperationByHash, RpcUserOperationReceipt}; + +#[derive(Debug)] +pub(crate) struct UserOperationEventProviderV0_6 { + chain_id: u64, + address: Address, + provider: Arc

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

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

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

, + event_block_distance: Option, + ) -> Self { + Self { + chain_id, + address, + provider, + event_block_distance, + } + } + + async fn get_event_by_hash(&self, hash: H256) -> anyhow::Result> { + let to_block = self.provider.get_block_number().await?; + + let from_block = match self.event_block_distance { + Some(distance) => to_block.saturating_sub(distance), + None => 0, + }; + + let filter = Filter::new() + .address(self.address) + .event(&UserOperationEventFilter::abi_signature()) + .from_block(from_block) + .to_block(to_block) + .topic1(hash); + + let logs = self.provider.get_logs(&filter).await?; + Ok(logs.into_iter().next()) + } + + fn get_user_operations_from_tx_data(&self, tx_data: Bytes) -> Vec { + let entry_point_calls = match IEntryPointCalls::decode(tx_data) { + Ok(entry_point_calls) => entry_point_calls, + Err(_) => return vec![], + }; + + match entry_point_calls { + IEntryPointCalls::HandleOps(handle_ops_call) => handle_ops_call.ops, + IEntryPointCalls::HandleAggregatedOps(handle_aggregated_ops_call) => { + handle_aggregated_ops_call + .ops_per_aggregator + .into_iter() + .flat_map(|ops| ops.user_ops) + .collect() + } + _ => vec![], + } + } + + fn decode_user_operation_event(&self, log: Log) -> anyhow::Result { + UserOperationEventFilter::decode_log(ð::log_to_raw_log(log)) + .context("log should be a user operation event") + } + + /// This method takes a transaction hash and a user operation hash and returns the full user operation if it exists. + /// This is meant to be used when a user operation event is found in the logs of a transaction, but the top level call + /// wasn't to an entrypoint, so we need to trace the transaction to find the user operation by inspecting each call frame + /// and returning the user operation that matches the hash. + async fn trace_find_user_operation( + &self, + tx_hash: H256, + user_op_hash: H256, + ) -> anyhow::Result> { + // initial call wasn't to an entrypoint, so we need to trace the transaction to find the user operation + let trace_options = GethDebugTracingOptions { + tracer: Some(GethDebugTracerType::BuiltInTracer( + GethDebugBuiltInTracerType::CallTracer, + )), + ..Default::default() + }; + let trace = self + .provider + .debug_trace_transaction(tx_hash, trace_options) + .await + .context("should have fetched trace from provider")?; + + // breadth first search for the user operation in the trace + let mut frame_queue = VecDeque::new(); + + if let GethTrace::Known(GethTraceFrame::CallTracer(call_frame)) = trace { + frame_queue.push_back(call_frame); + } + + while let Some(call_frame) = frame_queue.pop_front() { + // check if the call is to an entrypoint, if not enqueue the child calls if any + if let Some(to) = call_frame + .to + .as_ref() + .and_then(|to| to.as_address()) + .filter(|to| **to == self.address) + { + // check if the user operation is in the call frame + if let Some(uo) = self + .get_user_operations_from_tx_data(call_frame.input) + .into_iter() + .find(|op| op.hash(*to, self.chain_id) == user_op_hash) + { + return Ok(Some(uo)); + } + } else if let Some(calls) = call_frame.calls { + frame_queue.extend(calls) + } + } + + Ok(None) + } + + fn get_failure_reason(logs: &[Log], hash: H256) -> anyhow::Result> { + let revert_reason_evt: Option = logs + .iter() + .filter(|l| l.topics.len() > 1 && l.topics[1] == hash) + .map_while(|l| { + UserOperationRevertReasonFilter::decode_log(&RawLog { + topics: l.topics.clone(), + data: l.data.to_vec(), + }) + .ok() + }) + .next(); + + Ok(revert_reason_evt.map(|r| r.revert_reason.to_string())) + } +} diff --git a/crates/rpc/src/eth/events/v0_7.rs b/crates/rpc/src/eth/events/v0_7.rs new file mode 100644 index 000000000..f892b64bb --- /dev/null +++ b/crates/rpc/src/eth/events/v0_7.rs @@ -0,0 +1,34 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use ethers::types::H256; + +use super::UserOperationEventProvider; +use crate::types::{RpcUserOperationByHash, RpcUserOperationReceipt}; + +#[derive(Debug)] +pub(crate) struct UserOperationEventProviderV0_7; + +#[async_trait::async_trait] +impl UserOperationEventProvider for UserOperationEventProviderV0_7 { + async fn get_mined_by_hash( + &self, + _hash: H256, + ) -> anyhow::Result> { + unimplemented!() + } + + async fn get_receipt(&self, _hash: H256) -> anyhow::Result> { + unimplemented!() + } +} diff --git a/crates/rpc/src/eth/mod.rs b/crates/rpc/src/eth/mod.rs index c4f560b91..dfd9b4f71 100644 --- a/crates/rpc/src/eth/mod.rs +++ b/crates/rpc/src/eth/mod.rs @@ -15,15 +15,22 @@ mod api; pub(crate) use api::EthApi; pub use api::Settings as EthApiSettings; +mod router; +pub(crate) use router::*; + mod error; pub(crate) use error::EthRpcError; +mod events; +pub(crate) use events::UserOperationEventProviderV0_6; mod server; use ethers::types::{spoof, Address, H256, U64}; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; -use rundler_types::v0_6; -use crate::types::{RichUserOperation, RpcGasEstimate, RpcUserOperation, UserOperationReceipt}; +use crate::types::{ + RpcGasEstimate, RpcUserOperation, RpcUserOperationByHash, RpcUserOperationOptionalGas, + RpcUserOperationReceipt, +}; /// Eth API #[rpc(client, server, namespace = "eth")] @@ -41,21 +48,24 @@ pub trait EthApi { #[method(name = "estimateUserOperationGas")] async fn estimate_user_operation_gas( &self, - op: v0_6::UserOperationOptionalGas, + op: RpcUserOperationOptionalGas, entry_point: Address, state_override: Option, ) -> RpcResult; /// Returns the user operation with the given hash. #[method(name = "getUserOperationByHash")] - async fn get_user_operation_by_hash(&self, hash: H256) -> RpcResult>; + async fn get_user_operation_by_hash( + &self, + hash: H256, + ) -> RpcResult>; /// Returns the user operation receipt with the given hash. #[method(name = "getUserOperationReceipt")] async fn get_user_operation_receipt( &self, hash: H256, - ) -> RpcResult>; + ) -> RpcResult>; /// Returns the supported entry points addresses #[method(name = "supportedEntryPoints")] diff --git a/crates/rpc/src/eth/router.rs b/crates/rpc/src/eth/router.rs new file mode 100644 index 000000000..285c44608 --- /dev/null +++ b/crates/rpc/src/eth/router.rs @@ -0,0 +1,305 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use std::{fmt::Debug, sync::Arc}; + +use ethers::types::{spoof, Address, H256}; +use rundler_provider::{EntryPoint, SimulationProvider}; +use rundler_sim::{GasEstimationError, GasEstimator}; +use rundler_types::{ + EntryPointVersion, GasEstimate, UserOperation, UserOperationOptionalGas, UserOperationVariant, +}; + +use super::events::UserOperationEventProvider; +use crate::{ + eth::{error::EthResult, EthRpcError}, + types::{RpcGasEstimate, RpcUserOperationByHash, RpcUserOperationReceipt}, +}; + +#[derive(Default)] +pub(crate) struct EntryPointRouterBuilder { + entry_points: Vec

, + v0_6: Option<(Address, Arc>)>, + v0_7: Option<(Address, Arc>)>, +} + +impl EntryPointRouterBuilder { + pub(crate) fn v0_6(mut self, route: R) -> Self + where + R: EntryPointRoute, + { + if route.version() != EntryPointVersion::V0_6 { + panic!( + "Invalid entry point version for route: {:?}", + route.version() + ); + } + + self.entry_points.push(route.address()); + self.v0_6 = Some((route.address(), Arc::new(Box::new(route)))); + self + } + + pub(crate) fn _v0_7(mut self, route: R) -> Self + where + R: EntryPointRoute, + { + if route.version() != EntryPointVersion::V0_7 { + panic!( + "Invalid entry point version for route: {:?}", + route.version() + ); + } + + self.entry_points.push(route.address()); + self.v0_7 = Some((route.address(), Arc::new(Box::new(route)))); + self + } + + pub(crate) fn build(self) -> EntryPointRouter { + EntryPointRouter { + entry_points: self.entry_points, + v0_6: self.v0_6, + v0_7: self.v0_7, + } + } +} + +#[derive(Clone)] +pub(crate) struct EntryPointRouter { + entry_points: Vec
, + v0_6: Option<(Address, Arc>)>, + v0_7: Option<(Address, Arc>)>, +} + +impl EntryPointRouter { + pub(crate) fn entry_points(&self) -> impl Iterator { + self.entry_points.iter() + } + + pub(crate) fn check_and_get_route( + &self, + entry_point: &Address, + uo: &UserOperationVariant, + ) -> EthResult<&Arc>> { + match self.get_ep_version(entry_point)? { + EntryPointVersion::V0_6 => { + if !matches!(uo, UserOperationVariant::V0_6(_)) { + return Err(EthRpcError::InvalidParams(format!( + "Invalid user operation for entry point: {:?}", + entry_point + ))); + } + Ok(&self.v0_6.as_ref().unwrap().1) + } + EntryPointVersion::V0_7 => { + if !matches!(uo, UserOperationVariant::V0_7(_)) { + return Err(EthRpcError::InvalidParams(format!( + "Invalid user operation for entry point: {:?}", + entry_point + ))); + } + Ok(&self.v0_7.as_ref().unwrap().1) + } + EntryPointVersion::Unspecified => unreachable!("unspecified entry point version"), + } + } + + pub(crate) async fn get_mined_by_hash( + &self, + entry_point: &Address, + hash: H256, + ) -> EthResult> { + self.get_route(entry_point)? + .get_mined_by_hash(hash) + .await + .map_err(Into::into) + } + + pub(crate) async fn get_receipt( + &self, + entry_point: &Address, + hash: H256, + ) -> EthResult> { + self.get_route(entry_point)? + .get_receipt(hash) + .await + .map_err(Into::into) + } + + pub(crate) async fn estimate_gas( + &self, + entry_point: &Address, + uo: UserOperationOptionalGas, + state_override: Option, + ) -> EthResult { + let route = match self.get_ep_version(entry_point)? { + EntryPointVersion::V0_6 => { + if !matches!(uo, UserOperationOptionalGas::V0_6(_)) { + return Err(EthRpcError::InvalidParams(format!( + "Invalid user operation for entry point: {:?}", + entry_point + ))); + } + &self.v0_6.as_ref().unwrap().1 + } + EntryPointVersion::V0_7 => { + if !matches!(uo, UserOperationOptionalGas::V0_7(_)) { + return Err(EthRpcError::InvalidParams(format!( + "Invalid user operation for entry point: {:?}", + entry_point + ))); + } + &self.v0_7.as_ref().unwrap().1 + } + EntryPointVersion::Unspecified => unreachable!("unspecified entry point version"), + }; + + let estimate = route.estimate_gas(uo, state_override).await?; + Ok(estimate.into()) + } + + pub(crate) async fn check_signature( + &self, + entry_point: &Address, + uo: UserOperationVariant, + max_verification_gas: u64, + ) -> EthResult { + self.check_and_get_route(entry_point, &uo)? + .check_signature(uo, max_verification_gas) + .await + .map_err(Into::into) + } + + fn get_ep_version(&self, entry_point: &Address) -> EthResult { + if let Some((addr, _)) = self.v0_6 { + if addr == *entry_point { + return Ok(EntryPointVersion::V0_6); + } + } else if let Some((addr, _)) = self.v0_7 { + if addr == *entry_point { + return Ok(EntryPointVersion::V0_7); + } + } + + Err(EthRpcError::InvalidParams(format!( + "No entry point found for address: {:?}", + entry_point + ))) + } + + fn get_route(&self, entry_point: &Address) -> EthResult<&Arc>> { + let ep = self.get_ep_version(entry_point)?; + + match ep { + EntryPointVersion::V0_6 => Ok(&self.v0_6.as_ref().unwrap().1), + EntryPointVersion::V0_7 => Ok(&self.v0_7.as_ref().unwrap().1), + EntryPointVersion::Unspecified => unreachable!("unspecified entry point version"), + } + } +} + +#[async_trait::async_trait] +pub(crate) trait EntryPointRoute: Send + Sync + 'static { + fn version(&self) -> EntryPointVersion; + + fn address(&self) -> Address; + + async fn get_mined_by_hash(&self, hash: H256) + -> anyhow::Result>; + + async fn get_receipt(&self, hash: H256) -> anyhow::Result>; + + async fn estimate_gas( + &self, + uo: UserOperationOptionalGas, + state_override: Option, + ) -> Result; + + async fn check_signature( + &self, + uo: UserOperationVariant, + max_verification_gas: u64, + ) -> anyhow::Result; +} + +#[derive(Debug)] +pub(crate) struct EntryPointRouteImpl { + entry_point: E, + gas_estimator: G, + event_provider: EV, + _uo_type: std::marker::PhantomData, +} + +#[async_trait::async_trait] +impl EntryPointRoute for EntryPointRouteImpl +where + UO: UserOperation + From, + E: EntryPoint + SimulationProvider, + G: GasEstimator, + G::UserOperationOptionalGas: From, + EV: UserOperationEventProvider, +{ + fn version(&self) -> EntryPointVersion { + UO::entry_point_version() + } + + fn address(&self) -> Address { + self.entry_point.address() + } + + async fn get_mined_by_hash( + &self, + hash: H256, + ) -> anyhow::Result> { + self.event_provider.get_mined_by_hash(hash).await + } + + async fn get_receipt(&self, hash: H256) -> anyhow::Result> { + self.event_provider.get_receipt(hash).await + } + + async fn estimate_gas( + &self, + uo: UserOperationOptionalGas, + state_override: Option, + ) -> Result { + self.gas_estimator + .estimate_op_gas(uo.into(), state_override.unwrap_or_default()) + .await + } + + async fn check_signature( + &self, + uo: UserOperationVariant, + max_verification_gas: u64, + ) -> anyhow::Result { + let output = self + .entry_point + .call_simulate_validation(uo.into(), max_verification_gas) + .await?; + + Ok(!output.return_info.sig_failed) + } +} + +impl EntryPointRouteImpl { + pub(crate) fn new(entry_point: E, gas_estimator: G, event_provider: EP) -> Self { + Self { + entry_point, + gas_estimator, + event_provider, + _uo_type: std::marker::PhantomData, + } + } +} diff --git a/crates/rpc/src/eth/server.rs b/crates/rpc/src/eth/server.rs index 755279f8c..8ec47a777 100644 --- a/crates/rpc/src/eth/server.rs +++ b/crates/rpc/src/eth/server.rs @@ -11,23 +11,19 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use async_trait::async_trait; use ethers::types::{spoof, Address, H256, U64}; use jsonrpsee::core::RpcResult; use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, L1GasProvider, Provider, SimulationProvider}; -use rundler_types::v0_6; use super::{api::EthApi, EthApiServer}; -use crate::types::{RichUserOperation, RpcGasEstimate, RpcUserOperation, UserOperationReceipt}; +use crate::types::{ + RpcGasEstimate, RpcUserOperation, RpcUserOperationByHash, RpcUserOperationOptionalGas, + RpcUserOperationReceipt, +}; -#[async_trait] -impl EthApiServer for EthApi +#[async_trait::async_trait] +impl EthApiServer for EthApi where - P: Provider, - E: EntryPoint - + L1GasProvider - + SimulationProvider, PS: PoolServer, { async fn send_user_operation( @@ -35,26 +31,32 @@ where op: RpcUserOperation, entry_point: Address, ) -> RpcResult { - Ok(EthApi::send_user_operation(self, op, entry_point).await?) + Ok(EthApi::send_user_operation(self, op.into(), entry_point).await?) } async fn estimate_user_operation_gas( &self, - op: v0_6::UserOperationOptionalGas, + op: RpcUserOperationOptionalGas, entry_point: Address, state_override: Option, ) -> RpcResult { - Ok(EthApi::estimate_user_operation_gas(self, op, entry_point, state_override).await?) + Ok( + EthApi::estimate_user_operation_gas(self, op.into(), entry_point, state_override) + .await?, + ) } - async fn get_user_operation_by_hash(&self, hash: H256) -> RpcResult> { + async fn get_user_operation_by_hash( + &self, + hash: H256, + ) -> RpcResult> { Ok(EthApi::get_user_operation_by_hash(self, hash).await?) } async fn get_user_operation_receipt( &self, hash: H256, - ) -> RpcResult> { + ) -> RpcResult> { Ok(EthApi::get_user_operation_receipt(self, hash).await?) } diff --git a/crates/rpc/src/lib.rs b/crates/rpc/src/lib.rs index b44893641..bda4e98ef 100644 --- a/crates/rpc/src/lib.rs +++ b/crates/rpc/src/lib.rs @@ -40,4 +40,3 @@ mod task; pub use task::{Args as RpcTaskArgs, RpcTask}; mod types; -pub use types::{RichUserOperation, RpcUserOperation, UserOperationReceipt}; diff --git a/crates/rpc/src/rundler.rs b/crates/rpc/src/rundler.rs index 98ef5c6bc..14b1f8beb 100644 --- a/crates/rpc/src/rundler.rs +++ b/crates/rpc/src/rundler.rs @@ -21,15 +21,15 @@ use jsonrpsee::{ types::error::{INTERNAL_ERROR_CODE, INVALID_REQUEST_CODE}, }; use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, Provider, SimulationProvider}; +use rundler_provider::Provider; use rundler_sim::{gas, FeeEstimator}; -use rundler_types::{ - chain::ChainSpec, - v0_6::{self, UserOperation}, - UserOperationId, -}; +use rundler_types::{chain::ChainSpec, UserOperation, UserOperationVariant}; -use crate::{error::rpc_err, eth::EthRpcError, RpcUserOperation}; +use crate::{ + error::rpc_err, + eth::{EntryPointRouter, EthRpcError}, + types::RpcUserOperation, +}; /// Settings for the `rundler_` API #[derive(Copy, Clone, Debug)] @@ -65,23 +65,22 @@ pub trait RundlerApi { ) -> RpcResult>; } -pub(crate) struct RundlerApi { +pub(crate) struct RundlerApi { settings: Settings, fee_estimator: FeeEstimator

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

, - entry_point: E, + entry_point_router: EntryPointRouter, pool_server: PS, settings: Settings, ) -> Self { @@ -93,17 +92,16 @@ where settings.priority_fee_mode, settings.bundle_priority_fee_overhead_percent, ), - entry_point, + entry_point_router, pool_server, } } } #[async_trait] -impl RundlerApiServer for RundlerApi +impl RundlerApiServer for RundlerApi where P: Provider, - E: EntryPoint + SimulationProvider, PS: PoolServer, { async fn max_priority_fee_per_gas(&self) -> RpcResult { @@ -123,23 +121,13 @@ where user_op: RpcUserOperation, entry_point: Address, ) -> RpcResult> { - if entry_point != self.entry_point.address() { - return Err(rpc_err( - INVALID_REQUEST_CODE, - format!("entry point {} not supported", entry_point), - )); - } - - let uo: v0_6::UserOperation = user_op.try_into()?; - let id = UserOperationId { - sender: uo.sender, - nonce: uo.nonce, - }; + let uo = UserOperationVariant::from(user_op); + let id = uo.id(); - if uo.pre_verification_gas != U256::zero() - || uo.call_gas_limit != U256::zero() - || uo.call_data.len() != 0 - || uo.max_fee_per_gas != U256::zero() + if uo.pre_verification_gas() != U256::zero() + || uo.call_gas_limit() != U256::zero() + || uo.call_data().len() != 0 + || uo.max_fee_per_gas() != U256::zero() { return Err(rpc_err( INVALID_REQUEST_CODE, @@ -147,16 +135,14 @@ where )); } - let output = self - .entry_point - .call_simulate_validation(uo, self.settings.max_verification_gas) - .await - .map_err(|e| rpc_err(INTERNAL_ERROR_CODE, e.to_string()))?; - - if output.return_info.sig_failed { + let valid = self + .entry_point_router + .check_signature(&entry_point, uo, self.settings.max_verification_gas) + .await?; + if !valid { return Err(rpc_err( INVALID_REQUEST_CODE, - "User operation for drop failed simulateValidation", + "Invalid user operation for drop: invalid signature", )); } diff --git a/crates/rpc/src/task.rs b/crates/rpc/src/task.rs index cf5435590..533cce4c4 100644 --- a/crates/rpc/src/task.rs +++ b/crates/rpc/src/task.rs @@ -22,20 +22,23 @@ use jsonrpsee::{ }; use rundler_builder::BuilderServer; use rundler_pool::PoolServer; -use rundler_provider::{EntryPoint, EthersEntryPointV0_6, L1GasProvider, SimulationProvider}; -use rundler_sim::{EstimationSettings, PrecheckSettings}; +use rundler_provider::EthersEntryPointV0_6; +use rundler_sim::{EstimationSettings, FeeEstimator, GasEstimatorV0_6, PrecheckSettings}; use rundler_task::{ server::{format_socket_addr, HealthCheck}, Task, }; -use rundler_types::{chain::ChainSpec, v0_6::UserOperation}; +use rundler_types::chain::ChainSpec; use tokio_util::sync::CancellationToken; use tracing::info; use crate::{ admin::{AdminApi, AdminApiServer}, debug::{DebugApi, DebugApiServer}, - eth::{EthApi, EthApiServer, EthApiSettings}, + eth::{ + EntryPointRouteImpl, EntryPointRouter, EntryPointRouterBuilder, EthApi, EthApiServer, + EthApiSettings, UserOperationEventProviderV0_6, + }, health::{HealthChecker, SystemApiServer}, metrics::RpcMetricsLogger, rundler::{RundlerApi, RundlerApiServer, Settings as RundlerApiSettings}, @@ -91,8 +94,37 @@ where let ep = EthersEntryPointV0_6::new(self.args.chain_spec.entry_point_address, provider.clone()); + // create the entry point router + let router = EntryPointRouterBuilder::default() + .v0_6(EntryPointRouteImpl::new( + ep.clone(), + GasEstimatorV0_6::new( + self.args.chain_spec.clone(), + provider.clone(), + ep, + self.args.estimation_settings, + FeeEstimator::new( + &self.args.chain_spec, + Arc::clone(&provider), + self.args.precheck_settings.priority_fee_mode, + self.args + .precheck_settings + .bundle_priority_fee_overhead_percent, + ), + ), + UserOperationEventProviderV0_6::new( + self.args.chain_spec.id, + self.args.chain_spec.entry_point_address, + provider.clone(), + self.args + .eth_api_settings + .user_operation_event_block_distance, + ), + )) + .build(); + let mut module = RpcModule::new(()); - self.attach_namespaces(provider, ep, &mut module)?; + self.attach_namespaces(provider, router, &mut module)?; let servers: Vec> = vec![Box::new(self.pool.clone()), Box::new(self.builder.clone())]; @@ -148,48 +180,45 @@ where Box::new(self) } - fn attach_namespaces( + fn attach_namespaces( &self, provider: Arc>, - entry_point: E, + entry_point_router: EntryPointRouter, module: &mut RpcModule<()>, ) -> anyhow::Result<()> where - E: EntryPoint - + SimulationProvider - + L1GasProvider - + Clone, C: JsonRpcClient + 'static, { - for api in &self.args.api_namespaces { - match api { - ApiNamespace::Eth => module.merge( - EthApi::new( - self.args.chain_spec.clone(), - provider.clone(), - // TODO: support multiple entry points - vec![entry_point.clone()], - self.pool.clone(), - self.args.eth_api_settings, - self.args.estimation_settings, - self.args.precheck_settings, - ) - .into_rpc(), - )?, - ApiNamespace::Debug => module - .merge(DebugApi::new(self.pool.clone(), self.builder.clone()).into_rpc())?, - ApiNamespace::Admin => module.merge(AdminApi::new(self.pool.clone()).into_rpc())?, - ApiNamespace::Rundler => module.merge( - RundlerApi::new( - &self.args.chain_spec, - provider.clone(), - entry_point.clone(), - self.pool.clone(), - self.args.rundler_api_settings, - ) - .into_rpc(), - )?, - } + if self.args.api_namespaces.contains(&ApiNamespace::Eth) { + module.merge( + EthApi::new( + self.args.chain_spec.clone(), + entry_point_router.clone(), + self.pool.clone(), + ) + .into_rpc(), + )? + } + + if self.args.api_namespaces.contains(&ApiNamespace::Debug) { + module.merge(DebugApi::new(self.pool.clone(), self.builder.clone()).into_rpc())?; + } + + if self.args.api_namespaces.contains(&ApiNamespace::Admin) { + module.merge(AdminApi::new(self.pool.clone()).into_rpc())?; + } + + if self.args.api_namespaces.contains(&ApiNamespace::Rundler) { + module.merge( + RundlerApi::new( + &self.args.chain_spec, + provider.clone(), + entry_point_router, + self.pool.clone(), + self.args.rundler_api_settings, + ) + .into_rpc(), + )?; } Ok(()) diff --git a/crates/rpc/src/types.rs b/crates/rpc/src/types/mod.rs similarity index 66% rename from crates/rpc/src/types.rs rename to crates/rpc/src/types/mod.rs index 24fcbb54c..c5d383533 100644 --- a/crates/rpc/src/types.rs +++ b/crates/rpc/src/types/mod.rs @@ -12,14 +12,23 @@ // If not, see https://www.gnu.org/licenses/. use ethers::{ - types::{Address, Bytes, Log, TransactionReceipt, H160, H256, U256}, + types::{Address, Log, TransactionReceipt, H160, H256, U256}, utils::to_checksum, }; use rundler_pool::{Reputation, ReputationStatus}; -use rundler_types::{v0_6, GasEstimate}; +use rundler_types::{GasEstimate, UserOperationOptionalGas, UserOperationVariant}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::eth::EthRpcError; +mod v0_6; +pub(crate) use v0_6::{ + RpcUserOperation as RpcUserOperationV0_6, + RpcUserOperationOptionalGas as RpcUserOperationOptionalGasV0_6, +}; +mod v0_7; +pub(crate) use v0_7::{ + RpcUserOperation as RpcUserOperationV0_7, + RpcUserOperationOptionalGas as RpcUserOperationOptionalGasV0_7, +}; /// API namespace #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum::EnumString)] @@ -68,104 +77,102 @@ impl From

for RpcAddress { /// Stake info definition for RPC #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct RpcStakeStatus { - pub is_staked: bool, - pub stake_info: RpcStakeInfo, +pub(crate) struct RpcStakeStatus { + pub(crate) is_staked: bool, + pub(crate) stake_info: RpcStakeInfo, } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct RpcStakeInfo { - pub addr: Address, - pub stake: u128, - pub unstake_delay_sec: u32, +pub(crate) struct RpcStakeInfo { + pub(crate) addr: Address, + pub(crate) stake: u128, + pub(crate) unstake_delay_sec: u32, } -/// User operation definition for RPC -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct RpcUserOperation { - sender: RpcAddress, - nonce: U256, - init_code: Bytes, - call_data: Bytes, - call_gas_limit: U256, - verification_gas_limit: U256, - pre_verification_gas: U256, - max_fee_per_gas: U256, - max_priority_fee_per_gas: U256, - paymaster_and_data: Bytes, - signature: Bytes, +#[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] +#[serde(untagged)] +pub(crate) enum RpcUserOperation { + V0_6(RpcUserOperationV0_6), + V0_7(RpcUserOperationV0_7), } -impl From for RpcUserOperation { - fn from(op: v0_6::UserOperation) -> Self { - RpcUserOperation { - sender: op.sender.into(), - nonce: op.nonce, - init_code: op.init_code, - call_data: op.call_data, - call_gas_limit: op.call_gas_limit, - verification_gas_limit: op.verification_gas_limit, - pre_verification_gas: op.pre_verification_gas, - max_fee_per_gas: op.max_fee_per_gas, - max_priority_fee_per_gas: op.max_priority_fee_per_gas, - paymaster_and_data: op.paymaster_and_data, - signature: op.signature, +impl From for RpcUserOperation { + fn from(op: UserOperationVariant) -> Self { + match op { + UserOperationVariant::V0_6(op) => RpcUserOperation::V0_6(op.into()), + UserOperationVariant::V0_7(op) => RpcUserOperation::V0_7(op.into()), } } } -impl TryFrom for v0_6::UserOperation { - type Error = EthRpcError; - - fn try_from(def: RpcUserOperation) -> Result { - if def.init_code.len() > 0 && def.init_code.len() < 20 { - return Err(EthRpcError::InvalidParams( - "init_code must be empty or at least 20 bytes".to_string(), - )); - } else if def.paymaster_and_data.len() > 0 && def.paymaster_and_data.len() < 20 { - return Err(EthRpcError::InvalidParams( - "paymaster_and_data must be empty or at least 20 bytes".to_string(), - )); +impl From for UserOperationVariant { + fn from(op: RpcUserOperation) -> Self { + match op { + RpcUserOperation::V0_6(op) => UserOperationVariant::V0_6(op.into()), + RpcUserOperation::V0_7(op) => UserOperationVariant::V0_7(op.into()), } - - Ok(v0_6::UserOperation { - sender: def.sender.into(), - nonce: def.nonce, - init_code: def.init_code, - call_data: def.call_data, - call_gas_limit: def.call_gas_limit, - verification_gas_limit: def.verification_gas_limit, - pre_verification_gas: def.pre_verification_gas, - max_fee_per_gas: def.max_fee_per_gas, - max_priority_fee_per_gas: def.max_priority_fee_per_gas, - paymaster_and_data: def.paymaster_and_data, - signature: def.signature, - }) } } /// User operation with additional metadata -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] -pub struct RichUserOperation { +pub(crate) struct RpcUserOperationByHash { /// The full user operation - pub user_operation: RpcUserOperation, + pub(crate) user_operation: RpcUserOperation, /// The entry point address this operation was sent to - pub entry_point: RpcAddress, + pub(crate) entry_point: RpcAddress, /// The number of the block this operation was included in - pub block_number: Option, + pub(crate) block_number: Option, /// The hash of the block this operation was included in - pub block_hash: Option, + pub(crate) block_hash: Option, /// The hash of the transaction this operation was included in - pub transaction_hash: Option, + pub(crate) transaction_hash: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub(crate) enum RpcUserOperationOptionalGas { + V0_6(RpcUserOperationOptionalGasV0_6), + V0_7(RpcUserOperationOptionalGasV0_7), +} + +impl From for UserOperationOptionalGas { + fn from(op: RpcUserOperationOptionalGas) -> Self { + match op { + RpcUserOperationOptionalGas::V0_6(op) => UserOperationOptionalGas::V0_6(op.into()), + RpcUserOperationOptionalGas::V0_7(op) => UserOperationOptionalGas::V0_7(op.into()), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RpcGasEstimate { + pre_verification_gas: U256, + call_gas_limit: U256, + verification_gas_limit: U256, + paymaster_verification_gas_limit: Option, + paymaster_post_op_gas_limit: Option, +} + +impl From for RpcGasEstimate { + fn from(estimate: GasEstimate) -> Self { + RpcGasEstimate { + pre_verification_gas: estimate.pre_verification_gas, + call_gas_limit: estimate.call_gas_limit, + verification_gas_limit: estimate.verification_gas_limit, + paymaster_verification_gas_limit: estimate.paymaster_verification_gas_limit, + paymaster_post_op_gas_limit: estimate.paymaster_post_op_gas_limit, + } + } } /// User operation receipt #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct UserOperationReceipt { +pub struct RpcUserOperationReceipt { /// The hash of the user operation pub user_op_hash: H256, /// The entry point address this operation was sent to @@ -273,34 +280,3 @@ pub struct RpcDebugPaymasterBalance { /// Paymaster confirmed balance onchain pub confirmed_balance: U256, } - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct RpcGasEstimate { - /// The pre-verification gas estimate - pub pre_verification_gas: U256, - /// The call gas limit estimate - pub call_gas_limit: U256, - /// The verification gas limit estimate - pub verification_gas_limit: U256, - /// The paymaster verification gas limit estimate - /// 0.6: unused - /// 0.7: populated if a paymaster is used - pub paymaster_verification_gas_limit: Option, - /// The paymaster post op gas limit - /// 0.6: unused - /// 0.7: populated if a paymaster is used - pub paymaster_post_op_gas_limit: Option, -} - -impl From for RpcGasEstimate { - fn from(estimate: GasEstimate) -> Self { - RpcGasEstimate { - pre_verification_gas: estimate.pre_verification_gas, - call_gas_limit: estimate.call_gas_limit, - verification_gas_limit: estimate.verification_gas_limit, - paymaster_verification_gas_limit: estimate.paymaster_verification_gas_limit, - paymaster_post_op_gas_limit: estimate.paymaster_post_op_gas_limit, - } - } -} diff --git a/crates/rpc/src/types/v0_6.rs b/crates/rpc/src/types/v0_6.rs new file mode 100644 index 000000000..1c764fc86 --- /dev/null +++ b/crates/rpc/src/types/v0_6.rs @@ -0,0 +1,105 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use ethers::types::{Address, Bytes, U256}; +use rundler_types::v0_6::{UserOperation, UserOperationOptionalGas}; +use serde::{Deserialize, Serialize}; + +use super::RpcAddress; + +/// User operation definition for RPC +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RpcUserOperation { + sender: RpcAddress, + nonce: U256, + init_code: Bytes, + call_data: Bytes, + call_gas_limit: U256, + verification_gas_limit: U256, + pre_verification_gas: U256, + max_fee_per_gas: U256, + max_priority_fee_per_gas: U256, + paymaster_and_data: Bytes, + signature: Bytes, +} + +impl From for RpcUserOperation { + fn from(op: UserOperation) -> Self { + RpcUserOperation { + sender: op.sender.into(), + nonce: op.nonce, + init_code: op.init_code, + call_data: op.call_data, + call_gas_limit: op.call_gas_limit, + verification_gas_limit: op.verification_gas_limit, + pre_verification_gas: op.pre_verification_gas, + max_fee_per_gas: op.max_fee_per_gas, + max_priority_fee_per_gas: op.max_priority_fee_per_gas, + paymaster_and_data: op.paymaster_and_data, + signature: op.signature, + } + } +} + +impl From for UserOperation { + fn from(def: RpcUserOperation) -> Self { + UserOperation { + sender: def.sender.into(), + nonce: def.nonce, + init_code: def.init_code, + call_data: def.call_data, + call_gas_limit: def.call_gas_limit, + verification_gas_limit: def.verification_gas_limit, + pre_verification_gas: def.pre_verification_gas, + max_fee_per_gas: def.max_fee_per_gas, + max_priority_fee_per_gas: def.max_priority_fee_per_gas, + paymaster_and_data: def.paymaster_and_data, + signature: def.signature, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RpcUserOperationOptionalGas { + sender: Address, + nonce: U256, + init_code: Bytes, + call_data: Bytes, + call_gas_limit: Option, + verification_gas_limit: Option, + pre_verification_gas: Option, + max_fee_per_gas: Option, + max_priority_fee_per_gas: Option, + paymaster_and_data: Bytes, + signature: Bytes, +} + +impl From for UserOperationOptionalGas { + fn from(def: RpcUserOperationOptionalGas) -> Self { + UserOperationOptionalGas { + sender: def.sender, + nonce: def.nonce, + init_code: def.init_code, + call_data: def.call_data, + call_gas_limit: def.call_gas_limit, + verification_gas_limit: def.verification_gas_limit, + pre_verification_gas: def.pre_verification_gas, + max_fee_per_gas: def.max_fee_per_gas, + max_priority_fee_per_gas: def.max_priority_fee_per_gas, + paymaster_and_data: def.paymaster_and_data, + signature: def.signature, + } + } +} diff --git a/crates/rpc/src/types/v0_7.rs b/crates/rpc/src/types/v0_7.rs new file mode 100644 index 000000000..8fcb53d83 --- /dev/null +++ b/crates/rpc/src/types/v0_7.rs @@ -0,0 +1,88 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use ethers::types::{Address, Bytes, H256, U128, U256}; +use rundler_types::v0_7::{UserOperation, UserOperationOptionalGas}; +use serde::{Deserialize, Serialize}; + +use super::RpcAddress; + +/// User operation definition for RPC +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RpcUserOperation { + sender: Address, + nonce: U256, + call_data: Bytes, + call_gas_limit: U128, + verification_gas_limit: U128, + pre_verification_gas: U256, + max_priority_fee_per_gas: U128, + max_fee_per_gas: U128, + factory: Option
, + factory_data: Option, + paymaster: Option
, + paymaster_verification_gas_limit: Option, + paymaster_post_op_gas_limit: Option, + paymaster_data: Option, + signature: Bytes, +} + +impl From for RpcUserOperation { + fn from(_op: UserOperation) -> Self { + todo!() + } +} + +impl From for UserOperation { + fn from(_def: RpcUserOperation) -> Self { + todo!() + } +} + +/// User operation with additional metadata +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RpcUserOperationByHash { + user_operation: RpcUserOperation, + entry_point: RpcAddress, + block_number: Option, + block_hash: Option, + transaction_hash: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct RpcUserOperationOptionalGas { + sender: Address, + nonce: U256, + call_data: Bytes, + call_gas_limit: Option, + verification_gas_limit: Option, + pre_verification_gas: Option, + max_priority_fee_per_gas: Option, + max_fee_per_gas: Option, + factory: Option
, + factory_data: Option, + paymaster: Option
, + paymaster_verification_gas_limit: Option, + paymaster_post_op_gas_limit: Option, + paymaster_data: Option, + signature: Bytes, +} + +impl From for UserOperationOptionalGas { + fn from(_def: RpcUserOperationOptionalGas) -> Self { + todo!() + } +} diff --git a/crates/sim/src/estimation/mod.rs b/crates/sim/src/estimation/mod.rs index feacea06e..7848ff1cf 100644 --- a/crates/sim/src/estimation/mod.rs +++ b/crates/sim/src/estimation/mod.rs @@ -19,7 +19,10 @@ use rundler_types::GasEstimate; use crate::precheck::MIN_CALL_GAS_LIMIT; /// Gas estimation module for Entry Point v0.6 -pub mod v0_6; +mod v0_6; +pub use v0_6::GasEstimator as GasEstimatorV0_6; +mod v0_7; +pub use v0_7::GasEstimator as GasEstimatorV0_7; /// Error type for gas estimation #[derive(Debug, thiserror::Error)] diff --git a/crates/sim/src/estimation/v0_6.rs b/crates/sim/src/estimation/v0_6.rs index 9f09b752e..ab0a11308 100644 --- a/crates/sim/src/estimation/v0_6.rs +++ b/crates/sim/src/estimation/v0_6.rs @@ -42,12 +42,8 @@ use rundler_types::{ use rundler_utils::{eth, math}; use tokio::join; -use crate::{ - estimation::{GasEstimationError, Settings}, - gas, - precheck::MIN_CALL_GAS_LIMIT, - simulation, utils, FeeEstimator, -}; +use super::{GasEstimationError, Settings}; +use crate::{gas, precheck::MIN_CALL_GAS_LIMIT, simulation, utils, FeeEstimator}; /// Gas estimates will be rounded up to the next multiple of this. Increasing /// this value reduces the number of rounds of `eth_call` needed in binary diff --git a/crates/sim/src/estimation/v0_7.rs b/crates/sim/src/estimation/v0_7.rs new file mode 100644 index 000000000..a4a2b9646 --- /dev/null +++ b/crates/sim/src/estimation/v0_7.rs @@ -0,0 +1,36 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use ethers::types::spoof; +use rundler_types::{v0_7::UserOperationOptionalGas, GasEstimate}; + +use super::GasEstimationError; + +/// Gas estimator for entry point v0.7 +#[derive(Debug)] +pub struct GasEstimator {} + +#[async_trait::async_trait] +impl super::GasEstimator for GasEstimator { + type UserOperationOptionalGas = UserOperationOptionalGas; + + /// Returns a gas estimate or a revert message, or an anyhow error on any + /// other error. + async fn estimate_op_gas( + &self, + _op: UserOperationOptionalGas, + _state_override: spoof::State, + ) -> Result { + unimplemented!() + } +} diff --git a/crates/sim/src/lib.rs b/crates/sim/src/lib.rs index 8b1d54c36..6111b1c98 100644 --- a/crates/sim/src/lib.rs +++ b/crates/sim/src/lib.rs @@ -31,8 +31,11 @@ //! - `test-utils`: Export mocks and utilities for testing. /// Gas estimation -pub mod estimation; -pub use estimation::{GasEstimationError, GasEstimator, Settings as EstimationSettings}; +mod estimation; +pub use estimation::{ + GasEstimationError, GasEstimator, GasEstimatorV0_6, GasEstimatorV0_7, + Settings as EstimationSettings, +}; pub mod gas; pub use gas::{FeeEstimator, PriorityFeeMode}; diff --git a/crates/types/src/user_operation/mod.rs b/crates/types/src/user_operation/mod.rs index 84513fb1f..906d8f3a0 100644 --- a/crates/types/src/user_operation/mod.rs +++ b/crates/types/src/user_operation/mod.rs @@ -28,6 +28,8 @@ use crate::Entity; /// ERC-4337 Entry point version #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum EntryPointVersion { + /// Unspecified version + Unspecified, /// Version 0.6 V0_6, /// Version 0.7 @@ -50,14 +52,12 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { /// Associated type for the version of a user operation that has optional gas and fee fields type OptionalGas; - /// Hash a user operation with the given entry point and chain ID. - /// - /// The hash is used to uniquely identify a user operation in the entry point. - /// It does not include the signature field. - fn hash(&self, entry_point: Address, chain_id: u64) -> H256; + /// Get the entry point version for this UO + fn entry_point_version() -> EntryPointVersion; - /// Get the user operation id - fn id(&self) -> UserOperationId; + /* + * Getters + */ /// Get the user operation sender address fn sender(&self) -> Address; @@ -68,21 +68,43 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { /// Get the user operation factory address, if any fn factory(&self) -> Option
; + /// Get the user operation calldata + fn call_data(&self) -> &Bytes; + + /// Returns the call gas limit + fn call_gas_limit(&self) -> U256; + + /// Returns the verification gas limit + fn verification_gas_limit(&self) -> U256; + + /// Returns the max fee per gas + fn max_fee_per_gas(&self) -> U256; + + /// Returns the max priority fee per gas + fn max_priority_fee_per_gas(&self) -> U256; + /// Returns the maximum cost, in wei, of this user operation fn max_gas_cost(&self) -> U256; + /* + * Enhanced functions + */ + + /// Hash a user operation with the given entry point and chain ID. + /// + /// The hash is used to uniquely identify a user operation in the entry point. + /// It does not include the signature field. + fn hash(&self, entry_point: Address, chain_id: u64) -> H256; + + /// Get the user operation id + fn id(&self) -> UserOperationId; + /// Gets an iterator on all entities associated with this user operation fn entities(&'_ self) -> Vec; /// Returns the heap size of the user operation fn heap_size(&self) -> usize; - /// Returns the call gas limit - fn call_gas_limit(&self) -> U256; - - /// Returns the verification gas limit - fn verification_gas_limit(&self) -> U256; - /// Returns the total verification gas limit fn total_verification_gas_limit(&self) -> U256; @@ -99,12 +121,6 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { /// Calculate the static portion of the pre-verification gas for this user operation fn calc_static_pre_verification_gas(&self, include_fixed_gas_overhead: bool) -> U256; - /// Returns the max fee per gas - fn max_fee_per_gas(&self) -> U256; - - /// Returns the max priority fee per gas - fn max_priority_fee_per_gas(&self) -> U256; - /// Clear the signature field of the user op /// /// Used when a user op is using a signature aggregator prior to being submitted @@ -132,6 +148,10 @@ pub enum UserOperationVariant { impl UserOperation for UserOperationVariant { type OptionalGas = UserOperationOptionalGas; + fn entry_point_version() -> EntryPointVersion { + EntryPointVersion::Unspecified + } + fn hash(&self, entry_point: Address, chain_id: u64) -> H256 { match self { UserOperationVariant::V0_6(op) => op.hash(entry_point, chain_id), @@ -167,6 +187,13 @@ impl UserOperation for UserOperationVariant { } } + fn call_data(&self) -> &Bytes { + match self { + UserOperationVariant::V0_6(op) => op.call_data(), + UserOperationVariant::V0_7(op) => op.call_data(), + } + } + fn max_gas_cost(&self) -> U256 { match self { UserOperationVariant::V0_6(op) => op.max_gas_cost(), diff --git a/crates/types/src/user_operation/v0_6.rs b/crates/types/src/user_operation/v0_6.rs index 958d445b1..9677b7a3f 100644 --- a/crates/types/src/user_operation/v0_6.rs +++ b/crates/types/src/user_operation/v0_6.rs @@ -24,11 +24,18 @@ use super::{ GasOverheads, UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant, }; pub use crate::contracts::v0_6::shared_types::{UserOperation, UserOpsPerAggregator}; -use crate::entity::{Entity, EntityType}; +use crate::{ + entity::{Entity, EntityType}, + EntryPointVersion, +}; impl UserOperationTrait for UserOperation { type OptionalGas = UserOperationOptionalGas; + fn entry_point_version() -> EntryPointVersion { + EntryPointVersion::V0_6 + } + fn hash(&self, entry_point: Address, chain_id: u64) -> H256 { keccak256(encode(&[ Token::FixedBytes(keccak256(self.pack_for_hash()).to_vec()), @@ -57,6 +64,10 @@ impl UserOperationTrait for UserOperation { Self::get_address_from_field(&self.paymaster_and_data) } + fn call_data(&self) -> &Bytes { + &self.call_data + } + fn max_gas_cost(&self) -> U256 { let mul = if self.paymaster().is_some() { 3 } else { 1 }; self.max_fee_per_gas diff --git a/crates/types/src/user_operation/v0_7.rs b/crates/types/src/user_operation/v0_7.rs index dbe1a66e6..e9c6af957 100644 --- a/crates/types/src/user_operation/v0_7.rs +++ b/crates/types/src/user_operation/v0_7.rs @@ -18,7 +18,9 @@ use ethers::{ }; use super::{UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant}; -use crate::{contracts::v0_7::shared_types::PackedUserOperation, Entity, GasOverheads}; +use crate::{ + contracts::v0_7::shared_types::PackedUserOperation, Entity, EntryPointVersion, GasOverheads, +}; const ENTRY_POINT_INNER_GAS_OVERHEAD: U256 = U256([10_000, 0, 0, 0]); @@ -79,6 +81,10 @@ pub struct UserOperation { impl UserOperationTrait for UserOperation { type OptionalGas = UserOperationOptionalGas; + fn entry_point_version() -> EntryPointVersion { + EntryPointVersion::V0_7 + } + fn hash(&self, _entry_point: Address, _chain_id: u64) -> H256 { self.hash } @@ -102,6 +108,10 @@ impl UserOperationTrait for UserOperation { self.factory } + fn call_data(&self) -> &Bytes { + &self.call_data + } + fn max_gas_cost(&self) -> U256 { U256::from(self.max_fee_per_gas) * (self.pre_verification_gas From 30f23a5c5e0cfeed17303a6a0c211e39d4d4c7e2 Mon Sep 17 00:00:00 2001 From: dancoombs Date: Thu, 14 Mar 2024 18:48:14 -0400 Subject: [PATCH 04/12] refactor: move builder and pool traits to types --- Cargo.lock | 8 +- crates/builder/Cargo.toml | 3 +- crates/builder/src/bundle_proposer.rs | 26 +- crates/builder/src/bundle_sender.rs | 10 +- crates/builder/src/lib.rs | 5 +- crates/builder/src/server/local.rs | 14 +- crates/builder/src/server/mod.rs | 55 +--- crates/builder/src/server/remote/client.rs | 20 +- crates/builder/src/server/remote/error.rs | 31 +- crates/builder/src/server/remote/protos.rs | 15 +- crates/builder/src/server/remote/server.rs | 28 +- crates/builder/src/task.rs | 7 +- crates/pool/Cargo.toml | 3 - crates/pool/src/lib.rs | 9 +- crates/pool/src/mempool/error.rs | 114 ------- crates/pool/src/mempool/mod.rs | 189 +----------- crates/pool/src/mempool/paymaster.rs | 22 +- crates/pool/src/mempool/pool.rs | 18 +- crates/pool/src/mempool/reputation.rs | 52 +--- crates/pool/src/mempool/uo_pool.rs | 23 +- crates/pool/src/server/local.rs | 68 ++-- crates/pool/src/server/mod.rs | 139 +-------- crates/pool/src/server/remote/client.rs | 109 ++++--- crates/pool/src/server/remote/error.rs | 37 +-- crates/pool/src/server/remote/protos.rs | 50 ++- crates/pool/src/server/remote/server.rs | 10 +- crates/rpc/Cargo.toml | 4 +- crates/rpc/src/admin.rs | 4 +- crates/rpc/src/debug.rs | 14 +- crates/rpc/src/eth/api.rs | 29 +- crates/rpc/src/eth/error.rs | 18 +- crates/rpc/src/eth/server.rs | 6 +- crates/rpc/src/rundler.rs | 17 +- crates/rpc/src/task.rs | 12 +- crates/rpc/src/types/mod.rs | 6 +- crates/sim/src/lib.rs | 6 +- crates/sim/src/precheck.rs | 69 ++--- crates/sim/src/simulation/mempool.rs | 3 +- crates/sim/src/simulation/mod.rs | 292 +++++------------- crates/sim/src/simulation/v0_6/simulator.rs | 21 +- crates/types/Cargo.toml | 11 + .../src/server => types/src/builder}/error.rs | 24 +- crates/types/src/builder/mod.rs | 23 ++ crates/types/src/builder/traits.rs | 37 +++ crates/types/src/builder/types.rs | 30 ++ crates/types/src/entity.rs | 73 ++++- crates/types/src/lib.rs | 9 +- crates/types/src/opcode.rs | 34 ++ crates/types/src/pool/error.rs | 225 ++++++++++++++ crates/types/src/pool/mod.rs | 36 +++ crates/types/src/pool/traits.rs | 130 ++++++++ crates/types/src/pool/types.rs | 250 +++++++++++++++ 52 files changed, 1293 insertions(+), 1155 deletions(-) delete mode 100644 crates/pool/src/mempool/error.rs rename crates/{pool/src/server => types/src/builder}/error.rs (59%) create mode 100644 crates/types/src/builder/mod.rs create mode 100644 crates/types/src/builder/traits.rs create mode 100644 crates/types/src/builder/types.rs create mode 100644 crates/types/src/opcode.rs create mode 100644 crates/types/src/pool/error.rs create mode 100644 crates/types/src/pool/mod.rs create mode 100644 crates/types/src/pool/traits.rs create mode 100644 crates/types/src/pool/types.rs diff --git a/Cargo.lock b/Cargo.lock index 41808c810..a319778d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4122,7 +4122,6 @@ dependencies = [ "prost", "reqwest", "rslock", - "rundler-pool", "rundler-provider", "rundler-sim", "rundler-task", @@ -4217,8 +4216,6 @@ dependencies = [ "jsonrpsee", "metrics 0.22.1", "mockall", - "rundler-builder", - "rundler-pool", "rundler-provider", "rundler-sim", "rundler-task", @@ -4304,15 +4301,20 @@ name = "rundler-types" version = "0.1.0-rc0" dependencies = [ "anyhow", + "async-trait", "chrono", "constcat", "ethers", + "futures-util", + "mockall", "parse-display", "rand", + "rundler-types", "rundler-utils", "serde", "serde_json", "strum 0.26.1", + "thiserror", ] [[package]] diff --git a/crates/builder/Cargo.toml b/crates/builder/Cargo.toml index d2d809106..52d0cc27d 100644 --- a/crates/builder/Cargo.toml +++ b/crates/builder/Cargo.toml @@ -7,7 +7,6 @@ license.workspace = true repository.workspace = true [dependencies] -rundler-pool = { path = "../pool" } rundler-provider = { path = "../provider" } rundler-sim = { path = "../sim" } rundler-task = { path = "../task" } @@ -46,7 +45,7 @@ mockall = {workspace = true, optional = true } [dev-dependencies] mockall.workspace = true -rundler-pool = { path = "../pool", features = ["test-utils"] } +rundler-types = { path = "../types", features = ["test-utils"] } rundler-provider = { path = "../provider", features = ["test-utils"] } rundler-sim = { path = "../sim", features = ["test-utils"] } diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index 204d9a778..ed8c5159a 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -28,17 +28,18 @@ use futures_util::TryFutureExt; use linked_hash_map::LinkedHashMap; #[cfg(test)] use mockall::automock; -use rundler_pool::{FromPoolOperationVariant, PoolOperation, PoolServer}; use rundler_provider::{ BundleHandler, EntryPoint, HandleOpsOut, L1GasProvider, Provider, SignatureAggregator, }; use rundler_sim::{ - gas, EntityInfo, EntityInfos, ExpectedStorage, FeeEstimator, PriorityFeeMode, SimulationError, - SimulationResult, SimulationViolation, Simulator, ViolationError, + gas, ExpectedStorage, FeeEstimator, PriorityFeeMode, SimulationError, SimulationResult, + Simulator, ViolationError, }; use rundler_types::{ - chain::ChainSpec, Entity, EntityType, EntityUpdate, EntityUpdateType, GasFees, GasOverheads, - Timestamp, UserOperation, UserOperationVariant, UserOpsPerAggregator, + chain::ChainSpec, + pool::{FromPoolOperationVariant, Pool, PoolOperation, SimulationViolation}, + Entity, EntityInfo, EntityInfos, EntityType, EntityUpdate, EntityUpdateType, GasFees, + GasOverheads, Timestamp, UserOperation, UserOperationVariant, UserOpsPerAggregator, }; use rundler_utils::{emit::WithEntryPoint, math}; use tokio::{sync::broadcast, try_join}; @@ -133,7 +134,7 @@ where S: Simulator, E: EntryPoint + SignatureAggregator + BundleHandler + L1GasProvider, P: Provider, - C: PoolServer, + C: Pool, { type UO = UO; @@ -238,7 +239,7 @@ where S: Simulator, E: EntryPoint + SignatureAggregator + BundleHandler + L1GasProvider, P: Provider, - C: PoolServer, + C: Pool, { pub(crate) fn new( builder_index: u64, @@ -1246,10 +1247,13 @@ mod tests { types::{H160, U64}, utils::parse_units, }; - use rundler_pool::{IntoPoolOperationVariant, MockPoolServer}; use rundler_provider::{AggregatorSimOut, MockEntryPointV0_6, MockProvider}; - use rundler_sim::{MockSimulator, SimulationViolation, ViolationError}; - use rundler_types::{v0_6::UserOperation, UserOperation as UserOperationTrait, ValidTimeRange}; + use rundler_sim::MockSimulator; + use rundler_types::{ + pool::{IntoPoolOperationVariant, MockPool, SimulationViolation}, + v0_6::UserOperation, + UserOperation as UserOperationTrait, ValidTimeRange, + }; use super::*; @@ -2034,7 +2038,7 @@ mod tests { }) .collect(); - let mut pool_client = MockPoolServer::new(); + let mut pool_client = MockPool::new(); pool_client.expect_get_ops().returning(move |_, _, _| { Ok(ops .iter() diff --git a/crates/builder/src/bundle_sender.rs b/crates/builder/src/bundle_sender.rs index af6eb2748..3814ae648 100644 --- a/crates/builder/src/bundle_sender.rs +++ b/crates/builder/src/bundle_sender.rs @@ -17,10 +17,11 @@ use anyhow::{bail, Context}; use async_trait::async_trait; use ethers::types::{transaction::eip2718::TypedTransaction, Address, H256, U256}; use futures_util::StreamExt; -use rundler_pool::PoolServer; use rundler_provider::{BundleHandler, EntryPoint}; use rundler_sim::ExpectedStorage; -use rundler_types::{chain::ChainSpec, EntityUpdate, GasFees, UserOperation}; +use rundler_types::{ + builder::BundlingMode, chain::ChainSpec, pool::Pool, EntityUpdate, GasFees, UserOperation, +}; use rundler_utils::emit::WithEntryPoint; use tokio::{ join, @@ -32,7 +33,6 @@ use crate::{ bundle_proposer::BundleProposer, emit::{BuilderEvent, BundleTxDetails}, transaction_tracker::{SendResult, TrackerUpdate, TransactionTracker}, - BundlingMode, }; #[async_trait] @@ -100,7 +100,7 @@ where P: BundleProposer, E: EntryPoint + BundleHandler, T: TransactionTracker, - C: PoolServer, + C: Pool, { /// Loops forever, attempting to form and send a bundle on each new block, /// then waiting for one bundle to be mined or dropped before forming the @@ -249,7 +249,7 @@ where P: BundleProposer, E: EntryPoint + BundleHandler, T: TransactionTracker, - C: PoolServer, + C: Pool, { #[allow(clippy::too_many_arguments)] pub(crate) fn new( diff --git a/crates/builder/src/lib.rs b/crates/builder/src/lib.rs index 8e7d9ccb4..66107d3c5 100644 --- a/crates/builder/src/lib.rs +++ b/crates/builder/src/lib.rs @@ -29,10 +29,7 @@ mod sender; pub use sender::TransactionSenderType; mod server; -pub use server::{ - BuilderResult, BuilderServer, BuilderServerError, BundlingMode, LocalBuilderBuilder, - LocalBuilderHandle, RemoteBuilderClient, -}; +pub use server::{LocalBuilderBuilder, LocalBuilderHandle, RemoteBuilderClient}; mod signer; diff --git a/crates/builder/src/server/local.rs b/crates/builder/src/server/local.rs index d057fc82e..76bc0cf87 100644 --- a/crates/builder/src/server/local.rs +++ b/crates/builder/src/server/local.rs @@ -14,16 +14,14 @@ use async_trait::async_trait; use ethers::types::{Address, H256}; use rundler_task::server::{HealthCheck, ServerStatus}; +use rundler_types::builder::{Builder, BuilderError, BuilderResult, BundlingMode}; use tokio::{ sync::{mpsc, oneshot}, task::JoinHandle, }; use tokio_util::sync::CancellationToken; -use crate::{ - bundle_sender::{BundleSenderAction, SendBundleRequest, SendBundleResult}, - server::{BuilderResult, BuilderServer, BuilderServerError, BundlingMode}, -}; +use crate::bundle_sender::{BundleSenderAction, SendBundleRequest, SendBundleResult}; /// Local builder server builder #[derive(Debug)] @@ -92,13 +90,13 @@ impl LocalBuilderHandle { } #[async_trait] -impl BuilderServer for LocalBuilderHandle { +impl Builder for LocalBuilderHandle { async fn get_supported_entry_points(&self) -> BuilderResult> { let req = ServerRequestKind::GetSupportedEntryPoints; let resp = self.send(req).await?; match resp { ServerResponse::GetSupportedEntryPoints { entry_points } => Ok(entry_points), - _ => Err(BuilderServerError::UnexpectedResponse), + _ => Err(BuilderError::UnexpectedResponse), } } @@ -107,7 +105,7 @@ impl BuilderServer for LocalBuilderHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugSendBundleNow { hash, block_number } => Ok((hash, block_number)), - _ => Err(BuilderServerError::UnexpectedResponse), + _ => Err(BuilderError::UnexpectedResponse), } } @@ -116,7 +114,7 @@ impl BuilderServer for LocalBuilderHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugSetBundlingMode => Ok(()), - _ => Err(BuilderServerError::UnexpectedResponse), + _ => Err(BuilderError::UnexpectedResponse), } } } diff --git a/crates/builder/src/server/mod.rs b/crates/builder/src/server/mod.rs index 4193e3ad3..adf8664be 100644 --- a/crates/builder/src/server/mod.rs +++ b/crates/builder/src/server/mod.rs @@ -12,59 +12,8 @@ // If not, see https://www.gnu.org/licenses/. mod local; -mod remote; - -use async_trait::async_trait; -use ethers::types::{Address, H256}; pub use local::{LocalBuilderBuilder, LocalBuilderHandle}; -#[cfg(feature = "test-utils")] -use mockall::automock; -use parse_display::Display; + +mod remote; pub(crate) use remote::spawn_remote_builder_server; pub use remote::RemoteBuilderClient; -use serde::{Deserialize, Serialize}; - -/// Builder server errors -#[derive(Debug, thiserror::Error)] -pub enum BuilderServerError { - /// Builder returned an unexpected response type for the given request - #[error("Unexpected response from BuilderServer")] - UnexpectedResponse, - /// Internal errors - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -/// Builder server result -pub type BuilderResult = std::result::Result; - -/// Builder server -#[cfg_attr(feature = "test-utils", automock)] -#[async_trait] -pub trait BuilderServer: Send + Sync + 'static { - /// Get the supported entry points of this builder - async fn get_supported_entry_points(&self) -> BuilderResult>; - - /// Trigger the builder to send a bundle now, used for debugging. - /// - /// Bundling mode must be set to `Manual`, or this will error - async fn debug_send_bundle_now(&self) -> BuilderResult<(H256, u64)>; - - /// Set the bundling mode - async fn debug_set_bundling_mode(&self, mode: BundlingMode) -> BuilderResult<()>; -} - -/// Builder bundling mode -#[derive(Display, Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] -#[display(style = "lowercase")] -#[serde(rename_all = "lowercase")] -pub enum BundlingMode { - /// Manual bundling mode for debugging. - /// - /// Bundles will only be sent when `debug_send_bundle_now` is called. - Manual, - /// Auto bundling mode for normal operation. - /// - /// Bundles will be sent automatically. - Auto, -} diff --git a/crates/builder/src/server/remote/client.rs b/crates/builder/src/server/remote/client.rs index 34d23e372..494ae0a78 100644 --- a/crates/builder/src/server/remote/client.rs +++ b/crates/builder/src/server/remote/client.rs @@ -18,6 +18,7 @@ use rundler_task::{ grpc::protos::{from_bytes, ConversionError}, server::{HealthCheck, ServerStatus}, }; +use rundler_types::builder::{Builder, BuilderError, BuilderResult, BundlingMode}; use tonic::{ async_trait, transport::{Channel, Uri}, @@ -32,7 +33,6 @@ use super::protos::{ debug_set_bundling_mode_response, BundlingMode as ProtoBundlingMode, DebugSendBundleNowRequest, DebugSetBundlingModeRequest, GetSupportedEntryPointsRequest, }; -use crate::server::{BuilderResult, BuilderServer, BuilderServerError, BundlingMode}; /// Remote builder client, used for communicating with a remote builder server #[derive(Debug, Clone)] @@ -55,18 +55,20 @@ impl RemoteBuilderClient { } #[async_trait] -impl BuilderServer for RemoteBuilderClient { +impl Builder for RemoteBuilderClient { async fn get_supported_entry_points(&self) -> BuilderResult> { Ok(self .grpc_client .clone() .get_supported_entry_points(GetSupportedEntryPointsRequest {}) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .entry_points .into_iter() .map(|ep| from_bytes(ep.as_slice())) - .collect::>()?) + .collect::>() + .map_err(anyhow::Error::from)?) } async fn debug_send_bundle_now(&self) -> BuilderResult<(H256, u64)> { @@ -74,7 +76,8 @@ impl BuilderServer for RemoteBuilderClient { .grpc_client .clone() .debug_send_bundle_now(DebugSendBundleNowRequest {}) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -83,7 +86,7 @@ impl BuilderServer for RemoteBuilderClient { Ok((H256::from_slice(&s.transaction_hash), s.block_number)) } Some(debug_send_bundle_now_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(BuilderServerError::Other(anyhow::anyhow!( + None => Err(BuilderError::Other(anyhow::anyhow!( "should have received result from builder" )))?, } @@ -96,14 +99,15 @@ impl BuilderServer for RemoteBuilderClient { .debug_set_bundling_mode(DebugSetBundlingModeRequest { mode: ProtoBundlingMode::from(mode) as i32, }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(debug_set_bundling_mode_response::Result::Success(_)) => Ok(()), Some(debug_set_bundling_mode_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(BuilderServerError::Other(anyhow::anyhow!( + None => Err(BuilderError::Other(anyhow::anyhow!( "should have received result from builder" )))?, } diff --git a/crates/builder/src/server/remote/error.rs b/crates/builder/src/server/remote/error.rs index 0b49ea6aa..d2264fcb1 100644 --- a/crates/builder/src/server/remote/error.rs +++ b/crates/builder/src/server/remote/error.rs @@ -11,45 +11,30 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use rundler_task::grpc::protos::ConversionError; +use rundler_types::builder::BuilderError; use super::protos::{builder_error, BuilderError as ProtoBuilderError}; -use crate::server::BuilderServerError; -impl From for BuilderServerError { - fn from(value: tonic::Status) -> Self { - BuilderServerError::Other(anyhow::anyhow!(value.to_string())) - } -} - -impl From for BuilderServerError { - fn from(value: ConversionError) -> Self { - BuilderServerError::Other(anyhow::anyhow!(value.to_string())) - } -} - -impl TryFrom for BuilderServerError { +impl TryFrom for BuilderError { type Error = anyhow::Error; fn try_from(value: ProtoBuilderError) -> Result { match value.error { - Some(builder_error::Error::Internal(e)) => { - Ok(BuilderServerError::Other(anyhow::anyhow!(e))) - } - None => Ok(BuilderServerError::Other(anyhow::anyhow!("Unknown error"))), + Some(builder_error::Error::Internal(e)) => Ok(BuilderError::Other(anyhow::anyhow!(e))), + None => Ok(BuilderError::Other(anyhow::anyhow!("Unknown error"))), } } } -impl From for ProtoBuilderError { - fn from(value: BuilderServerError) -> Self { +impl From for ProtoBuilderError { + fn from(value: BuilderError) -> Self { match value { - BuilderServerError::UnexpectedResponse => ProtoBuilderError { + BuilderError::UnexpectedResponse => ProtoBuilderError { error: Some(builder_error::Error::Internal( "Unexpected response".to_string(), )), }, - BuilderServerError::Other(e) => ProtoBuilderError { + BuilderError::Other(e) => ProtoBuilderError { error: Some(builder_error::Error::Internal(e.to_string())), }, } diff --git a/crates/builder/src/server/remote/protos.rs b/crates/builder/src/server/remote/protos.rs index e3f4ae66a..35c100cb4 100644 --- a/crates/builder/src/server/remote/protos.rs +++ b/crates/builder/src/server/remote/protos.rs @@ -12,8 +12,7 @@ // If not, see https://www.gnu.org/licenses/. use rundler_task::grpc::protos::ConversionError; - -use crate::server::BundlingMode as RpcBundlingMode; +use rundler_types::builder::BundlingMode as RpcBundlingMode; tonic::include_proto!("builder"); @@ -40,15 +39,3 @@ impl TryFrom for RpcBundlingMode { } } } - -impl TryFrom for RpcBundlingMode { - type Error = ConversionError; - - fn try_from(status: i32) -> Result { - match status { - x if x == BundlingMode::Auto as i32 => Ok(Self::Auto), - x if x == BundlingMode::Manual as i32 => Ok(Self::Manual), - _ => Err(ConversionError::InvalidEnumValue(status)), - } - } -} diff --git a/crates/builder/src/server/remote/server.rs b/crates/builder/src/server/remote/server.rs index 6c25be149..a94259747 100644 --- a/crates/builder/src/server/remote/server.rs +++ b/crates/builder/src/server/remote/server.rs @@ -13,20 +13,19 @@ use std::net::SocketAddr; +use rundler_types::builder::Builder; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; use tonic::{async_trait, transport::Server, Request, Response, Status}; use super::protos::{ builder_server::{Builder as GrpcBuilder, BuilderServer as GrpcBuilderServer}, - debug_send_bundle_now_response, debug_set_bundling_mode_response, DebugSendBundleNowRequest, - DebugSendBundleNowResponse, DebugSetBundlingModeRequest, DebugSetBundlingModeResponse, - DebugSetBundlingModeSuccess, GetSupportedEntryPointsRequest, GetSupportedEntryPointsResponse, - BUILDER_FILE_DESCRIPTOR_SET, -}; -use crate::server::{ - local::LocalBuilderHandle, remote::protos::DebugSendBundleNowSuccess, BuilderServer, + debug_send_bundle_now_response, debug_set_bundling_mode_response, BundlingMode, + DebugSendBundleNowRequest, DebugSendBundleNowResponse, DebugSetBundlingModeRequest, + DebugSetBundlingModeResponse, DebugSetBundlingModeSuccess, GetSupportedEntryPointsRequest, + GetSupportedEntryPointsResponse, BUILDER_FILE_DESCRIPTOR_SET, }; +use crate::server::{local::LocalBuilderHandle, remote::protos::DebugSendBundleNowSuccess}; /// Spawn a remote builder server pub(crate) async fn spawn_remote_builder_server( @@ -122,13 +121,14 @@ impl GrpcBuilder for GrpcBuilderServerImpl { &self, request: Request, ) -> tonic::Result> { - let resp = match self - .local_builder - .debug_set_bundling_mode(request.into_inner().mode.try_into().map_err(|e| { - Status::internal(format!("Failed to convert from proto reputation {e}")) - })?) - .await - { + let mode = BundlingMode::try_from(request.into_inner().mode).map_err(|e| { + Status::internal(format!("Failed to convert from proto reputation {e}")) + })?; + let mode = mode.try_into().map_err(|e| { + Status::internal(format!("Failed to convert from proto reputation {e}")) + })?; + + let resp = match self.local_builder.debug_set_bundling_mode(mode).await { Ok(()) => DebugSetBundlingModeResponse { result: Some(debug_set_bundling_mode_response::Result::Success( DebugSetBundlingModeSuccess {}, diff --git a/crates/builder/src/task.rs b/crates/builder/src/task.rs index 7ad6e3def..80389ea2c 100644 --- a/crates/builder/src/task.rs +++ b/crates/builder/src/task.rs @@ -22,7 +22,6 @@ use ethers::{ use ethers_signers::Signer; use futures::future; use futures_util::TryFutureExt; -use rundler_pool::PoolServer; use rundler_provider::EthersEntryPointV0_6; use rundler_sim::{ simulation::v0_6::{ @@ -32,7 +31,7 @@ use rundler_sim::{ MempoolConfig, PriorityFeeMode, SimulationSettings, }; use rundler_task::Task; -use rundler_types::chain::ChainSpec; +use rundler_types::{chain::ChainSpec, pool::Pool}; use rundler_utils::{emit::WithEntryPoint, handle}; use rusoto_core::Region; use tokio::{ @@ -122,7 +121,7 @@ pub struct BuilderTask

{ #[async_trait] impl

Task for BuilderTask

where - P: PoolServer + Clone, + P: Pool + Clone, { async fn run(mut self: Box, shutdown_token: CancellationToken) -> anyhow::Result<()> { info!("Mempool config: {:?}", self.args.mempool_configs); @@ -190,7 +189,7 @@ where impl

BuilderTask

where - P: PoolServer + Clone, + P: Pool + Clone, { /// Create a new builder task pub fn new( diff --git a/crates/pool/Cargo.toml b/crates/pool/Cargo.toml index 192976810..85ee659ab 100644 --- a/crates/pool/Cargo.toml +++ b/crates/pool/Cargo.toml @@ -44,6 +44,3 @@ rundler-provider = { path = "../provider", features = ["test-utils"] } [build-dependencies] tonic-build.workspace = true - -[features] -test-utils = [ "mockall" ] diff --git a/crates/pool/src/lib.rs b/crates/pool/src/lib.rs index 2a44d2509..8dc63e071 100644 --- a/crates/pool/src/lib.rs +++ b/crates/pool/src/lib.rs @@ -25,17 +25,12 @@ mod emit; pub use emit::OpPoolEvent as PoolEvent; mod mempool; -pub use mempool::{ - FromPoolOperationVariant, IntoPoolOperationVariant, MempoolError, PoolConfig, PoolOperation, - Reputation, ReputationStatus, StakeStatus, -}; +pub use mempool::PoolConfig; mod server; #[cfg(feature = "test-utils")] pub use server::MockPoolServer; -pub use server::{ - LocalPoolBuilder, LocalPoolHandle, PoolResult, PoolServer, PoolServerError, RemotePoolClient, -}; +pub use server::{LocalPoolBuilder, LocalPoolHandle, RemotePoolClient}; mod task; pub use task::{Args as PoolTaskArgs, PoolTask}; diff --git a/crates/pool/src/mempool/error.rs b/crates/pool/src/mempool/error.rs deleted file mode 100644 index 69500459c..000000000 --- a/crates/pool/src/mempool/error.rs +++ /dev/null @@ -1,114 +0,0 @@ -// This file is part of Rundler. -// -// Rundler is free software: you can redistribute it and/or modify it under the -// terms of the GNU Lesser General Public License as published by the Free Software -// Foundation, either version 3 of the License, or (at your option) any later version. -// -// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -// See the GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along with Rundler. -// If not, see https://www.gnu.org/licenses/. - -use std::mem; - -use ethers::{abi::Address, types::U256}; -use rundler_sim::{ - PrecheckError, PrecheckViolation, SimulationError, SimulationViolation, ViolationError, -}; -use rundler_types::Entity; - -/// Mempool result type. -pub(crate) type MempoolResult = std::result::Result; - -/// Mempool error type. -#[derive(Debug, thiserror::Error)] -pub enum MempoolError { - /// Some other error occurred - #[error(transparent)] - Other(#[from] anyhow::Error), - /// Operation with the same hash already in pool - #[error("Operation already known")] - OperationAlreadyKnown, - /// Operation with same sender/nonce already in pool - /// and the replacement operation has lower gas price. - #[error("Replacement operation underpriced. Existing priority fee: {0}. Existing fee: {1}")] - ReplacementUnderpriced(U256, U256), - /// Max operations reached for unstaked sender [UREP-010] or unstaked non-sender entity [UREP-020] - #[error("Max operations ({0}) reached for entity {1}")] - MaxOperationsReached(usize, Address), - /// Multiple roles violation - /// Spec rule: STO-040 - #[error("A {} at {} in this UserOperation is used as a sender entity in another UserOperation currently in mempool.", .0.kind, .0.address)] - MultipleRolesViolation(Entity), - /// An associated storage slot that is accessed in the UserOperation is being used as a sender by another UserOperation in the mempool. - /// Spec rule: STO-041 - #[error("An associated storage slot that is accessed in the UserOperation is being used as a sender by another UserOperation in the mempool")] - AssociatedStorageIsAlternateSender, - /// Sender address used as different entity in another UserOperation currently in the mempool. - /// Spec rule: STO-040 - #[error("The sender address {0} is used as a different entity in another UserOperation currently in mempool")] - SenderAddressUsedAsAlternateEntity(Address), - /// An entity associated with the operation is throttled/banned. - #[error("Entity {0} is throttled/banned")] - EntityThrottled(Entity), - /// Operation was discarded on inserting due to size limit - #[error("Operation was discarded on inserting")] - DiscardedOnInsert, - /// Paymaster balance too low - /// Spec rule: EREP-010 - #[error("Paymaster balance too low. Required balance: {0}. Current balance {1}")] - PaymasterBalanceTooLow(U256, U256), - /// Operation was rejected due to a precheck violation - #[error("Operation violation during precheck {0}")] - PrecheckViolation(PrecheckViolation), - /// Operation was rejected due to a simulation violation - #[error("Operation violation during simulation {0}")] - SimulationViolation(SimulationViolation), - /// Operation was rejected because it used an unsupported aggregator - #[error("Unsupported aggregator {0}")] - UnsupportedAggregator(Address), - /// An unknown entry point was specified - #[error("Unknown entry point {0}")] - UnknownEntryPoint(Address), - /// The operation drop attempt too soon after being added to the pool - #[error("Operation drop attempt too soon after being added to the pool. Added at {0}, attempted to drop at {1}, must wait {2} blocks.")] - OperationDropTooSoon(u64, u64, u64), -} - -impl From for MempoolError { - fn from(mut error: SimulationError) -> Self { - let SimulationError { - violation_error, .. - } = &mut error; - let ViolationError::Violations(violations) = violation_error else { - return Self::Other((*violation_error).clone().into()); - }; - - let Some(violation) = violations.iter_mut().min() else { - return Self::Other((*violation_error).clone().into()); - }; - - // extract violation and replace with dummy - Self::SimulationViolation(mem::replace(violation, SimulationViolation::DidNotRevert)) - } -} - -impl From for MempoolError { - fn from(mut error: PrecheckError) -> Self { - let PrecheckError::Violations(violations) = &mut error else { - return Self::Other(error.into()); - }; - - let Some(violation) = violations.iter_mut().min() else { - return Self::Other(error.into()); - }; - - // extract violation and replace with dummy - Self::PrecheckViolation(mem::replace( - violation, - PrecheckViolation::SenderIsNotContractAndNoInitCode(Address::zero()), - )) - } -} diff --git a/crates/pool/src/mempool/mod.rs b/crates/pool/src/mempool/mod.rs index df2c80728..401e2e88e 100644 --- a/crates/pool/src/mempool/mod.rs +++ b/crates/pool/src/mempool/mod.rs @@ -11,15 +11,11 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -mod error; -pub use error::MempoolError; - mod entity_tracker; mod pool; mod reputation; pub(crate) use reputation::{AddressReputation, ReputationParams}; -pub use reputation::{Reputation, ReputationStatus}; mod size; @@ -32,20 +28,23 @@ use std::{ sync::Arc, }; -use ethers::types::{Address, H256, U256}; +use ethers::types::{Address, H256}; #[cfg(test)] use mockall::automock; -use rundler_sim::{EntityInfos, MempoolConfig, PrecheckSettings, SimulationSettings}; +use rundler_sim::{MempoolConfig, PrecheckSettings, SimulationSettings}; use rundler_types::{ - Entity, EntityType, EntityUpdate, UserOperation, UserOperationId, UserOperationVariant, - ValidTimeRange, + pool::{ + MempoolError, PaymasterMetadata, PoolOperation, Reputation, ReputationStatus, StakeStatus, + }, + EntityUpdate, UserOperation, UserOperationId, }; use tonic::async_trait; pub(crate) use uo_pool::UoPool; -use self::error::MempoolResult; use super::chain::ChainUpdate; +pub(crate) type MempoolResult = std::result::Result; + #[cfg_attr(test, automock(type UO = rundler_types::v0_6::UserOperation;))] #[async_trait] /// In-memory operation pool @@ -161,23 +160,6 @@ pub struct PoolConfig { pub drop_min_num_blocks: u64, } -/// Stake status structure -#[derive(Debug, Clone, Copy)] -pub struct StakeStatus { - /// Address is staked - pub is_staked: bool, - /// Stake information about address - pub stake_info: StakeInfo, -} - -#[derive(Debug, Clone, Copy)] -pub struct StakeInfo { - /// Stake amount - pub stake: u128, - /// Unstake delay in seconds - pub unstake_delay_sec: u32, -} - /// Origin of an operation. #[derive(Debug, Clone, Copy)] #[allow(dead_code)] // TODO(danc): remove once implemented @@ -191,162 +173,9 @@ pub enum OperationOrigin { ReturnedAfterReorg, } -/// A user operation with additional metadata from validation. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct PoolOperation { - /// The user operation stored in the pool - pub uo: UO, - /// The entry point address for this operation - pub entry_point: Address, - /// The aggregator address for this operation, if any. - pub aggregator: Option

, - /// The valid time range for this operation. - pub valid_time_range: ValidTimeRange, - /// The expected code hash for all contracts accessed during validation for this operation. - pub expected_code_hash: H256, - /// The block hash simulation was completed at - pub sim_block_hash: H256, - /// The block number simulation was completed at - pub sim_block_number: u64, - /// List of entities that need to stake for this operation. - pub entities_needing_stake: Vec, - /// Whether the account is staked. - pub account_is_staked: bool, - /// Staking information about all the entities. - pub entity_infos: EntityInfos, -} - -#[derive(Debug, Default, Clone, Eq, PartialEq, Copy)] -pub struct PaymasterMetadata { - /// Paymaster address - pub address: Address, - /// The on-chain balance of the paymaster - pub confirmed_balance: U256, - /// The pending balance is the confirm balance subtracted by - /// the max cost of all the pending user operations that use the paymaster - pub pending_balance: U256, -} - -impl PoolOperation { - /// Returns true if the operation contains the given entity. - pub fn contains_entity(&self, entity: &Entity) -> bool { - if let Some(e) = self.entity_infos.get(entity.kind) { - e.address == entity.address - } else { - false - } - } - - /// Returns true if the operation requires the given entity to stake. - /// - /// For non-accounts, its possible that the entity is staked, but doesn't - /// _need_ to stake for this operation. For example, if the operation does not - /// access any storage slots that require staking. In that case this function - /// will return false. - /// - /// For staked accounts, this function will always return true. Staked accounts - /// are able to circumvent the mempool operation limits always need their reputation - /// checked to prevent them from filling the pool. - pub fn requires_stake(&self, entity: EntityType) -> bool { - match entity { - EntityType::Account => self.account_is_staked, - _ => self.entities_needing_stake.contains(&entity), - } - } - - /// Returns an iterator over all entities that are included in this operation. - pub fn entities(&'_ self) -> impl Iterator + '_ { - self.entity_infos - .entities() - .map(|(t, entity)| Entity::new(t, entity.address)) - } - - /// Returns an iterator over all entities that need stake in this operation. This can be a subset of entities that are staked in the operation. - pub fn entities_requiring_stake(&'_ self) -> impl Iterator + '_ { - self.entity_infos.entities().filter_map(|(t, entity)| { - if self.requires_stake(t) { - Entity::new(t, entity.address).into() - } else { - None - } - }) - } - - /// Return all the unstaked entities that are used in this operation. - pub fn unstaked_entities(&'_ self) -> impl Iterator + '_ { - self.entity_infos.entities().filter_map(|(t, entity)| { - if entity.is_staked { - None - } else { - Entity::new(t, entity.address).into() - } - }) - } - - /// Compute the amount of heap memory the PoolOperation takes up. - pub fn mem_size(&self) -> usize { - std::mem::size_of::() - + self.uo.heap_size() - + self.entities_needing_stake.len() * std::mem::size_of::() - } -} - -/// Trait to convert a [PoolOperation] holding a [UserOperationVariant] to a [PoolOperation] with a different user operation type. -pub trait FromPoolOperationVariant { - /// Conversion - fn from_variant(op: PoolOperation) -> Self; -} - -/// Trait to convert a [PoolOperation] holding a user operation to a [PoolOperation] with a [UserOperationVariant]. -pub trait IntoPoolOperationVariant { - /// Conversion - fn into_variant(self) -> PoolOperation; -} - -impl FromPoolOperationVariant for PoolOperation -where - UO: UserOperation + From, -{ - fn from_variant(op: PoolOperation) -> Self { - PoolOperation { - uo: op.uo.into(), - entry_point: op.entry_point, - aggregator: op.aggregator, - valid_time_range: op.valid_time_range, - expected_code_hash: op.expected_code_hash, - sim_block_hash: op.sim_block_hash, - sim_block_number: op.sim_block_number, - entities_needing_stake: op.entities_needing_stake, - account_is_staked: op.account_is_staked, - entity_infos: op.entity_infos, - } - } -} - -impl IntoPoolOperationVariant for PoolOperation -where - UO: UserOperation + Into, -{ - fn into_variant(self) -> PoolOperation { - PoolOperation { - uo: self.uo.into(), - entry_point: self.entry_point, - aggregator: self.aggregator, - valid_time_range: self.valid_time_range, - expected_code_hash: self.expected_code_hash, - sim_block_hash: self.sim_block_hash, - sim_block_number: self.sim_block_number, - entities_needing_stake: self.entities_needing_stake, - account_is_staked: self.account_is_staked, - entity_infos: self.entity_infos, - } - } -} - #[cfg(test)] mod tests { - use rundler_sim::EntityInfo; - use rundler_types::v0_6::UserOperation; + use rundler_types::{v0_6::UserOperation, EntityInfo, EntityInfos, EntityType, ValidTimeRange}; use super::*; diff --git a/crates/pool/src/mempool/paymaster.rs b/crates/pool/src/mempool/paymaster.rs index f1e3fee97..5fda21486 100644 --- a/crates/pool/src/mempool/paymaster.rs +++ b/crates/pool/src/mempool/paymaster.rs @@ -19,14 +19,14 @@ use anyhow::Context; use ethers::{abi::Address, types::U256}; use parking_lot::RwLock; use rundler_provider::EntryPoint; -use rundler_types::{UserOperation, UserOperationId}; +use rundler_types::{ + pool::{MempoolError, PaymasterMetadata, PoolOperation, StakeStatus}, + StakeInfo, UserOperation, UserOperationId, +}; use rundler_utils::cache::LruMap; -use super::{error::MempoolResult, PaymasterMetadata, StakeInfo}; -use crate::{ - chain::{BalanceUpdate, MinedOp}, - MempoolError, PoolOperation, StakeStatus, -}; +use super::MempoolResult; +use crate::chain::{BalanceUpdate, MinedOp}; /// Keeps track of current and pending paymaster balances #[derive(Debug)] @@ -86,8 +86,8 @@ where let stake_status = StakeStatus { stake_info: StakeInfo { - stake: deposit_info.stake, - unstake_delay_sec: deposit_info.unstake_delay_sec, + stake: deposit_info.stake.into(), + unstake_delay_sec: deposit_info.unstake_delay_sec.into(), }, is_staked, }; @@ -530,11 +530,7 @@ mod tests { }; use super::*; - use crate::{ - chain::BalanceUpdate, - mempool::{paymaster::PaymasterTracker, PaymasterMetadata}, - PoolOperation, - }; + use crate::{chain::BalanceUpdate, mempool::paymaster::PaymasterTracker}; fn demo_pool_op(uo: UserOperation) -> PoolOperation { PoolOperation { diff --git a/crates/pool/src/mempool/pool.rs b/crates/pool/src/mempool/pool.rs index 5bfca439d..87095ab0e 100644 --- a/crates/pool/src/mempool/pool.rs +++ b/crates/pool/src/mempool/pool.rs @@ -22,16 +22,14 @@ use ethers::{ abi::Address, types::{H256, U256}, }; -use rundler_types::{Entity, EntityType, Timestamp, UserOperation, UserOperationId}; +use rundler_types::{ + pool::{MempoolError, PoolOperation}, + Entity, EntityType, Timestamp, UserOperation, UserOperationId, +}; use rundler_utils::math; use tracing::info; -use super::{ - entity_tracker::EntityCounter, - error::{MempoolError, MempoolResult}, - size::SizeTracker, - PoolConfig, PoolOperation, -}; +use super::{entity_tracker::EntityCounter, size::SizeTracker, MempoolResult, PoolConfig}; use crate::chain::MinedOp; #[derive(Debug, Clone)] @@ -545,8 +543,10 @@ impl PoolMetrics { #[cfg(test)] mod tests { - use rundler_sim::{EntityInfo, EntityInfos}; - use rundler_types::{v0_6::UserOperation, UserOperation as UserOperationTrait, ValidTimeRange}; + use rundler_types::{ + v0_6::UserOperation, EntityInfo, EntityInfos, UserOperation as UserOperationTrait, + ValidTimeRange, + }; use super::*; diff --git a/crates/pool/src/mempool/reputation.rs b/crates/pool/src/mempool/reputation.rs index 87e79ab60..aa81a0d28 100644 --- a/crates/pool/src/mempool/reputation.rs +++ b/crates/pool/src/mempool/reputation.rs @@ -18,59 +18,9 @@ use std::{ use ethers::types::Address; use parking_lot::RwLock; -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use rundler_types::pool::{Reputation, ReputationStatus}; use tokio::time::interval; -/// Reputation status for an entity -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum ReputationStatus { - /// Entity is not throttled or banned - Ok, - /// Entity is throttled - Throttled, - /// Entity is banned - Banned, -} - -impl Serialize for ReputationStatus { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match self { - ReputationStatus::Ok => serializer.serialize_str("ok"), - ReputationStatus::Throttled => serializer.serialize_str("throttled"), - ReputationStatus::Banned => serializer.serialize_str("banned"), - } - } -} - -impl<'de> Deserialize<'de> for ReputationStatus { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - match s.as_str() { - "ok" => Ok(ReputationStatus::Ok), - "throttled" => Ok(ReputationStatus::Throttled), - "banned" => Ok(ReputationStatus::Banned), - _ => Err(de::Error::custom(format!("Invalid reputation status {s}"))), - } - } -} - -/// The reputation of an entity -#[derive(Debug, Clone)] -pub struct Reputation { - /// The entity's address - pub address: Address, - /// Number of ops seen in the current interval - pub ops_seen: u64, - /// Number of ops included in the current interval - pub ops_included: u64, -} - #[derive(Debug, Clone, Copy)] pub(crate) struct ReputationParams { bundle_invalidation_ops_seen_staked_penalty: u64, diff --git a/crates/pool/src/mempool/uo_pool.rs b/crates/pool/src/mempool/uo_pool.rs index 20c36705b..197652bc2 100644 --- a/crates/pool/src/mempool/uo_pool.rs +++ b/crates/pool/src/mempool/uo_pool.rs @@ -22,6 +22,9 @@ use parking_lot::RwLock; use rundler_provider::EntryPoint; use rundler_sim::{Prechecker, Simulator}; use rundler_types::{ + pool::{ + MempoolError, PaymasterMetadata, PoolOperation, Reputation, ReputationStatus, StakeStatus, + }, Entity, EntityUpdate, EntityUpdateType, UserOperation, UserOperationId, UserOperationVariant, }; use rundler_utils::emit::WithEntryPoint; @@ -30,16 +33,12 @@ use tonic::async_trait; use tracing::info; use super::{ - error::{MempoolError, MempoolResult}, - paymaster::PaymasterTracker, - pool::PoolInner, - reputation::{AddressReputation, Reputation, ReputationStatus}, - Mempool, OperationOrigin, PaymasterMetadata, PoolConfig, PoolOperation, + paymaster::PaymasterTracker, pool::PoolInner, reputation::AddressReputation, Mempool, + MempoolResult, OperationOrigin, PoolConfig, }; use crate::{ chain::ChainUpdate, emit::{EntityReputation, EntityStatus, EntitySummary, OpPoolEvent, OpRemovalReason}, - StakeStatus, }; /// User Operation Mempool @@ -673,13 +672,15 @@ mod tests { use ethers::types::{Bytes, H160}; use rundler_provider::MockEntryPointV0_6; use rundler_sim::{ - EntityInfo, EntityInfos, MockPrechecker, MockSimulator, PrecheckError, PrecheckSettings, - PrecheckViolation, SimulationError, SimulationResult, SimulationSettings, - SimulationViolation, ViolationError, + MockPrechecker, MockSimulator, PrecheckError, PrecheckSettings, SimulationError, + SimulationResult, SimulationSettings, ViolationError, }; use rundler_types::{ - contracts::v0_6::verifying_paymaster::DepositInfo, v0_6::UserOperation, EntityType, - GasFees, UserOperation as UserOperationTrait, ValidTimeRange, + contracts::v0_6::verifying_paymaster::DepositInfo, + pool::{PrecheckViolation, SimulationViolation}, + v0_6::UserOperation, + EntityInfo, EntityInfos, EntityType, GasFees, UserOperation as UserOperationTrait, + ValidTimeRange, }; use super::*; diff --git a/crates/pool/src/server/local.rs b/crates/pool/src/server/local.rs index ef14abf3b..d68807a47 100644 --- a/crates/pool/src/server/local.rs +++ b/crates/pool/src/server/local.rs @@ -19,7 +19,13 @@ use ethers::types::{Address, H256}; use futures::future; use futures_util::Stream; use rundler_task::server::{HealthCheck, ServerStatus}; -use rundler_types::{v0_6, EntityUpdate, UserOperationId, UserOperationVariant}; +use rundler_types::{ + pool::{ + IntoPoolOperationVariant, MempoolError, NewHead, PaymasterMetadata, Pool, PoolError, + PoolOperation, PoolResult, Reputation, ReputationStatus, StakeStatus, + }, + v0_6, EntityUpdate, UserOperationId, UserOperationVariant, +}; use tokio::{ sync::{broadcast, mpsc, oneshot}, task::JoinHandle, @@ -27,15 +33,9 @@ use tokio::{ use tokio_util::sync::CancellationToken; use tracing::error; -use super::{PoolResult, PoolServerError}; use crate::{ chain::ChainUpdate, - mempool::{ - IntoPoolOperationVariant, Mempool, MempoolError, OperationOrigin, PaymasterMetadata, - PoolOperation, StakeStatus, - }, - server::{NewHead, PoolServer, Reputation}, - ReputationStatus, + mempool::{Mempool, OperationOrigin}, }; /// Local pool server builder @@ -116,13 +116,13 @@ impl LocalPoolHandle { } #[async_trait] -impl PoolServer for LocalPoolHandle { +impl Pool for LocalPoolHandle { async fn get_supported_entry_points(&self) -> PoolResult> { let req = ServerRequestKind::GetSupportedEntryPoints; let resp = self.send(req).await?; match resp { ServerResponse::GetSupportedEntryPoints { entry_points } => Ok(entry_points), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -135,7 +135,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::AddOp { hash } => Ok(hash), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -153,7 +153,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::GetOps { ops } => Ok(ops), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -165,7 +165,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::GetOpByHash { op } => Ok(op), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -174,7 +174,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::RemoveOps => Ok(()), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -187,7 +187,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::RemoveOpById { hash } => Ok(hash), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -203,7 +203,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::UpdateEntities => Ok(()), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -212,7 +212,7 @@ impl PoolServer for LocalPoolHandle { clear_mempool: bool, clear_paymaster: bool, clear_reputation: bool, - ) -> Result<(), PoolServerError> { + ) -> Result<(), PoolError> { let req = ServerRequestKind::DebugClearState { clear_mempool, clear_reputation, @@ -221,7 +221,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugClearState => Ok(()), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -230,7 +230,7 @@ impl PoolServer for LocalPoolHandle { entry_point: Address, paymaster: bool, reputation: bool, - ) -> Result<(), PoolServerError> { + ) -> Result<(), PoolError> { let req = ServerRequestKind::AdminSetTracking { entry_point, paymaster, @@ -239,7 +239,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::AdminSetTracking => Ok(()), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -251,7 +251,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugDumpMempool { ops } => Ok(ops), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -267,7 +267,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugSetReputations => Ok(()), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -276,7 +276,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugDumpReputation { reputations } => Ok(reputations), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -288,7 +288,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::DebugDumpPaymasterBalances { balances } => Ok(balances), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -304,7 +304,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::GetStakeStatus { status } => Ok(status), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -320,7 +320,7 @@ impl PoolServer for LocalPoolHandle { let resp = self.send(req).await?; match resp { ServerResponse::GetReputationStatus { status } => Ok(status), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } @@ -342,7 +342,7 @@ impl PoolServer for LocalPoolHandle { } } })), - _ => Err(PoolServerError::UnexpectedResponse), + _ => Err(PoolError::UnexpectedResponse), } } } @@ -381,9 +381,9 @@ where } fn get_pool(&self, entry_point: Address) -> PoolResult<&Arc> { - self.mempools.get(&entry_point).ok_or_else(|| { - PoolServerError::MempoolError(MempoolError::UnknownEntryPoint(entry_point)) - }) + self.mempools + .get(&entry_point) + .ok_or_else(|| PoolError::MempoolError(MempoolError::UnknownEntryPoint(entry_point))) } fn get_ops( @@ -511,10 +511,10 @@ where fn get_pool_and_spawn( &self, entry_point: Address, - response: oneshot::Sender>, + response: oneshot::Sender>, f: F, ) where - F: FnOnce(Arc, oneshot::Sender>) -> Fut, + F: FnOnce(Arc, oneshot::Sender>) -> Fut, Fut: Future + Send + 'static, { match self.get_pool(entry_point) { @@ -564,7 +564,7 @@ where // Async methods // Responses are sent in the spawned task ServerRequestKind::AddOp { entry_point, op, origin } => { - let fut = |mempool: Arc, response: oneshot::Sender>| async move { + let fut = |mempool: Arc, response: oneshot::Sender>| async move { let resp = match mempool.add_operation(origin, op.into()).await { Ok(hash) => Ok(ServerResponse::AddOp { hash }), Err(e) => Err(e.into()), @@ -578,7 +578,7 @@ where continue; }, ServerRequestKind::GetStakeStatus { entry_point, address }=> { - let fut = |mempool: Arc, response: oneshot::Sender>| async move { + let fut = |mempool: Arc, response: oneshot::Sender>| async move { let resp = match mempool.get_stake_status(address).await { Ok(status) => Ok(ServerResponse::GetStakeStatus { status }), Err(e) => Err(e.into()), diff --git a/crates/pool/src/server/mod.rs b/crates/pool/src/server/mod.rs index a016bc669..2cf32bef2 100644 --- a/crates/pool/src/server/mod.rs +++ b/crates/pool/src/server/mod.rs @@ -11,144 +11,9 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -mod error; mod local; -mod remote; - -use std::pin::Pin; - -use async_trait::async_trait; -pub use error::PoolServerError; -use ethers::types::{Address, H256}; -use futures_util::Stream; pub use local::{LocalPoolBuilder, LocalPoolHandle}; -#[cfg(feature = "test-utils")] -use mockall::automock; + +mod remote; pub(crate) use remote::spawn_remote_mempool_server; pub use remote::RemotePoolClient; -use rundler_types::{EntityUpdate, UserOperationId, UserOperationVariant}; - -use crate::{ - mempool::{PaymasterMetadata, PoolOperation, Reputation, StakeStatus}, - ReputationStatus, -}; - -/// Result type for pool server operations. -pub type PoolResult = std::result::Result; - -#[derive(Clone, Debug)] -pub struct NewHead { - pub block_hash: H256, - pub block_number: u64, -} - -impl Default for NewHead { - fn default() -> NewHead { - NewHead { - block_hash: H256::zero(), - block_number: 0, - } - } -} - -/// Pool server trait -#[cfg_attr(feature = "test-utils", automock)] -#[async_trait] -pub trait PoolServer: Send + Sync + 'static { - /// Get the supported entry points of the pool - async fn get_supported_entry_points(&self) -> PoolResult>; - - /// Add an operation to the pool - async fn add_op(&self, entry_point: Address, op: UserOperationVariant) -> PoolResult; - - /// Get operations from the pool - async fn get_ops( - &self, - entry_point: Address, - max_ops: u64, - shard_index: u64, - ) -> PoolResult>>; - - /// Get an operation from the pool by hash - /// Checks each entry point in order until the operation is found - /// Returns None if the operation is not found - async fn get_op_by_hash( - &self, - hash: H256, - ) -> PoolResult>>; - - /// Remove operations from the pool by hash - async fn remove_ops(&self, entry_point: Address, ops: Vec) -> PoolResult<()>; - - /// Remove an operation from the pool by id - async fn remove_op_by_id( - &self, - entry_point: Address, - id: UserOperationId, - ) -> PoolResult>; - - /// Update operations associated with entities from the pool - async fn update_entities( - &self, - entry_point: Address, - entities: Vec, - ) -> PoolResult<()>; - - /// Subscribe to new chain heads from the pool. - /// - /// The pool will notify the subscriber when a new chain head is received, and the pool - /// has processed all operations up to that head. - async fn subscribe_new_heads(&self) -> PoolResult + Send>>>; - - /// Get reputation status given entrypoint and address - async fn get_reputation_status( - &self, - entry_point: Address, - address: Address, - ) -> PoolResult; - - /// Get stake status given entrypoint and address - async fn get_stake_status( - &self, - entry_point: Address, - address: Address, - ) -> PoolResult; - - /// Clear the pool state, used for debug methods - async fn debug_clear_state( - &self, - clear_mempool: bool, - clear_paymaster: bool, - clear_reputation: bool, - ) -> PoolResult<()>; - - /// Dump all operations in the pool, used for debug methods - async fn debug_dump_mempool( - &self, - entry_point: Address, - ) -> PoolResult>>; - - /// Set reputations for entities, used for debug methods - async fn debug_set_reputations( - &self, - entry_point: Address, - reputations: Vec, - ) -> PoolResult<()>; - - /// Dump reputations for entities, used for debug methods - async fn debug_dump_reputation(&self, entry_point: Address) -> PoolResult>; - - /// Dump paymaster balances, used for debug methods - async fn debug_dump_paymaster_balances( - &self, - entry_point: Address, - ) -> PoolResult>; - - /// Controls whether or not the certain tracking data structures are used to block user operations - async fn admin_set_tracking( - &self, - entry_point: Address, - paymaster: bool, - reputation: bool, - ) -> PoolResult<()>; -} diff --git a/crates/pool/src/server/remote/client.rs b/crates/pool/src/server/remote/client.rs index a00c55d48..11e3b324a 100644 --- a/crates/pool/src/server/remote/client.rs +++ b/crates/pool/src/server/remote/client.rs @@ -19,7 +19,13 @@ use rundler_task::{ grpc::protos::{from_bytes, to_le_bytes, ConversionError}, server::{HealthCheck, ServerStatus}, }; -use rundler_types::{EntityUpdate, UserOperationId, UserOperationVariant}; +use rundler_types::{ + pool::{ + NewHead, PaymasterMetadata, Pool, PoolError, PoolOperation, PoolResult, Reputation, + ReputationStatus, StakeStatus, + }, + EntityUpdate, UserOperationId, UserOperationVariant, +}; use rundler_utils::retry::{self, UnlimitedRetryOpts}; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -41,12 +47,8 @@ use super::protos::{ update_entities_response, AddOpRequest, AdminSetTrackingRequest, DebugClearStateRequest, DebugDumpMempoolRequest, DebugDumpPaymasterBalancesRequest, DebugDumpReputationRequest, DebugSetReputationRequest, GetOpsRequest, GetReputationStatusRequest, GetStakeStatusRequest, - RemoveOpsRequest, SubscribeNewHeadsRequest, SubscribeNewHeadsResponse, UpdateEntitiesRequest, -}; -use crate::{ - mempool::{PaymasterMetadata, PoolOperation, Reputation, StakeStatus}, - server::{error::PoolServerError, NewHead, PoolResult, PoolServer}, - ReputationStatus, + RemoveOpsRequest, ReputationStatus as ProtoReputationStatus, SubscribeNewHeadsRequest, + SubscribeNewHeadsResponse, UpdateEntitiesRequest, }; /// Remote pool client @@ -123,18 +125,20 @@ impl RemotePoolClient { } #[async_trait] -impl PoolServer for RemotePoolClient { +impl Pool for RemotePoolClient { async fn get_supported_entry_points(&self) -> PoolResult> { Ok(self .op_pool_client .clone() .get_supported_entry_points(protos::GetSupportedEntryPointsRequest {}) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .entry_points .into_iter() .map(|ep| from_bytes(ep.as_slice())) - .collect::>()?) + .collect::>() + .map_err(anyhow::Error::from)?) } async fn add_op(&self, entry_point: Address, op: UserOperationVariant) -> PoolResult { @@ -145,14 +149,15 @@ impl PoolServer for RemotePoolClient { entry_point: entry_point.as_bytes().to_vec(), op: Some(protos::UserOperation::from(&op)), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(add_op_response::Result::Success(s)) => Ok(H256::from_slice(&s.hash)), Some(add_op_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -172,7 +177,8 @@ impl PoolServer for RemotePoolClient { max_ops, shard_index, }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -181,10 +187,10 @@ impl PoolServer for RemotePoolClient { .ops .into_iter() .map(PoolOperation::try_from) - .map(|res| res.map_err(PoolServerError::from)) + .map(|res| res.map_err(PoolError::from)) .collect(), Some(get_ops_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -200,7 +206,8 @@ impl PoolServer for RemotePoolClient { .get_op_by_hash(protos::GetOpByHashRequest { hash: hash.as_bytes().to_vec(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -210,11 +217,11 @@ impl PoolServer for RemotePoolClient { } Some(get_op_by_hash_response::Result::Failure(e)) => match e.error { Some(_) => Err(e.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received error from op pool" )))?, }, - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -228,14 +235,15 @@ impl PoolServer for RemotePoolClient { entry_point: entry_point.as_bytes().to_vec(), hashes: ops.into_iter().map(|h| h.as_bytes().to_vec()).collect(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(remove_ops_response::Result::Success(_)) => Ok(()), Some(remove_ops_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -254,7 +262,8 @@ impl PoolServer for RemotePoolClient { sender: id.sender.as_bytes().to_vec(), nonce: to_le_bytes(id.nonce), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -267,7 +276,7 @@ impl PoolServer for RemotePoolClient { } } Some(remove_op_by_id_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -288,14 +297,15 @@ impl PoolServer for RemotePoolClient { .map(protos::EntityUpdate::from) .collect(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(update_entities_response::Result::Success(_)) => Ok(()), Some(update_entities_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -315,14 +325,15 @@ impl PoolServer for RemotePoolClient { clear_paymaster, clear_reputation, }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(debug_clear_state_response::Result::Success(_)) => Ok(()), Some(debug_clear_state_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -342,14 +353,15 @@ impl PoolServer for RemotePoolClient { reputation, paymaster, }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(admin_set_tracking_response::Result::Success(_)) => Ok(()), Some(admin_set_tracking_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -365,7 +377,8 @@ impl PoolServer for RemotePoolClient { .debug_dump_mempool(DebugDumpMempoolRequest { entry_point: entry_point.as_bytes().to_vec(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -374,10 +387,10 @@ impl PoolServer for RemotePoolClient { .ops .into_iter() .map(PoolOperation::try_from) - .map(|res| res.map_err(PoolServerError::from)) + .map(|res| res.map_err(PoolError::from)) .collect(), Some(debug_dump_mempool_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -398,14 +411,15 @@ impl PoolServer for RemotePoolClient { .map(protos::Reputation::from) .collect(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(debug_set_reputation_response::Result::Success(_)) => Ok(()), Some(debug_set_reputation_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -418,7 +432,8 @@ impl PoolServer for RemotePoolClient { .debug_dump_reputation(DebugDumpReputationRequest { entry_point: entry_point.as_bytes().to_vec(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -427,10 +442,10 @@ impl PoolServer for RemotePoolClient { .reputations .into_iter() .map(Reputation::try_from) - .map(|res| res.map_err(PoolServerError::from)) + .map(|res| res.map_err(anyhow::Error::from).map_err(PoolError::from)) .collect(), Some(debug_dump_reputation_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -446,7 +461,8 @@ impl PoolServer for RemotePoolClient { .debug_dump_paymaster_balances(DebugDumpPaymasterBalancesRequest { entry_point: entry_point.as_bytes().to_vec(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -455,10 +471,10 @@ impl PoolServer for RemotePoolClient { .balances .into_iter() .map(PaymasterMetadata::try_from) - .map(|res| res.map_err(PoolServerError::from)) + .map(|res| res.map_err(anyhow::Error::from).map_err(PoolError::from)) .collect(), Some(debug_dump_paymaster_balances_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -476,16 +492,20 @@ impl PoolServer for RemotePoolClient { entry_point: entry_point.as_bytes().to_vec(), address: address.as_bytes().to_vec(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; match res { Some(get_reputation_status_response::Result::Success(s)) => { - Ok(ReputationStatus::try_from(s.status)?) + Ok(ProtoReputationStatus::try_from(s.status) + .map_err(anyhow::Error::from)? + .try_into() + .map_err(anyhow::Error::from)?) } Some(get_reputation_status_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } @@ -503,7 +523,8 @@ impl PoolServer for RemotePoolClient { entry_point: entry_point.as_bytes().to_vec(), address: address.as_bytes().to_vec(), }) - .await? + .await + .map_err(anyhow::Error::from)? .into_inner() .result; @@ -512,7 +533,7 @@ impl PoolServer for RemotePoolClient { Ok(s.status.unwrap_or_default().try_into()?) } Some(get_stake_status_response::Result::Failure(f)) => Err(f.try_into()?), - None => Err(PoolServerError::Other(anyhow::anyhow!( + None => Err(PoolError::Other(anyhow::anyhow!( "should have received result from op pool" )))?, } diff --git a/crates/pool/src/server/remote/error.rs b/crates/pool/src/server/remote/error.rs index 5f51faacd..d9eb975f4 100644 --- a/crates/pool/src/server/remote/error.rs +++ b/crates/pool/src/server/remote/error.rs @@ -13,9 +13,13 @@ use anyhow::{bail, Context}; use ethers::types::Opcode; -use rundler_sim::{NeedsStakeInformation, PrecheckViolation, SimulationViolation, ViolationOpCode}; -use rundler_task::grpc::protos::{from_bytes, to_le_bytes, ConversionError}; -use rundler_types::StorageSlot; +use rundler_task::grpc::protos::{from_bytes, to_le_bytes}; +use rundler_types::{ + pool::{ + MempoolError, NeedsStakeInformation, PoolError, PrecheckViolation, SimulationViolation, + }, + StorageSlot, ViolationOpCode, +}; use super::protos::{ mempool_error, precheck_violation_error, simulation_violation_error, @@ -35,38 +39,25 @@ use super::protos::{ UsedForbiddenPrecompile, VerificationGasLimitBufferTooLow, VerificationGasLimitTooHigh, WrongNumberOfPhases, }; -use crate::{mempool::MempoolError, server::error::PoolServerError}; - -impl From for PoolServerError { - fn from(value: tonic::Status) -> Self { - PoolServerError::Other(anyhow::anyhow!(value.to_string())) - } -} - -impl From for PoolServerError { - fn from(value: ConversionError) -> Self { - PoolServerError::Other(anyhow::anyhow!(value.to_string())) - } -} -impl TryFrom for PoolServerError { +impl TryFrom for PoolError { type Error = anyhow::Error; fn try_from(value: ProtoMempoolError) -> Result { - Ok(PoolServerError::MempoolError(value.try_into()?)) + Ok(PoolError::MempoolError(value.try_into()?)) } } -impl From for ProtoMempoolError { - fn from(value: PoolServerError) -> Self { +impl From for ProtoMempoolError { + fn from(value: PoolError) -> Self { match value { - PoolServerError::MempoolError(e) => e.into(), - PoolServerError::UnexpectedResponse => ProtoMempoolError { + PoolError::MempoolError(e) => e.into(), + PoolError::UnexpectedResponse => ProtoMempoolError { error: Some(mempool_error::Error::Internal( "unexpected response from pool server".to_string(), )), }, - PoolServerError::Other(e) => ProtoMempoolError { + PoolError::Other(e) => ProtoMempoolError { error: Some(mempool_error::Error::Internal(e.to_string())), }, } diff --git a/crates/pool/src/server/remote/protos.rs b/crates/pool/src/server/remote/protos.rs index e4966cecd..f822b5d49 100644 --- a/crates/pool/src/server/remote/protos.rs +++ b/crates/pool/src/server/remote/protos.rs @@ -15,18 +15,14 @@ use anyhow::{anyhow, Context}; use ethers::types::{Address, H256}; use rundler_task::grpc::protos::{from_bytes, to_le_bytes, ConversionError}; use rundler_types::{ - v0_6, Entity as RundlerEntity, EntityType as RundlerEntityType, - EntityUpdate as RundlerEntityUpdate, EntityUpdateType as RundlerEntityUpdateType, - UserOperationVariant, ValidTimeRange, -}; - -use crate::{ - mempool::{ - PaymasterMetadata as PoolPaymasterMetadata, PoolOperation, Reputation as PoolReputation, - ReputationStatus as PoolReputationStatus, StakeInfo as RundlerStakeInfo, + pool::{ + NewHead as PoolNewHead, PaymasterMetadata as PoolPaymasterMetadata, PoolOperation, + Reputation as PoolReputation, ReputationStatus as PoolReputationStatus, StakeStatus as RundlerStakeStatus, }, - server::NewHead as PoolNewHead, + v0_6, Entity as RundlerEntity, EntityInfos, EntityType as RundlerEntityType, + EntityUpdate as RundlerEntityUpdate, EntityUpdateType as RundlerEntityUpdateType, + StakeInfo as RundlerStakeInfo, UserOperationVariant, ValidTimeRange, }; tonic::include_proto!("op_pool"); @@ -209,6 +205,19 @@ impl From for ReputationStatus { } } +impl TryFrom for PoolReputationStatus { + type Error = ConversionError; + + fn try_from(status: ReputationStatus) -> Result { + match status { + ReputationStatus::Ok => Ok(PoolReputationStatus::Ok), + ReputationStatus::Throttled => Ok(PoolReputationStatus::Throttled), + ReputationStatus::Banned => Ok(PoolReputationStatus::Banned), + ReputationStatus::Unspecified => Err(ConversionError::InvalidEnumValue(status as i32)), + } + } +} + impl From for Reputation { fn from(rep: PoolReputation) -> Self { Reputation { @@ -219,19 +228,6 @@ impl From for Reputation { } } -impl TryFrom for PoolReputationStatus { - type Error = ConversionError; - - fn try_from(status: i32) -> Result { - match status { - x if x == ReputationStatus::Ok as i32 => Ok(Self::Ok), - x if x == ReputationStatus::Throttled as i32 => Ok(Self::Throttled), - x if x == ReputationStatus::Banned as i32 => Ok(Self::Banned), - _ => Err(ConversionError::InvalidEnumValue(status)), - } - } -} - impl TryFrom for PoolReputation { type Error = ConversionError; @@ -253,7 +249,7 @@ impl TryFrom for RundlerStakeStatus { is_staked: stake_status.is_staked, stake_info: RundlerStakeInfo { stake: stake_info.stake.into(), - unstake_delay_sec: stake_info.unstake_delay_sec, + unstake_delay_sec: stake_info.unstake_delay_sec.into(), }, }); } @@ -267,8 +263,8 @@ impl From for StakeStatus { StakeStatus { is_staked: stake_status.is_staked, stake_info: Some(StakeInfo { - stake: stake_status.stake_info.stake as u64, - unstake_delay_sec: stake_status.stake_info.unstake_delay_sec, + stake: stake_status.stake_info.stake.as_u64(), + unstake_delay_sec: stake_status.stake_info.unstake_delay_sec.as_u32(), }), } } @@ -333,7 +329,7 @@ impl TryFrom for PoolOperation { sim_block_hash, sim_block_number: 0, account_is_staked: op.account_is_staked, - entity_infos: rundler_sim::EntityInfos::default(), + entity_infos: EntityInfos::default(), }) } } diff --git a/crates/pool/src/server/remote/server.rs b/crates/pool/src/server/remote/server.rs index d8dd3d1c2..6ebc87002 100644 --- a/crates/pool/src/server/remote/server.rs +++ b/crates/pool/src/server/remote/server.rs @@ -23,7 +23,10 @@ use async_trait::async_trait; use ethers::types::{Address, H256}; use futures_util::StreamExt; use rundler_task::grpc::{metrics::GrpcMetricsLayer, protos::from_bytes}; -use rundler_types::{EntityUpdate, UserOperationId}; +use rundler_types::{ + pool::{Pool, Reputation}, + EntityUpdate, UserOperationId, +}; use tokio::{sync::mpsc, task::JoinHandle}; use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_util::sync::CancellationToken; @@ -51,10 +54,7 @@ use super::protos::{ SubscribeNewHeadsRequest, SubscribeNewHeadsResponse, UpdateEntitiesRequest, UpdateEntitiesResponse, UpdateEntitiesSuccess, OP_POOL_FILE_DESCRIPTOR_SET, }; -use crate::{ - mempool::Reputation, - server::{local::LocalPoolHandle, PoolServer}, -}; +use crate::server::local::LocalPoolHandle; const MAX_REMOTE_BLOCK_SUBSCRIPTIONS: usize = 32; diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index b3f8a4adb..6307787ba 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -7,8 +7,6 @@ license.workspace = true repository.workspace = true [dependencies] -rundler-builder = { path = "../builder" } -rundler-pool = { path = "../pool" } rundler-provider = { path = "../provider" } rundler-sim = { path = "../sim" } rundler-task = { path = "../task" } @@ -34,4 +32,4 @@ futures-util.workspace = true [dev-dependencies] mockall.workspace = true rundler-provider = { path = "../provider", features = ["test-utils"]} -rundler-pool = { path = "../pool", features = ["test-utils"] } +rundler-types= { path = "../types", features = ["test-utils"]} diff --git a/crates/rpc/src/admin.rs b/crates/rpc/src/admin.rs index 958ceafc1..12147d683 100644 --- a/crates/rpc/src/admin.rs +++ b/crates/rpc/src/admin.rs @@ -14,7 +14,7 @@ use async_trait::async_trait; use ethers::types::Address; use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::error::INTERNAL_ERROR_CODE}; -use rundler_pool::PoolServer; +use rundler_types::pool::Pool; use crate::{ error::rpc_err, @@ -50,7 +50,7 @@ impl

AdminApi

{ #[async_trait] impl

AdminApiServer for AdminApi

where - P: PoolServer, + P: Pool, { async fn clear_state(&self, clear_params: RpcAdminClearState) -> RpcResult { let _ = self diff --git a/crates/rpc/src/debug.rs b/crates/rpc/src/debug.rs index a7b3b20bd..48899880f 100644 --- a/crates/rpc/src/debug.rs +++ b/crates/rpc/src/debug.rs @@ -15,8 +15,10 @@ use async_trait::async_trait; use ethers::types::{Address, H256}; use futures_util::StreamExt; use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::error::INTERNAL_ERROR_CODE}; -use rundler_builder::{BuilderServer, BundlingMode}; -use rundler_pool::PoolServer; +use rundler_types::{ + builder::{Builder, BundlingMode}, + pool::Pool, +}; use crate::{ error::rpc_err, @@ -96,8 +98,8 @@ impl DebugApi { #[async_trait] impl DebugApiServer for DebugApi where - P: PoolServer, - B: BuilderServer, + P: Pool, + B: Builder, { async fn bundler_clear_state(&self) -> RpcResult { let _ = self @@ -234,8 +236,8 @@ where is_staked: result.is_staked, stake_info: RpcStakeInfo { addr: address, - stake: result.stake_info.stake, - unstake_delay_sec: result.stake_info.unstake_delay_sec, + stake: result.stake_info.stake.as_u128(), + unstake_delay_sec: result.stake_info.unstake_delay_sec.as_u32(), }, }) } diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 85304281b..1f3e550d2 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -18,8 +18,7 @@ use ethers::{ utils::to_checksum, }; use futures_util::future; -use rundler_pool::PoolServer; -use rundler_types::{chain::ChainSpec, UserOperationOptionalGas, UserOperationVariant}; +use rundler_types::{chain::ChainSpec, pool::Pool, UserOperationOptionalGas, UserOperationVariant}; use rundler_utils::log::LogOnError; use tracing::Level; @@ -45,20 +44,17 @@ impl Settings { } } -pub(crate) struct EthApi -where - PS: PoolServer, -{ +pub(crate) struct EthApi

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

EthApi

where - PS: PoolServer, + P: Pool, { - pub(crate) fn new(chain_spec: ChainSpec, router: EntryPointRouter, pool: PS) -> Self { + pub(crate) fn new(chain_spec: ChainSpec, router: EntryPointRouter, pool: P) -> Self { Self { router, pool, @@ -181,8 +177,9 @@ mod tests { use rundler_sim::{EntityInfos, PriorityFeeMode}; use rundler_types::{ contracts::v0_6::i_entry_point::{HandleOpsCall, IEntryPointCalls}, + pool::{IntoPoolOperationVariant, MockPool, PoolOperation}, v0_6::UserOperation, - UserOperation as UserOperationTrait, ValidTimeRange, + EntityInfos, UserOperation as UserOperationTrait, ValidTimeRange, }; use super::*; @@ -209,7 +206,7 @@ mod tests { entity_infos: EntityInfos::default(), }; - let mut pool = MockPoolServer::default(); + let mut pool = MockPool::default(); pool.expect_get_op_by_hash() .with(eq(hash)) .times(1) @@ -242,7 +239,7 @@ mod tests { let block_number = 1000; let block_hash = H256::random(); - let mut pool = MockPoolServer::default(); + let mut pool = MockPool::default(); pool.expect_get_op_by_hash() .with(eq(hash)) .returning(move |_| Ok(None)); @@ -299,7 +296,7 @@ mod tests { let uo = UserOperation::default(); let hash = uo.hash(ep, 1); - let mut pool = MockPoolServer::default(); + let mut pool = MockPool::default(); pool.expect_get_op_by_hash() .with(eq(hash)) .times(1) @@ -320,8 +317,8 @@ mod tests { fn create_api( provider: MockProvider, ep: MockEntryPointV0_6, - pool: MockPoolServer, - ) -> EthApi { + pool: MockPool, + ) -> EthApi { let provider = Arc::new(provider); let chain_spec = ChainSpec { id: 1, diff --git a/crates/rpc/src/eth/error.rs b/crates/rpc/src/eth/error.rs index 3b356c8c3..3a6447897 100644 --- a/crates/rpc/src/eth/error.rs +++ b/crates/rpc/src/eth/error.rs @@ -16,10 +16,12 @@ use jsonrpsee::types::{ error::{CALL_EXECUTION_FAILED_CODE, INTERNAL_ERROR_CODE, INVALID_PARAMS_CODE}, ErrorObjectOwned, }; -use rundler_pool::{MempoolError, PoolServerError}; use rundler_provider::ProviderError; -use rundler_sim::{GasEstimationError, PrecheckViolation, SimulationViolation}; -use rundler_types::{Entity, EntityType, Timestamp}; +use rundler_sim::GasEstimationError; +use rundler_types::{ + pool::{MempoolError, PoolError, PrecheckViolation, SimulationViolation}, + Entity, EntityType, Timestamp, +}; use serde::Serialize; use crate::error::{rpc_err, rpc_err_with_data}; @@ -199,14 +201,14 @@ pub struct ExecutionRevertedWithBytesData { pub revert_data: Bytes, } -impl From for EthRpcError { - fn from(value: PoolServerError) -> Self { +impl From for EthRpcError { + fn from(value: PoolError) -> Self { match value { - PoolServerError::MempoolError(e) => e.into(), - PoolServerError::UnexpectedResponse => { + PoolError::MempoolError(e) => e.into(), + PoolError::UnexpectedResponse => { EthRpcError::Internal(anyhow::anyhow!("unexpected response from pool server")) } - PoolServerError::Other(e) => EthRpcError::Internal(e), + PoolError::Other(e) => EthRpcError::Internal(e), } } } diff --git a/crates/rpc/src/eth/server.rs b/crates/rpc/src/eth/server.rs index 8ec47a777..e07736825 100644 --- a/crates/rpc/src/eth/server.rs +++ b/crates/rpc/src/eth/server.rs @@ -13,7 +13,7 @@ use ethers::types::{spoof, Address, H256, U64}; use jsonrpsee::core::RpcResult; -use rundler_pool::PoolServer; +use rundler_types::pool::Pool; use super::{api::EthApi, EthApiServer}; use crate::types::{ @@ -22,9 +22,9 @@ use crate::types::{ }; #[async_trait::async_trait] -impl EthApiServer for EthApi +impl

EthApiServer for EthApi

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

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

, entry_point_router: EntryPointRouter, - pool_server: PS, + pool_server: PL, settings: Settings, ) -> Self { Self { @@ -99,10 +98,10 @@ where } #[async_trait] -impl RundlerApiServer for RundlerApi +impl RundlerApiServer for RundlerApi where P: Provider, - PS: PoolServer, + PL: Pool, { async fn max_priority_fee_per_gas(&self) -> RpcResult { let (bundle_fees, _) = self diff --git a/crates/rpc/src/task.rs b/crates/rpc/src/task.rs index 533cce4c4..2cc1bb5d4 100644 --- a/crates/rpc/src/task.rs +++ b/crates/rpc/src/task.rs @@ -20,15 +20,13 @@ use jsonrpsee::{ server::{middleware::ProxyGetRequestLayer, ServerBuilder}, RpcModule, }; -use rundler_builder::BuilderServer; -use rundler_pool::PoolServer; use rundler_provider::EthersEntryPointV0_6; use rundler_sim::{EstimationSettings, FeeEstimator, GasEstimatorV0_6, PrecheckSettings}; use rundler_task::{ server::{format_socket_addr, HealthCheck}, Task, }; -use rundler_types::chain::ChainSpec; +use rundler_types::{builder::Builder, chain::ChainSpec, pool::Pool}; use tokio_util::sync::CancellationToken; use tracing::info; @@ -83,8 +81,8 @@ pub struct RpcTask { #[async_trait] impl Task for RpcTask where - P: PoolServer + HealthCheck + Clone, - B: BuilderServer + HealthCheck + Clone, + P: Pool + HealthCheck + Clone, + B: Builder + HealthCheck + Clone, { async fn run(mut self: Box, shutdown_token: CancellationToken) -> anyhow::Result<()> { let addr: SocketAddr = format_socket_addr(&self.args.host, self.args.port).parse()?; @@ -163,8 +161,8 @@ where impl RpcTask where - P: PoolServer + HealthCheck + Clone, - B: BuilderServer + HealthCheck + Clone, + P: Pool + HealthCheck + Clone, + B: Builder + HealthCheck + Clone, { /// Creates a new RPC server task. pub fn new(args: Args, pool: P, builder: B) -> Self { diff --git a/crates/rpc/src/types/mod.rs b/crates/rpc/src/types/mod.rs index c5d383533..c97bf3017 100644 --- a/crates/rpc/src/types/mod.rs +++ b/crates/rpc/src/types/mod.rs @@ -15,8 +15,10 @@ use ethers::{ types::{Address, Log, TransactionReceipt, H160, H256, U256}, utils::to_checksum, }; -use rundler_pool::{Reputation, ReputationStatus}; -use rundler_types::{GasEstimate, UserOperationOptionalGas, UserOperationVariant}; +use rundler_types::{ + pool::{Reputation, ReputationStatus}, + GasEstimate, UserOperationOptionalGas, UserOperationVariant, +}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; mod v0_6; diff --git a/crates/sim/src/lib.rs b/crates/sim/src/lib.rs index 6111b1c98..2cbaa4952 100644 --- a/crates/sim/src/lib.rs +++ b/crates/sim/src/lib.rs @@ -44,8 +44,7 @@ mod precheck; #[cfg(feature = "test-utils")] pub use precheck::MockPrechecker; pub use precheck::{ - PrecheckError, PrecheckViolation, Prechecker, PrecheckerImpl, Settings as PrecheckSettings, - MIN_CALL_GAS_LIMIT, + PrecheckError, Prechecker, PrecheckerImpl, Settings as PrecheckSettings, MIN_CALL_GAS_LIMIT, }; /// Simulation and violation checking @@ -53,8 +52,7 @@ pub mod simulation; #[cfg(feature = "test-utils")] pub use simulation::MockSimulator; pub use simulation::{ - EntityInfo, EntityInfos, MempoolConfig, NeedsStakeInformation, Settings as SimulationSettings, - SimulationError, SimulationResult, SimulationViolation, Simulator, ViolationOpCode, + MempoolConfig, Settings as SimulationSettings, SimulationError, SimulationResult, Simulator, }; mod types; diff --git a/crates/sim/src/precheck.rs b/crates/sim/src/precheck.rs index fb0443c57..95391febe 100644 --- a/crates/sim/src/precheck.rs +++ b/crates/sim/src/precheck.rs @@ -19,7 +19,11 @@ use ethers::types::{Address, U256}; #[cfg(feature = "test-utils")] use mockall::automock; use rundler_provider::{EntryPoint, L1GasProvider, Provider}; -use rundler_types::{chain::ChainSpec, GasFees, UserOperation}; +use rundler_types::{ + chain::ChainSpec, + pool::{MempoolError, PrecheckViolation}, + GasFees, UserOperation, +}; use rundler_utils::math; use crate::{gas, types::ViolationError}; @@ -44,6 +48,24 @@ pub trait Prechecker: Send + Sync + 'static { /// Precheck error pub type PrecheckError = ViolationError; +impl From for MempoolError { + fn from(mut error: PrecheckError) -> Self { + let PrecheckError::Violations(violations) = &mut error else { + return Self::Other(error.into()); + }; + + let Some(violation) = violations.iter_mut().min() else { + return Self::Other(error.into()); + }; + + // extract violation and replace with dummy + Self::PrecheckViolation(std::mem::replace( + violation, + PrecheckViolation::SenderIsNotContractAndNoInitCode(Address::zero()), + )) + } +} + /// Prechecker implementation #[derive(Debug)] pub struct PrecheckerImpl { @@ -387,51 +409,6 @@ where } } -/// Precheck violation enumeration -/// -/// All possible errors that can be returned from a precheck. -#[derive(Clone, Debug, parse_display::Display, Eq, PartialEq, Ord, PartialOrd)] -pub enum PrecheckViolation { - /// The sender is not deployed, and no init code is provided. - #[display("sender {0:?} is not a contract and initCode is empty")] - SenderIsNotContractAndNoInitCode(Address), - /// The sender is already deployed, and an init code is provided. - #[display("sender {0:?} is an existing contract, but initCode is nonempty")] - ExistingSenderWithInitCode(Address), - /// An init code contains a factory address that is not deployed. - #[display("initCode indicates factory with no code: {0:?}")] - FactoryIsNotContract(Address), - /// The total gas limit of the user operation is too high. - /// See `gas::user_operation_execution_gas_limit` for calculation. - #[display("total gas limit is {0} but must be at most {1}")] - TotalGasLimitTooHigh(U256, U256), - /// The verification gas limit of the user operation is too high. - #[display("verificationGasLimit is {0} but must be at most {1}")] - VerificationGasLimitTooHigh(U256, U256), - /// The pre-verification gas of the user operation is too low. - #[display("preVerificationGas is {0} but must be at least {1}")] - PreVerificationGasTooLow(U256, U256), - /// A paymaster is provided, but the address is not deployed. - #[display("paymasterAndData indicates paymaster with no code: {0:?}")] - PaymasterIsNotContract(Address), - /// The paymaster deposit is too low to pay for the user operation's maximum cost. - #[display("paymaster deposit is {0} but must be at least {1} to pay for this operation")] - PaymasterDepositTooLow(U256, U256), - /// The sender balance is too low to pay for the user operation's maximum cost. - /// (when not using a paymaster) - #[display("sender balance and deposit together is {0} but must be at least {1} to pay for this operation")] - SenderFundsTooLow(U256, U256), - /// The provided max priority fee per gas is too low based on the current network rate. - #[display("maxPriorityFeePerGas is {0} but must be at least {1}")] - MaxPriorityFeePerGasTooLow(U256, U256), - /// The provided max fee per gas is too low based on the current network rate. - #[display("maxFeePerGas is {0} but must be at least {1}")] - MaxFeePerGasTooLow(U256, U256), - /// The call gas limit is too low to account for any possible call. - #[display("callGasLimit is {0} but must be at least {1}")] - CallGasLimitTooLow(U256, U256), -} - #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/crates/sim/src/simulation/mempool.rs b/crates/sim/src/simulation/mempool.rs index 913284004..44c1dfd1a 100644 --- a/crates/sim/src/simulation/mempool.rs +++ b/crates/sim/src/simulation/mempool.rs @@ -201,10 +201,9 @@ pub(crate) fn match_mempools( #[cfg(test)] mod tests { use ethers::types::U256; - use rundler_types::StorageSlot; + use rundler_types::{pool::NeedsStakeInformation, StorageSlot, ViolationOpCode}; use super::*; - use crate::simulation::{NeedsStakeInformation, ViolationOpCode}; #[test] fn test_allow_entity_any() { diff --git a/crates/sim/src/simulation/mod.rs b/crates/sim/src/simulation/mod.rs index 1b289db43..70ab685cf 100644 --- a/crates/sim/src/simulation/mod.rs +++ b/crates/sim/src/simulation/mod.rs @@ -14,15 +14,16 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use anyhow::Error; -use ethers::types::{Address, Opcode, H256, U256}; +use ethers::types::{Address, H256, U256}; #[cfg(feature = "test-utils")] use mockall::automock; use rundler_provider::AggregatorSimOut; use rundler_types::{ - Entity, EntityType, StakeInfo, StorageSlot, UserOperation, ValidTimeRange, ValidationOutput, + pool::{MempoolError, SimulationViolation}, + Entity, EntityInfo, EntityInfos, EntityType, StakeInfo, UserOperation, ValidTimeRange, + ValidationOutput, }; use serde::{Deserialize, Serialize}; -use strum::IntoEnumIterator; /// Simulation module for Entry Point v0.6 pub mod v0_6; @@ -95,6 +96,27 @@ impl From for SimulationError { } } +impl From for MempoolError { + fn from(mut error: SimulationError) -> Self { + let SimulationError { + violation_error, .. + } = &mut error; + let ViolationError::Violations(violations) = violation_error else { + return Self::Other((*violation_error).clone().into()); + }; + + let Some(violation) = violations.iter_mut().min() else { + return Self::Other((*violation_error).clone().into()); + }; + + // extract violation and replace with dummy + Self::SimulationViolation(std::mem::replace( + violation, + SimulationViolation::DidNotRevert, + )) + } +} + /// Simulator trait for running user operation simulations #[cfg_attr(feature = "test-utils", automock(type UO = rundler_types::v0_6::UserOperation;))] #[async_trait::async_trait] @@ -112,93 +134,6 @@ pub trait Simulator: Send + Sync + 'static { ) -> Result; } -/// All possible simulation violations -#[derive(Clone, Debug, parse_display::Display, Ord, Eq, PartialOrd, PartialEq)] -pub enum SimulationViolation { - // Make sure to maintain the order here based on the importance - // of the violation for converting to an JSON RPC error - /// The user operation signature is invalid - #[display("invalid signature")] - InvalidSignature, - /// The user operation used an opcode that is not allowed - #[display("{0.kind} uses banned opcode: {2} in contract {1:?}")] - UsedForbiddenOpcode(Entity, Address, ViolationOpCode), - /// The user operation used a precompile that is not allowed - #[display("{0.kind} uses banned precompile: {2:?} in contract {1:?}")] - UsedForbiddenPrecompile(Entity, Address, Address), - /// The user operation accessed a contract that has not been deployed - #[display( - "{0.kind} tried to access code at {1} during validation, but that address is not a contract" - )] - AccessedUndeployedContract(Entity, Address), - /// The user operation factory entity called CREATE2 more than once during initialization - #[display("factory may only call CREATE2 once during initialization")] - FactoryCalledCreate2Twice(Address), - /// The user operation accessed a storage slot that is not allowed - #[display("{0.kind} accessed forbidden storage at address {1:?} during validation")] - InvalidStorageAccess(Entity, StorageSlot), - /// The user operation called an entry point method that is not allowed - #[display("{0.kind} called entry point method other than depositTo")] - CalledBannedEntryPointMethod(Entity), - /// The user operation made a call that contained value to a contract other than the entrypoint - /// during validation - #[display("{0.kind} must not send ETH during validation (except from account to entry point)")] - CallHadValue(Entity), - /// The code hash of accessed contracts changed on the second simulation - #[display("code accessed by validation has changed since the last time validation was run")] - CodeHashChanged, - /// The user operation contained an entity that accessed storage without being staked - #[display("{0.needs_stake} needs to be staked: {0.accessing_entity} accessed storage at {0.accessed_address} slot {0.slot} (associated with {0.accessed_entity:?})")] - NotStaked(Box), - /// The user operation uses a paymaster that returns a context while being unstaked - #[display("Unstaked paymaster must not return context")] - UnstakedPaymasterContext, - /// The user operation uses an aggregator entity and it is not staked - #[display("An aggregator must be staked, regardless of storager usage")] - UnstakedAggregator, - /// Simulation reverted with an unintended reason, containing a message - #[display("reverted while simulating {0} validation: {1}")] - UnintendedRevertWithMessage(EntityType, String, Option

), - /// Simulation reverted with an unintended reason - #[display("reverted while simulating {0} validation")] - UnintendedRevert(EntityType, Option
), - /// Simulation did not revert, a revert is always expected - #[display("simulateValidation did not revert. Make sure your EntryPoint is valid")] - DidNotRevert, - /// Simulation had the wrong number of phases - #[display("simulateValidation should have 3 parts but had {0} instead. Make sure your EntryPoint is valid")] - WrongNumberOfPhases(u32), - /// The user operation ran out of gas during validation - #[display("ran out of gas during {0.kind} validation")] - OutOfGas(Entity), - /// The user operation aggregator signature validation failed - #[display("aggregator signature validation failed")] - AggregatorValidationFailed, - /// Verification gas limit doesn't have the required buffer on the measured gas - #[display("verification gas limit doesn't have the required buffer on the measured gas, limit: {0}, needed: {1}")] - VerificationGasLimitBufferTooLow(U256, U256), -} - -/// A wrapper around Opcode that implements extra traits -#[derive(Debug, PartialEq, Clone, parse_display::Display, Eq)] -#[display("{0:?}")] -pub struct ViolationOpCode(pub Opcode); - -impl PartialOrd for ViolationOpCode { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for ViolationOpCode { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - let left = self.0 as i32; - let right = other.0 as i32; - - left.cmp(&right) - } -} - fn entity_type_from_simulation_phase(i: usize) -> Option { match i { 0 => Some(EntityType::Factory), @@ -208,107 +143,6 @@ fn entity_type_from_simulation_phase(i: usize) -> Option { } } -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -/// additional context about an entity -pub struct EntityInfo { - /// The address of an entity - pub address: Address, - /// Whether the entity is staked or not - pub is_staked: bool, -} - -impl EntityInfo { - fn override_is_staked(&mut self, allow_unstaked_addresses: &HashSet
) { - self.is_staked = allow_unstaked_addresses.contains(&self.address) || self.is_staked; - } -} - -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -/// additional context for all the entities used in an op -pub struct EntityInfos { - /// The entity info for the factory - pub factory: Option, - /// The entity info for the op sender - pub sender: EntityInfo, - /// The entity info for the paymaster - pub paymaster: Option, - /// The entity info for the aggregator - pub aggregator: Option, -} - -impl EntityInfos { - fn new( - factory_address: Option
, - sender_address: Address, - paymaster_address: Option
, - entry_point_out: &ValidationOutput, - sim_settings: Settings, - ) -> Self { - let factory = factory_address.map(|address| EntityInfo { - address, - is_staked: is_staked(entry_point_out.factory_info, sim_settings), - }); - let sender = EntityInfo { - address: sender_address, - is_staked: is_staked(entry_point_out.sender_info, sim_settings), - }; - let paymaster = paymaster_address.map(|address| EntityInfo { - address, - is_staked: is_staked(entry_point_out.paymaster_info, sim_settings), - }); - let aggregator = entry_point_out - .aggregator_info - .map(|aggregator_info| EntityInfo { - address: aggregator_info.address, - is_staked: is_staked(aggregator_info.stake_info, sim_settings), - }); - - Self { - factory, - sender, - paymaster, - aggregator, - } - } - - /// Get iterator over the entities - pub fn entities(&'_ self) -> impl Iterator + '_ { - EntityType::iter().filter_map(|t| self.get(t).map(|info| (t, info))) - } - - fn override_is_staked(&mut self, allow_unstaked_addresses: &HashSet
) { - if let Some(mut factory) = self.factory { - factory.override_is_staked(allow_unstaked_addresses) - } - self.sender.override_is_staked(allow_unstaked_addresses); - if let Some(mut paymaster) = self.paymaster { - paymaster.override_is_staked(allow_unstaked_addresses) - } - if let Some(mut aggregator) = self.aggregator { - aggregator.override_is_staked(allow_unstaked_addresses) - } - } - - /// Get the EntityInfo of a specific entity - pub fn get(self, entity: EntityType) -> Option { - match entity { - EntityType::Factory => self.factory, - EntityType::Account => Some(self.sender), - EntityType::Paymaster => self.paymaster, - EntityType::Aggregator => self.aggregator, - } - } - - fn sender_address(self) -> Address { - self.sender.address - } -} - -fn is_staked(info: StakeInfo, sim_settings: Settings) -> bool { - info.stake >= sim_settings.min_stake_value.into() - && info.unstake_delay_sec >= sim_settings.min_unstake_delay.into() -} - #[derive(Clone, Debug, Eq, PartialEq)] enum StorageRestriction { /// (Entity needing stake, accessing entity type, accessed entity type, accessed address, accessed slot) @@ -316,25 +150,6 @@ enum StorageRestriction { Banned(U256), } -/// Information about a storage violation based on stake status -#[derive(Debug, PartialEq, Clone, PartialOrd, Eq, Ord)] -pub struct NeedsStakeInformation { - /// Entity needing stake info - pub needs_stake: Entity, - /// The entity that accessed the storage requiring stake - pub accessing_entity: EntityType, - /// Type of accessed entity, if it is a known entity - pub accessed_entity: Option, - /// Address that was accessed while unstaked - pub accessed_address: Address, - /// The accessed slot number - pub slot: U256, - /// Minumum stake - pub min_stake: U256, - /// Minumum delay after an unstake event - pub min_unstake_delay: U256, -} - #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub(crate) struct AccessInfo { @@ -511,3 +326,60 @@ impl Default for Settings { } } } + +fn override_is_staked(ei: &mut EntityInfo, allow_unstaked_addresses: &HashSet
) { + ei.is_staked = allow_unstaked_addresses.contains(&ei.address) || ei.is_staked; +} + +fn override_infos_staked(eis: &mut EntityInfos, allow_unstaked_addresses: &HashSet
) { + override_is_staked(&mut eis.sender, allow_unstaked_addresses); + + if let Some(mut factory) = eis.factory { + override_is_staked(&mut factory, allow_unstaked_addresses); + } + if let Some(mut paymaster) = eis.paymaster { + override_is_staked(&mut paymaster, allow_unstaked_addresses); + } + if let Some(mut aggregator) = eis.aggregator { + override_is_staked(&mut aggregator, allow_unstaked_addresses); + } +} + +fn infos_from_validation_output( + factory_address: Option
, + sender_address: Address, + paymaster_address: Option
, + entry_point_out: &ValidationOutput, + sim_settings: Settings, +) -> EntityInfos { + let factory = factory_address.map(|address| EntityInfo { + address, + is_staked: is_staked(entry_point_out.factory_info, sim_settings), + }); + let sender = EntityInfo { + address: sender_address, + is_staked: is_staked(entry_point_out.sender_info, sim_settings), + }; + let paymaster = paymaster_address.map(|address| EntityInfo { + address, + is_staked: is_staked(entry_point_out.paymaster_info, sim_settings), + }); + let aggregator = entry_point_out + .aggregator_info + .map(|aggregator_info| EntityInfo { + address: aggregator_info.address, + is_staked: is_staked(aggregator_info.stake_info, sim_settings), + }); + + EntityInfos { + factory, + sender, + paymaster, + aggregator, + } +} + +pub(crate) fn is_staked(info: StakeInfo, sim_settings: Settings) -> bool { + info.stake >= sim_settings.min_stake_value.into() + && info.unstake_delay_sec >= sim_settings.min_unstake_delay.into() +} diff --git a/crates/sim/src/simulation/v0_6/simulator.rs b/crates/sim/src/simulation/v0_6/simulator.rs index 961fe45e4..bab4818cf 100644 --- a/crates/sim/src/simulation/v0_6/simulator.rs +++ b/crates/sim/src/simulation/v0_6/simulator.rs @@ -28,8 +28,11 @@ use rundler_provider::{ AggregatorOut, AggregatorSimOut, EntryPoint, Provider, SignatureAggregator, SimulationProvider, }; use rundler_types::{ - contracts::v0_6::i_entry_point::FailedOp, v0_6::UserOperation, Entity, EntityType, StorageSlot, - UserOperation as UserOperationTrait, ValidTimeRange, ValidationOutput, ValidationReturnInfo, + contracts::v0_6::i_entry_point::FailedOp, + pool::{NeedsStakeInformation, SimulationViolation}, + v0_6::UserOperation, + Entity, EntityInfos, EntityType, StorageSlot, UserOperation as UserOperationTrait, + ValidTimeRange, ValidationOutput, ValidationReturnInfo, ViolationOpCode, }; use super::{ @@ -43,8 +46,7 @@ use crate::{ ParseStorageAccess, Settings, StorageRestriction, }, types::ViolationError, - utils, EntityInfos, NeedsStakeInformation, SimulationError, SimulationResult, - SimulationViolation, ViolationOpCode, + utils, SimulationError, SimulationResult, }; /// Simulator implementation. @@ -187,7 +189,7 @@ where entity_infos: None, })? }; - let entity_infos = EntityInfos::new( + let entity_infos = simulation::infos_from_validation_output( factory_address, sender_address, paymaster_address, @@ -578,9 +580,10 @@ where } = return_info; // Conduct any stake overrides before assigning entity_infos - context - .entity_infos - .override_is_staked(&self.allow_unstaked_addresses); + simulation::override_infos_staked( + &mut context.entity_infos, + &self.allow_unstaked_addresses, + ); Ok(SimulationResult { mempools, @@ -893,7 +896,7 @@ mod tests { has_factory: true, associated_addresses: HashSet::new(), block_id: BlockId::Number(BlockNumber::Latest), - entity_infos: EntityInfos::new( + entity_infos: simulation::infos_from_validation_output( Some(Address::from_str("0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789").unwrap()), Address::from_str("0xb856dbd4fa1a79a46d426f537455e7d3e79ab7c4").unwrap(), Some(Address::from_str("0x8abb13360b87be5eeb1b98647a016add927a136c").unwrap()), diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index cdef39c41..a53917109 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -10,14 +10,25 @@ repository.workspace = true rundler-utils = { path = "../utils" } anyhow.workspace = true +async-trait.workspace = true chrono = "0.4.24" constcat = "0.4.1" ethers.workspace = true +futures-util.workspace = true parse-display = "0.9.0" rand.workspace = true serde.workspace = true serde_json.workspace = true strum.workspace = true +thiserror.workspace = true + +mockall = {workspace = true, optional = true } [build-dependencies] ethers.workspace = true + +[dev-dependencies] +rundler-types = { path = ".", features = ["test-utils"] } + +[features] +test-utils = [ "mockall" ] diff --git a/crates/pool/src/server/error.rs b/crates/types/src/builder/error.rs similarity index 59% rename from crates/pool/src/server/error.rs rename to crates/types/src/builder/error.rs index fc07035ad..bfe06d6ee 100644 --- a/crates/pool/src/server/error.rs +++ b/crates/types/src/builder/error.rs @@ -11,27 +11,13 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use crate::mempool::MempoolError; - -/// Pool server error type +/// Builder server errors #[derive(Debug, thiserror::Error)] -pub enum PoolServerError { - /// Mempool error occurred - #[error(transparent)] - MempoolError(MempoolError), - /// Unexpected response from PoolServer - #[error("Unexpected response from PoolServer")] +pub enum BuilderError { + /// Builder returned an unexpected response type for the given request + #[error("Unexpected response from Builder")] UnexpectedResponse, - /// Internal error + /// Internal errors #[error(transparent)] Other(#[from] anyhow::Error), } - -impl From for PoolServerError { - fn from(error: MempoolError) -> Self { - match error { - MempoolError::Other(e) => Self::Other(e), - _ => Self::MempoolError(error), - } - } -} diff --git a/crates/types/src/builder/mod.rs b/crates/types/src/builder/mod.rs new file mode 100644 index 000000000..152a572eb --- /dev/null +++ b/crates/types/src/builder/mod.rs @@ -0,0 +1,23 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +//! Rundler builder types + +mod error; +pub use error::*; + +mod traits; +pub use traits::*; + +mod types; +pub use types::*; diff --git a/crates/types/src/builder/traits.rs b/crates/types/src/builder/traits.rs new file mode 100644 index 000000000..208969a79 --- /dev/null +++ b/crates/types/src/builder/traits.rs @@ -0,0 +1,37 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use ethers::types::{Address, H256}; +#[cfg(feature = "test-utils")] +use mockall::automock; + +use super::{error::BuilderError, types::BundlingMode}; + +/// Builder result +pub type BuilderResult = std::result::Result; + +/// Builder +#[cfg_attr(feature = "test-utils", automock)] +#[async_trait::async_trait] +pub trait Builder: Send + Sync + 'static { + /// Get the supported entry points of this builder + async fn get_supported_entry_points(&self) -> BuilderResult>; + + /// Trigger the builder to send a bundle now, used for debugging. + /// + /// Bundling mode must be set to `Manual`, or this will error + async fn debug_send_bundle_now(&self) -> BuilderResult<(H256, u64)>; + + /// Set the bundling mode + async fn debug_set_bundling_mode(&self, mode: BundlingMode) -> BuilderResult<()>; +} diff --git a/crates/types/src/builder/types.rs b/crates/types/src/builder/types.rs new file mode 100644 index 000000000..52a9e00e8 --- /dev/null +++ b/crates/types/src/builder/types.rs @@ -0,0 +1,30 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use parse_display::Display; +use serde::{Deserialize, Serialize}; + +/// Builder bundling mode +#[derive(Display, Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] +#[display(style = "lowercase")] +#[serde(rename_all = "lowercase")] +pub enum BundlingMode { + /// Manual bundling mode for debugging. + /// + /// Bundles will only be sent when `debug_send_bundle_now` is called. + Manual, + /// Auto bundling mode for normal operation. + /// + /// Bundles will be sent automatically. + Auto, +} diff --git a/crates/types/src/entity.rs b/crates/types/src/entity.rs index 8da03ebb7..55896a6b3 100644 --- a/crates/types/src/entity.rs +++ b/crates/types/src/entity.rs @@ -17,7 +17,7 @@ use anyhow::bail; use ethers::{types::Address, utils::to_checksum}; use parse_display::Display; use serde::{ser::SerializeStruct, Deserialize, Serialize}; -use strum::EnumIter; +use strum::{EnumIter, IntoEnumIterator}; /// The type of an entity #[derive( @@ -156,3 +156,74 @@ pub struct EntityUpdate { /// The kind of update to perform for the entity pub update_type: EntityUpdateType, } + +/// additional context about an entity +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct EntityInfo { + /// The address of an entity + pub address: Address, + /// Whether the entity is staked or not + pub is_staked: bool, +} + +/// additional context for all the entities used in an op +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct EntityInfos { + /// The entity info for the factory + pub factory: Option, + /// The entity info for the op sender + pub sender: EntityInfo, + /// The entity info for the paymaster + pub paymaster: Option, + /// The entity info for the aggregator + pub aggregator: Option, +} + +impl EntityInfos { + /// Get iterator over the entities + pub fn entities(&'_ self) -> impl Iterator + '_ { + EntityType::iter().filter_map(|t| self.get(t).map(|info| (t, info))) + } + + /// Get the EntityInfo of a specific entity + pub fn get(self, entity: EntityType) -> Option { + match entity { + EntityType::Factory => self.factory, + EntityType::Account => Some(self.sender), + EntityType::Paymaster => self.paymaster, + EntityType::Aggregator => self.aggregator, + } + } + + /// Get the type of an entity from its address, if any + pub fn type_from_address(self, address: Address) -> Option { + if address.eq(&self.sender.address) { + return Some(EntityType::Account); + } + + if let Some(factory) = self.factory { + if address.eq(&factory.address) { + return Some(EntityType::Factory); + } + } + + if let Some(paymaster) = self.paymaster { + if address.eq(&paymaster.address) { + return Some(EntityType::Paymaster); + } + } + + if let Some(aggregator) = self.aggregator { + if address.eq(&aggregator.address) { + return Some(EntityType::Aggregator); + } + } + + None + } + + /// Get the sender address + pub fn sender_address(self) -> Address { + self.sender.address + } +} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 10366d99c..b9faf6d97 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -20,17 +20,24 @@ //! Rundler common types +pub mod builder; + pub mod chain; #[rustfmt::skip] pub mod contracts; mod entity; -pub use entity::{Entity, EntityType, EntityUpdate, EntityUpdateType}; +pub use entity::{Entity, EntityInfo, EntityInfos, EntityType, EntityUpdate, EntityUpdateType}; + +mod opcode; +pub use opcode::ViolationOpCode; mod gas; pub use gas::GasFees; +pub mod pool; + mod timestamp; pub use timestamp::{Timestamp, ValidTimeRange}; diff --git a/crates/types/src/opcode.rs b/crates/types/src/opcode.rs new file mode 100644 index 000000000..85a478175 --- /dev/null +++ b/crates/types/src/opcode.rs @@ -0,0 +1,34 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use ethers::types::Opcode; + +/// A wrapper around Opcode that implements extra traits +#[derive(Debug, PartialEq, Clone, parse_display::Display, Eq)] +#[display("{0:?}")] +pub struct ViolationOpCode(pub Opcode); + +impl PartialOrd for ViolationOpCode { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ViolationOpCode { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let left = self.0 as i32; + let right = other.0 as i32; + + left.cmp(&right) + } +} diff --git a/crates/types/src/pool/error.rs b/crates/types/src/pool/error.rs new file mode 100644 index 000000000..1ea690aee --- /dev/null +++ b/crates/types/src/pool/error.rs @@ -0,0 +1,225 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use ethers::types::{Address, U256}; + +use crate::{Entity, EntityType, StorageSlot, ViolationOpCode}; + +/// Pool server error type +#[derive(Debug, thiserror::Error)] +pub enum PoolError { + /// Mempool error occurred + #[error(transparent)] + MempoolError(MempoolError), + /// Unexpected response from PoolServer + #[error("Unexpected response from PoolServer")] + UnexpectedResponse, + /// Internal error + #[error(transparent)] + Other(#[from] anyhow::Error), +} + +impl From for PoolError { + fn from(error: MempoolError) -> Self { + match error { + MempoolError::Other(e) => Self::Other(e), + _ => Self::MempoolError(error), + } + } +} + +/// Mempool error type. +#[derive(Debug, thiserror::Error)] +pub enum MempoolError { + /// Some other error occurred + #[error(transparent)] + Other(#[from] anyhow::Error), + /// Operation with the same hash already in pool + #[error("Operation already known")] + OperationAlreadyKnown, + /// Operation with same sender/nonce already in pool + /// and the replacement operation has lower gas price. + #[error("Replacement operation underpriced. Existing priority fee: {0}. Existing fee: {1}")] + ReplacementUnderpriced(U256, U256), + /// Max operations reached for unstaked sender [UREP-010] or unstaked non-sender entity [UREP-020] + #[error("Max operations ({0}) reached for entity {1}")] + MaxOperationsReached(usize, Address), + /// Multiple roles violation + /// Spec rule: STO-040 + #[error("A {} at {} in this UserOperation is used as a sender entity in another UserOperation currently in mempool.", .0.kind, .0.address)] + MultipleRolesViolation(Entity), + /// An associated storage slot that is accessed in the UserOperation is being used as a sender by another UserOperation in the mempool. + /// Spec rule: STO-041 + #[error("An associated storage slot that is accessed in the UserOperation is being used as a sender by another UserOperation in the mempool")] + AssociatedStorageIsAlternateSender, + /// Sender address used as different entity in another UserOperation currently in the mempool. + /// Spec rule: STO-040 + #[error("The sender address {0} is used as a different entity in another UserOperation currently in mempool")] + SenderAddressUsedAsAlternateEntity(Address), + /// An entity associated with the operation is throttled/banned. + #[error("Entity {0} is throttled/banned")] + EntityThrottled(Entity), + /// Operation was discarded on inserting due to size limit + #[error("Operation was discarded on inserting")] + DiscardedOnInsert, + /// Paymaster balance too low + /// Spec rule: EREP-010 + #[error("Paymaster balance too low. Required balance: {0}. Current balance {1}")] + PaymasterBalanceTooLow(U256, U256), + /// Operation was rejected due to a precheck violation + #[error("Operation violation during precheck {0}")] + PrecheckViolation(PrecheckViolation), + /// Operation was rejected due to a simulation violation + #[error("Operation violation during simulation {0}")] + SimulationViolation(SimulationViolation), + /// Operation was rejected because it used an unsupported aggregator + #[error("Unsupported aggregator {0}")] + UnsupportedAggregator(Address), + /// An unknown entry point was specified + #[error("Unknown entry point {0}")] + UnknownEntryPoint(Address), + /// The operation drop attempt too soon after being added to the pool + #[error("Operation drop attempt too soon after being added to the pool. Added at {0}, attempted to drop at {1}, must wait {2} blocks.")] + OperationDropTooSoon(u64, u64, u64), +} + +/// Precheck violation enumeration +/// +/// All possible errors that can be returned from a precheck. +#[derive(Clone, Debug, parse_display::Display, Eq, PartialEq, Ord, PartialOrd)] +pub enum PrecheckViolation { + /// The sender is not deployed, and no init code is provided. + #[display("sender {0:?} is not a contract and initCode is empty")] + SenderIsNotContractAndNoInitCode(Address), + /// The sender is already deployed, and an init code is provided. + #[display("sender {0:?} is an existing contract, but initCode is nonempty")] + ExistingSenderWithInitCode(Address), + /// An init code contains a factory address that is not deployed. + #[display("initCode indicates factory with no code: {0:?}")] + FactoryIsNotContract(Address), + /// The total gas limit of the user operation is too high. + /// See `gas::user_operation_execution_gas_limit` for calculation. + #[display("total gas limit is {0} but must be at most {1}")] + TotalGasLimitTooHigh(U256, U256), + /// The verification gas limit of the user operation is too high. + #[display("verificationGasLimit is {0} but must be at most {1}")] + VerificationGasLimitTooHigh(U256, U256), + /// The pre-verification gas of the user operation is too low. + #[display("preVerificationGas is {0} but must be at least {1}")] + PreVerificationGasTooLow(U256, U256), + /// A paymaster is provided, but the address is not deployed. + #[display("paymasterAndData indicates paymaster with no code: {0:?}")] + PaymasterIsNotContract(Address), + /// The paymaster deposit is too low to pay for the user operation's maximum cost. + #[display("paymaster deposit is {0} but must be at least {1} to pay for this operation")] + PaymasterDepositTooLow(U256, U256), + /// The sender balance is too low to pay for the user operation's maximum cost. + /// (when not using a paymaster) + #[display("sender balance and deposit together is {0} but must be at least {1} to pay for this operation")] + SenderFundsTooLow(U256, U256), + /// The provided max priority fee per gas is too low based on the current network rate. + #[display("maxPriorityFeePerGas is {0} but must be at least {1}")] + MaxPriorityFeePerGasTooLow(U256, U256), + /// The provided max fee per gas is too low based on the current network rate. + #[display("maxFeePerGas is {0} but must be at least {1}")] + MaxFeePerGasTooLow(U256, U256), + /// The call gas limit is too low to account for any possible call. + #[display("callGasLimit is {0} but must be at least {1}")] + CallGasLimitTooLow(U256, U256), +} + +/// All possible simulation violations +#[derive(Clone, Debug, parse_display::Display, Ord, Eq, PartialOrd, PartialEq)] +pub enum SimulationViolation { + // Make sure to maintain the order here based on the importance + // of the violation for converting to an JSON RPC error + /// The user operation signature is invalid + #[display("invalid signature")] + InvalidSignature, + /// The user operation used an opcode that is not allowed + #[display("{0.kind} uses banned opcode: {2} in contract {1:?}")] + UsedForbiddenOpcode(Entity, Address, ViolationOpCode), + /// The user operation used a precompile that is not allowed + #[display("{0.kind} uses banned precompile: {2:?} in contract {1:?}")] + UsedForbiddenPrecompile(Entity, Address, Address), + /// The user operation accessed a contract that has not been deployed + #[display( + "{0.kind} tried to access code at {1} during validation, but that address is not a contract" + )] + AccessedUndeployedContract(Entity, Address), + /// The user operation factory entity called CREATE2 more than once during initialization + #[display("factory may only call CREATE2 once during initialization")] + FactoryCalledCreate2Twice(Address), + /// The user operation accessed a storage slot that is not allowed + #[display("{0.kind} accessed forbidden storage at address {1:?} during validation")] + InvalidStorageAccess(Entity, StorageSlot), + /// The user operation called an entry point method that is not allowed + #[display("{0.kind} called entry point method other than depositTo")] + CalledBannedEntryPointMethod(Entity), + /// The user operation made a call that contained value to a contract other than the entrypoint + /// during validation + #[display("{0.kind} must not send ETH during validation (except from account to entry point)")] + CallHadValue(Entity), + /// The code hash of accessed contracts changed on the second simulation + #[display("code accessed by validation has changed since the last time validation was run")] + CodeHashChanged, + /// The user operation contained an entity that accessed storage without being staked + #[display("{0.needs_stake} needs to be staked: {0.accessing_entity} accessed storage at {0.accessed_address} slot {0.slot} (associated with {0.accessed_entity:?})")] + NotStaked(Box), + /// The user operation uses a paymaster that returns a context while being unstaked + #[display("Unstaked paymaster must not return context")] + UnstakedPaymasterContext, + /// The user operation uses an aggregator entity and it is not staked + #[display("An aggregator must be staked, regardless of storager usage")] + UnstakedAggregator, + /// Simulation reverted with an unintended reason, containing a message + #[display("reverted while simulating {0} validation: {1}")] + UnintendedRevertWithMessage(EntityType, String, Option
), + /// Simulation reverted with an unintended reason + #[display("reverted while simulating {0} validation")] + UnintendedRevert(EntityType, Option
), + /// Simulation did not revert, a revert is always expected + #[display("simulateValidation did not revert. Make sure your EntryPoint is valid")] + DidNotRevert, + /// Simulation had the wrong number of phases + #[display("simulateValidation should have 3 parts but had {0} instead. Make sure your EntryPoint is valid")] + WrongNumberOfPhases(u32), + /// The user operation ran out of gas during validation + #[display("ran out of gas during {0.kind} validation")] + OutOfGas(Entity), + /// The user operation aggregator signature validation failed + #[display("aggregator signature validation failed")] + AggregatorValidationFailed, + /// Verification gas limit doesn't have the required buffer on the measured gas + #[display("verification gas limit doesn't have the required buffer on the measured gas, limit: {0}, needed: {1}")] + VerificationGasLimitBufferTooLow(U256, U256), +} + +/// Information about a storage violation based on stake status +#[derive(Debug, PartialEq, Clone, PartialOrd, Eq, Ord)] +pub struct NeedsStakeInformation { + /// Entity needing stake info + pub needs_stake: Entity, + /// The entity that accessed the storage requiring stake + pub accessing_entity: EntityType, + /// Type of accessed entity, if it is a known entity + pub accessed_entity: Option, + /// Address that was accessed while unstaked + pub accessed_address: Address, + /// The accessed slot number + pub slot: U256, + /// Minumum stake + pub min_stake: U256, + /// Minumum delay after an unstake event + pub min_unstake_delay: U256, +} diff --git a/crates/types/src/pool/mod.rs b/crates/types/src/pool/mod.rs new file mode 100644 index 000000000..d17f5bfd9 --- /dev/null +++ b/crates/types/src/pool/mod.rs @@ -0,0 +1,36 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +//! Rundler pool types + +mod error; +pub use error::*; + +mod traits; +pub use traits::*; + +mod types; +pub use types::*; diff --git a/crates/types/src/pool/traits.rs b/crates/types/src/pool/traits.rs new file mode 100644 index 000000000..d52ef06c2 --- /dev/null +++ b/crates/types/src/pool/traits.rs @@ -0,0 +1,130 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use std::pin::Pin; + +use ethers::types::{Address, H256}; +use futures_util::Stream; +#[cfg(feature = "test-utils")] +use mockall::automock; + +use super::{ + error::PoolError, + types::{NewHead, PaymasterMetadata, PoolOperation, Reputation, ReputationStatus, StakeStatus}, +}; +use crate::{EntityUpdate, UserOperationId, UserOperationVariant}; + +/// Result type for pool server operations. +pub type PoolResult = std::result::Result; + +/// Pool server trait +#[cfg_attr(feature = "test-utils", automock)] +#[async_trait::async_trait] +pub trait Pool: Send + Sync + 'static { + /// Get the supported entry points of the pool + async fn get_supported_entry_points(&self) -> PoolResult>; + + /// Add an operation to the pool + async fn add_op(&self, entry_point: Address, op: UserOperationVariant) -> PoolResult; + + /// Get operations from the pool + async fn get_ops( + &self, + entry_point: Address, + max_ops: u64, + shard_index: u64, + ) -> PoolResult>>; + + /// Get an operation from the pool by hash + /// Checks each entry point in order until the operation is found + /// Returns None if the operation is not found + async fn get_op_by_hash( + &self, + hash: H256, + ) -> PoolResult>>; + + /// Remove operations from the pool by hash + async fn remove_ops(&self, entry_point: Address, ops: Vec) -> PoolResult<()>; + + /// Remove an operation from the pool by id + async fn remove_op_by_id( + &self, + entry_point: Address, + id: UserOperationId, + ) -> PoolResult>; + + /// Update operations associated with entities from the pool + async fn update_entities( + &self, + entry_point: Address, + entities: Vec, + ) -> PoolResult<()>; + + /// Subscribe to new chain heads from the pool. + /// + /// The pool will notify the subscriber when a new chain head is received, and the pool + /// has processed all operations up to that head. + async fn subscribe_new_heads(&self) -> PoolResult + Send>>>; + + /// Get reputation status given entrypoint and address + async fn get_reputation_status( + &self, + entry_point: Address, + address: Address, + ) -> PoolResult; + + /// Get stake status given entrypoint and address + async fn get_stake_status( + &self, + entry_point: Address, + address: Address, + ) -> PoolResult; + + /// Clear the pool state, used for debug methods + async fn debug_clear_state( + &self, + clear_mempool: bool, + clear_paymaster: bool, + clear_reputation: bool, + ) -> PoolResult<()>; + + /// Dump all operations in the pool, used for debug methods + async fn debug_dump_mempool( + &self, + entry_point: Address, + ) -> PoolResult>>; + + /// Set reputations for entities, used for debug methods + async fn debug_set_reputations( + &self, + entry_point: Address, + reputations: Vec, + ) -> PoolResult<()>; + + /// Dump reputations for entities, used for debug methods + async fn debug_dump_reputation(&self, entry_point: Address) -> PoolResult>; + + /// Dump paymaster balances, used for debug methods + async fn debug_dump_paymaster_balances( + &self, + entry_point: Address, + ) -> PoolResult>; + + /// Controls whether or not the certain tracking data structures are used to block user operations + async fn admin_set_tracking( + &self, + entry_point: Address, + paymaster: bool, + reputation: bool, + ) -> PoolResult<()>; +} diff --git a/crates/types/src/pool/types.rs b/crates/types/src/pool/types.rs new file mode 100644 index 000000000..6d1ec71cf --- /dev/null +++ b/crates/types/src/pool/types.rs @@ -0,0 +1,250 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use ethers::types::{Address, H256, U256}; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + +use crate::{ + entity::EntityInfos, Entity, EntityType, StakeInfo, UserOperation, UserOperationVariant, + ValidTimeRange, +}; + +/// The new head of the chain, as viewed by the pool +#[derive(Clone, Debug)] +pub struct NewHead { + /// The hash of the new head + pub block_hash: H256, + /// The number of the new head + pub block_number: u64, +} + +impl Default for NewHead { + fn default() -> NewHead { + NewHead { + block_hash: H256::zero(), + block_number: 0, + } + } +} + +/// The reputation of an entity +#[derive(Debug, Clone)] +pub struct Reputation { + /// The entity's address + pub address: Address, + /// Number of ops seen in the current interval + pub ops_seen: u64, + /// Number of ops included in the current interval + pub ops_included: u64, +} + +/// Reputation status for an entity +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum ReputationStatus { + /// Entity is not throttled or banned + Ok, + /// Entity is throttled + Throttled, + /// Entity is banned + Banned, +} + +impl Serialize for ReputationStatus { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + ReputationStatus::Ok => serializer.serialize_str("ok"), + ReputationStatus::Throttled => serializer.serialize_str("throttled"), + ReputationStatus::Banned => serializer.serialize_str("banned"), + } + } +} + +impl<'de> Deserialize<'de> for ReputationStatus { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + "ok" => Ok(ReputationStatus::Ok), + "throttled" => Ok(ReputationStatus::Throttled), + "banned" => Ok(ReputationStatus::Banned), + _ => Err(de::Error::custom(format!("Invalid reputation status {s}"))), + } + } +} + +/// Stake status structure +#[derive(Debug, Clone, Copy)] +pub struct StakeStatus { + /// Address is staked + pub is_staked: bool, + /// Stake information about address + pub stake_info: StakeInfo, +} + +/// The metadata for a paymaster +#[derive(Debug, Default, Clone, Eq, PartialEq, Copy)] +pub struct PaymasterMetadata { + /// Paymaster address + pub address: Address, + /// The on-chain balance of the paymaster + pub confirmed_balance: U256, + /// The pending balance is the confirm balance subtracted by + /// the max cost of all the pending user operations that use the paymaster + pub pending_balance: U256, +} + +/// A user operation with additional metadata from validation. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct PoolOperation { + /// The user operation stored in the pool + pub uo: UO, + /// The entry point address for this operation + pub entry_point: Address, + /// The aggregator address for this operation, if any. + pub aggregator: Option
, + /// The valid time range for this operation. + pub valid_time_range: ValidTimeRange, + /// The expected code hash for all contracts accessed during validation for this operation. + pub expected_code_hash: H256, + /// The block hash simulation was completed at + pub sim_block_hash: H256, + /// The block number simulation was completed at + pub sim_block_number: u64, + /// List of entities that need to stake for this operation. + pub entities_needing_stake: Vec, + /// Whether the account is staked. + pub account_is_staked: bool, + /// Staking information about all the entities. + pub entity_infos: EntityInfos, +} + +impl PoolOperation { + /// Returns true if the operation contains the given entity. + pub fn contains_entity(&self, entity: &Entity) -> bool { + if let Some(e) = self.entity_infos.get(entity.kind) { + e.address == entity.address + } else { + false + } + } + + /// Returns true if the operation requires the given entity to stake. + /// + /// For non-accounts, its possible that the entity is staked, but doesn't + /// _need_ to stake for this operation. For example, if the operation does not + /// access any storage slots that require staking. In that case this function + /// will return false. + /// + /// For staked accounts, this function will always return true. Staked accounts + /// are able to circumvent the mempool operation limits always need their reputation + /// checked to prevent them from filling the pool. + pub fn requires_stake(&self, entity: EntityType) -> bool { + match entity { + EntityType::Account => self.account_is_staked, + _ => self.entities_needing_stake.contains(&entity), + } + } + + /// Returns an iterator over all entities that are included in this operation. + pub fn entities(&'_ self) -> impl Iterator + '_ { + self.entity_infos + .entities() + .map(|(t, entity)| Entity::new(t, entity.address)) + } + + /// Returns an iterator over all entities that need stake in this operation. This can be a subset of entities that are staked in the operation. + pub fn entities_requiring_stake(&'_ self) -> impl Iterator + '_ { + self.entity_infos.entities().filter_map(|(t, entity)| { + if self.requires_stake(t) { + Entity::new(t, entity.address).into() + } else { + None + } + }) + } + + /// Return all the unstaked entities that are used in this operation. + pub fn unstaked_entities(&'_ self) -> impl Iterator + '_ { + self.entity_infos.entities().filter_map(|(t, entity)| { + if entity.is_staked { + None + } else { + Entity::new(t, entity.address).into() + } + }) + } + + /// Compute the amount of heap memory the PoolOperation takes up. + pub fn mem_size(&self) -> usize { + std::mem::size_of::() + + self.uo.heap_size() + + self.entities_needing_stake.len() * std::mem::size_of::() + } +} + +/// Trait to convert a [PoolOperation] holding a [UserOperationVariant] to a [PoolOperation] with a different user operation type. +pub trait FromPoolOperationVariant { + /// Conversion + fn from_variant(op: PoolOperation) -> Self; +} + +/// Trait to convert a [PoolOperation] holding a user operation to a [PoolOperation] with a [UserOperationVariant]. +pub trait IntoPoolOperationVariant { + /// Conversion + fn into_variant(self) -> PoolOperation; +} + +impl FromPoolOperationVariant for PoolOperation +where + UO: UserOperation + From, +{ + fn from_variant(op: PoolOperation) -> Self { + PoolOperation { + uo: op.uo.into(), + entry_point: op.entry_point, + aggregator: op.aggregator, + valid_time_range: op.valid_time_range, + expected_code_hash: op.expected_code_hash, + sim_block_hash: op.sim_block_hash, + sim_block_number: op.sim_block_number, + entities_needing_stake: op.entities_needing_stake, + account_is_staked: op.account_is_staked, + entity_infos: op.entity_infos, + } + } +} + +impl IntoPoolOperationVariant for PoolOperation +where + UO: UserOperation + Into, +{ + fn into_variant(self) -> PoolOperation { + PoolOperation { + uo: self.uo.into(), + entry_point: self.entry_point, + aggregator: self.aggregator, + valid_time_range: self.valid_time_range, + expected_code_hash: self.expected_code_hash, + sim_block_hash: self.sim_block_hash, + sim_block_number: self.sim_block_number, + entities_needing_stake: self.entities_needing_stake, + account_is_staked: self.account_is_staked, + entity_infos: self.entity_infos, + } + } +} From 16076b7e32ab73b1b4c83aea6adcc337f4478192 Mon Sep 17 00:00:00 2001 From: dancoombs Date: Thu, 21 Mar 2024 20:21:36 -0400 Subject: [PATCH 05/12] feat(pool): Add entry point routing to pool --- bin/rundler/src/cli/pool.rs | 4 +- crates/builder/src/bundle_proposer.rs | 63 +- crates/pool/src/chain.rs | 738 +++++++++++++++++++----- crates/pool/src/mempool/mod.rs | 27 +- crates/pool/src/mempool/paymaster.rs | 36 +- crates/pool/src/mempool/pool.rs | 121 ++-- crates/pool/src/mempool/uo_pool.rs | 121 ++-- crates/pool/src/server/local.rs | 125 ++-- crates/pool/src/server/remote/client.rs | 12 +- crates/pool/src/server/remote/protos.rs | 6 +- crates/pool/src/task.rs | 69 ++- crates/rpc/src/eth/api.rs | 6 +- crates/rpc/src/task.rs | 1 + crates/types/src/pool/traits.rs | 12 +- crates/types/src/pool/types.rs | 58 +- crates/types/src/user_operation/mod.rs | 10 + crates/types/src/user_operation/v0_6.rs | 22 + crates/types/src/user_operation/v0_7.rs | 22 + 18 files changed, 982 insertions(+), 471 deletions(-) diff --git a/bin/rundler/src/cli/pool.rs b/bin/rundler/src/cli/pool.rs index 66df53cf9..639103f2c 100644 --- a/bin/rundler/src/cli/pool.rs +++ b/bin/rundler/src/cli/pool.rs @@ -19,7 +19,7 @@ use ethers::types::H256; use rundler_pool::{LocalPoolBuilder, PoolConfig, PoolTask, PoolTaskArgs}; use rundler_sim::MempoolConfig; use rundler_task::spawn_tasks_with_shutdown; -use rundler_types::chain::ChainSpec; +use rundler_types::{chain::ChainSpec, EntryPointVersion}; use rundler_utils::emit::{self, EVENT_CHANNEL_CAPACITY}; use tokio::sync::broadcast; @@ -181,8 +181,10 @@ impl PoolArgs { tracing::info!("Mempool channel configs: {:?}", mempool_channel_configs); let chain_id = chain_spec.id; + // TODO(danc): multiple pool configs let pool_config = PoolConfig { entry_point: chain_spec.entry_point_address, + entry_point_version: EntryPointVersion::V0_6, chain_id, // Currently use the same shard count as the number of builders num_shards: common.num_builders, diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index ed8c5159a..6ec830050 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -37,7 +37,7 @@ use rundler_sim::{ }; use rundler_types::{ chain::ChainSpec, - pool::{FromPoolOperationVariant, Pool, PoolOperation, SimulationViolation}, + pool::{Pool, PoolOperation, SimulationViolation}, Entity, EntityInfo, EntityInfos, EntityType, EntityUpdate, EntityUpdateType, GasFees, GasOverheads, Timestamp, UserOperation, UserOperationVariant, UserOpsPerAggregator, }; @@ -131,6 +131,7 @@ pub(crate) struct Settings { impl BundleProposer for BundleProposerImpl where UO: UserOperation + From, + UserOperationVariant: AsRef, S: Simulator, E: EntryPoint + SignatureAggregator + BundleHandler + L1GasProvider, P: Provider, @@ -236,6 +237,7 @@ where impl BundleProposerImpl where UO: UserOperation + From, + UserOperationVariant: AsRef, S: Simulator, E: EntryPoint + SignatureAggregator + BundleHandler + L1GasProvider, P: Provider, @@ -276,11 +278,13 @@ where // - any errors async fn filter_and_simulate( &self, - op: PoolOperation, + op: PoolOperation, block_hash: H256, base_fee: U256, required_op_fees: GasFees, - ) -> Option<(PoolOperation, Result)> { + ) -> Option<(PoolOperation, Result)> { + let op_hash = self.op_hash(&op.uo); + // filter by fees if op.uo.max_fee_per_gas() < required_op_fees.max_fee_per_gas || op.uo.max_priority_fee_per_gas() < required_op_fees.max_priority_fee_per_gas @@ -303,14 +307,14 @@ where let required_pvg = gas::calc_required_pre_verification_gas( &self.settings.chain_spec, &self.entry_point, - &op.uo, + op.uo.as_ref(), base_fee, ) .await .map_err(|e| { self.emit(BuilderEvent::skipped_op( self.builder_index, - self.op_hash(&op.uo), + op_hash, SkipReason::Other { reason: Arc::new(format!( "Failed to calculate required pre-verification gas for op: {e:?}, skipping" @@ -324,7 +328,7 @@ where if op.uo.pre_verification_gas() < required_pvg { self.emit(BuilderEvent::skipped_op( self.builder_index, - self.op_hash(&op.uo), + op_hash, SkipReason::InsufficientPreVerificationGas { base_fee, op_fees: GasFees { @@ -341,7 +345,11 @@ where // Simulate let result = self .simulator - .simulate_validation(op.uo.clone(), Some(block_hash), Some(op.expected_code_hash)) + .simulate_validation( + op.uo.clone().into(), + Some(block_hash), + Some(op.expected_code_hash), + ) .await; let result = match result { Ok(success) => (op, Ok(success)), @@ -356,7 +364,7 @@ where } => { self.emit(BuilderEvent::skipped_op( self.builder_index, - self.op_hash(&op.uo), + op_hash, SkipReason::Other { reason: Arc::new(format!("Failed to simulate op: {error:?}, skipping")), }, @@ -371,14 +379,14 @@ where async fn assemble_context( &self, - ops_with_simulations: Vec<(PoolOperation, Result)>, + ops_with_simulations: Vec<(PoolOperation, Result)>, mut balances_by_paymaster: HashMap, ) -> ProposalContext { let all_sender_addresses: HashSet
= ops_with_simulations .iter() .map(|(op, _)| op.uo.sender()) .collect(); - let mut context = ProposalContext::new(); + let mut context = ProposalContext::::new(); let mut paymasters_to_reject = Vec::::new(); let ov = GasOverheads::default(); @@ -403,7 +411,7 @@ where // try to use EntityInfos from the latest simulation, but if it doesn't exist use the EntityInfos from the previous simulation let infos = entity_infos.map_or(po.entity_infos, |e| e); context.process_simulation_violations(violations, infos); - context.rejected_ops.push((op, po.entity_infos)); + context.rejected_ops.push((op.into(), po.entity_infos)); } continue; } @@ -421,7 +429,7 @@ where valid_range: simulation.valid_time_range, }, )); - context.rejected_ops.push((op, po.entity_infos)); + context.rejected_ops.push((op.into(), po.entity_infos)); continue; } @@ -471,7 +479,10 @@ where .entry(simulation.aggregator_address()) .or_default() .ops_with_simulations - .push(OpWithSimulation { op, simulation }); + .push(OpWithSimulation { + op: op.into(), + simulation, + }); } for paymaster in paymasters_to_reject { // No need to update aggregator signatures because we haven't computed them yet. @@ -578,7 +589,7 @@ where } } - async fn get_ops_from_pool(&self) -> anyhow::Result>> { + async fn get_ops_from_pool(&self) -> anyhow::Result> { // Use builder's index as the shard index to ensure that two builders don't // attempt to bundle the same operations. // @@ -594,7 +605,6 @@ where .await .context("should get ops from pool")? .into_iter() - .map(PoolOperation::::from_variant) .collect()) } @@ -818,8 +828,8 @@ where fn limit_user_operations_for_simulation( &self, - ops: Vec>, - ) -> (Vec>, u64) { + ops: Vec, + ) -> (Vec, u64) { // Make the bundle gas limit 10% higher here so that we simulate more UOs than we need in case that we end up dropping some UOs later so we can still pack a full bundle let mut gas_left = math::increase_by_percent(U256::from(self.settings.max_bundle_gas), 10); let mut ops_in_bundle = Vec::new(); @@ -854,7 +864,10 @@ where }); } - fn op_hash(&self, op: &UO) -> H256 { + fn op_hash(&self, op: &T) -> H256 + where + T: UserOperation, + { op.hash(self.entry_point.address(), self.settings.chain_spec.id) } } @@ -1250,7 +1263,7 @@ mod tests { use rundler_provider::{AggregatorSimOut, MockEntryPointV0_6, MockProvider}; use rundler_sim::MockSimulator; use rundler_types::{ - pool::{IntoPoolOperationVariant, MockPool, SimulationViolation}, + pool::{MockPool, SimulationViolation}, v0_6::UserOperation, UserOperation as UserOperationTrait, ValidTimeRange, }; @@ -2025,7 +2038,7 @@ mod tests { let ops: Vec<_> = mock_ops .iter() .map(|MockOp { op, .. }| PoolOperation { - uo: op.clone(), + uo: op.clone().into(), expected_code_hash, entry_point: entry_point_address, sim_block_hash: current_block_hash, @@ -2039,13 +2052,9 @@ mod tests { .collect(); let mut pool_client = MockPool::new(); - pool_client.expect_get_ops().returning(move |_, _, _| { - Ok(ops - .iter() - .cloned() - .map(IntoPoolOperationVariant::into_variant) - .collect()) - }); + pool_client + .expect_get_ops() + .returning(move |_, _, _| Ok(ops.clone())); let simulations_by_op: HashMap<_, _> = mock_ops .into_iter() diff --git a/crates/pool/src/chain.rs b/crates/pool/src/chain.rs index b35bfcb3c..c62754866 100644 --- a/crates/pool/src/chain.rs +++ b/crates/pool/src/chain.rs @@ -12,14 +12,14 @@ // If not, see https://www.gnu.org/licenses/. use std::{ - collections::{HashSet, VecDeque}, + collections::{HashMap, HashSet, VecDeque}, sync::Arc, time::Duration, }; use anyhow::{ensure, Context}; use ethers::{ - contract, + contract::EthLogDecode, prelude::EthEvent, types::{Address, Block, Filter, Log, H256, U256}, }; @@ -27,11 +27,8 @@ use futures::future; use rundler_provider::Provider; use rundler_task::block_watcher; use rundler_types::{ - contracts::v0_6::{ - entry_point::{DepositedFilter, WithdrawnFilter}, - i_entry_point::UserOperationEventFilter, - }, - Timestamp, UserOperationId, + contracts::{v0_6::entry_point as entry_point_v0_6, v0_7::entry_point as entry_point_v0_7}, + EntryPointVersion, Timestamp, UserOperationId, }; use tokio::{ select, @@ -110,7 +107,7 @@ impl MinedOp { pub(crate) struct Settings { pub(crate) history_size: u64, pub(crate) poll_interval: Duration, - pub(crate) entry_point_addresses: Vec
, + pub(crate) entry_point_addresses: HashMap, } #[derive(Debug)] @@ -402,12 +399,33 @@ impl Chain

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

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

, withdrawal_addresses: Vec
, } impl MockBlock { - fn new( - hash: H256, + fn new(hash: H256) -> Self { + Self { + hash, + events: vec![], + } + } + + fn add_ep( + mut self, + address: Address, op_hashes: Vec, deposit_addresses: Vec
, withdrawal_addresses: Vec
, ) -> Self { - Self { - hash, + self.events.push(MockEntryPointEvents { + address, op_hashes, deposit_addresses, withdrawal_addresses, - } + }); + self } } @@ -683,21 +762,44 @@ mod tests { }; let mut joined_logs: Vec = Vec::new(); - joined_logs.extend(block.op_hashes.iter().copied().map(fake_log)); - joined_logs.extend( - block - .deposit_addresses - .iter() - .copied() - .map(fake_deposit_log), - ); - joined_logs.extend( - block - .withdrawal_addresses - .iter() - .copied() - .map(fake_withdrawal_log), - ); + + for events in &block.events { + if events.address == ENTRY_POINT_ADDRESS_V0_6 { + joined_logs.extend(events.op_hashes.iter().copied().map(fake_mined_log_v0_6)); + joined_logs.extend( + events + .deposit_addresses + .iter() + .copied() + .map(fake_deposit_log_v0_6), + ); + joined_logs.extend( + events + .withdrawal_addresses + .iter() + .copied() + .map(fake_withdrawal_log_v0_6), + ); + } else if events.address == ENTRY_POINT_ADDRESS_V0_7 { + joined_logs.extend(events.op_hashes.iter().copied().map(fake_mined_log_v0_7)); + joined_logs.extend( + events + .deposit_addresses + .iter() + .copied() + .map(fake_deposit_log_v0_7), + ); + joined_logs.extend( + events + .withdrawal_addresses + .iter() + .copied() + .map(fake_withdrawal_log_v0_7), + ); + } else { + panic!("Unknown entry point address: {:?}", events.address); + } + } joined_logs } @@ -707,10 +809,25 @@ mod tests { async fn test_initial_load() { let (mut chain, controller) = new_chain(); controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(101), hash(102)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(103)], vec![], vec![]), - MockBlock::new(hash(2), vec![], vec![], vec![]), - MockBlock::new(hash(3), vec![hash(104), hash(105)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101), hash(102)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(103)], + vec![], + vec![], + ), + MockBlock::new(hash(2)).add_ep(ENTRY_POINT_ADDRESS_V0_6, vec![], vec![], vec![]), + MockBlock::new(hash(3)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(104), hash(105)], + vec![], + vec![], + ), ]); let update = chain.sync_to_block(controller.get_head()).await.unwrap(); // With a history size of 3, we should get updates from all blocks except the first one. @@ -722,7 +839,11 @@ mod tests { latest_block_timestamp: 0.into(), earliest_remembered_block_number: 1, reorg_depth: 0, - mined_ops: vec![fake_mined_op(103), fake_mined_op(104), fake_mined_op(105),], + mined_ops: vec![ + fake_mined_op(103, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(104, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(105, ENTRY_POINT_ADDRESS_V0_6), + ], unmined_ops: vec![], entity_balance_updates: vec![], unmined_entity_balance_updates: vec![], @@ -735,15 +856,35 @@ mod tests { async fn test_simple_advance() { let (mut chain, controller) = new_chain(); controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(101), hash(102)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(103)], vec![], vec![]), - MockBlock::new(hash(2), vec![], vec![], vec![]), - MockBlock::new(hash(3), vec![hash(104), hash(105)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101), hash(102)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(103)], + vec![], + vec![], + ), + MockBlock::new(hash(2)).add_ep(ENTRY_POINT_ADDRESS_V0_6, vec![], vec![], vec![]), + MockBlock::new(hash(3)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(104), hash(105)], + vec![], + vec![], + ), ]); chain.sync_to_block(controller.get_head()).await.unwrap(); controller .get_blocks_mut() - .push(MockBlock::new(hash(4), vec![hash(106)], vec![], vec![])); + .push(MockBlock::new(hash(4)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(106)], + vec![], + vec![], + )); let update = chain.sync_to_block(controller.get_head()).await.unwrap(); assert_eq!( update, @@ -753,7 +894,7 @@ mod tests { latest_block_timestamp: 0.into(), earliest_remembered_block_number: 2, reorg_depth: 0, - mined_ops: vec![fake_mined_op(106)], + mined_ops: vec![fake_mined_op(106, ENTRY_POINT_ADDRESS_V0_6)], unmined_ops: vec![], entity_balance_updates: vec![], unmined_entity_balance_updates: vec![], @@ -766,10 +907,20 @@ mod tests { async fn test_forward_reorg() { let (mut chain, controller) = new_chain(); controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(100)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(101)], vec![], vec![]), - MockBlock::new( - hash(2), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(100)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101)], + vec![], + vec![], + ), + MockBlock::new(hash(2)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, vec![hash(102)], vec![Address::zero()], vec![addr(1)], @@ -781,9 +932,24 @@ mod tests { let mut blocks = controller.get_blocks_mut(); blocks.pop(); blocks.extend([ - MockBlock::new(hash(12), vec![hash(112)], vec![], vec![]), - MockBlock::new(hash(13), vec![hash(113)], vec![], vec![]), - MockBlock::new(hash(14), vec![hash(114)], vec![], vec![addr(3)]), + MockBlock::new(hash(12)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(112)], + vec![], + vec![], + ), + MockBlock::new(hash(13)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(113)], + vec![], + vec![], + ), + MockBlock::new(hash(14)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(114)], + vec![], + vec![addr(3)], + ), ]); } let update = chain.sync_to_block(controller.get_head()).await.unwrap(); @@ -795,12 +961,21 @@ mod tests { latest_block_timestamp: 0.into(), earliest_remembered_block_number: 2, reorg_depth: 1, - mined_ops: vec![fake_mined_op(112), fake_mined_op(113), fake_mined_op(114)], - unmined_ops: vec![fake_mined_op(102)], - entity_balance_updates: vec![fake_mined_balance_update(addr(3), 0.into(), false)], + mined_ops: vec![ + fake_mined_op(112, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(113, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(114, ENTRY_POINT_ADDRESS_V0_6) + ], + unmined_ops: vec![fake_mined_op(102, ENTRY_POINT_ADDRESS_V0_6)], + entity_balance_updates: vec![fake_mined_balance_update( + addr(3), + 0.into(), + false, + ENTRY_POINT_ADDRESS_V0_6 + )], unmined_entity_balance_updates: vec![ - fake_mined_balance_update(addr(0), 0.into(), true), - fake_mined_balance_update(addr(1), 0.into(), false), + fake_mined_balance_update(addr(0), 0.into(), true, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_balance_update(addr(1), 0.into(), false, ENTRY_POINT_ADDRESS_V0_6), ], reorg_larger_than_history: false, } @@ -811,9 +986,24 @@ mod tests { async fn test_sideways_reorg() { let (mut chain, controller) = new_chain(); controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(100)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(101)], vec![addr(1)], vec![addr(9)]), - MockBlock::new(hash(2), vec![hash(102)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(100)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101)], + vec![addr(1)], + vec![addr(9)], + ), + MockBlock::new(hash(2)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(102)], + vec![], + vec![], + ), ]); chain.sync_to_block(controller.get_head()).await.unwrap(); { @@ -822,25 +1012,46 @@ mod tests { blocks.pop(); blocks.pop(); blocks.extend([ - MockBlock::new(hash(11), vec![hash(111)], vec![addr(2)], vec![]), - MockBlock::new(hash(12), vec![hash(112)], vec![], vec![]), + MockBlock::new(hash(11)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(111)], + vec![addr(2)], + vec![], + ), + MockBlock::new(hash(12)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(112)], + vec![], + vec![], + ), ]); } let update = chain.sync_to_block(controller.get_head()).await.unwrap(); assert_eq!( update, ChainUpdate { - entity_balance_updates: vec![fake_mined_balance_update(addr(2), 0.into(), true)], + entity_balance_updates: vec![fake_mined_balance_update( + addr(2), + 0.into(), + true, + ENTRY_POINT_ADDRESS_V0_6 + )], latest_block_number: 2, latest_block_hash: hash(12), latest_block_timestamp: 0.into(), earliest_remembered_block_number: 0, reorg_depth: 2, - mined_ops: vec![fake_mined_op(111), fake_mined_op(112)], - unmined_ops: vec![fake_mined_op(101), fake_mined_op(102)], + mined_ops: vec![ + fake_mined_op(111, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(112, ENTRY_POINT_ADDRESS_V0_6) + ], + unmined_ops: vec![ + fake_mined_op(101, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(102, ENTRY_POINT_ADDRESS_V0_6) + ], unmined_entity_balance_updates: vec![ - fake_mined_balance_update(addr(1), 0.into(), true), - fake_mined_balance_update(addr(9), 0.into(), false), + fake_mined_balance_update(addr(1), 0.into(), true, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_balance_update(addr(9), 0.into(), false, ENTRY_POINT_ADDRESS_V0_6), ], reorg_larger_than_history: false, } @@ -851,9 +1062,24 @@ mod tests { async fn test_backwards_reorg() { let (mut chain, controller) = new_chain(); controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(100)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(101)], vec![], vec![]), - MockBlock::new(hash(2), vec![hash(102)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(100)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101)], + vec![], + vec![], + ), + MockBlock::new(hash(2)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(102)], + vec![], + vec![], + ), ]); chain.sync_to_block(controller.get_head()).await.unwrap(); { @@ -861,8 +1087,8 @@ mod tests { let mut blocks = controller.get_blocks_mut(); blocks.pop(); blocks.pop(); - blocks.push(MockBlock::new( - hash(11), + blocks.push(MockBlock::new(hash(11)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, vec![hash(111)], vec![addr(1)], vec![], @@ -873,13 +1099,21 @@ mod tests { update, ChainUpdate { latest_block_number: 1, - entity_balance_updates: vec![fake_mined_balance_update(addr(1), 0.into(), true)], + entity_balance_updates: vec![fake_mined_balance_update( + addr(1), + 0.into(), + true, + ENTRY_POINT_ADDRESS_V0_6 + )], latest_block_hash: hash(11), latest_block_timestamp: 0.into(), earliest_remembered_block_number: 0, reorg_depth: 2, - mined_ops: vec![fake_mined_op(111)], - unmined_ops: vec![fake_mined_op(101), fake_mined_op(102)], + mined_ops: vec![fake_mined_op(111, ENTRY_POINT_ADDRESS_V0_6)], + unmined_ops: vec![ + fake_mined_op(101, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(102, ENTRY_POINT_ADDRESS_V0_6) + ], unmined_entity_balance_updates: vec![], reorg_larger_than_history: false, } @@ -890,18 +1124,58 @@ mod tests { async fn test_reorg_longer_than_history() { let (mut chain, controller) = new_chain(); controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(100)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(101)], vec![], vec![]), - MockBlock::new(hash(2), vec![hash(102)], vec![], vec![]), - MockBlock::new(hash(3), vec![hash(103)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(100)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101)], + vec![], + vec![], + ), + MockBlock::new(hash(2)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(102)], + vec![], + vec![], + ), + MockBlock::new(hash(3)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(103)], + vec![], + vec![], + ), ]); chain.sync_to_block(controller.get_head()).await.unwrap(); // The history has size 3, so after this update it's completely unrecognizable. controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(100)], vec![], vec![]), - MockBlock::new(hash(11), vec![hash(111)], vec![], vec![]), - MockBlock::new(hash(12), vec![hash(112)], vec![], vec![]), - MockBlock::new(hash(13), vec![hash(113)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(100)], + vec![], + vec![], + ), + MockBlock::new(hash(11)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(111)], + vec![], + vec![], + ), + MockBlock::new(hash(12)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(112)], + vec![], + vec![], + ), + MockBlock::new(hash(13)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(113)], + vec![], + vec![], + ), ]); let update = chain.sync_to_block(controller.get_head()).await.unwrap(); assert_eq!( @@ -912,8 +1186,16 @@ mod tests { latest_block_timestamp: 0.into(), earliest_remembered_block_number: 1, reorg_depth: 3, - mined_ops: vec![fake_mined_op(111), fake_mined_op(112), fake_mined_op(113)], - unmined_ops: vec![fake_mined_op(101), fake_mined_op(102), fake_mined_op(103)], + mined_ops: vec![ + fake_mined_op(111, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(112, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(113, ENTRY_POINT_ADDRESS_V0_6) + ], + unmined_ops: vec![ + fake_mined_op(101, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(102, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(103, ENTRY_POINT_ADDRESS_V0_6) + ], entity_balance_updates: vec![], unmined_entity_balance_updates: vec![], reorg_larger_than_history: true, @@ -925,16 +1207,31 @@ mod tests { async fn test_advance_larger_than_history_size() { let (mut chain, controller) = new_chain(); controller.set_blocks(vec![ - MockBlock::new(hash(0), vec![hash(100)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(101)], vec![], vec![]), - MockBlock::new(hash(2), vec![hash(102)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(100)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101)], + vec![], + vec![], + ), + MockBlock::new(hash(2)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(102)], + vec![], + vec![], + ), ]); chain.sync_to_block(controller.get_head()).await.unwrap(); { let mut blocks = controller.get_blocks_mut(); for i in 3..7 { - blocks.push(MockBlock::new( - hash(10 + i), + blocks.push(MockBlock::new(hash(10 + i)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, vec![hash(100 + i)], vec![], vec![], @@ -952,7 +1249,11 @@ mod tests { reorg_depth: 0, entity_balance_updates: vec![], unmined_entity_balance_updates: vec![], - mined_ops: vec![fake_mined_op(104), fake_mined_op(105), fake_mined_op(106)], + mined_ops: vec![ + fake_mined_op(104, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(105, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(106, ENTRY_POINT_ADDRESS_V0_6) + ], unmined_ops: vec![], reorg_larger_than_history: false, } @@ -964,8 +1265,18 @@ mod tests { async fn test_latest_block_number_smaller_than_history_size() { let (mut chain, controller) = new_chain(); let blocks = vec![ - MockBlock::new(hash(0), vec![hash(101), hash(102)], vec![], vec![]), - MockBlock::new(hash(1), vec![hash(103)], vec![], vec![]), + MockBlock::new(hash(0)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101), hash(102)], + vec![], + vec![], + ), + MockBlock::new(hash(1)).add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(103)], + vec![], + vec![], + ), ]; controller.set_blocks(blocks); let update = chain.sync_to_block(controller.get_head()).await.unwrap(); @@ -977,7 +1288,11 @@ mod tests { latest_block_timestamp: 0.into(), earliest_remembered_block_number: 0, reorg_depth: 0, - mined_ops: vec![fake_mined_op(101), fake_mined_op(102), fake_mined_op(103),], + mined_ops: vec![ + fake_mined_op(101, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(102, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(103, ENTRY_POINT_ADDRESS_V0_6), + ], unmined_ops: vec![], entity_balance_updates: vec![], unmined_entity_balance_updates: vec![], @@ -986,6 +1301,54 @@ mod tests { ); } + #[tokio::test] + async fn test_mixed_event_types() { + let (mut chain, controller) = new_chain(); + controller.set_blocks(vec![MockBlock::new(hash(0)) + .add_ep( + ENTRY_POINT_ADDRESS_V0_6, + vec![hash(101), hash(102)], + vec![addr(1), addr(2)], + vec![addr(3), addr(4)], + ) + .add_ep( + ENTRY_POINT_ADDRESS_V0_7, + vec![hash(201), hash(202)], + vec![addr(5), addr(6)], + vec![addr(7), addr(8)], + )]); + let update = chain.sync_to_block(controller.get_head()).await.unwrap(); + assert_eq!( + update, + ChainUpdate { + latest_block_number: 0, + latest_block_hash: hash(0), + latest_block_timestamp: 0.into(), + earliest_remembered_block_number: 0, + reorg_depth: 0, + mined_ops: vec![ + fake_mined_op(101, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(102, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_op(201, ENTRY_POINT_ADDRESS_V0_7), + fake_mined_op(202, ENTRY_POINT_ADDRESS_V0_7), + ], + unmined_ops: vec![], + entity_balance_updates: vec![ + fake_mined_balance_update(addr(1), 0.into(), true, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_balance_update(addr(2), 0.into(), true, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_balance_update(addr(3), 0.into(), false, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_balance_update(addr(4), 0.into(), false, ENTRY_POINT_ADDRESS_V0_6), + fake_mined_balance_update(addr(5), 0.into(), true, ENTRY_POINT_ADDRESS_V0_7), + fake_mined_balance_update(addr(6), 0.into(), true, ENTRY_POINT_ADDRESS_V0_7), + fake_mined_balance_update(addr(7), 0.into(), false, ENTRY_POINT_ADDRESS_V0_7), + fake_mined_balance_update(addr(8), 0.into(), false, ENTRY_POINT_ADDRESS_V0_7), + ], + unmined_entity_balance_updates: vec![], + reorg_larger_than_history: false, + } + ); + } + fn new_chain() -> (Chain, ProviderController) { let (provider, controller) = new_mock_provider(); let chain = Chain::new( @@ -993,7 +1356,10 @@ mod tests { Settings { history_size: HISTORY_SIZE, poll_interval: Duration::from_secs(250), // Not used in tests. - entry_point_addresses: vec![ENTRY_POINT_ADDRESS], + entry_point_addresses: HashMap::from([ + (ENTRY_POINT_ADDRESS_V0_6, EntryPointVersion::V0_6), + (ENTRY_POINT_ADDRESS_V0_7, EntryPointVersion::V0_7), + ]), }, ); (chain, controller) @@ -1023,12 +1389,69 @@ mod tests { (provider, controller) } - fn fake_log(op_hash: H256) -> Log { + fn fake_mined_log_v0_6(op_hash: H256) -> Log { + Log { + address: ENTRY_POINT_ADDRESS_V0_6, + topics: vec![ + H256::from(utils::keccak256( + entry_point_v0_6::UserOperationEventFilter::abi_signature().as_bytes(), + )), + op_hash, + H256::zero(), // sender + H256::zero(), // paymaster + ], + data: AbiEncode::encode(( + U256::zero(), // nonce + true, // success + U256::zero(), // actual_gas_cost + U256::zero(), // actual_gas_used + )) + .into(), + ..Default::default() + } + } + + fn fake_deposit_log_v0_6(deposit_address: Address) -> Log { + Log { + address: ENTRY_POINT_ADDRESS_V0_6, + topics: vec![ + H256::from(utils::keccak256( + entry_point_v0_6::DepositedFilter::abi_signature().as_bytes(), + )), + H256::from(deposit_address), + ], + data: AbiEncode::encode(( + U256::zero(), // totalDeposits + )) + .into(), + ..Default::default() + } + } + + fn fake_withdrawal_log_v0_6(withdrawal_address: Address) -> Log { + Log { + address: ENTRY_POINT_ADDRESS_V0_6, + topics: vec![ + H256::from(utils::keccak256( + entry_point_v0_6::WithdrawnFilter::abi_signature().as_bytes(), + )), + H256::from(withdrawal_address), + ], + data: AbiEncode::encode(( + Address::zero(), // withdrawAddress + U256::zero(), // amount + )) + .into(), + ..Default::default() + } + } + + fn fake_mined_log_v0_7(op_hash: H256) -> Log { Log { - address: ENTRY_POINT_ADDRESS, + address: ENTRY_POINT_ADDRESS_V0_7, topics: vec![ H256::from(utils::keccak256( - UserOperationEventFilter::abi_signature().as_bytes(), + entry_point_v0_7::UserOperationEventFilter::abi_signature().as_bytes(), )), op_hash, H256::zero(), // sender @@ -1045,12 +1468,12 @@ mod tests { } } - fn fake_deposit_log(deposit_address: Address) -> Log { + fn fake_deposit_log_v0_7(deposit_address: Address) -> Log { Log { - address: ENTRY_POINT_ADDRESS, + address: ENTRY_POINT_ADDRESS_V0_7, topics: vec![ H256::from(utils::keccak256( - DepositedFilter::abi_signature().as_bytes(), + entry_point_v0_7::DepositedFilter::abi_signature().as_bytes(), )), H256::from(deposit_address), ], @@ -1062,12 +1485,12 @@ mod tests { } } - fn fake_withdrawal_log(withdrawal_address: Address) -> Log { + fn fake_withdrawal_log_v0_7(withdrawal_address: Address) -> Log { Log { - address: ENTRY_POINT_ADDRESS, + address: ENTRY_POINT_ADDRESS_V0_7, topics: vec![ H256::from(utils::keccak256( - WithdrawnFilter::abi_signature().as_bytes(), + entry_point_v0_7::WithdrawnFilter::abi_signature().as_bytes(), )), H256::from(withdrawal_address), ], @@ -1080,10 +1503,10 @@ mod tests { } } - fn fake_mined_op(n: u8) -> MinedOp { + fn fake_mined_op(n: u8, ep: Address) -> MinedOp { MinedOp { hash: hash(n), - entry_point: ENTRY_POINT_ADDRESS, + entry_point: ep, sender: Address::zero(), nonce: U256::zero(), actual_gas_cost: U256::zero(), @@ -1095,10 +1518,11 @@ mod tests { address: Address, amount: U256, is_addition: bool, + ep: Address, ) -> BalanceUpdate { BalanceUpdate { address, - entrypoint: ENTRY_POINT_ADDRESS, + entrypoint: ep, amount, is_addition, } diff --git a/crates/pool/src/mempool/mod.rs b/crates/pool/src/mempool/mod.rs index 401e2e88e..d034d2d96 100644 --- a/crates/pool/src/mempool/mod.rs +++ b/crates/pool/src/mempool/mod.rs @@ -36,7 +36,7 @@ use rundler_types::{ pool::{ MempoolError, PaymasterMetadata, PoolOperation, Reputation, ReputationStatus, StakeStatus, }, - EntityUpdate, UserOperation, UserOperationId, + EntityUpdate, EntryPointVersion, UserOperationId, UserOperationVariant, }; use tonic::async_trait; pub(crate) use uo_pool::UoPool; @@ -45,21 +45,25 @@ use super::chain::ChainUpdate; pub(crate) type MempoolResult = std::result::Result; -#[cfg_attr(test, automock(type UO = rundler_types::v0_6::UserOperation;))] +#[cfg_attr(test, automock)] #[async_trait] /// In-memory operation pool pub trait Mempool: Send + Sync + 'static { - /// The type of user operation this pool stores - type UO: UserOperation; - /// Call to update the mempool with a new chain update async fn on_chain_update(&self, update: &ChainUpdate); /// Returns the entry point address this pool targets. fn entry_point(&self) -> Address; + /// Returns the entry point version this pool targets. + fn entry_point_version(&self) -> EntryPointVersion; + /// Adds a user operation to the pool - async fn add_operation(&self, origin: OperationOrigin, op: Self::UO) -> MempoolResult; + async fn add_operation( + &self, + origin: OperationOrigin, + op: UserOperationVariant, + ) -> MempoolResult; /// Removes a set of operations from the pool. fn remove_operations(&self, hashes: &[H256]); @@ -82,13 +86,13 @@ pub trait Mempool: Send + Sync + 'static { &self, max: usize, shard_index: u64, - ) -> MempoolResult>>>; + ) -> MempoolResult>>; /// Returns the all operations from the pool up to a max size - fn all_operations(&self, max: usize) -> Vec>>; + fn all_operations(&self, max: usize) -> Vec>; /// Looks up a user operation by hash, returns None if not found - fn get_user_operation_by_hash(&self, hash: H256) -> Option>>; + fn get_user_operation_by_hash(&self, hash: H256) -> Option>; /// Debug methods @@ -122,6 +126,8 @@ pub trait Mempool: Send + Sync + 'static { pub struct PoolConfig { /// Address of the entry point this pool targets pub entry_point: Address, + /// Version of the entry point this pool targets + pub entry_point_version: EntryPointVersion, /// Chain ID this pool targets pub chain_id: u64, /// The maximum number of operations an unstaked sender can have in the mempool @@ -192,7 +198,8 @@ mod tests { paymaster_and_data: paymaster.as_fixed_bytes().into(), init_code: factory.as_fixed_bytes().into(), ..Default::default() - }, + } + .into(), entry_point: Address::random(), aggregator: Some(aggregator), valid_time_range: ValidTimeRange::all_time(), diff --git a/crates/pool/src/mempool/paymaster.rs b/crates/pool/src/mempool/paymaster.rs index 5fda21486..a9280188d 100644 --- a/crates/pool/src/mempool/paymaster.rs +++ b/crates/pool/src/mempool/paymaster.rs @@ -21,7 +21,7 @@ use parking_lot::RwLock; use rundler_provider::EntryPoint; use rundler_types::{ pool::{MempoolError, PaymasterMetadata, PoolOperation, StakeStatus}, - StakeInfo, UserOperation, UserOperationId, + StakeInfo, UserOperation, UserOperationId, UserOperationVariant, }; use rundler_utils::cache::LruMap; @@ -30,9 +30,9 @@ use crate::chain::{BalanceUpdate, MinedOp}; /// Keeps track of current and pending paymaster balances #[derive(Debug)] -pub(crate) struct PaymasterTracker { +pub(crate) struct PaymasterTracker { entry_point: E, - state: RwLock>, + state: RwLock, config: PaymasterConfig, } @@ -60,9 +60,8 @@ impl PaymasterConfig { } } -impl PaymasterTracker +impl PaymasterTracker where - UO: UserOperation, E: EntryPoint, { pub(crate) fn new(entry_point: E, config: PaymasterConfig) -> Self { @@ -151,7 +150,10 @@ where Ok(paymaster_meta) } - pub(crate) async fn check_operation_cost(&self, op: &UO) -> MempoolResult<()> { + pub(crate) async fn check_operation_cost( + &self, + op: &UserOperationVariant, + ) -> MempoolResult<()> { if let Some(paymaster) = op.paymaster() { let balance = self.paymaster_balance(paymaster).await?; self.state.read().check_operation_cost(op, &balance)? @@ -206,7 +208,7 @@ where .unmine_actual_cost(paymaster, actual_cost); } - pub(crate) async fn add_or_update_balance(&self, po: &PoolOperation) -> MempoolResult<()> { + pub(crate) async fn add_or_update_balance(&self, po: &PoolOperation) -> MempoolResult<()> { if let Some(paymaster) = po.uo.paymaster() { let paymaster_metadata = self.paymaster_balance(paymaster).await?; return self @@ -221,23 +223,21 @@ where // Keeps track of current and pending paymaster balances #[derive(Debug)] -struct PaymasterTrackerInner { +struct PaymasterTrackerInner { // map for userop based on id user_op_fees: HashMap, // map for paymaster balance status paymaster_balances: LruMap, // boolean for operation of tracker tracker_enabled: bool, - _uo_type: std::marker::PhantomData, } -impl PaymasterTrackerInner { +impl PaymasterTrackerInner { fn new(tracker_enabled: bool, cache_size: u32) -> Self { Self { user_op_fees: HashMap::new(), tracker_enabled, paymaster_balances: LruMap::new(cache_size), - _uo_type: std::marker::PhantomData, } } @@ -251,7 +251,7 @@ impl PaymasterTrackerInner { fn check_operation_cost( &self, - op: &UO, + op: &UserOperationVariant, paymaster_metadata: &PaymasterMetadata, ) -> MempoolResult<()> { let max_op_cost = op.max_gas_cost(); @@ -370,7 +370,7 @@ impl PaymasterTrackerInner { fn add_or_update_balance( &mut self, - po: &PoolOperation, + po: &PoolOperation, paymaster_metadata: &PaymasterMetadata, ) -> MempoolResult<()> { let id = po.uo.id(); @@ -532,9 +532,9 @@ mod tests { use super::*; use crate::{chain::BalanceUpdate, mempool::paymaster::PaymasterTracker}; - fn demo_pool_op(uo: UserOperation) -> PoolOperation { + fn demo_pool_op(uo: UserOperation) -> PoolOperation { PoolOperation { - uo, + uo: uo.into(), entry_point: Address::random(), aggregator: None, valid_time_range: ValidTimeRange::all_time(), @@ -945,7 +945,7 @@ mod tests { #[test] fn test_inner_cache_full() { - let mut inner = PaymasterTrackerInner::::new(true, 2); + let mut inner = PaymasterTrackerInner::new(true, 2); let paymaster_0 = Address::random(); let paymaster_1 = Address::random(); @@ -964,7 +964,7 @@ mod tests { assert!(inner.paymaster_exists(paymaster_2)); } - fn new_paymaster_tracker() -> PaymasterTracker { + fn new_paymaster_tracker() -> PaymasterTracker { let mut entrypoint = MockEntryPointV0_6::new(); entrypoint.expect_get_deposit_info().returning(|_| { @@ -990,7 +990,7 @@ mod tests { PaymasterTracker::new(entrypoint, config) } - impl PaymasterTracker { + impl PaymasterTracker { fn add_new_user_op( &self, id: &UserOperationId, diff --git a/crates/pool/src/mempool/pool.rs b/crates/pool/src/mempool/pool.rs index 87095ab0e..7471b890f 100644 --- a/crates/pool/src/mempool/pool.rs +++ b/crates/pool/src/mempool/pool.rs @@ -24,7 +24,7 @@ use ethers::{ }; use rundler_types::{ pool::{MempoolError, PoolOperation}, - Entity, EntityType, Timestamp, UserOperation, UserOperationId, + Entity, EntityType, Timestamp, UserOperation, UserOperationId, UserOperationVariant, }; use rundler_utils::math; use tracing::info; @@ -57,19 +57,19 @@ impl From for PoolInnerConfig { /// Pool of user operations #[derive(Debug)] -pub(crate) struct PoolInner { +pub(crate) struct PoolInner { /// Pool settings config: PoolInnerConfig, /// Operations by hash - by_hash: HashMap>, + by_hash: HashMap, /// Operations by operation ID - by_id: HashMap>, + by_id: HashMap, /// Best operations, sorted by gas price - best: BTreeSet>, + best: BTreeSet, /// Removed operations, temporarily kept around in case their blocks are /// reorged away. Stored along with the block number at which it was /// removed. - mined_at_block_number_by_hash: HashMap, u64)>, + mined_at_block_number_by_hash: HashMap, /// Removed operation hashes sorted by block number, so we can forget them /// when enough new blocks have passed. mined_hashes_with_block_numbers: BTreeSet<(u64, H256)>, @@ -83,7 +83,7 @@ pub(crate) struct PoolInner { cache_size: SizeTracker, } -impl PoolInner { +impl PoolInner { pub(crate) fn new(config: PoolInnerConfig) -> Self { Self { config, @@ -100,7 +100,10 @@ impl PoolInner { } /// Returns hash of operation to replace if operation is a replacement - pub(crate) fn check_replacement(&self, op: &UO) -> MempoolResult> { + pub(crate) fn check_replacement( + &self, + op: &UserOperationVariant, + ) -> MempoolResult> { // Check if operation already known if self .by_hash @@ -132,13 +135,13 @@ impl PoolInner { } } - pub(crate) fn add_operation(&mut self, op: PoolOperation) -> MempoolResult { + pub(crate) fn add_operation(&mut self, op: PoolOperation) -> MempoolResult { let ret = self.add_operation_internal(Arc::new(op), None); self.update_metrics(); ret } - pub(crate) fn best_operations(&self) -> impl Iterator>> { + pub(crate) fn best_operations(&self) -> impl Iterator> { self.best.clone().into_iter().map(|v| v.po) } @@ -169,28 +172,25 @@ impl PoolInner { 0 } - pub(crate) fn get_operation_by_hash(&self, hash: H256) -> Option>> { + pub(crate) fn get_operation_by_hash(&self, hash: H256) -> Option> { self.by_hash.get(&hash).map(|o| o.po.clone()) } - pub(crate) fn get_operation_by_id( - &self, - id: &UserOperationId, - ) -> Option>> { + pub(crate) fn get_operation_by_id(&self, id: &UserOperationId) -> Option> { self.by_id.get(id).map(|o| o.po.clone()) } - pub(crate) fn remove_operation_by_hash( - &mut self, - hash: H256, - ) -> Option>> { + pub(crate) fn remove_operation_by_hash(&mut self, hash: H256) -> Option> { let ret = self.remove_operation_internal(hash, None); self.update_metrics(); ret } // STO-040 - pub(crate) fn check_multiple_roles_violation(&self, uo: &UO) -> MempoolResult<()> { + pub(crate) fn check_multiple_roles_violation( + &self, + uo: &UserOperationVariant, + ) -> MempoolResult<()> { if let Some(ec) = self.count_by_address.get(&uo.sender()) { if ec.includes_non_sender() { return Err(MempoolError::SenderAddressUsedAsAlternateEntity( @@ -219,7 +219,7 @@ impl PoolInner { pub(crate) fn check_associated_storage( &self, accessed_storage: &HashSet
, - uo: &UO, + uo: &UserOperationVariant, ) -> MempoolResult<()> { for storage_address in accessed_storage { if let Some(ec) = self.count_by_address.get(storage_address) { @@ -241,7 +241,7 @@ impl PoolInner { &mut self, mined_op: &MinedOp, block_number: u64, - ) -> Option>> { + ) -> Option> { let tx_in_pool = self.by_id.get(&mined_op.id())?; let hash = tx_in_pool @@ -254,10 +254,7 @@ impl PoolInner { ret } - pub(crate) fn unmine_operation( - &mut self, - mined_op: &MinedOp, - ) -> Option>> { + pub(crate) fn unmine_operation(&mut self, mined_op: &MinedOp) -> Option> { let hash = mined_op.hash; let (op, block_number) = self.mined_at_block_number_by_hash.remove(&hash)?; self.mined_hashes_with_block_numbers @@ -365,13 +362,13 @@ impl PoolInner { Ok(removed) } - fn put_back_unmined_operation(&mut self, op: OrderedPoolOperation) -> MempoolResult { + fn put_back_unmined_operation(&mut self, op: OrderedPoolOperation) -> MempoolResult { self.add_operation_internal(op.po, Some(op.submission_id)) } fn add_operation_internal( &mut self, - op: Arc>, + op: Arc, submission_id: Option, ) -> MempoolResult { // Check if operation already known or replacing an existing operation @@ -418,7 +415,7 @@ impl PoolInner { &mut self, hash: H256, block_number: Option, - ) -> Option>> { + ) -> Option> { let op = self.by_hash.remove(&hash)?; let id = &op.po.uo.id(); self.by_id.remove(id); @@ -455,7 +452,7 @@ impl PoolInner { id } - fn get_min_replacement_fees(&self, op: &UO) -> (U256, U256) { + fn get_min_replacement_fees(&self, op: &UserOperationVariant) -> (U256, U256) { let replacement_priority_fee = math::increase_by_percent( op.max_priority_fee_per_gas(), self.config.min_replacement_fee_increase_percentage, @@ -484,13 +481,13 @@ impl PoolInner { /// Wrapper around PoolOperation that adds a submission ID to implement /// a custom ordering for the best operations #[derive(Debug, Clone)] -struct OrderedPoolOperation { - po: Arc>, +struct OrderedPoolOperation { + po: Arc, submission_id: u64, } -impl OrderedPoolOperation { - fn uo(&self) -> &UO { +impl OrderedPoolOperation { + fn uo(&self) -> &UserOperationVariant { &self.po.uo } @@ -499,9 +496,9 @@ impl OrderedPoolOperation { } } -impl Eq for OrderedPoolOperation {} +impl Eq for OrderedPoolOperation {} -impl Ord for OrderedPoolOperation { +impl Ord for OrderedPoolOperation { fn cmp(&self, other: &Self) -> Ordering { // Sort by gas price descending then by id ascending other @@ -512,13 +509,13 @@ impl Ord for OrderedPoolOperation { } } -impl PartialOrd for OrderedPoolOperation { +impl PartialOrd for OrderedPoolOperation { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl PartialEq for OrderedPoolOperation { +impl PartialEq for OrderedPoolOperation { fn eq(&self, other: &Self) -> bool { self.cmp(other) == Ordering::Equal } @@ -783,7 +780,9 @@ mod tests { create_op(Address::random(), 0, 1), ]; for mut op in ops.into_iter() { - op.uo.paymaster_and_data = paymaster.as_bytes().to_vec().into(); + let uo: &mut UserOperation = op.uo.as_mut(); + + uo.paymaster_and_data = paymaster.as_bytes().to_vec().into(); op.entity_infos.paymaster = Some(EntityInfo { address: op.uo.paymaster().unwrap(), is_staked: false, @@ -807,12 +806,13 @@ mod tests { let aggregator = Address::random(); let mut op = create_op(sender, 0, 1); - op.uo.paymaster_and_data = paymaster.as_bytes().to_vec().into(); + let uo: &mut UserOperation = op.uo.as_mut(); + uo.paymaster_and_data = paymaster.as_bytes().to_vec().into(); op.entity_infos.paymaster = Some(EntityInfo { - address: op.uo.paymaster().unwrap(), + address: uo.paymaster().unwrap(), is_staked: false, }); - op.uo.init_code = factory.as_bytes().to_vec().into(); + uo.init_code = factory.as_bytes().to_vec().into(); op.entity_infos.factory = Some(EntityInfo { address: op.uo.factory().unwrap(), is_staked: false, @@ -827,7 +827,8 @@ mod tests { let mut hashes = vec![]; for i in 0..count { let mut op = op.clone(); - op.uo.nonce = i.into(); + let uo: &mut UserOperation = op.uo.as_mut(); + uo.nonce = i.into(); hashes.push(pool.add_operation(op).unwrap()); } @@ -884,11 +885,13 @@ mod tests { let mut pool = PoolInner::new(conf()); let sender = Address::random(); let mut po1 = create_op(sender, 0, 100); - po1.uo.max_priority_fee_per_gas = 100.into(); + let uo1: &mut UserOperation = po1.uo.as_mut(); + uo1.max_priority_fee_per_gas = 100.into(); let _ = pool.add_operation(po1.clone()).unwrap(); let mut po2 = create_op(sender, 0, 101); - po2.uo.max_priority_fee_per_gas = 101.into(); + let uo2: &mut UserOperation = po2.uo.as_mut(); + uo2.max_priority_fee_per_gas = 101.into(); let res = pool.add_operation(po2); assert!(res.is_err()); match res.err().unwrap() { @@ -916,8 +919,9 @@ mod tests { let sender = Address::random(); let paymaster1 = Address::random(); let mut po1 = create_op(sender, 0, 10); - po1.uo.max_priority_fee_per_gas = 10.into(); - po1.uo.paymaster_and_data = paymaster1.as_bytes().to_vec().into(); + let uo1: &mut UserOperation = po1.uo.as_mut(); + uo1.max_priority_fee_per_gas = 10.into(); + uo1.paymaster_and_data = paymaster1.as_bytes().to_vec().into(); po1.entity_infos.paymaster = Some(EntityInfo { address: po1.uo.paymaster().unwrap(), is_staked: false, @@ -927,8 +931,9 @@ mod tests { let paymaster2 = Address::random(); let mut po2 = create_op(sender, 0, 11); - po2.uo.max_priority_fee_per_gas = 11.into(); - po2.uo.paymaster_and_data = paymaster2.as_bytes().to_vec().into(); + let uo2: &mut UserOperation = po2.uo.as_mut(); + uo2.max_priority_fee_per_gas = 11.into(); + uo2.paymaster_and_data = paymaster2.as_bytes().to_vec().into(); po2.entity_infos.paymaster = Some(EntityInfo { address: po2.uo.paymaster().unwrap(), is_staked: false, @@ -953,7 +958,8 @@ mod tests { let mut pool = PoolInner::new(conf()); let sender = Address::random(); let mut po1 = create_op(sender, 0, 10); - po1.uo.max_priority_fee_per_gas = 10.into(); + let uo1: &mut UserOperation = po1.uo.as_mut(); + uo1.max_priority_fee_per_gas = 10.into(); let _ = pool.add_operation(po1.clone()).unwrap(); let res = pool.add_operation(po1); @@ -1020,19 +1026,15 @@ mod tests { .mem_size() } - fn create_op( - sender: Address, - nonce: usize, - max_fee_per_gas: usize, - ) -> PoolOperation { + fn create_op(sender: Address, nonce: usize, max_fee_per_gas: usize) -> PoolOperation { PoolOperation { uo: UserOperation { sender, nonce: nonce.into(), max_fee_per_gas: max_fee_per_gas.into(), - ..UserOperation::default() - }, + } + .into(), entity_infos: EntityInfos { factory: None, sender: EntityInfo { @@ -1053,10 +1055,7 @@ mod tests { } } - fn check_map_entry( - actual: Option<&OrderedPoolOperation>, - expected: Option<&PoolOperation>, - ) { + fn check_map_entry(actual: Option<&OrderedPoolOperation>, expected: Option<&PoolOperation>) { match (actual, expected) { (Some(actual), Some(expected)) => assert_eq!(*actual.po, *expected), (None, None) => (), diff --git a/crates/pool/src/mempool/uo_pool.rs b/crates/pool/src/mempool/uo_pool.rs index 197652bc2..9af9c3794 100644 --- a/crates/pool/src/mempool/uo_pool.rs +++ b/crates/pool/src/mempool/uo_pool.rs @@ -25,7 +25,8 @@ use rundler_types::{ pool::{ MempoolError, PaymasterMetadata, PoolOperation, Reputation, ReputationStatus, StakeStatus, }, - Entity, EntityUpdate, EntityUpdateType, UserOperation, UserOperationId, UserOperationVariant, + Entity, EntityUpdate, EntityUpdateType, EntryPointVersion, UserOperation, UserOperationId, + UserOperationVariant, }; use rundler_utils::emit::WithEntryPoint; use tokio::sync::broadcast; @@ -48,16 +49,17 @@ use crate::{ /// block on write locks. pub(crate) struct UoPool { config: PoolConfig, - state: RwLock>, - paymaster: PaymasterTracker, + state: RwLock, + paymaster: PaymasterTracker, reputation: Arc, event_sender: broadcast::Sender>, prechecker: P, simulator: S, + _uo_type: std::marker::PhantomData, } -struct UoPoolState { - pool: PoolInner, +struct UoPoolState { + pool: PoolInner, throttled_ops: HashSet, block_number: u64, } @@ -74,7 +76,7 @@ where event_sender: broadcast::Sender>, prechecker: P, simulator: S, - paymaster: PaymasterTracker, + paymaster: PaymasterTracker, reputation: Arc, ) -> Self { Self { @@ -89,6 +91,7 @@ where prechecker, simulator, config, + _uo_type: std::marker::PhantomData, } } @@ -135,13 +138,11 @@ where #[async_trait] impl Mempool for UoPool where - UO: UserOperation + Into, + UO: UserOperation + From + Into, P: Prechecker, S: Simulator, E: EntryPoint, { - type UO = UO; - async fn on_chain_update(&self, update: &ChainUpdate) { { let deduped_ops = update.deduped_ops(); @@ -312,6 +313,10 @@ where self.config.entry_point } + fn entry_point_version(&self) -> EntryPointVersion { + self.config.entry_point_version + } + fn set_tracking(&self, paymaster: bool, reputation: bool) { self.paymaster.set_tracking(paymaster); self.reputation.set_tracking(reputation); @@ -325,7 +330,11 @@ where self.paymaster.get_stake_status(address).await } - async fn add_operation(&self, origin: OperationOrigin, op: UO) -> MempoolResult { + async fn add_operation( + &self, + origin: OperationOrigin, + op: UserOperationVariant, + ) -> MempoolResult { // TODO(danc) aggregator reputation is not implemented // TODO(danc) catch ops with aggregators prior to simulation and reject @@ -376,12 +385,13 @@ where self.paymaster.check_operation_cost(&op).await?; // Prechecks - self.prechecker.check(&op).await?; + let versioned_op = op.clone().into(); + self.prechecker.check(&versioned_op).await?; // Only let ops with successful simulations through let sim_result = self .simulator - .simulate_validation(op.clone(), None, None) + .simulate_validation(versioned_op, None, None) .await?; // No aggregators supported for now @@ -470,7 +480,7 @@ where let valid_until = pool_op.valid_time_range.valid_until; self.emit(OpPoolEvent::ReceivedOp { op_hash, - op: pool_op.uo.into(), + op: pool_op.uo, block_number: pool_op.sim_block_number, origin, valid_after, @@ -563,7 +573,7 @@ where &self, max: usize, shard_index: u64, - ) -> MempoolResult>>> { + ) -> MempoolResult>> { if shard_index >= self.config.num_shards { Err(anyhow::anyhow!("Invalid shard ID"))?; } @@ -586,14 +596,15 @@ where senders.insert(op.uo.sender()) }) .take(max) + .map(Into::into) .collect()) } - fn all_operations(&self, max: usize) -> Vec>> { + fn all_operations(&self, max: usize) -> Vec> { self.state.read().pool.best_operations().take(max).collect() } - fn get_user_operation_by_hash(&self, hash: H256) -> Option>> { + fn get_user_operation_by_hash(&self, hash: H256) -> Option> { self.state.read().pool.get_operation_by_hash(hash) } @@ -679,8 +690,8 @@ mod tests { contracts::v0_6::verifying_paymaster::DepositInfo, pool::{PrecheckViolation, SimulationViolation}, v0_6::UserOperation, - EntityInfo, EntityInfos, EntityType, GasFees, UserOperation as UserOperationTrait, - ValidTimeRange, + EntityInfo, EntityInfos, EntityType, EntryPointVersion, GasFees, + UserOperation as UserOperationTrait, ValidTimeRange, }; use super::*; @@ -772,8 +783,8 @@ mod tests { mined_ops: vec![MinedOp { entry_point: pool.config.entry_point, hash: uos[0].hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: U256::zero(), paymaster: None, }], @@ -812,10 +823,11 @@ mod tests { // add pending max cost of 30 for each uo for op in &mut ops { - op.op.call_gas_limit = 10.into(); - op.op.verification_gas_limit = 10.into(); - op.op.pre_verification_gas = 10.into(); - op.op.max_fee_per_gas = 1.into(); + let uo: &mut UserOperation = op.op.as_mut(); + uo.call_gas_limit = 10.into(); + uo.verification_gas_limit = 10.into(); + uo.pre_verification_gas = 10.into(); + uo.max_fee_per_gas = 1.into(); } let (pool, uos) = create_pool_insert_ops(ops).await; @@ -834,8 +846,8 @@ mod tests { mined_ops: vec![MinedOp { entry_point: pool.config.entry_point, hash: uos[0].hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: 10.into(), paymaster: Some(paymaster), }], @@ -874,8 +886,8 @@ mod tests { unmined_ops: vec![MinedOp { entry_point: pool.config.entry_point, hash: uos[0].hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: 10.into(), paymaster: None, }], @@ -915,8 +927,8 @@ mod tests { mined_ops: vec![MinedOp { entry_point: Address::random(), hash: uos[0].hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: U256::zero(), paymaster: None, }], @@ -957,8 +969,8 @@ mod tests { mined_ops: vec![MinedOp { entry_point: pool.config.entry_point, hash: uos[0].hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: U256::zero(), paymaster: None, }], @@ -1033,8 +1045,8 @@ mod tests { mined_ops: vec![MinedOp { entry_point: pool.config.entry_point, hash: uos[0].hash(pool.config.entry_point, 1), - sender: uos[0].sender, - nonce: uos[0].nonce, + sender: uos[0].sender(), + nonce: uos[0].nonce(), actual_gas_cost: U256::zero(), paymaster: None, }], @@ -1089,10 +1101,11 @@ mod tests { async fn test_paymaster_balance_insufficient() { let paymaster = Address::random(); let mut op = create_op(Address::random(), 0, 0, Some(paymaster)); - op.op.call_gas_limit = 1000.into(); - op.op.verification_gas_limit = 1000.into(); - op.op.pre_verification_gas = 1000.into(); - op.op.max_fee_per_gas = 1.into(); + let uo: &mut UserOperation = op.op.as_mut(); + uo.call_gas_limit = 1000.into(); + uo.verification_gas_limit = 1000.into(); + uo.pre_verification_gas = 1000.into(); + uo.max_fee_per_gas = 1.into(); let uo = op.op.clone(); let pool = create_pool(vec![op]); @@ -1178,7 +1191,8 @@ mod tests { .unwrap(); let mut replacement = op.op.clone(); - replacement.max_fee_per_gas = replacement.max_fee_per_gas + 1; + let r: &mut UserOperation = replacement.as_mut(); + r.max_fee_per_gas = r.max_fee_per_gas + 1; let err = pool .add_operation(OperationOrigin::Local, replacement) @@ -1207,10 +1221,11 @@ mod tests { let paymaster = Address::random(); let mut op = create_op(Address::random(), 0, 5, Some(paymaster)); - op.op.call_gas_limit = 10.into(); - op.op.verification_gas_limit = 10.into(); - op.op.pre_verification_gas = 10.into(); - op.op.max_fee_per_gas = 1.into(); + let uo: &mut UserOperation = op.op.as_mut(); + uo.call_gas_limit = 10.into(); + uo.verification_gas_limit = 10.into(); + uo.pre_verification_gas = 10.into(); + uo.max_fee_per_gas = 1.into(); let pool = create_pool(vec![op.clone()]); @@ -1220,7 +1235,8 @@ mod tests { .unwrap(); let mut replacement = op.op.clone(); - replacement.max_fee_per_gas = replacement.max_fee_per_gas + 1; + let r: &mut UserOperation = replacement.as_mut(); + r.max_fee_per_gas = r.max_fee_per_gas + 1; let _ = pool .add_operation(OperationOrigin::Local, replacement.clone()) @@ -1233,7 +1249,7 @@ mod tests { assert_eq!(paymaster_balance.pending_balance, U256::from(900)); let rep = pool.dump_reputation(); assert_eq!(rep.len(), 1); - assert_eq!(rep[0].address, op.op.sender); + assert_eq!(rep[0].address, op.op.sender()); assert_eq!(rep[0].ops_seen, 1); assert_eq!(rep[0].ops_included, 0); } @@ -1371,7 +1387,7 @@ mod tests { #[derive(Clone, Debug)] struct OpWithErrors { - op: UserOperation, + op: UserOperationVariant, valid_time_range: ValidTimeRange, precheck_error: Option, simulation_error: Option, @@ -1388,6 +1404,7 @@ mod tests { > { let args = PoolConfig { entry_point: Address::random(), + entry_point_version: EntryPointVersion::V0_6, chain_id: 1, min_replacement_fee_increase_percentage: 10, max_size_of_pool_bytes: 10000, @@ -1471,7 +1488,7 @@ mod tests { valid_time_range: op.valid_time_range, entity_infos: EntityInfos { sender: EntityInfo { - address: op.op.sender, + address: op.op.sender(), is_staked: false, }, ..EntityInfos::default() @@ -1503,7 +1520,7 @@ mod tests { impl Simulator, impl EntryPoint, >, - Vec, + Vec, ) { let uos = ops.iter().map(|op| op.op.clone()).collect::>(); let pool = create_pool(ops); @@ -1532,7 +1549,8 @@ mod tests { max_fee_per_gas: max_fee_per_gas.into(), paymaster_and_data, ..UserOperation::default() - }, + } + .into(), valid_time_range: ValidTimeRange::default(), precheck_error: None, simulation_error: None, @@ -1554,7 +1572,8 @@ mod tests { nonce: nonce.into(), max_fee_per_gas: max_fee_per_gas.into(), ..UserOperation::default() - }, + } + .into(), valid_time_range: ValidTimeRange::default(), precheck_error, simulation_error, @@ -1562,7 +1581,7 @@ mod tests { } } - fn check_ops(ops: Vec>>, expected: Vec) { + fn check_ops(ops: Vec>, expected: Vec) { assert_eq!(ops.len(), expected.len()); for (actual, expected) in ops.into_iter().zip(expected) { assert_eq!(actual.uo, expected); diff --git a/crates/pool/src/server/local.rs b/crates/pool/src/server/local.rs index d68807a47..2b41a79c3 100644 --- a/crates/pool/src/server/local.rs +++ b/crates/pool/src/server/local.rs @@ -21,10 +21,10 @@ use futures_util::Stream; use rundler_task::server::{HealthCheck, ServerStatus}; use rundler_types::{ pool::{ - IntoPoolOperationVariant, MempoolError, NewHead, PaymasterMetadata, Pool, PoolError, - PoolOperation, PoolResult, Reputation, ReputationStatus, StakeStatus, + MempoolError, NewHead, PaymasterMetadata, Pool, PoolError, PoolOperation, PoolResult, + Reputation, ReputationStatus, StakeStatus, }, - v0_6, EntityUpdate, UserOperationId, UserOperationVariant, + EntityUpdate, EntryPointVersion, UserOperationId, UserOperationVariant, }; use tokio::{ sync::{broadcast, mpsc, oneshot}, @@ -66,15 +66,12 @@ impl LocalPoolBuilder { } /// Run the local pool server, consumes the builder - pub fn run( + pub fn run( self, - mempools: HashMap>, + mempools: HashMap>>, chain_updates: broadcast::Receiver>, shutdown_token: CancellationToken, - ) -> JoinHandle> - where - M: Mempool, - { + ) -> JoinHandle> { let mut runner = LocalPoolServerRunner::new( self.req_receiver, self.block_sender, @@ -93,10 +90,10 @@ pub struct LocalPoolHandle { req_sender: mpsc::Sender, } -struct LocalPoolServerRunner { +struct LocalPoolServerRunner { req_receiver: mpsc::Receiver, block_sender: broadcast::Sender, - mempools: HashMap>, + mempools: HashMap>>, chain_updates: broadcast::Receiver>, } @@ -144,7 +141,7 @@ impl Pool for LocalPoolHandle { entry_point: Address, max_ops: u64, shard_index: u64, - ) -> PoolResult>> { + ) -> PoolResult> { let req = ServerRequestKind::GetOps { entry_point, max_ops, @@ -157,10 +154,7 @@ impl Pool for LocalPoolHandle { } } - async fn get_op_by_hash( - &self, - hash: H256, - ) -> PoolResult>> { + async fn get_op_by_hash(&self, hash: H256) -> PoolResult> { let req = ServerRequestKind::GetOpByHash { hash }; let resp = self.send(req).await?; match resp { @@ -243,10 +237,7 @@ impl Pool for LocalPoolHandle { } } - async fn debug_dump_mempool( - &self, - entry_point: Address, - ) -> PoolResult>> { + async fn debug_dump_mempool(&self, entry_point: Address) -> PoolResult> { let req = ServerRequestKind::DebugDumpMempool { entry_point }; let resp = self.send(req).await?; match resp { @@ -362,14 +353,11 @@ impl HealthCheck for LocalPoolHandle { } } -impl LocalPoolServerRunner -where - M: Mempool, -{ +impl LocalPoolServerRunner { fn new( req_receiver: mpsc::Receiver, block_sender: broadcast::Sender, - mempools: HashMap>, + mempools: HashMap>>, chain_updates: broadcast::Receiver>, ) -> Self { Self { @@ -380,7 +368,7 @@ where } } - fn get_pool(&self, entry_point: Address) -> PoolResult<&Arc> { + fn get_pool(&self, entry_point: Address) -> PoolResult<&Arc>> { self.mempools .get(&entry_point) .ok_or_else(|| PoolError::MempoolError(MempoolError::UnknownEntryPoint(entry_point))) @@ -391,22 +379,19 @@ where entry_point: Address, max_ops: u64, shard_index: u64, - ) -> PoolResult>> { + ) -> PoolResult> { let mempool = self.get_pool(entry_point)?; Ok(mempool .best_operations(max_ops as usize, shard_index)? .iter() - .map(|op| (**op).clone().into_variant()) + .map(|op| (**op).clone()) .collect()) } - fn get_op_by_hash( - &self, - hash: H256, - ) -> PoolResult>> { + fn get_op_by_hash(&self, hash: H256) -> PoolResult> { for mempool in self.mempools.values() { if let Some(op) = mempool.get_user_operation_by_hash(hash) { - return Ok(Some((*op).clone().into_variant())); + return Ok(Some((*op).clone())); } } Ok(None) @@ -462,15 +447,12 @@ where Ok(()) } - fn debug_dump_mempool( - &self, - entry_point: Address, - ) -> PoolResult>> { + fn debug_dump_mempool(&self, entry_point: Address) -> PoolResult> { let mempool = self.get_pool(entry_point)?; Ok(mempool .all_operations(usize::MAX) .iter() - .map(|op| (**op).clone().into_variant()) + .map(|op| (**op).clone()) .collect()) } @@ -514,7 +496,7 @@ where response: oneshot::Sender>, f: F, ) where - F: FnOnce(Arc, oneshot::Sender>) -> Fut, + F: FnOnce(Arc>, oneshot::Sender>) -> Fut, Fut: Future + Send + 'static, { match self.get_pool(entry_point) { @@ -564,11 +546,30 @@ where // Async methods // Responses are sent in the spawned task ServerRequestKind::AddOp { entry_point, op, origin } => { - let fut = |mempool: Arc, response: oneshot::Sender>| async move { - let resp = match mempool.add_operation(origin, op.into()).await { - Ok(hash) => Ok(ServerResponse::AddOp { hash }), - Err(e) => Err(e.into()), + let fut = |mempool: Arc>, response: oneshot::Sender>| async move { + let resp = 'resp: { + match mempool.entry_point_version() { + EntryPointVersion::V0_6 => { + if !matches!(&op, UserOperationVariant::V0_6(_)){ + break 'resp Err(anyhow::anyhow!("Invalid user operation version for mempool v0.6 {:?}", op.uo_type()).into()); + } + } + EntryPointVersion::V0_7 => { + if !matches!(&op, UserOperationVariant::V0_7(_)){ + break 'resp Err(anyhow::anyhow!("Invalid user operation version for mempool v0.7 {:?}", op.uo_type()).into()); + } + } + EntryPointVersion::Unspecified => { + panic!("Found mempool with unspecified entry point version") + } + } + + match mempool.add_operation(origin, op).await { + Ok(hash) => Ok(ServerResponse::AddOp { hash }), + Err(e) => Err(e.into()), + } }; + if let Err(e) = response.send(resp) { tracing::error!("Failed to send response: {:?}", e); } @@ -578,7 +579,7 @@ where continue; }, ServerRequestKind::GetStakeStatus { entry_point, address }=> { - let fut = |mempool: Arc, response: oneshot::Sender>| async move { + let fut = |mempool: Arc>, response: oneshot::Sender>| async move { let resp = match mempool.get_stake_status(address).await { Ok(status) => Ok(ServerResponse::GetStakeStatus { status }), Err(e) => Err(e.into()), @@ -762,10 +763,10 @@ enum ServerResponse { hash: H256, }, GetOps { - ops: Vec>, + ops: Vec, }, GetOpByHash { - op: Option>, + op: Option, }, RemoveOps, RemoveOpById { @@ -775,7 +776,7 @@ enum ServerResponse { DebugClearState, AdminSetTracking, DebugDumpMempool { - ops: Vec>, + ops: Vec, }, DebugSetReputations, DebugDumpReputation { @@ -809,12 +810,16 @@ mod tests { async fn test_add_op() { let mut mock_pool = MockMempool::new(); let hash0 = H256::random(); + mock_pool + .expect_entry_point_version() + .returning(|| EntryPointVersion::V0_6); mock_pool .expect_add_operation() .returning(move |_, _| Ok(hash0)); let ep = Address::random(); - let state = setup(HashMap::from([(ep, Arc::new(mock_pool))])); + let pool: Arc> = Arc::new(Box::new(mock_pool)); + let state = setup(HashMap::from([(ep, pool)])); let hash1 = state.handle.add_op(ep, mock_op()).await.unwrap(); assert_eq!(hash0, hash1); @@ -826,7 +831,8 @@ mod tests { mock_pool.expect_on_chain_update().returning(|_| ()); let ep = Address::random(); - let state = setup(HashMap::from([(ep, Arc::new(mock_pool))])); + let pool: Arc> = Arc::new(Box::new(mock_pool)); + let state = setup(HashMap::from([(ep, pool)])); let mut sub = state.handle.subscribe_new_heads().await.unwrap(); @@ -852,7 +858,10 @@ mod tests { let state = setup( eps0.iter() - .map(|ep| (*ep, Arc::new(MockMempool::new()))) + .map(|ep| { + let pool: Arc> = Arc::new(Box::new(MockMempool::new())); + (*ep, pool) + }) .collect(), ); @@ -871,19 +880,31 @@ mod tests { let h1 = H256::random(); let h2 = H256::random(); let hashes = [h0, h1, h2]; + pools[0] + .expect_entry_point_version() + .returning(|| EntryPointVersion::V0_6); pools[0] .expect_add_operation() .returning(move |_, _| Ok(h0)); + pools[1] + .expect_entry_point_version() + .returning(|| EntryPointVersion::V0_6); pools[1] .expect_add_operation() .returning(move |_, _| Ok(h1)); + pools[2] + .expect_entry_point_version() + .returning(|| EntryPointVersion::V0_6); pools[2] .expect_add_operation() .returning(move |_, _| Ok(h2)); let state = setup( zip(eps.iter(), pools.into_iter()) - .map(|(ep, pool)| (*ep, Arc::new(pool))) + .map(|(ep, pool)| { + let pool: Arc> = Arc::new(Box::new(pool)); + (*ep, pool) + }) .collect(), ); @@ -898,7 +919,7 @@ mod tests { _run_handle: JoinHandle>, } - fn setup(pools: HashMap>) -> State { + fn setup(pools: HashMap>>) -> State { let builder = LocalPoolBuilder::new(10, 10); let handle = builder.get_handle(); let (tx, rx) = broadcast::channel(10); diff --git a/crates/pool/src/server/remote/client.rs b/crates/pool/src/server/remote/client.rs index 11e3b324a..e8cce9a46 100644 --- a/crates/pool/src/server/remote/client.rs +++ b/crates/pool/src/server/remote/client.rs @@ -168,7 +168,7 @@ impl Pool for RemotePoolClient { entry_point: Address, max_ops: u64, shard_index: u64, - ) -> PoolResult>> { + ) -> PoolResult> { let res = self .op_pool_client .clone() @@ -196,10 +196,7 @@ impl Pool for RemotePoolClient { } } - async fn get_op_by_hash( - &self, - hash: H256, - ) -> PoolResult>> { + async fn get_op_by_hash(&self, hash: H256) -> PoolResult> { let res = self .op_pool_client .clone() @@ -367,10 +364,7 @@ impl Pool for RemotePoolClient { } } - async fn debug_dump_mempool( - &self, - entry_point: Address, - ) -> PoolResult>> { + async fn debug_dump_mempool(&self, entry_point: Address) -> PoolResult> { let res = self .op_pool_client .clone() diff --git a/crates/pool/src/server/remote/protos.rs b/crates/pool/src/server/remote/protos.rs index f822b5d49..ec4f034ce 100644 --- a/crates/pool/src/server/remote/protos.rs +++ b/crates/pool/src/server/remote/protos.rs @@ -270,8 +270,8 @@ impl From for StakeStatus { } } -impl From<&PoolOperation> for MempoolOp { - fn from(op: &PoolOperation) -> Self { +impl From<&PoolOperation> for MempoolOp { + fn from(op: &PoolOperation) -> Self { MempoolOp { uo: Some(UserOperation::from(&op.uo)), entry_point: op.entry_point.as_bytes().to_vec(), @@ -291,7 +291,7 @@ impl From<&PoolOperation> for MempoolOp { } pub const MISSING_USER_OP_ERR_STR: &str = "Mempool op should contain user operation"; -impl TryFrom for PoolOperation { +impl TryFrom for PoolOperation { type Error = anyhow::Error; fn try_from(op: MempoolOp) -> Result { diff --git a/crates/pool/src/task.rs b/crates/pool/src/task.rs index 6cd9a9808..7dc39540d 100644 --- a/crates/pool/src/task.rs +++ b/crates/pool/src/task.rs @@ -16,10 +16,10 @@ use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; use anyhow::{bail, Context}; use async_trait::async_trait; use ethers::providers::Middleware; -use rundler_provider::{EntryPoint, EthersEntryPointV0_6, Provider}; -use rundler_sim::{simulation::v0_6 as sim_v0_6, Prechecker, PrecheckerImpl, Simulator}; +use rundler_provider::{EthersEntryPointV0_6, Provider}; +use rundler_sim::{simulation::v0_6 as sim_v0_6, PrecheckerImpl}; use rundler_task::Task; -use rundler_types::{chain::ChainSpec, v0_6}; +use rundler_types::{chain::ChainSpec, EntryPointVersion}; use rundler_utils::{emit::WithEntryPoint, handle}; use tokio::{sync::broadcast, try_join}; use tokio_util::sync::CancellationToken; @@ -28,7 +28,9 @@ use super::mempool::PoolConfig; use crate::{ chain::{self, Chain}, emit::OpPoolEvent, - mempool::{AddressReputation, PaymasterConfig, PaymasterTracker, ReputationParams, UoPool}, + mempool::{ + AddressReputation, Mempool, PaymasterConfig, PaymasterTracker, ReputationParams, UoPool, + }, server::{spawn_remote_mempool_server, LocalPoolBuilder}, }; @@ -73,7 +75,7 @@ impl Task for PoolTask { .args .pool_configs .iter() - .map(|config| config.entry_point) + .map(|config| (config.entry_point, config.entry_point_version)) .collect(), }; let provider = rundler_provider::new_provider( @@ -87,6 +89,39 @@ impl Task for PoolTask { // create mempools let mut mempools = HashMap::new(); for pool_config in &self.args.pool_configs { + match pool_config.entry_point_version { + EntryPointVersion::V0_6 => { + let pool = PoolTask::create_mempool_v0_6( + self.args.chain_spec.clone(), + pool_config, + self.event_sender.clone(), + provider.clone(), + ) + .await + .context("should have created mempool")?; + + mempools.insert(pool_config.entry_point, pool); + } + EntryPointVersion::V0_7 => { + let pool = PoolTask::create_mempool_v0_7( + self.args.chain_spec.clone(), + pool_config, + self.event_sender.clone(), + provider.clone(), + ) + .await + .context("should have created mempool")?; + + mempools.insert(pool_config.entry_point, pool); + } + EntryPointVersion::Unspecified => { + bail!( + "Unsupported entry point version: {:?}", + pool_config.entry_point_version + ); + } + } + let pool = PoolTask::create_mempool_v0_6( self.args.chain_spec.clone(), pool_config, @@ -96,7 +131,7 @@ impl Task for PoolTask { .await .context("should have created mempool")?; - mempools.insert(pool_config.entry_point, Arc::new(pool)); + mempools.insert(pool_config.entry_point, pool); } let pool_handle = self.pool_builder.get_handle(); @@ -155,19 +190,23 @@ impl PoolTask { Box::new(self) } + async fn create_mempool_v0_7( + _chain_spec: ChainSpec, + _pool_config: &PoolConfig, + _event_sender: broadcast::Sender>, + _provider: Arc

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

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

; @@ -173,6 +176,13 @@ impl UserOperation for UserOperationVariant { } } + fn nonce(&self) -> U256 { + match self { + UserOperationVariant::V0_6(op) => op.nonce(), + UserOperationVariant::V0_7(op) => op.nonce(), + } + } + fn paymaster(&self) -> Option
{ match self { UserOperationVariant::V0_6(op) => op.paymaster(), diff --git a/crates/types/src/user_operation/v0_6.rs b/crates/types/src/user_operation/v0_6.rs index 9677b7a3f..e7ec3bbdf 100644 --- a/crates/types/src/user_operation/v0_6.rs +++ b/crates/types/src/user_operation/v0_6.rs @@ -56,6 +56,10 @@ impl UserOperationTrait for UserOperation { self.sender } + fn nonce(&self) -> U256 { + self.nonce + } + fn factory(&self) -> Option
{ Self::get_address_from_field(&self.init_code) } @@ -191,6 +195,24 @@ impl From for super::UserOperationVariant { } } +impl AsRef for super::UserOperationVariant { + fn as_ref(&self) -> &UserOperation { + match self { + super::UserOperationVariant::V0_6(op) => op, + _ => panic!("Expected UserOperationV0_6"), + } + } +} + +impl AsMut for super::UserOperationVariant { + fn as_mut(&mut self) -> &mut UserOperation { + match self { + super::UserOperationVariant::V0_6(op) => op, + _ => panic!("Expected UserOperationV0_6"), + } + } +} + /// User operation with optional gas fields for gas estimation #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] diff --git a/crates/types/src/user_operation/v0_7.rs b/crates/types/src/user_operation/v0_7.rs index e9c6af957..a4a7af70d 100644 --- a/crates/types/src/user_operation/v0_7.rs +++ b/crates/types/src/user_operation/v0_7.rs @@ -100,6 +100,10 @@ impl UserOperationTrait for UserOperation { self.sender } + fn nonce(&self) -> U256 { + self.nonce + } + fn paymaster(&self) -> Option
{ self.paymaster } @@ -227,6 +231,24 @@ impl From for super::UserOperationVariant { } } +impl AsRef for super::UserOperationVariant { + fn as_ref(&self) -> &UserOperation { + match self { + super::UserOperationVariant::V0_7(op) => op, + _ => panic!("Expected UserOperationV0_7"), + } + } +} + +impl AsMut for super::UserOperationVariant { + fn as_mut(&mut self) -> &mut UserOperation { + match self { + super::UserOperationVariant::V0_7(op) => op, + _ => panic!("Expected UserOperationV0_7"), + } + } +} + /// User Operation with optional gas for Entry Point v0.7 #[derive(Debug, Clone, Eq, PartialEq)] pub struct UserOperationOptionalGas { From 5573b1ff8b8ccc27d2a2ab7b2aefcb45ee699605 Mon Sep 17 00:00:00 2001 From: Dan Coombs Date: Fri, 29 Mar 2024 16:49:26 -0500 Subject: [PATCH 06/12] feat(builder): start support for multiple entry points in builder (#645) --- bin/rundler/src/cli/builder.rs | 20 ++- crates/builder/src/lib.rs | 2 +- crates/builder/src/task.rs | 123 ++++++++++++------ .../provider/src/ethers/entry_point/v0_6.rs | 8 +- crates/provider/src/lib.rs | 6 +- crates/provider/src/traits/entry_point.rs | 10 ++ crates/provider/src/traits/mod.rs | 4 +- 7 files changed, 122 insertions(+), 51 deletions(-) diff --git a/bin/rundler/src/cli/builder.rs b/bin/rundler/src/cli/builder.rs index 1b9ebe260..afe26cc3b 100644 --- a/bin/rundler/src/cli/builder.rs +++ b/bin/rundler/src/cli/builder.rs @@ -17,8 +17,8 @@ use anyhow::Context; use clap::Args; use ethers::types::H256; use rundler_builder::{ - self, BuilderEvent, BuilderEventKind, BuilderTask, BuilderTaskArgs, LocalBuilderBuilder, - TransactionSenderType, + self, BuilderEvent, BuilderEventKind, BuilderTask, BuilderTaskArgs, EntryPointBuilderSettings, + LocalBuilderBuilder, TransactionSenderType, }; use rundler_pool::RemotePoolClient; use rundler_sim::{MempoolConfig, PriorityFeeMode}; @@ -26,7 +26,7 @@ use rundler_task::{ server::{connect_with_retries_shutdown, format_socket_addr}, spawn_tasks_with_shutdown, }; -use rundler_types::chain::ChainSpec; +use rundler_types::{chain::ChainSpec, EntryPointVersion}; use rundler_utils::emit::{self, WithEntryPoint, EVENT_CHANNEL_CAPACITY}; use tokio::sync::broadcast; @@ -110,7 +110,7 @@ pub struct BuilderArgs { pub submit_url: Option, /// Choice of what sender type to to use for transaction submission. - /// Defaults to the value of `raw`. Other options inclue `flashbots`, + /// Defaults to the value of `raw`. Other options include `flashbots`, /// `conditional` and `polygon_bloxroute` #[arg( long = "builder.sender", @@ -191,6 +191,7 @@ impl BuilderArgs { .context("should have a node HTTP URL")?; let submit_url = self.submit_url.clone().unwrap_or_else(|| rpc_url.clone()); + // TODO these should be scoped by entry point let mempool_configs = match &common.mempool_config_path { Some(path) => { get_json_config::>(path, &common.aws_region).await? @@ -199,6 +200,14 @@ impl BuilderArgs { }; Ok(BuilderTaskArgs { + // TODO: support multiple entry points + entry_points: vec![EntryPointBuilderSettings { + address: chain_spec.entry_point_address, + version: EntryPointVersion::V0_6, + num_bundle_builders: common.num_builders, + bundle_builder_index_offset: self.builder_index_offset, + mempool_configs, + }], chain_spec, rpc_url, private_key: self.private_key.clone(), @@ -217,14 +226,11 @@ impl BuilderArgs { sender_type: self.sender_type, eth_poll_interval: Duration::from_millis(common.eth_poll_interval_millis), sim_settings: common.into(), - mempool_configs, max_blocks_to_wait_for_mine: self.max_blocks_to_wait_for_mine, replacement_fee_percent_increase: self.replacement_fee_percent_increase, max_fee_increases: self.max_fee_increases, remote_address, bloxroute_auth_header: self.bloxroute_auth_header.clone(), - num_bundle_builders: common.num_builders, - bundle_builder_index_offset: self.builder_index_offset, }) } } diff --git a/crates/builder/src/lib.rs b/crates/builder/src/lib.rs index 66107d3c5..3c4173f08 100644 --- a/crates/builder/src/lib.rs +++ b/crates/builder/src/lib.rs @@ -34,6 +34,6 @@ pub use server::{LocalBuilderBuilder, LocalBuilderHandle, RemoteBuilderClient}; mod signer; mod task; -pub use task::{Args as BuilderTaskArgs, BuilderTask}; +pub use task::{Args as BuilderTaskArgs, BuilderTask, EntryPointBuilderSettings}; mod transaction_tracker; diff --git a/crates/builder/src/task.rs b/crates/builder/src/task.rs index 80389ea2c..03b8b011e 100644 --- a/crates/builder/src/task.rs +++ b/crates/builder/src/task.rs @@ -16,22 +16,24 @@ use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; use anyhow::{bail, Context}; use async_trait::async_trait; use ethers::{ - providers::{JsonRpcClient, Provider}, - types::H256, + providers::{JsonRpcClient, Provider as EthersProvider}, + types::{Address, H256}, }; use ethers_signers::Signer; use futures::future; use futures_util::TryFutureExt; -use rundler_provider::EthersEntryPointV0_6; +use rundler_provider::{EntryPointProvider, EthersEntryPointV0_6, Provider}; use rundler_sim::{ simulation::v0_6::{ SimulateValidationTracerImpl as SimulateValidationTracerImplV0_6, Simulator as SimulatorV0_6, }, - MempoolConfig, PriorityFeeMode, SimulationSettings, + MempoolConfig, PriorityFeeMode, SimulationSettings, Simulator, }; use rundler_task::Task; -use rundler_types::{chain::ChainSpec, pool::Pool}; +use rundler_types::{ + chain::ChainSpec, pool::Pool, v0_6, EntryPointVersion, UserOperation, UserOperationVariant, +}; use rundler_utils::{emit::WithEntryPoint, handle}; use rusoto_core::Region; use tokio::{ @@ -87,8 +89,6 @@ pub struct Args { pub eth_poll_interval: Duration, /// Operation simulation settings pub sim_settings: SimulationSettings, - /// Alt-mempool configs - pub mempool_configs: HashMap, /// Maximum number of blocks to wait for a transaction to be mined pub max_blocks_to_wait_for_mine: u64, /// Percentage to increase the fees by when replacing a bundle transaction @@ -103,10 +103,23 @@ pub struct Args { /// /// Checked ~after~ checking for conditional sender or Flashbots sender. pub bloxroute_auth_header: Option, + /// Entry points to start builders for + pub entry_points: Vec, +} + +/// Builder settings for an entrypoint +#[derive(Debug)] +pub struct EntryPointBuilderSettings { + /// Entry point address + pub address: Address, + /// Entry point version + pub version: EntryPointVersion, /// Number of bundle builders to start pub num_bundle_builders: u64, /// Index offset for bundle builders pub bundle_builder_index_offset: u64, + /// Mempool configs + pub mempool_configs: HashMap, } /// Builder task @@ -124,23 +137,43 @@ where P: Pool + Clone, { async fn run(mut self: Box, shutdown_token: CancellationToken) -> anyhow::Result<()> { - info!("Mempool config: {:?}", self.args.mempool_configs); - let provider = rundler_provider::new_provider(&self.args.rpc_url, Some(self.args.eth_poll_interval))?; + let ep_v0_6 = EthersEntryPointV0_6::new( + self.args.chain_spec.entry_point_address, + Arc::clone(&provider), + ); + let mut sender_handles = vec![]; let mut bundle_sender_actions = vec![]; - for i in 0..self.args.num_bundle_builders { - let (spawn_guard, bundle_sender_action) = self - .create_bundle_builder( - i + self.args.bundle_builder_index_offset, - Arc::clone(&provider), - ) - .await?; - sender_handles.push(spawn_guard); - bundle_sender_actions.push(bundle_sender_action); + + for ep in &self.args.entry_points { + // TODO entry point v0.7: needs 0.7 EP and simulator + if ep.version != EntryPointVersion::V0_6 { + bail!("Unsupported entry point version: {:?}", ep.version); + } + + info!("Mempool config for ep v0.6: {:?}", ep.mempool_configs); + + for i in 0..ep.num_bundle_builders { + let (spawn_guard, bundle_sender_action) = self + .create_bundle_builder( + i + ep.bundle_builder_index_offset, + Arc::clone(&provider), + ep_v0_6.clone(), + self.create_simulator_v0_6( + Arc::clone(&provider), + ep_v0_6.clone(), + ep.mempool_configs.clone(), + ), + ) + .await?; + sender_handles.push(spawn_guard); + bundle_sender_actions.push(bundle_sender_action); + } } + // flatten the senders handles to one handle, short-circuit on errors let sender_handle = tokio::spawn( future::try_join_all(sender_handles) @@ -211,14 +244,23 @@ where Box::new(self) } - async fn create_bundle_builder( + async fn create_bundle_builder( &self, index: u64, - provider: Arc>, + provider: Arc>, + entry_point: E, + simulator: S, ) -> anyhow::Result<( JoinHandle>, mpsc::Sender, - )> { + )> + where + UO: UserOperation + From, + UserOperationVariant: AsRef, + E: EntryPointProvider + Clone, + S: Simulator, + C: JsonRpcClient + 'static, + { let (send_bundle_tx, send_bundle_rx) = mpsc::channel(1); let signer = if let Some(pk) = &self.args.private_key { @@ -265,20 +307,6 @@ where bundle_priority_fee_overhead_percent: self.args.bundle_priority_fee_overhead_percent, }; - let ep = EthersEntryPointV0_6::new( - self.args.chain_spec.entry_point_address, - Arc::clone(&provider), - ); - let simulate_validation_tracer = - SimulateValidationTracerImplV0_6::new(Arc::clone(&provider), ep.clone()); - let simulator = SimulatorV0_6::new( - Arc::clone(&provider), - ep.clone(), - simulate_validation_tracer, - self.args.sim_settings, - self.args.mempool_configs.clone(), - ); - let submit_provider = rundler_provider::new_provider( &self.args.submit_url, Some(self.args.eth_poll_interval), @@ -314,7 +342,7 @@ where index, self.pool.clone(), simulator, - ep.clone(), + entry_point.clone(), Arc::clone(&provider), proposer_settings, self.event_sender.clone(), @@ -325,7 +353,7 @@ where self.args.chain_spec.clone(), beneficiary, proposer, - ep, + entry_point, transaction_tracker, self.pool.clone(), builder_settings, @@ -335,4 +363,25 @@ where // Spawn each sender as its own independent task Ok((tokio::spawn(builder.send_bundles_in_loop()), send_bundle_tx)) } + + fn create_simulator_v0_6( + &self, + provider: Arc, + ep: E, + mempool_configs: HashMap, + ) -> SimulatorV0_6> + where + C: Provider, + E: EntryPointProvider + Clone, + { + let simulate_validation_tracer = + SimulateValidationTracerImplV0_6::new(Arc::clone(&provider), ep.clone()); + SimulatorV0_6::new( + Arc::clone(&provider), + ep, + simulate_validation_tracer, + self.args.sim_settings, + mempool_configs, + ) + } } diff --git a/crates/provider/src/ethers/entry_point/v0_6.rs b/crates/provider/src/ethers/entry_point/v0_6.rs index 9bc7ad76e..e84ed769a 100644 --- a/crates/provider/src/ethers/entry_point/v0_6.rs +++ b/crates/provider/src/ethers/entry_point/v0_6.rs @@ -45,7 +45,8 @@ use rundler_utils::eth::{self, ContractRevertError}; use crate::{ traits::HandleOpsOut, AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, - ExecutionResult, L1GasProvider, Provider, SignatureAggregator, SimulationProvider, + EntryPointProvider, ExecutionResult, L1GasProvider, Provider, SignatureAggregator, + SimulationProvider, }; const ARBITRUM_NITRO_NODE_INTERFACE_ADDRESS: Address = H160([ @@ -388,6 +389,11 @@ where } } +impl

EntryPointProvider for EntryPoint

where + P: Provider + Middleware + Send + Sync + 'static +{ +} + fn get_handle_ops_call( entry_point: &IEntryPoint, ops_per_aggregator: Vec>, diff --git a/crates/provider/src/lib.rs b/crates/provider/src/lib.rs index e05a69cb1..a56c92bc0 100644 --- a/crates/provider/src/lib.rs +++ b/crates/provider/src/lib.rs @@ -30,7 +30,7 @@ pub use traits::test_utils::*; #[cfg(any(test, feature = "test-utils"))] pub use traits::MockProvider; pub use traits::{ - AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint, ExecutionResult, - HandleOpsOut, L1GasProvider, Provider, ProviderError, ProviderResult, SignatureAggregator, - SimulationProvider, + AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint, EntryPointProvider, + ExecutionResult, HandleOpsOut, L1GasProvider, Provider, ProviderError, ProviderResult, + SignatureAggregator, SimulationProvider, }; diff --git a/crates/provider/src/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index 8e8678b17..2c205b99b 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -212,3 +212,13 @@ pub trait SimulationProvider: Send + Sync + 'static { revert_data: Bytes, ) -> Result; } + +/// Trait for a provider that provides all entry point functionality +pub trait EntryPointProvider: + EntryPoint + + SignatureAggregator + + BundleHandler + + SimulationProvider + + L1GasProvider +{ +} diff --git a/crates/provider/src/traits/mod.rs b/crates/provider/src/traits/mod.rs index 42aa230d4..be618c09e 100644 --- a/crates/provider/src/traits/mod.rs +++ b/crates/provider/src/traits/mod.rs @@ -18,8 +18,8 @@ pub use error::ProviderError; mod entry_point; pub use entry_point::{ - AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint, ExecutionResult, - HandleOpsOut, L1GasProvider, SignatureAggregator, SimulationProvider, + AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPoint, EntryPointProvider, + ExecutionResult, HandleOpsOut, L1GasProvider, SignatureAggregator, SimulationProvider, }; mod provider; From b1bc31df71ae6c8caef235ef1f2f46eb5af2e1ba Mon Sep 17 00:00:00 2001 From: dancoombs Date: Mon, 25 Mar 2024 16:20:10 -0400 Subject: [PATCH 07/12] feat(provider): add an entry point v0.7 provider --- crates/dev/src/lib.rs | 8 +- crates/pool/src/chain.rs | 18 +- crates/pool/src/mempool/paymaster.rs | 5 +- crates/pool/src/mempool/uo_pool.rs | 5 +- crates/provider/src/ethers/entry_point/mod.rs | 44 ++ .../provider/src/ethers/entry_point/v0_6.rs | 38 +- .../provider/src/ethers/entry_point/v0_7.rs | 436 ++++++++++++++++++ crates/provider/src/ethers/mod.rs | 2 +- crates/provider/src/lib.rs | 5 +- crates/provider/src/traits/entry_point.rs | 4 +- crates/provider/src/traits/test_utils.rs | 4 +- crates/rpc/src/eth/router.rs | 2 +- crates/sim/src/simulation/v0_6/simulator.rs | 8 +- crates/sim/src/simulation/v0_6/tracer.rs | 7 +- crates/types/build.rs | 4 +- crates/types/contracts/src/v0_6/imports.sol | 2 +- .../types/contracts/src/v0_7/GetBalances.sol | 23 + crates/types/contracts/src/v0_7/imports.sol | 3 +- crates/types/src/lib.rs | 4 +- crates/types/src/timestamp.rs | 17 + crates/types/src/user_operation/v0_6.rs | 2 +- crates/types/src/validation_results.rs | 224 ++++++++- 22 files changed, 782 insertions(+), 83 deletions(-) create mode 100644 crates/provider/src/ethers/entry_point/v0_7.rs create mode 100644 crates/types/contracts/src/v0_7/GetBalances.sol diff --git a/crates/dev/src/lib.rs b/crates/dev/src/lib.rs index 9206fa63e..0d70c9724 100644 --- a/crates/dev/src/lib.rs +++ b/crates/dev/src/lib.rs @@ -43,7 +43,7 @@ use ethers::{ }; use rundler_types::{ contracts::v0_6::{ - entry_point::EntryPoint, simple_account::SimpleAccount, + i_entry_point::IEntryPoint, simple_account::SimpleAccount, simple_account_factory::SimpleAccountFactory, verifying_paymaster::VerifyingPaymaster, }, v0_6, UserOperation, @@ -281,7 +281,7 @@ pub async fn deploy_dev_contracts(entry_point_bytecode: &str) -> anyhow::Result< let entry_point_address = deterministic_deploy .deploy_bytecode(entry_point_bytecode, 0) .await?; - let entry_point = EntryPoint::new(entry_point_address, Arc::clone(&deployer_client)); + let entry_point = IEntryPoint::new(entry_point_address, Arc::clone(&deployer_client)); // TODO use deterministic deployment // account factory @@ -348,7 +348,7 @@ pub struct DevClients { /// The client used by the bundler. pub bundler_client: Arc, /// The entry point contract. - pub entry_point: EntryPoint, + pub entry_point: IEntryPoint, /// The account factory contract. pub factory: SimpleAccountFactory>, /// The wallet contract. @@ -373,7 +373,7 @@ impl DevClients { let provider = new_local_provider(); let bundler_client = new_test_client(Arc::clone(&provider), BUNDLER_ACCOUNT_ID); let wallet_owner_client = new_test_client(Arc::clone(&provider), WALLET_OWNER_ACCOUNT_ID); - let entry_point = EntryPoint::new(entry_point_address, Arc::clone(&bundler_client)); + let entry_point = IEntryPoint::new(entry_point_address, Arc::clone(&bundler_client)); let factory = SimpleAccountFactory::new(factory_address, Arc::clone(&provider)); let wallet = SimpleAccount::new(wallet_address, Arc::clone(&provider)); let paymaster = VerifyingPaymaster::new(paymaster_address, Arc::clone(&provider)); diff --git a/crates/pool/src/chain.rs b/crates/pool/src/chain.rs index c62754866..2d885bd58 100644 --- a/crates/pool/src/chain.rs +++ b/crates/pool/src/chain.rs @@ -27,7 +27,7 @@ use futures::future; use rundler_provider::Provider; use rundler_task::block_watcher; use rundler_types::{ - contracts::{v0_6::entry_point as entry_point_v0_6, v0_7::entry_point as entry_point_v0_7}, + contracts::{v0_6::i_entry_point as entry_point_v0_6, v0_7::i_entry_point as entry_point_v0_7}, EntryPointVersion, Timestamp, UserOperationId, }; use tokio::{ @@ -459,9 +459,9 @@ impl Chain

{ fn load_v0_6(log: Log, mined_ops: &mut Vec, balance_updates: &mut Vec) { let address = log.address; - if let Ok(event) = entry_point_v0_6::EntryPointEvents::decode_log(&log.into()) { + if let Ok(event) = entry_point_v0_6::IEntryPointEvents::decode_log(&log.into()) { match event { - entry_point_v0_6::EntryPointEvents::UserOperationEventFilter(event) => { + entry_point_v0_6::IEntryPointEvents::UserOperationEventFilter(event) => { let paymaster = if event.paymaster.is_zero() { None } else { @@ -477,7 +477,7 @@ impl Chain

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

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

{ fn load_v0_7(log: Log, mined_ops: &mut Vec, balance_updates: &mut Vec) { let address = log.address; - if let Ok(event) = entry_point_v0_7::EntryPointEvents::decode_log(&log.into()) { + if let Ok(event) = entry_point_v0_7::IEntryPointEvents::decode_log(&log.into()) { match event { - entry_point_v0_7::EntryPointEvents::UserOperationEventFilter(event) => { + entry_point_v0_7::IEntryPointEvents::UserOperationEventFilter(event) => { let paymaster = if event.paymaster.is_zero() { None } else { @@ -520,7 +520,7 @@ impl Chain

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

{ }; balance_updates.push(info); } - entry_point_v0_7::EntryPointEvents::WithdrawnFilter(event) => { + entry_point_v0_7::IEntryPointEvents::WithdrawnFilter(event) => { let info = BalanceUpdate { entrypoint: address, address: event.account, diff --git a/crates/pool/src/mempool/paymaster.rs b/crates/pool/src/mempool/paymaster.rs index a9280188d..28599459d 100644 --- a/crates/pool/src/mempool/paymaster.rs +++ b/crates/pool/src/mempool/paymaster.rs @@ -524,9 +524,10 @@ impl PaymasterBalance { mod tests { use ethers::types::{Address, H256, U256}; use rundler_provider::{DepositInfo, MockEntryPointV0_6}; - use rundler_sim::EntityInfos; use rundler_types::{ - v0_6::UserOperation, UserOperation as UserOperationTrait, UserOperationId, ValidTimeRange, + pool::{PaymasterMetadata, PoolOperation}, + v0_6::UserOperation, + EntityInfos, UserOperation as UserOperationTrait, UserOperationId, ValidTimeRange, }; use super::*; diff --git a/crates/pool/src/mempool/uo_pool.rs b/crates/pool/src/mempool/uo_pool.rs index 9af9c3794..23622ceb6 100644 --- a/crates/pool/src/mempool/uo_pool.rs +++ b/crates/pool/src/mempool/uo_pool.rs @@ -681,13 +681,12 @@ mod tests { use std::collections::HashMap; use ethers::types::{Bytes, H160}; - use rundler_provider::MockEntryPointV0_6; + use rundler_provider::{DepositInfo, MockEntryPointV0_6}; use rundler_sim::{ MockPrechecker, MockSimulator, PrecheckError, PrecheckSettings, SimulationError, SimulationResult, SimulationSettings, ViolationError, }; use rundler_types::{ - contracts::v0_6::verifying_paymaster::DepositInfo, pool::{PrecheckViolation, SimulationViolation}, v0_6::UserOperation, EntityInfo, EntityInfos, EntityType, EntryPointVersion, GasFees, @@ -1428,7 +1427,7 @@ mod tests { let mut entrypoint = MockEntryPointV0_6::new(); entrypoint.expect_get_deposit_info().returning(|_| { Ok(DepositInfo { - deposit: 1000, + deposit: 1000.into(), staked: true, stake: 10000, unstake_delay_sec: 100, diff --git a/crates/provider/src/ethers/entry_point/mod.rs b/crates/provider/src/ethers/entry_point/mod.rs index b93f2790a..d90460131 100644 --- a/crates/provider/src/ethers/entry_point/mod.rs +++ b/crates/provider/src/ethers/entry_point/mod.rs @@ -11,4 +11,48 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. +use ethers::{ + providers::Middleware, + types::{Address, Bytes, Eip1559TransactionRequest, U256, U64}, +}; +use rundler_types::contracts::{ + arbitrum::node_interface::NodeInterface, optimism::gas_price_oracle::GasPriceOracle, +}; + pub(crate) mod v0_6; +pub(crate) mod v0_7; + +async fn estimate_arbitrum_l1_gas( + arb_node: &NodeInterface

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

, + address: Address, + data: Bytes, + gas_price: U256, +) -> anyhow::Result { + // construct an unsigned transaction with default values just for L1 gas estimation + let tx = Eip1559TransactionRequest::new() + .from(Address::random()) + .to(address) + .gas(U256::from(1_000_000)) + .max_priority_fee_per_gas(U256::from(100_000_000)) + .max_fee_per_gas(U256::from(100_000_000)) + .value(U256::from(0)) + .data(data) + .nonce(U256::from(100_000)) + .chain_id(U64::from(100_000)) + .rlp(); + + let l1_fee = opt_oracle.get_l1_fee(tx).call().await?; + Ok(l1_fee.checked_div(gas_price).unwrap_or(U256::MAX)) +} diff --git a/crates/provider/src/ethers/entry_point/v0_6.rs b/crates/provider/src/ethers/entry_point/v0_6.rs index e84ed769a..3bf96678b 100644 --- a/crates/provider/src/ethers/entry_point/v0_6.rs +++ b/crates/provider/src/ethers/entry_point/v0_6.rs @@ -20,7 +20,7 @@ use ethers::{ providers::{spoof, Middleware, RawCall}, types::{ transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, Eip1559TransactionRequest, - H160, H256, U256, U64, + H160, H256, U256, }, utils::hex, }; @@ -34,8 +34,8 @@ use rundler_types::{ i_entry_point::{ DepositInfo as DepositInfoV0_6, ExecutionResult as ExecutionResultV0_6, FailedOp, IEntryPoint, SignatureValidationFailed, + UserOpsPerAggregator as UserOpsPerAggregatorV0_6, }, - shared_types::UserOpsPerAggregator as UserOpsPerAggregatorV0_6, }, }, v0_6::UserOperation, @@ -270,12 +270,7 @@ where .calldata() .context("should get calldata for entry point handle ops")?; - let gas = self - .arb_node - .gas_estimate_l1_component(entry_point_address, false, data) - .call() - .await?; - Ok(U256::from(gas.0)) + super::estimate_arbitrum_l1_gas(&self.arb_node, entry_point_address, data).await } async fn calc_optimism_l1_gas( @@ -290,21 +285,8 @@ where .calldata() .context("should get calldata for entry point handle ops")?; - // construct an unsigned transaction with default values just for L1 gas estimation - let tx = Eip1559TransactionRequest::new() - .from(Address::random()) - .to(entry_point_address) - .gas(U256::from(1_000_000)) - .max_priority_fee_per_gas(U256::from(100_000_000)) - .max_fee_per_gas(U256::from(100_000_000)) - .value(U256::from(0)) - .data(data) - .nonce(U256::from(100_000)) - .chain_id(U64::from(100_000)) - .rlp(); - - let l1_fee = self.opt_gas_oracle.get_l1_fee(tx).call().await?; - Ok(l1_fee.checked_div(gas_price).unwrap_or(U256::MAX)) + super::estimate_optimism_l1_gas(&self.opt_gas_oracle, entry_point_address, data, gas_price) + .await } } @@ -315,18 +297,18 @@ where { type UO = UserOperation; - async fn get_simulate_validation_call( + fn get_tracer_simulate_validation_call( &self, user_op: UserOperation, max_validation_gas: u64, - ) -> anyhow::Result { + ) -> (TypedTransaction, spoof::State) { let pvg = user_op.pre_verification_gas; - let tx = self + let call = self .i_entry_point .simulate_validation(user_op) .gas(U256::from(max_validation_gas) + pvg) .tx; - Ok(tx) + (call, spoof::State::default()) } async fn call_simulate_validation( @@ -343,7 +325,7 @@ where .await { Ok(()) => anyhow::bail!("simulateValidation should always revert"), - Err(ContractError::Revert(revert_data)) => ValidationOutput::decode(revert_data) + Err(ContractError::Revert(revert_data)) => ValidationOutput::decode_v0_6(revert_data) .context("entry point should return validation output"), Err(error) => Err(error).context("call simulation RPC failed")?, } diff --git a/crates/provider/src/ethers/entry_point/v0_7.rs b/crates/provider/src/ethers/entry_point/v0_7.rs new file mode 100644 index 000000000..ef08cc44f --- /dev/null +++ b/crates/provider/src/ethers/entry_point/v0_7.rs @@ -0,0 +1,436 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use std::sync::Arc; + +use anyhow::Context; +use ethers::{ + abi::AbiDecode, + contract::{ContractError, FunctionCall}, + providers::{Middleware, RawCall}, + types::{ + spoof, transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, + Eip1559TransactionRequest, H160, H256, U256, + }, + utils::hex, +}; +use rundler_types::{ + contracts::{ + arbitrum::node_interface::NodeInterface, + optimism::gas_price_oracle::GasPriceOracle, + v0_7::{ + entry_point_simulations::{ + EntryPointSimulations, ExecutionResult as ExecutionResultV0_7, + ENTRYPOINTSIMULATIONS_BYTECODE, + }, + get_balances::{GetBalancesResult, GETBALANCES_BYTECODE}, + i_aggregator::IAggregator, + i_entry_point::{ + DepositInfo as DepositInfoV0_7, FailedOp, IEntryPoint, SignatureValidationFailed, + UserOpsPerAggregator as UserOpsPerAggregatorV0_7, + }, + }, + }, + v0_7::UserOperation, + GasFees, UserOpsPerAggregator, ValidationOutput, +}; +use rundler_utils::eth::{self, ContractRevertError}; + +use crate::{ + AggregatorOut, AggregatorSimOut, BundleHandler, DepositInfo, EntryPointProvider, + ExecutionResult, HandleOpsOut, L1GasProvider, Provider, SignatureAggregator, + SimulationProvider, +}; + +// From v0.7 EP contract +const REVERT_REASON_MAX_LEN: usize = 2048; + +// TODO(danc): These should be configurable from chain spec +const ARBITRUM_NITRO_NODE_INTERFACE_ADDRESS: Address = H160([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xc8, +]); +const OPTIMISM_BEDROCK_GAS_ORACLE_ADDRESS: Address = H160([ + 0x42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0F, +]); + +/// Entry point for the v0.7 contract. +#[derive(Debug, Clone)] +pub struct EntryPoint

{ + i_entry_point: IEntryPoint

, + provider: Arc

, + arb_node: NodeInterface

, + opt_gas_oracle: GasPriceOracle

, +} + +impl

EntryPoint

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

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

crate::traits::EntryPoint for EntryPoint

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

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

SignatureAggregator for EntryPoint

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

BundleHandler for EntryPoint

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

L1GasProvider for EntryPoint

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

SimulationProvider for EntryPoint

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

EntryPointProvider for EntryPoint

where + P: Provider + Middleware + Send + Sync + 'static +{ +} + +fn get_handle_ops_call( + entry_point: &IEntryPoint, + ops_per_aggregator: Vec>, + beneficiary: Address, + gas: U256, +) -> FunctionCall, M, ()> { + let mut ops_per_aggregator: Vec = ops_per_aggregator + .into_iter() + .map(|uoa| UserOpsPerAggregatorV0_7 { + user_ops: uoa.user_ops.into_iter().map(|op| op.pack()).collect(), + aggregator: uoa.aggregator, + signature: uoa.signature, + }) + .collect(); + let call = + if ops_per_aggregator.len() == 1 && ops_per_aggregator[0].aggregator == Address::zero() { + entry_point.handle_ops(ops_per_aggregator.swap_remove(0).user_ops, beneficiary) + } else { + entry_point.handle_aggregated_ops(ops_per_aggregator, beneficiary) + }; + call.gas(gas) +} + +impl From for ExecutionResult { + fn from(result: ExecutionResultV0_7) -> Self { + let account = rundler_types::parse_validation_data(result.account_validation_data); + let paymaster = rundler_types::parse_validation_data(result.paymaster_validation_data); + let intersect_range = account + .valid_time_range() + .intersect(paymaster.valid_time_range()); + + ExecutionResult { + pre_op_gas: result.pre_op_gas, + paid: result.paid, + valid_after: intersect_range.valid_after, + valid_until: intersect_range.valid_until, + target_success: result.target_success, + target_result: result.target_result, + } + } +} + +impl From for DepositInfo { + fn from(deposit_info: DepositInfoV0_7) -> Self { + Self { + deposit: deposit_info.deposit, + staked: deposit_info.staked, + stake: deposit_info.stake, + unstake_delay_sec: deposit_info.unstake_delay_sec, + withdraw_time: deposit_info.withdraw_time, + } + } +} diff --git a/crates/provider/src/ethers/mod.rs b/crates/provider/src/ethers/mod.rs index 8ddcb7d21..ad0f669b0 100644 --- a/crates/provider/src/ethers/mod.rs +++ b/crates/provider/src/ethers/mod.rs @@ -14,6 +14,6 @@ //! Provider implementations using [ethers-rs](https://github.com/gakonst/ethers-rs) mod entry_point; -pub use entry_point::v0_6::EntryPoint as EntryPointV0_6; +pub use entry_point::{v0_6::EntryPoint as EntryPointV0_6, v0_7::EntryPoint as EntryPointV0_7}; mod metrics_middleware; pub(crate) mod provider; diff --git a/crates/provider/src/lib.rs b/crates/provider/src/lib.rs index a56c92bc0..9501c6dd7 100644 --- a/crates/provider/src/lib.rs +++ b/crates/provider/src/lib.rs @@ -22,7 +22,10 @@ //! A provider is a type that provides access to blockchain data and functions mod ethers; -pub use ethers::{provider::new_provider, EntryPointV0_6 as EthersEntryPointV0_6}; +pub use ethers::{ + provider::new_provider, EntryPointV0_6 as EthersEntryPointV0_6, + EntryPointV0_7 as EthersEntryPointV0_7, +}; mod traits; #[cfg(any(test, feature = "test-utils"))] diff --git a/crates/provider/src/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index 2c205b99b..e2554cf89 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -181,11 +181,11 @@ pub trait SimulationProvider: Send + Sync + 'static { type UO: UserOperation; /// Construct a call for the entry point contract's `simulateValidation` function - async fn get_simulate_validation_call( + fn get_tracer_simulate_validation_call( &self, user_op: Self::UO, max_validation_gas: u64, - ) -> anyhow::Result; + ) -> (TypedTransaction, spoof::State); /// Call the entry point contract's `simulateValidation` function. async fn call_simulate_validation( diff --git a/crates/provider/src/traits/test_utils.rs b/crates/provider/src/traits/test_utils.rs index 62cf78815..a49ae96cd 100644 --- a/crates/provider/src/traits/test_utils.rs +++ b/crates/provider/src/traits/test_utils.rs @@ -52,11 +52,11 @@ mockall::mock! { #[async_trait::async_trait] impl SimulationProvider for EntryPointV0_6 { type UO = v0_6::UserOperation; - async fn get_simulate_validation_call( + fn get_tracer_simulate_validation_call( &self, user_op: v0_6::UserOperation, max_validation_gas: u64, - ) -> anyhow::Result; + ) -> (TypedTransaction, spoof::State); async fn call_simulate_validation( &self, user_op: v0_6::UserOperation, diff --git a/crates/rpc/src/eth/router.rs b/crates/rpc/src/eth/router.rs index 285c44608..b4959907c 100644 --- a/crates/rpc/src/eth/router.rs +++ b/crates/rpc/src/eth/router.rs @@ -289,7 +289,7 @@ where .call_simulate_validation(uo.into(), max_verification_gas) .await?; - Ok(!output.return_info.sig_failed) + Ok(!output.return_info.account_sig_failed) } } diff --git a/crates/sim/src/simulation/v0_6/simulator.rs b/crates/sim/src/simulation/v0_6/simulator.rs index bab4818cf..d0a30c2b8 100644 --- a/crates/sim/src/simulation/v0_6/simulator.rs +++ b/crates/sim/src/simulation/v0_6/simulator.rs @@ -175,7 +175,7 @@ where entity_infos: None, })? } - let Ok(entry_point_out) = ValidationOutput::decode_hex(revert_data) else { + let Ok(entry_point_out) = ValidationOutput::decode_v0_6_hex(revert_data) else { let entity_addr = match last_entity_type { EntityType::Factory => factory_address, EntityType::Paymaster => paymaster_address, @@ -256,7 +256,11 @@ where let mut violations = vec![]; - if entry_point_out.return_info.sig_failed { + // v0.6 doesn't distinguish between the different types of signature failures + // both of these will be set to true if the signature failed. + if entry_point_out.return_info.account_sig_failed + || entry_point_out.return_info.paymaster_sig_failed + { violations.push(SimulationViolation::InvalidSignature); } diff --git a/crates/sim/src/simulation/v0_6/tracer.rs b/crates/sim/src/simulation/v0_6/tracer.rs index 15854408a..27af6b1e3 100644 --- a/crates/sim/src/simulation/v0_6/tracer.rs +++ b/crates/sim/src/simulation/v0_6/tracer.rs @@ -101,10 +101,9 @@ where block_id: BlockId, max_validation_gas: u64, ) -> anyhow::Result { - let tx = self + let (tx, state_override) = self .entry_point - .get_simulate_validation_call(op, max_validation_gas) - .await?; + .get_tracer_simulate_validation_call(op, max_validation_gas); SimulationTracerOutput::try_from( self.provider @@ -118,7 +117,7 @@ where )), ..Default::default() }, - ..Default::default() + state_overrides: Some(state_override), }, ) .await?, diff --git a/crates/types/build.rs b/crates/types/build.rs index e47ae8fdc..b6ffe04e2 100644 --- a/crates/types/build.rs +++ b/crates/types/build.rs @@ -38,7 +38,6 @@ fn generate_v0_6_bindings() -> Result<(), Box> { MultiAbigen::from_abigens([ abigen_of("v0_6", "IEntryPoint")?, - abigen_of("v0_6", "EntryPoint")?, abigen_of("v0_6", "IAggregator")?, abigen_of("v0_6", "IStakeManager")?, abigen_of("v0_6", "GetBalances")?, @@ -64,9 +63,10 @@ fn generate_v0_7_bindings() -> Result<(), Box> { MultiAbigen::from_abigens([ abigen_of("v0_7", "IEntryPoint")?, - abigen_of("v0_7", "EntryPoint")?, abigen_of("v0_7", "IAggregator")?, abigen_of("v0_7", "IStakeManager")?, + abigen_of("v0_7", "GetBalances")?, + abigen_of("v0_7", "EntryPointSimulations")?, ]) .build()? .write_to_module("src/contracts/v0_7", false)?; diff --git a/crates/types/contracts/src/v0_6/imports.sol b/crates/types/contracts/src/v0_6/imports.sol index fbb9f4ab1..625263d49 100644 --- a/crates/types/contracts/src/v0_6/imports.sol +++ b/crates/types/contracts/src/v0_6/imports.sol @@ -6,6 +6,6 @@ pragma solidity ^0.8.13; import "account-abstraction/v0_6/samples/SimpleAccount.sol"; import "account-abstraction/v0_6/samples/SimpleAccountFactory.sol"; import "account-abstraction/v0_6/samples/VerifyingPaymaster.sol"; -import "account-abstraction/v0_6/core/EntryPoint.sol"; +import "account-abstraction/v0_6/interfaces/IEntryPoint.sol"; import "account-abstraction/v0_6/interfaces/IAggregator.sol"; import "account-abstraction/v0_6/interfaces/IStakeManager.sol"; diff --git a/crates/types/contracts/src/v0_7/GetBalances.sol b/crates/types/contracts/src/v0_7/GetBalances.sol new file mode 100644 index 000000000..bda81addc --- /dev/null +++ b/crates/types/contracts/src/v0_7/GetBalances.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.12; + +import "account-abstraction/v0_7/interfaces/IStakeManager.sol"; + +contract GetBalances { + error GetBalancesResult(uint256[] balances); + + constructor(address stakeManager, address[] memory addresses) { + revert GetBalancesResult(getBalancesHelper(stakeManager, addresses)); + } + + function getBalancesHelper(address stakeManager, address[] memory addresses) public view returns (uint256[] memory) { + uint256[] memory balances = new uint256[](addresses.length); + IStakeManager istakeManager = IStakeManager(stakeManager); + + for (uint256 i = 0; i < addresses.length; i++) { + balances[i] = istakeManager.balanceOf(addresses[i]); + } + + return balances; + } +} diff --git a/crates/types/contracts/src/v0_7/imports.sol b/crates/types/contracts/src/v0_7/imports.sol index 72be598ea..592dddd3a 100644 --- a/crates/types/contracts/src/v0_7/imports.sol +++ b/crates/types/contracts/src/v0_7/imports.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.13; // Simply importing a dependency is enough for Forge to include it in builds. -import "account-abstraction/v0_7/core/EntryPoint.sol"; +import "account-abstraction/v0_7/interfaces/IEntryPoint.sol"; import "account-abstraction/v0_7/interfaces/IAggregator.sol"; import "account-abstraction/v0_7/interfaces/IStakeManager.sol"; +import "account-abstraction/v0_7/core/EntryPointSimulations.sol"; diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index b9faf6d97..febe53299 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -48,4 +48,6 @@ mod storage; pub use storage::StorageSlot; mod validation_results; -pub use validation_results::{AggregatorInfo, StakeInfo, ValidationOutput, ValidationReturnInfo}; +pub use validation_results::{ + parse_validation_data, AggregatorInfo, StakeInfo, ValidationOutput, ValidationReturnInfo, +}; diff --git a/crates/types/src/timestamp.rs b/crates/types/src/timestamp.rs index 78d6bb14f..5e5187d24 100644 --- a/crates/types/src/timestamp.rs +++ b/crates/types/src/timestamp.rs @@ -192,6 +192,14 @@ impl ValidTimeRange { pub fn contains(self, timestamp: Timestamp, buffer: Duration) -> bool { self.valid_after <= timestamp && (timestamp + buffer) <= self.valid_until } + + /// Intersect two time ranges into a single time range that is valid whenever both are valid + pub fn intersect(self, other: Self) -> Self { + Self { + valid_after: self.valid_after.max(other.valid_after), + valid_until: self.valid_until.min(other.valid_until), + } + } } #[cfg(test)] @@ -286,6 +294,15 @@ mod test { assert_eq!(json, "\"0x64\""); } + #[test] + fn test_merge_time_ranges() { + let range1 = ValidTimeRange::new(Timestamp::new(100), Timestamp::new(200)); + let range2 = ValidTimeRange::new(Timestamp::new(150), Timestamp::new(250)); + let intersect = range1.intersect(range2); + assert_eq!(intersect.valid_after, Timestamp::new(150)); + assert_eq!(intersect.valid_until, Timestamp::new(200)); + } + fn get_timestamp_out_of_bounds_for_datetime() -> Timestamp { // This is just a bit further in the future than the maximum allowed // DateTime, which is just before the start of year 2^18 = 262144. diff --git a/crates/types/src/user_operation/v0_6.rs b/crates/types/src/user_operation/v0_6.rs index e7ec3bbdf..d13757346 100644 --- a/crates/types/src/user_operation/v0_6.rs +++ b/crates/types/src/user_operation/v0_6.rs @@ -23,7 +23,7 @@ use strum::IntoEnumIterator; use super::{ GasOverheads, UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant, }; -pub use crate::contracts::v0_6::shared_types::{UserOperation, UserOpsPerAggregator}; +pub use crate::contracts::v0_6::i_entry_point::{UserOperation, UserOpsPerAggregator}; use crate::{ entity::{Entity, EntityType}, EntryPointVersion, diff --git a/crates/types/src/validation_results.rs b/crates/types/src/validation_results.rs index dad3659be..4317b9a4d 100644 --- a/crates/types/src/validation_results.rs +++ b/crates/types/src/validation_results.rs @@ -12,16 +12,29 @@ // If not, see https://www.gnu.org/licenses/. use ethers::{ - abi, - abi::{AbiDecode, AbiError}, - types::{Address, Bytes, U256}, + abi::{self, AbiDecode, AbiError}, + types::{Address, Bytes, H160, U256}, }; use crate::{ - contracts::v0_6::entry_point::{ValidationResult, ValidationResultWithAggregation}, - Timestamp, + contracts::{ + v0_6::i_entry_point::{ + ValidationResult as ValidationResultV0_6, + ValidationResultWithAggregation as ValidationResultWithAggregationV0_6, + }, + v0_7::entry_point_simulations::{ + AggregatorStakeInfo as AggregatorStakeInfoV0_7, ReturnInfo as ReturnInfoV0_7, + StakeInfo as StakeInfoV0_7, ValidationResult as ValidationResultV0_7, + }, + }, + Timestamp, ValidTimeRange, }; +/// Both v0.6 and v0.7 contracts use this aggregator address to indicate that the signature validation failed +/// Zero is also used to indicate that no aggregator is used AND that the signature validation failed. +const SIG_VALIDATION_FAILED: Address = + H160([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); + /// Equivalent to the generated `ValidationResult` or /// `ValidationResultWithAggregation` from `EntryPoint`, but with named structs /// instead of tuples and with a helper for deserializing. @@ -39,21 +52,42 @@ pub struct ValidationOutput { pub aggregator_info: Option, } -impl AbiDecode for ValidationOutput { - fn decode(bytes: impl AsRef<[u8]>) -> Result { - if let Ok(result) = ValidationResult::decode(bytes.as_ref()) { +impl ValidationOutput { + /// Decode a v0.6 validation result from bytes. + pub fn decode_v0_6(bytes: impl AsRef<[u8]>) -> Result { + if let Ok(result) = ValidationResultV0_6::decode(bytes.as_ref()) { + return Ok(result.into()); + } + if let Ok(result) = ValidationResultWithAggregationV0_6::decode(bytes) { return Ok(result.into()); } - if let Ok(result) = ValidationResultWithAggregation::decode(bytes) { + Err(AbiError::DecodingError(abi::Error::InvalidData)) + } + + /// Decode a v0.6 validation result from hex. + pub fn decode_v0_6_hex(hex: impl AsRef) -> Result { + let bytes: Bytes = hex.as_ref().parse()?; + Self::decode_v0_6(&bytes) + } + + /// Decode a v0.7 validation result from bytes. + pub fn decode_v0_7(bytes: impl AsRef<[u8]>) -> Result { + if let Ok(result) = ValidationResultV0_7::decode(bytes.as_ref()) { return Ok(result.into()); } Err(AbiError::DecodingError(abi::Error::InvalidData)) } + + /// Decode a v0.7 validation result from hex. + pub fn decode_v0_7_hex(hex: impl AsRef) -> Result { + let bytes: Bytes = hex.as_ref().parse()?; + Self::decode_v0_7(&bytes) + } } -impl From for ValidationOutput { - fn from(value: ValidationResult) -> Self { - let ValidationResult { +impl From for ValidationOutput { + fn from(value: ValidationResultV0_6) -> Self { + let ValidationResultV0_6 { return_info, sender_info, factory_info, @@ -69,9 +103,9 @@ impl From for ValidationOutput { } } -impl From for ValidationOutput { - fn from(value: ValidationResultWithAggregation) -> Self { - let ValidationResultWithAggregation { +impl From for ValidationOutput { + fn from(value: ValidationResultWithAggregationV0_6) -> Self { + let ValidationResultWithAggregationV0_6 { return_info, sender_info, factory_info, @@ -88,13 +122,41 @@ impl From for ValidationOutput { } } +impl From for ValidationOutput { + fn from(value: ValidationResultV0_7) -> Self { + let ValidationResultV0_7 { + return_info, + sender_info, + factory_info, + paymaster_info, + aggregator_info, + } = value; + + let aggregator_info = if aggregator_info.aggregator.is_zero() { + None + } else { + Some(aggregator_info.into()) + }; + + Self { + return_info: return_info.into(), + sender_info: sender_info.into(), + factory_info: factory_info.into(), + paymaster_info: paymaster_info.into(), + aggregator_info, + } + } +} + /// ValidationReturnInfo from EntryPoint contract #[derive(Debug)] pub struct ValidationReturnInfo { /// The amount of gas used before the op was executed (pre verification gas and validation gas) pub pre_op_gas: U256, - /// Whether the signature verification failed - pub sig_failed: bool, + /// Whether the account signature verification failed + pub account_sig_failed: bool, + /// Whether the paymaster signature verification failed + pub paymaster_sig_failed: bool, /// The time after which the op is valid pub valid_after: Timestamp, /// The time until which the op is valid @@ -103,6 +165,7 @@ pub struct ValidationReturnInfo { pub paymaster_context: Bytes, } +// Conversion for v0.6 impl From<(U256, U256, bool, u64, u64, Bytes)> for ValidationReturnInfo { fn from(value: (U256, U256, bool, u64, u64, Bytes)) -> Self { let ( @@ -113,9 +176,11 @@ impl From<(U256, U256, bool, u64, u64, Bytes)> for ValidationReturnInfo { valid_until, paymaster_context, ) = value; + // In v0.6 if one signature fails both do Self { pre_op_gas, - sig_failed, + account_sig_failed: sig_failed, + paymaster_sig_failed: sig_failed, valid_after: valid_after.into(), valid_until: valid_until.into(), paymaster_context, @@ -123,6 +188,84 @@ impl From<(U256, U256, bool, u64, u64, Bytes)> for ValidationReturnInfo { } } +impl From for ValidationReturnInfo { + fn from(value: ReturnInfoV0_7) -> Self { + let ReturnInfoV0_7 { + pre_op_gas, + prefund: _, + account_validation_data, + paymaster_validation_data, + paymaster_context, + } = value; + + let account = parse_validation_data(account_validation_data); + let paymaster = parse_validation_data(paymaster_validation_data); + + let intersect_range = account + .valid_time_range() + .intersect(paymaster.valid_time_range()); + + Self { + pre_op_gas, + account_sig_failed: !account.signature_valid(), + paymaster_sig_failed: !paymaster.signature_valid(), + valid_after: intersect_range.valid_after, + valid_until: intersect_range.valid_until, + paymaster_context, + } + } +} + +/// ValidationData from EntryPoint contract +pub struct ValidationData { + aggregator: Address, + valid_after: u64, + valid_until: u64, +} + +impl ValidationData { + /// Valid time range for the validation data + pub fn valid_time_range(&self) -> ValidTimeRange { + ValidTimeRange::new(self.valid_after.into(), self.valid_until.into()) + } + + /// Whether the signature is valid + pub fn signature_valid(&self) -> bool { + self.aggregator != SIG_VALIDATION_FAILED + } + + /// The aggregator address, if any + pub fn aggregator(&self) -> Option

{ + if self.aggregator == SIG_VALIDATION_FAILED || self.aggregator.is_zero() { + None + } else { + Some(self.aggregator) + } + } +} + +/// Parse the validation data from a U256 +/// +/// Works for both v0.6 and v0.7 validation data +pub fn parse_validation_data(data: U256) -> ValidationData { + let slice: [u8; 32] = data.into(); + let aggregator = Address::from_slice(&slice[0..20]); + + let mut buf = [0_u8; 8]; + buf[..6].copy_from_slice(&slice[20..26]); + let valid_after = u64::from_le_bytes(buf); + + let mut buf = [0_u8; 8]; + buf[..6].copy_from_slice(&slice[26..32]); + let valid_until = u64::from_le_bytes(buf); + + ValidationData { + aggregator, + valid_after, + valid_until, + } +} + /// StakeInfo from EntryPoint contract #[derive(Clone, Copy, Debug)] pub struct StakeInfo { @@ -141,6 +284,19 @@ impl From<(U256, U256)> for StakeInfo { } } +impl From for StakeInfo { + fn from(value: StakeInfoV0_7) -> Self { + let StakeInfoV0_7 { + stake, + unstake_delay_sec, + } = value; + Self { + stake, + unstake_delay_sec, + } + } +} + /// AggregatorInfo from EntryPoint contract #[derive(Clone, Copy, Debug)] pub struct AggregatorInfo { @@ -158,3 +314,35 @@ impl From<(Address, (U256, U256))> for AggregatorInfo { } } } + +impl From for AggregatorInfo { + fn from(value: AggregatorStakeInfoV0_7) -> Self { + let AggregatorStakeInfoV0_7 { + aggregator, + stake_info, + } = value; + Self { + address: aggregator, + stake_info: stake_info.into(), + } + } +} + +#[cfg(test)] +mod tests { + use super::parse_validation_data; + + #[test] + fn test_parse_validation_data() { + let data = "0x00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"; + let parsed = parse_validation_data(data.into()); + assert_eq!( + parsed.aggregator, + "0x00112233445566778899aabbccddeeff00112233" + .parse() + .unwrap() + ); + assert_eq!(parsed.valid_after, 0x998877665544); // solidity is LE + assert_eq!(parsed.valid_until, 0xffeeddccbbaa); + } +} From ec3c822992d9e13495abd195799f3fb5c5cd8915 Mon Sep 17 00:00:00 2001 From: Dan Coombs Date: Fri, 29 Mar 2024 16:48:46 -0500 Subject: [PATCH 08/12] feat: unsafe mode for EP v0.6 (#648) --- bin/rundler/src/cli/builder.rs | 1 + bin/rundler/src/cli/mod.rs | 4 + bin/rundler/src/cli/pool.rs | 1 + bin/rundler/src/cli/rpc.rs | 1 + crates/builder/src/task.rs | 31 +++- crates/pool/src/task.rs | 72 +++++--- .../provider/src/ethers/entry_point/v0_6.rs | 14 +- .../provider/src/ethers/entry_point/v0_7.rs | 11 +- crates/provider/src/traits/entry_point.rs | 1 + crates/provider/src/traits/test_utils.rs | 1 + crates/rpc/src/eth/router.rs | 2 +- crates/rpc/src/task.rs | 2 + crates/sim/src/simulation/v0_6/mod.rs | 3 + crates/sim/src/simulation/v0_6/simulator.rs | 5 - crates/sim/src/simulation/v0_6/unsafe_sim.rs | 173 ++++++++++++++++++ 15 files changed, 275 insertions(+), 47 deletions(-) create mode 100644 crates/sim/src/simulation/v0_6/unsafe_sim.rs diff --git a/bin/rundler/src/cli/builder.rs b/bin/rundler/src/cli/builder.rs index afe26cc3b..f103bb7b1 100644 --- a/bin/rundler/src/cli/builder.rs +++ b/bin/rundler/src/cli/builder.rs @@ -209,6 +209,7 @@ impl BuilderArgs { mempool_configs, }], chain_spec, + unsafe_mode: common.unsafe_mode, rpc_url, private_key: self.private_key.clone(), aws_kms_key_ids: self.aws_kms_key_ids.clone(), diff --git a/bin/rundler/src/cli/mod.rs b/bin/rundler/src/cli/mod.rs index ae5816926..df060bd69 100644 --- a/bin/rundler/src/cli/mod.rs +++ b/bin/rundler/src/cli/mod.rs @@ -123,6 +123,10 @@ pub struct CommonArgs { )] node_http: Option, + /// Flag for turning unsafe bundling mode on + #[arg(long = "unsafe", env = "UNSAFE", global = true)] + unsafe_mode: bool, + #[arg( long = "max_verification_gas", name = "max_verification_gas", diff --git a/bin/rundler/src/cli/pool.rs b/bin/rundler/src/cli/pool.rs index 639103f2c..5e4169690 100644 --- a/bin/rundler/src/cli/pool.rs +++ b/bin/rundler/src/cli/pool.rs @@ -206,6 +206,7 @@ impl PoolArgs { Ok(PoolTaskArgs { chain_spec, + unsafe_mode: common.unsafe_mode, http_url: common .node_http .clone() diff --git a/bin/rundler/src/cli/rpc.rs b/bin/rundler/src/cli/rpc.rs index 475c6bc2d..64d5da482 100644 --- a/bin/rundler/src/cli/rpc.rs +++ b/bin/rundler/src/cli/rpc.rs @@ -97,6 +97,7 @@ impl RpcArgs { Ok(RpcTaskArgs { chain_spec, + unsafe_mode: common.unsafe_mode, port: self.port, host: self.host.clone(), rpc_url: common diff --git a/crates/builder/src/task.rs b/crates/builder/src/task.rs index 03b8b011e..4c2853249 100644 --- a/crates/builder/src/task.rs +++ b/crates/builder/src/task.rs @@ -26,7 +26,7 @@ use rundler_provider::{EntryPointProvider, EthersEntryPointV0_6, Provider}; use rundler_sim::{ simulation::v0_6::{ SimulateValidationTracerImpl as SimulateValidationTracerImplV0_6, - Simulator as SimulatorV0_6, + Simulator as SimulatorV0_6, UnsafeSimulator as UnsafeSimulatorV0_6, }, MempoolConfig, PriorityFeeMode, SimulationSettings, Simulator, }; @@ -61,6 +61,8 @@ pub struct Args { pub chain_spec: ChainSpec, /// Full node RPC url pub rpc_url: String, + /// True if using unsafe mode + pub unsafe_mode: bool, /// Private key to use for signing transactions /// If not provided, AWS KMS will be used pub private_key: Option, @@ -157,8 +159,16 @@ where info!("Mempool config for ep v0.6: {:?}", ep.mempool_configs); for i in 0..ep.num_bundle_builders { - let (spawn_guard, bundle_sender_action) = self - .create_bundle_builder( + let (spawn_guard, bundle_sender_action) = if self.args.unsafe_mode { + self.create_bundle_builder( + i + ep.bundle_builder_index_offset, + Arc::clone(&provider), + ep_v0_6.clone(), + self.create_unsafe_simulator_v0_6(Arc::clone(&provider), ep_v0_6.clone()), + ) + .await? + } else { + self.create_bundle_builder( i + ep.bundle_builder_index_offset, Arc::clone(&provider), ep_v0_6.clone(), @@ -168,7 +178,8 @@ where ep.mempool_configs.clone(), ), ) - .await?; + .await? + }; sender_handles.push(spawn_guard); bundle_sender_actions.push(bundle_sender_action); } @@ -384,4 +395,16 @@ where mempool_configs, ) } + + fn create_unsafe_simulator_v0_6( + &self, + provider: Arc, + ep: E, + ) -> UnsafeSimulatorV0_6 + where + C: Provider, + E: EntryPointProvider + Clone, + { + UnsafeSimulatorV0_6::new(Arc::clone(&provider), ep, self.args.sim_settings) + } } diff --git a/crates/pool/src/task.rs b/crates/pool/src/task.rs index 7dc39540d..ed8a13de4 100644 --- a/crates/pool/src/task.rs +++ b/crates/pool/src/task.rs @@ -39,6 +39,8 @@ use crate::{ pub struct Args { /// Chain specification. pub chain_spec: ChainSpec, + /// True if using unsafe mode. + pub unsafe_mode: bool, /// HTTP URL for the full node. pub http_url: String, /// Poll interval for full node requests. @@ -94,6 +96,7 @@ impl Task for PoolTask { let pool = PoolTask::create_mempool_v0_6( self.args.chain_spec.clone(), pool_config, + self.args.unsafe_mode, self.event_sender.clone(), provider.clone(), ) @@ -121,17 +124,6 @@ impl Task for PoolTask { ); } } - - let pool = PoolTask::create_mempool_v0_6( - self.args.chain_spec.clone(), - pool_config, - self.event_sender.clone(), - provider.clone(), - ) - .await - .context("should have created mempool")?; - - mempools.insert(pool_config.entry_point, pool); } let pool_handle = self.pool_builder.get_handle(); @@ -204,6 +196,7 @@ impl PoolTask { async fn create_mempool_v0_6( chain_spec: ChainSpec, pool_config: &PoolConfig, + unsafe_mode: bool, event_sender: broadcast::Sender>, provider: Arc

, ) -> anyhow::Result>> { @@ -216,16 +209,6 @@ impl PoolTask { pool_config.precheck_settings, ); - let simulate_validation_tracer = - sim_v0_6::SimulateValidationTracerImpl::new(Arc::clone(&provider), ep.clone()); - let simulator = sim_v0_6::Simulator::new( - Arc::clone(&provider), - ep.clone(), - simulate_validation_tracer, - pool_config.sim_settings, - pool_config.mempool_channel_configs.clone(), - ); - let reputation = Arc::new(AddressReputation::new( ReputationParams::new(pool_config.reputation_tracking_enabled), pool_config.blocklist.clone().unwrap_or_default(), @@ -246,15 +229,44 @@ impl PoolTask { ), ); - let uo_pool = UoPool::new( - pool_config.clone(), - event_sender, - prechecker, - simulator, - paymaster, - reputation, - ); + if unsafe_mode { + let simulator = sim_v0_6::UnsafeSimulator::new( + Arc::clone(&provider), + ep.clone(), + pool_config.sim_settings, + ); + + let uo_pool = UoPool::new( + pool_config.clone(), + event_sender, + prechecker, + simulator, + paymaster, + reputation, + ); - Ok(Arc::new(Box::new(uo_pool))) + Ok(Arc::new(Box::new(uo_pool))) + } else { + let simulate_validation_tracer = + sim_v0_6::SimulateValidationTracerImpl::new(Arc::clone(&provider), ep.clone()); + let simulator = sim_v0_6::Simulator::new( + Arc::clone(&provider), + ep.clone(), + simulate_validation_tracer, + pool_config.sim_settings, + pool_config.mempool_channel_configs.clone(), + ); + + let uo_pool = UoPool::new( + pool_config.clone(), + event_sender, + prechecker, + simulator, + paymaster, + reputation, + ); + + Ok(Arc::new(Box::new(uo_pool))) + } } } diff --git a/crates/provider/src/ethers/entry_point/v0_6.rs b/crates/provider/src/ethers/entry_point/v0_6.rs index 3bf96678b..b70c4212f 100644 --- a/crates/provider/src/ethers/entry_point/v0_6.rs +++ b/crates/provider/src/ethers/entry_point/v0_6.rs @@ -315,15 +315,19 @@ where &self, user_op: UserOperation, max_validation_gas: u64, + block_hash: Option, ) -> anyhow::Result { let pvg = user_op.pre_verification_gas; - match self + let blockless = self .i_entry_point .simulate_validation(user_op) - .gas(U256::from(max_validation_gas) + pvg) - .call() - .await - { + .gas(U256::from(max_validation_gas) + pvg); + let call = match block_hash { + Some(block_hash) => blockless.block(block_hash), + None => blockless, + }; + + match call.call().await { Ok(()) => anyhow::bail!("simulateValidation should always revert"), Err(ContractError::Revert(revert_data)) => ValidationOutput::decode_v0_6(revert_data) .context("entry point should return validation output"), diff --git a/crates/provider/src/ethers/entry_point/v0_7.rs b/crates/provider/src/ethers/entry_point/v0_7.rs index ef08cc44f..a30086590 100644 --- a/crates/provider/src/ethers/entry_point/v0_7.rs +++ b/crates/provider/src/ethers/entry_point/v0_7.rs @@ -310,6 +310,7 @@ where &self, user_op: UserOperation, max_validation_gas: u64, + block_hash: Option, ) -> anyhow::Result { let addr = self.i_entry_point.address(); let pvg = user_op.pre_verification_gas; @@ -319,9 +320,15 @@ where .code(ENTRYPOINTSIMULATIONS_BYTECODE.clone()); let ep_simulations = EntryPointSimulations::new(addr, Arc::clone(&self.provider)); - let result = ep_simulations + let blockless = ep_simulations .simulate_validation(user_op.pack()) - .gas(U256::from(max_validation_gas) + pvg) + .gas(U256::from(max_validation_gas) + pvg); + let call = match block_hash { + Some(block_hash) => blockless.block(block_hash), + None => blockless, + }; + + let result = call .call_raw() .state(&spoof_ep) .await diff --git a/crates/provider/src/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index e2554cf89..e77c03fba 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -192,6 +192,7 @@ pub trait SimulationProvider: Send + Sync + 'static { &self, user_op: Self::UO, max_validation_gas: u64, + block_hash: Option, ) -> anyhow::Result; /// Call the entry point contract's `simulateHandleOps` function diff --git a/crates/provider/src/traits/test_utils.rs b/crates/provider/src/traits/test_utils.rs index a49ae96cd..08b90a8c6 100644 --- a/crates/provider/src/traits/test_utils.rs +++ b/crates/provider/src/traits/test_utils.rs @@ -61,6 +61,7 @@ mockall::mock! { &self, user_op: v0_6::UserOperation, max_validation_gas: u64, + block_hash: Option ) -> anyhow::Result; async fn call_spoofed_simulate_op( &self, diff --git a/crates/rpc/src/eth/router.rs b/crates/rpc/src/eth/router.rs index b4959907c..982d27f49 100644 --- a/crates/rpc/src/eth/router.rs +++ b/crates/rpc/src/eth/router.rs @@ -286,7 +286,7 @@ where ) -> anyhow::Result { let output = self .entry_point - .call_simulate_validation(uo.into(), max_verification_gas) + .call_simulate_validation(uo.into(), max_verification_gas, None) .await?; Ok(!output.return_info.account_sig_failed) diff --git a/crates/rpc/src/task.rs b/crates/rpc/src/task.rs index a72763b8d..2962ca2bf 100644 --- a/crates/rpc/src/task.rs +++ b/crates/rpc/src/task.rs @@ -48,6 +48,8 @@ use crate::{ pub struct Args { /// Chain spec pub chain_spec: ChainSpec, + /// True if using unsafe mode + pub unsafe_mode: bool, /// Port to listen on. pub port: u16, /// Host to listen on. diff --git a/crates/sim/src/simulation/v0_6/mod.rs b/crates/sim/src/simulation/v0_6/mod.rs index 40dbc3a14..cf71b0816 100644 --- a/crates/sim/src/simulation/v0_6/mod.rs +++ b/crates/sim/src/simulation/v0_6/mod.rs @@ -19,5 +19,8 @@ pub use simulator::Simulator; mod tracer; pub use tracer::{SimulateValidationTracer, SimulateValidationTracerImpl}; +mod unsafe_sim; +pub use unsafe_sim::UnsafeSimulator; + /// Required buffer for verification gas limit when targeting the 0.6 entrypoint contract pub(crate) const REQUIRED_VERIFICATION_GAS_LIMIT_BUFFER: U256 = U256([2000, 0, 0, 0]); diff --git a/crates/sim/src/simulation/v0_6/simulator.rs b/crates/sim/src/simulation/v0_6/simulator.rs index d0a30c2b8..b553deb8d 100644 --- a/crates/sim/src/simulation/v0_6/simulator.rs +++ b/crates/sim/src/simulation/v0_6/simulator.rs @@ -113,11 +113,6 @@ where } } - /// Return the associated settings - pub fn settings(&self) -> &Settings { - &self.sim_settings - } - // Run the tracer and transform the output. // Any violations during this stage are errors. async fn create_context( diff --git a/crates/sim/src/simulation/v0_6/unsafe_sim.rs b/crates/sim/src/simulation/v0_6/unsafe_sim.rs new file mode 100644 index 000000000..032519e02 --- /dev/null +++ b/crates/sim/src/simulation/v0_6/unsafe_sim.rs @@ -0,0 +1,173 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use std::sync::Arc; + +use ethers::types::H256; +use rundler_provider::{ + AggregatorOut, EntryPoint, Provider, SignatureAggregator, SimulationProvider, +}; +use rundler_types::{ + pool::SimulationViolation, v0_6::UserOperation, EntityInfo, EntityInfos, + UserOperation as UserOperationTrait, ValidTimeRange, +}; + +use crate::{ + SimulationError, SimulationResult, SimulationSettings as Settings, Simulator, ViolationError, +}; + +/// An unsafe simulator that can be used in place of a regular simulator +/// to extract the information needed from simulation while avoiding the use +/// of debug_traceCall. +/// +/// WARNING: This is "unsafe" for a reason. None of the ERC-7562 checks are +/// performed. +pub struct UnsafeSimulator { + provider: Arc

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

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

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

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

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

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

{ i_entry_point: IEntryPoint

, provider: Arc

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

Clone for EntryPoint

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

crate::traits::EntryPoint for EntryPoint

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

EntryPointProvider for EntryPoint

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

{ - chain_spec: ChainSpec, + pub(crate) chain_spec: ChainSpec, pool: P, router: EntryPointRouter, } diff --git a/crates/rpc/src/eth/error.rs b/crates/rpc/src/eth/error.rs index 3a6447897..2a980f3bb 100644 --- a/crates/rpc/src/eth/error.rs +++ b/crates/rpc/src/eth/error.rs @@ -11,6 +11,8 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. +use std::fmt::Display; + use ethers::types::{Address, Bytes, Opcode, U256}; use jsonrpsee::types::{ error::{CALL_EXECUTION_FAILED_CODE, INTERNAL_ERROR_CODE, INVALID_PARAMS_CODE}, @@ -20,7 +22,7 @@ use rundler_provider::ProviderError; use rundler_sim::GasEstimationError; use rundler_types::{ pool::{MempoolError, PoolError, PrecheckViolation, SimulationViolation}, - Entity, EntityType, Timestamp, + Entity, EntityType, Timestamp, ValidationRevert, }; use serde::Serialize; @@ -117,6 +119,8 @@ pub enum EthRpcError { PrecheckFailed(PrecheckViolation), #[error("validation simulation failed: {0}")] SimulationFailed(SimulationViolation), + #[error("validation reverted: {0}")] + ValidationRevert(ValidationRevertData), #[error("{0}")] ExecutionReverted(String), #[error("execution reverted")] @@ -174,6 +178,25 @@ impl StakeTooLowData { } } +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ValidationRevertData { + reason: Option, + data: Option, +} + +impl Display for ValidationRevertData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(reason) = &self.reason { + write!(f, "{}", reason)?; + } + if let Some(data) = &self.data { + write!(f, " data len: {}", data.len())?; + } + Ok(()) + } +} + #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReplacementUnderpricedData { @@ -306,6 +329,23 @@ impl From for EthRpcError { } SimulationViolation::AggregatorValidationFailed => Self::SignatureCheckFailed, SimulationViolation::OutOfGas(entity) => Self::OutOfGas(entity), + SimulationViolation::ValidationRevert(revert) => { + let data = match revert { + ValidationRevert::EntryPoint(reason) => ValidationRevertData { + reason: Some(reason), + data: None, + }, + ValidationRevert::Operation(reason, data) => ValidationRevertData { + reason: Some(reason), + data: Some(data), + }, + ValidationRevert::Unknown(data) => ValidationRevertData { + reason: None, + data: Some(data), + }, + }; + Self::ValidationRevert(data) + } _ => Self::SimulationFailed(value), } } @@ -355,6 +395,7 @@ impl From for ErrorObjectOwned { EthRpcError::ExecutionRevertedWithBytes(data) => { rpc_err_with_data(EXECUTION_REVERTED, msg, data) } + EthRpcError::ValidationRevert(data) => rpc_err_with_data(EXECUTION_REVERTED, msg, data), EthRpcError::OperationRejected(_) => rpc_err(INVALID_PARAMS_CODE, msg), } } diff --git a/crates/rpc/src/eth/events/common.rs b/crates/rpc/src/eth/events/common.rs new file mode 100644 index 000000000..f0486b3ed --- /dev/null +++ b/crates/rpc/src/eth/events/common.rs @@ -0,0 +1,263 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use std::{collections::VecDeque, sync::Arc}; + +use anyhow::Context; +use ethers::{ + prelude::EthEvent, + types::{ + Address, Bytes, Filter, GethDebugBuiltInTracerType, GethDebugTracerType, + GethDebugTracingOptions, GethTrace, GethTraceFrame, Log, TransactionReceipt, H256, U256, + }, +}; +use rundler_provider::Provider; +use rundler_types::{UserOperation, UserOperationVariant}; +use rundler_utils::{eth, log::LogOnError}; + +use super::UserOperationEventProvider; +use crate::types::{RpcUserOperationByHash, RpcUserOperationReceipt}; + +#[derive(Debug)] +pub(crate) struct UserOperationEventProviderImpl { + chain_id: u64, + address: Address, + provider: Arc

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

{ provider: Arc

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

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

{ } } -fn get_fee_oracle

(chain_spec: &ChainSpec, provider: Arc

) -> Arc> +fn get_fee_oracle

(chain_spec: &ChainSpec, provider: Arc

) -> Arc where P: Provider + Debug, { if !chain_spec.eip1559_enabled { - return Arc::new(Box::new(ConstantOracle::new(U256::zero()))); + return Arc::new(ConstantOracle::new(U256::zero())); } match chain_spec.priority_fee_oracle_type { - chain::PriorityFeeOracleType::Provider => Arc::new(Box::new(ProviderOracle::new( + chain::PriorityFeeOracleType::Provider => Arc::new(ProviderOracle::new( provider, chain_spec.min_max_priority_fee_per_gas, - ))), + )), chain::PriorityFeeOracleType::UsageBased => { let config = UsageBasedFeeOracleConfig { minimum_fee: chain_spec.min_max_priority_fee_per_gas, maximum_fee: chain_spec.max_max_priority_fee_per_gas, ..Default::default() }; - Arc::new(Box::new(UsageBasedFeeOracle::new(provider, config))) + Arc::new(UsageBasedFeeOracle::new(provider, config)) } } } From 8d0f94d4fd6342e5668b0b33ab0cae77a3269253 Mon Sep 17 00:00:00 2001 From: dancoombs Date: Sat, 30 Mar 2024 13:26:17 -0500 Subject: [PATCH 12/12] chore: fixing PR comments --- crates/builder/src/bundle_proposer.rs | 14 ++--- crates/rpc/src/eth/api.rs | 61 +++++++++++---------- crates/sim/src/simulation/v0_6/simulator.rs | 4 +- crates/types/src/pool/types.rs | 11 +--- 4 files changed, 42 insertions(+), 48 deletions(-) diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index 6ec830050..49452b457 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -105,9 +105,9 @@ pub(crate) trait BundleProposer: Send + Sync + 'static { } #[derive(Debug)] -pub(crate) struct BundleProposerImpl { +pub(crate) struct BundleProposerImpl { builder_index: u64, - pool: C, + pool: M, simulator: S, entry_point: E, provider: Arc

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

, diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 47c3cf56e..1f12e0e7b 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -172,9 +172,8 @@ mod tests { types::{Bytes, Log, Transaction}, }; use mockall::predicate::eq; - use rundler_pool::{IntoPoolOperationVariant, MockPoolServer, PoolOperation}; - use rundler_provider::{MockEntryPoint, MockEntryPointV0_6, MockProvider}; - use rundler_sim::{EntityInfos, PriorityFeeMode}; + use rundler_provider::{EntryPoint, MockEntryPointV0_6, MockProvider}; + use rundler_sim::{EstimationSettings, FeeEstimator, GasEstimatorV0_6, PriorityFeeMode}; use rundler_types::{ contracts::v0_6::i_entry_point::{HandleOpsCall, IEntryPointCalls}, pool::{MockPool, PoolOperation}, @@ -319,38 +318,42 @@ mod tests { ep: MockEntryPointV0_6, pool: MockPool, ) -> EthApi { + let ep = Arc::new(ep); let provider = Arc::new(provider); let chain_spec = ChainSpec { id: 1, ..Default::default() }; - contexts_by_entry_point - .insert( - ep.address(), - EntryPointContext::new( - chain_spec.clone(), - Arc::clone(&provider), - ep, - EstimationSettings { - max_verification_gas: 1_000_000, - max_call_gas: 1_000_000, - max_simulate_handle_ops_gas: 1_000_000, - verification_estimation_gas_fee: 1_000_000_000_000, - }, - FeeEstimator::new( - &chain_spec, - Arc::clone(&provider), - PriorityFeeMode::BaseFeePercent(0), - 0, - ), - UserOperationEventProviderV0_6::new( - chain_spec.id, - ep.address(), - provider.clone(), - None, - ), + + let gas_estimator = GasEstimatorV0_6::new( + chain_spec.clone(), + provider.clone(), + ep.clone(), + EstimationSettings { + max_verification_gas: 1_000_000, + max_call_gas: 1_000_000, + max_simulate_handle_ops_gas: 1_000_000, + verification_estimation_gas_fee: 1_000_000_000_000, + }, + FeeEstimator::new( + &chain_spec, + Arc::clone(&provider), + PriorityFeeMode::BaseFeePercent(0), + 0, + ), + ); + + let router = EntryPointRouterBuilder::default() + .v0_6(EntryPointRouteImpl::new( + ep.clone(), + gas_estimator, + UserOperationEventProviderV0_6::new( + chain_spec.id, + ep.address(), + provider.clone(), + None, ), - ) + )) .build(); EthApi { diff --git a/crates/sim/src/simulation/v0_6/simulator.rs b/crates/sim/src/simulation/v0_6/simulator.rs index b553deb8d..2176ead0b 100644 --- a/crates/sim/src/simulation/v0_6/simulator.rs +++ b/crates/sim/src/simulation/v0_6/simulator.rs @@ -1009,7 +1009,7 @@ mod tests { has_factory: true, associated_addresses: HashSet::new(), block_id: BlockId::Number(BlockNumber::Latest), - entity_infos: EntityInfos::new( + entity_infos: simulation::infos_from_validation_output( Some(Address::from_str("0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789").unwrap()), Address::from_str("0xb856dbd4fa1a79a46d426f537455e7d3e79ab7c4").unwrap(), Some(Address::from_str("0x8abb13360b87be5eeb1b98647a016add927a136c").unwrap()), @@ -1126,7 +1126,7 @@ mod tests { has_factory: true, associated_addresses: HashSet::new(), block_id: BlockId::Number(BlockNumber::Latest), - entity_infos: EntityInfos::new( + entity_infos: simulation::infos_from_validation_output( Some(factory_address), sender_address, None, diff --git a/crates/types/src/pool/types.rs b/crates/types/src/pool/types.rs index d2753c159..694f29f02 100644 --- a/crates/types/src/pool/types.rs +++ b/crates/types/src/pool/types.rs @@ -20,7 +20,7 @@ use crate::{ }; /// The new head of the chain, as viewed by the pool -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct NewHead { /// The hash of the new head pub block_hash: H256, @@ -28,15 +28,6 @@ pub struct NewHead { pub block_number: u64, } -impl Default for NewHead { - fn default() -> NewHead { - NewHead { - block_hash: H256::zero(), - block_number: 0, - } - } -} - /// The reputation of an entity #[derive(Debug, Clone)] pub struct Reputation {