diff --git a/Cargo.lock b/Cargo.lock index 8d020a1e2..df2fd4073 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4216,6 +4216,7 @@ dependencies = [ "serde", "thiserror", "tokio", + "tracing", ] [[package]] diff --git a/bin/rundler/src/cli/mod.rs b/bin/rundler/src/cli/mod.rs index dccd39a8b..982cc6d29 100644 --- a/bin/rundler/src/cli/mod.rs +++ b/bin/rundler/src/cli/mod.rs @@ -27,7 +27,7 @@ use builder::BuilderCliArgs; use node::NodeCliArgs; use pool::PoolCliArgs; use rpc::RpcCliArgs; -use rundler_rpc::EthApiSettings; +use rundler_rpc::{EthApiSettings, RundlerApiSettings}; use rundler_sim::{ EstimationSettings, PrecheckSettings, PriorityFeeMode, SimulationSettings, MIN_CALL_GAS_LIMIT, }; @@ -298,7 +298,7 @@ impl TryFrom<&CommonArgs> for EstimationSettings { impl TryFrom<&CommonArgs> for PrecheckSettings { type Error = anyhow::Error; - fn try_from(value: &CommonArgs) -> anyhow::Result { + fn try_from(value: &CommonArgs) -> Result { Ok(Self { max_verification_gas: value.max_verification_gas.into(), max_total_execution_gas: value.max_bundle_gas.into(), @@ -330,6 +330,21 @@ impl From<&CommonArgs> for EthApiSettings { } } +impl TryFrom<&CommonArgs> for RundlerApiSettings { + type Error = anyhow::Error; + + fn try_from(value: &CommonArgs) -> Result { + Ok(Self { + priority_fee_mode: PriorityFeeMode::try_from( + value.priority_fee_mode_kind.as_str(), + value.priority_fee_mode_value, + )?, + bundle_priority_fee_overhead_percent: value.bundle_priority_fee_overhead_percent, + max_verification_gas: value.max_verification_gas, + }) + } +} + /// CLI options for the metrics server #[derive(Debug, Args)] #[command(next_help_heading = "Metrics")] diff --git a/bin/rundler/src/cli/node/mod.rs b/bin/rundler/src/cli/node/mod.rs index 1b27f8e30..8a4b804eb 100644 --- a/bin/rundler/src/cli/node/mod.rs +++ b/bin/rundler/src/cli/node/mod.rs @@ -67,6 +67,7 @@ pub async fn run( (&common_args).try_into()?, (&common_args).into(), (&common_args).try_into()?, + (&common_args).try_into()?, )?; let (event_sender, event_rx) = diff --git a/bin/rundler/src/cli/pool.rs b/bin/rundler/src/cli/pool.rs index bf3f3c148..5323595ce 100644 --- a/bin/rundler/src/cli/pool.rs +++ b/bin/rundler/src/cli/pool.rs @@ -134,6 +134,14 @@ pub struct PoolArgs { default_value = "true" )] pub reputation_tracking_enabled: bool, + + #[arg( + long = "pool.drop_min_num_blocks", + name = "pool.drop_min_num_blocks", + env = "POOL_DROP_MIN_NUM_BLOCKS", + default_value = "10" + )] + pub drop_min_num_blocks: u64, } impl PoolArgs { @@ -182,6 +190,7 @@ impl PoolArgs { throttled_entity_live_blocks: self.throttled_entity_live_blocks, paymaster_tracking_enabled: self.paymaster_tracking_enabled, reputation_tracking_enabled: self.reputation_tracking_enabled, + drop_min_num_blocks: self.drop_min_num_blocks, }; Ok(PoolTaskArgs { diff --git a/bin/rundler/src/cli/rpc.rs b/bin/rundler/src/cli/rpc.rs index f3607b382..475c6bc2d 100644 --- a/bin/rundler/src/cli/rpc.rs +++ b/bin/rundler/src/cli/rpc.rs @@ -17,7 +17,7 @@ use anyhow::Context; use clap::Args; use rundler_builder::RemoteBuilderClient; use rundler_pool::RemotePoolClient; -use rundler_rpc::{EthApiSettings, RpcTask, RpcTaskArgs}; +use rundler_rpc::{EthApiSettings, RpcTask, RpcTaskArgs, RundlerApiSettings}; use rundler_sim::{EstimationSettings, PrecheckSettings}; use rundler_task::{server::connect_with_retries_shutdown, spawn_tasks_with_shutdown}; use rundler_types::chain::ChainSpec; @@ -86,6 +86,7 @@ impl RpcArgs { common: &CommonArgs, precheck_settings: PrecheckSettings, eth_api_settings: EthApiSettings, + rundler_api_settings: RundlerApiSettings, estimation_settings: EstimationSettings, ) -> anyhow::Result { let apis = self @@ -105,6 +106,7 @@ impl RpcArgs { api_namespaces: apis, precheck_settings, eth_api_settings, + rundler_api_settings, estimation_settings, rpc_timeout: Duration::from_secs(self.timeout_seconds.parse()?), max_connections: self.max_connections, @@ -154,6 +156,7 @@ pub async fn run( (&common_args).try_into()?, (&common_args).into(), (&common_args).try_into()?, + (&common_args).try_into()?, )?; let pool = connect_with_retries_shutdown( diff --git a/crates/pool/proto/op_pool/op_pool.proto b/crates/pool/proto/op_pool/op_pool.proto index 58e9c5952..6135a7da1 100644 --- a/crates/pool/proto/op_pool/op_pool.proto +++ b/crates/pool/proto/op_pool/op_pool.proto @@ -133,6 +133,9 @@ service OpPool { // Removes UserOperations from the mempool rpc RemoveOps(RemoveOpsRequest) returns (RemoveOpsResponse); + // Remove a UserOperation by its id + rpc RemoveOpById(RemoveOpByIdRequest) returns (RemoveOpByIdResponse); + // Handles a list of updates to be performed on entities rpc UpdateEntities(UpdateEntitiesRequest) returns (UpdateEntitiesResponse); @@ -281,6 +284,21 @@ message RemoveOpsResponse { } message RemoveOpsSuccess {} +message RemoveOpByIdRequest { + bytes entry_point = 1; + bytes sender = 2; + bytes nonce = 3; +} +message RemoveOpByIdResponse { + oneof result { + RemoveOpByIdSuccess success = 1; + MempoolError failure = 2; + } +} +message RemoveOpByIdSuccess { + bytes hash = 1; +} + message UpdateEntitiesRequest { // The serilaized entry point address bytes entry_point = 1; @@ -428,6 +446,7 @@ message MempoolError { SenderAddressUsedAsAlternateEntity sender_address_used_as_alternate_entity = 13; AssociatedStorageIsAlternateSender associated_storage_is_alternate_sender = 14; PaymasterBalanceTooLow paymaster_balance_too_low = 15; + OperationDropTooSoon operation_drop_too_soon = 16; } } @@ -474,6 +493,12 @@ message UnsupportedAggregatorError { message InvalidSignatureError {} +message OperationDropTooSoon { + uint64 added_at = 1; + uint64 attempted_at = 2; + uint64 must_wait = 3; +} + // PRECHECK VIOLATIONS message PrecheckViolationError { oneof violation { diff --git a/crates/pool/src/mempool/error.rs b/crates/pool/src/mempool/error.rs index 5fed9ab6f..3d3370bbc 100644 --- a/crates/pool/src/mempool/error.rs +++ b/crates/pool/src/mempool/error.rs @@ -72,6 +72,9 @@ pub enum MempoolError { /// 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 { diff --git a/crates/pool/src/mempool/mod.rs b/crates/pool/src/mempool/mod.rs index 3ab046e75..fdf1f17f5 100644 --- a/crates/pool/src/mempool/mod.rs +++ b/crates/pool/src/mempool/mod.rs @@ -36,7 +36,9 @@ use ethers::types::{Address, H256, U256}; #[cfg(test)] use mockall::automock; use rundler_sim::{EntityInfos, MempoolConfig, PrecheckSettings, SimulationSettings}; -use rundler_types::{Entity, EntityType, EntityUpdate, UserOperation, ValidTimeRange}; +use rundler_types::{ + Entity, EntityType, EntityUpdate, UserOperation, UserOperationId, ValidTimeRange, +}; use tonic::async_trait; pub(crate) use uo_pool::UoPool; @@ -63,6 +65,9 @@ pub trait Mempool: Send + Sync + 'static { /// Removes a set of operations from the pool. fn remove_operations(&self, hashes: &[H256]); + /// Removes an operation from the pool by its ID. + fn remove_op_by_id(&self, id: UserOperationId) -> MempoolResult>; + /// Updates the reputation of an entity. fn update_entity(&self, entity_update: EntityUpdate); @@ -153,6 +158,8 @@ pub struct PoolConfig { pub paymaster_tracking_enabled: bool, /// Boolean field used to toggle the operation of the reputation tracker pub reputation_tracking_enabled: bool, + /// The minimum number of blocks a user operation must be in the mempool before it can be dropped + pub drop_min_num_blocks: u64, } /// Stake status structure diff --git a/crates/pool/src/mempool/pool.rs b/crates/pool/src/mempool/pool.rs index cb9ad98f4..74a994a1b 100644 --- a/crates/pool/src/mempool/pool.rs +++ b/crates/pool/src/mempool/pool.rs @@ -198,6 +198,10 @@ impl PoolInner { self.by_hash.get(&hash).map(|o| o.po.clone()) } + 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> { let ret = self.remove_operation_internal(hash, None); self.update_metrics(); @@ -634,6 +638,36 @@ mod tests { check_map_entry(pool.best.iter().next(), Some(&op)); } + #[test] + fn test_get_by_hash() { + let mut pool = PoolInner::new(conf()); + let op = create_op(Address::random(), 0, 1); + let hash = pool.add_operation(op.clone(), None).unwrap(); + + let get_op = pool.get_operation_by_hash(hash).unwrap(); + assert_eq!(op, *get_op); + + assert_eq!(pool.get_operation_by_hash(H256::random()), None); + } + + #[test] + fn test_get_by_id() { + let mut pool = PoolInner::new(conf()); + let op = create_op(Address::random(), 0, 1); + pool.add_operation(op.clone(), None).unwrap(); + let id = op.uo.id(); + + let get_op = pool.get_operation_by_id(id).unwrap(); + assert_eq!(op, *get_op); + + let bad_id = UserOperationId { + sender: Address::random(), + nonce: 0.into(), + }; + + assert_eq!(pool.get_operation_by_id(bad_id), None); + } + #[test] fn add_multiple_ops() { let mut pool = PoolInner::new(conf()); diff --git a/crates/pool/src/mempool/uo_pool.rs b/crates/pool/src/mempool/uo_pool.rs index 9954d043e..2dba60acc 100644 --- a/crates/pool/src/mempool/uo_pool.rs +++ b/crates/pool/src/mempool/uo_pool.rs @@ -21,7 +21,7 @@ use itertools::Itertools; use parking_lot::RwLock; use rundler_provider::{EntryPoint, PaymasterHelper, ProviderResult}; use rundler_sim::{Prechecker, Simulator}; -use rundler_types::{Entity, EntityUpdate, EntityUpdateType, UserOperation}; +use rundler_types::{Entity, EntityUpdate, EntityUpdateType, UserOperation, UserOperationId}; use rundler_utils::emit::WithEntryPoint; use tokio::sync::broadcast; use tonic::async_trait; @@ -509,6 +509,46 @@ where UoPoolMetrics::increment_removed_operations(count, self.config.entry_point); } + fn remove_op_by_id(&self, id: UserOperationId) -> MempoolResult> { + // Check for the operation in the pool and its age + let po = { + let state = self.state.read(); + match state.pool.get_operation_by_id(id) { + Some(po) => { + if po.sim_block_number + self.config.drop_min_num_blocks > state.block_number { + // TODO return error + return Err(MempoolError::OperationDropTooSoon( + po.sim_block_number, + state.block_number, + self.config.drop_min_num_blocks, + )); + } + po + } + None => return Ok(None), + } + }; + + let hash = po.uo.op_hash(self.config.entry_point, self.config.chain_id); + + // This can return none if the operation was removed by another thread + if self + .state + .write() + .pool + .remove_operation_by_hash(hash) + .is_none() + { + return Ok(None); + } + + self.emit(OpPoolEvent::RemovedOp { + op_hash: hash, + reason: OpRemovalReason::Requested, + }); + Ok(Some(hash)) + } + fn update_entity(&self, update: EntityUpdate) { let entity = update.entity; match update.update_type { @@ -1426,6 +1466,7 @@ mod tests { throttled_entity_live_blocks: 10, paymaster_tracking_enabled: true, reputation_tracking_enabled: true, + drop_min_num_blocks: 10, }; let (event_sender, _) = broadcast::channel(4); diff --git a/crates/pool/src/server/local.rs b/crates/pool/src/server/local.rs index 7abc931a7..b1ff2f4c8 100644 --- a/crates/pool/src/server/local.rs +++ b/crates/pool/src/server/local.rs @@ -18,7 +18,7 @@ use async_trait::async_trait; use ethers::types::{Address, H256}; use futures_util::Stream; use rundler_task::server::{HealthCheck, ServerStatus}; -use rundler_types::{EntityUpdate, UserOperation}; +use rundler_types::{EntityUpdate, UserOperation, UserOperationId}; use tokio::{ sync::{broadcast, mpsc, oneshot}, task::JoinHandle, @@ -170,6 +170,19 @@ impl PoolServer for LocalPoolHandle { } } + async fn remove_op_by_id( + &self, + entry_point: Address, + id: UserOperationId, + ) -> PoolResult> { + let req = ServerRequestKind::RemoveOpById { entry_point, id }; + let resp = self.send(req).await?; + match resp { + ServerResponse::RemoveOpById { hash } => Ok(hash), + _ => Err(PoolServerError::UnexpectedResponse), + } + } + async fn update_entities( &self, entry_point: Address, @@ -391,6 +404,15 @@ where Ok(()) } + fn remove_op_by_id( + &self, + entry_point: Address, + id: UserOperationId, + ) -> PoolResult> { + let mempool = self.get_pool(entry_point)?; + mempool.remove_op_by_id(id).map_err(|e| e.into()) + } + fn update_entities<'a>( &self, entry_point: Address, @@ -572,6 +594,12 @@ where Err(e) => Err(e), } }, + ServerRequestKind::RemoveOpById { entry_point, id } => { + match self.remove_op_by_id(entry_point, id) { + Ok(hash) => Ok(ServerResponse::RemoveOpById{ hash }), + Err(e) => Err(e), + } + }, ServerRequestKind::AdminSetTracking{ entry_point, paymaster, reputation } => { match self.admin_set_tracking(entry_point, paymaster, reputation) { Ok(_) => Ok(ServerResponse::AdminSetTracking), @@ -661,6 +689,10 @@ enum ServerRequestKind { entry_point: Address, ops: Vec, }, + RemoveOpById { + entry_point: Address, + id: UserOperationId, + }, UpdateEntities { entry_point: Address, entity_updates: Vec, @@ -714,6 +746,9 @@ enum ServerResponse { op: Option, }, RemoveOps, + RemoveOpById { + hash: Option, + }, UpdateEntities, DebugClearState, AdminSetTracking, diff --git a/crates/pool/src/server/mod.rs b/crates/pool/src/server/mod.rs index 41757d050..311317b49 100644 --- a/crates/pool/src/server/mod.rs +++ b/crates/pool/src/server/mod.rs @@ -26,7 +26,7 @@ pub use local::{LocalPoolBuilder, LocalPoolHandle}; use mockall::automock; pub(crate) use remote::spawn_remote_mempool_server; pub use remote::RemotePoolClient; -use rundler_types::{EntityUpdate, UserOperation}; +use rundler_types::{EntityUpdate, UserOperation, UserOperationId}; use crate::{ mempool::{PaymasterMetadata, PoolOperation, Reputation, StakeStatus}, @@ -77,6 +77,13 @@ pub trait PoolServer: Send + Sync + 'static { /// 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, diff --git a/crates/pool/src/server/remote/client.rs b/crates/pool/src/server/remote/client.rs index 6d481313c..91a045597 100644 --- a/crates/pool/src/server/remote/client.rs +++ b/crates/pool/src/server/remote/client.rs @@ -16,10 +16,10 @@ use std::{pin::Pin, str::FromStr}; use ethers::types::{Address, H256}; use futures_util::Stream; use rundler_task::{ - grpc::protos::{from_bytes, ConversionError}, + grpc::protos::{from_bytes, to_le_bytes, ConversionError}, server::{HealthCheck, ServerStatus}, }; -use rundler_types::{EntityUpdate, UserOperation}; +use rundler_types::{EntityUpdate, UserOperation, UserOperationId}; use rundler_utils::retry::{self, UnlimitedRetryOpts}; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -37,11 +37,11 @@ use super::protos::{ debug_dump_mempool_response, debug_dump_paymaster_balances_response, debug_dump_reputation_response, debug_set_reputation_response, get_op_by_hash_response, get_ops_response, get_reputation_status_response, get_stake_status_response, - op_pool_client::OpPoolClient, remove_ops_response, update_entities_response, AddOpRequest, - AdminSetTrackingRequest, DebugClearStateRequest, DebugDumpMempoolRequest, - DebugDumpPaymasterBalancesRequest, DebugDumpReputationRequest, DebugSetReputationRequest, - GetOpsRequest, GetReputationStatusRequest, GetStakeStatusRequest, RemoveOpsRequest, - SubscribeNewHeadsRequest, SubscribeNewHeadsResponse, UpdateEntitiesRequest, + op_pool_client::OpPoolClient, remove_op_by_id_response, remove_ops_response, + update_entities_response, AddOpRequest, AdminSetTrackingRequest, DebugClearStateRequest, + DebugDumpMempoolRequest, DebugDumpPaymasterBalancesRequest, DebugDumpReputationRequest, + DebugSetReputationRequest, GetOpsRequest, GetReputationStatusRequest, GetStakeStatusRequest, + RemoveOpsRequest, SubscribeNewHeadsRequest, SubscribeNewHeadsResponse, UpdateEntitiesRequest, }; use crate::{ mempool::{PaymasterMetadata, PoolOperation, Reputation, StakeStatus}, @@ -238,6 +238,38 @@ impl PoolServer for RemotePoolClient { } } + async fn remove_op_by_id( + &self, + entry_point: Address, + id: UserOperationId, + ) -> PoolResult> { + let res = self + .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), + }) + .await? + .into_inner() + .result; + + match res { + Some(remove_op_by_id_response::Result::Success(s)) => { + if s.hash.is_empty() { + Ok(None) + } else { + Ok(Some(H256::from_slice(&s.hash))) + } + } + Some(remove_op_by_id_response::Result::Failure(f)) => Err(f.try_into()?), + None => Err(PoolServerError::Other(anyhow::anyhow!( + "should have received result from op pool" + )))?, + } + } + async fn update_entities( &self, entry_point: Address, diff --git a/crates/pool/src/server/remote/error.rs b/crates/pool/src/server/remote/error.rs index 8f3b732a5..0a57e9e4f 100644 --- a/crates/pool/src/server/remote/error.rs +++ b/crates/pool/src/server/remote/error.rs @@ -25,7 +25,7 @@ use super::protos::{ FactoryCalledCreate2Twice, FactoryIsNotContract, InitCodeTooShort, InvalidSignature, InvalidStorageAccess, MaxFeePerGasTooLow, MaxOperationsReachedError, MaxPriorityFeePerGasTooLow, MempoolError as ProtoMempoolError, MultipleRolesViolation, - NotStaked, OperationAlreadyKnownError, OutOfGas, PaymasterBalanceTooLow, + NotStaked, OperationAlreadyKnownError, OperationDropTooSoon, OutOfGas, PaymasterBalanceTooLow, PaymasterDepositTooLow, PaymasterIsNotContract, PaymasterTooShort, PreVerificationGasTooLow, PrecheckViolationError as ProtoPrecheckViolationError, ReplacementUnderpricedError, SenderAddressUsedAsAlternateEntity, SenderFundsTooLow, SenderIsNotContractAndNoInitCode, @@ -130,6 +130,9 @@ impl TryFrom for MempoolError { (&e.entity.context("should have entity in error")?).try_into()?, ) } + Some(mempool_error::Error::OperationDropTooSoon(e)) => { + MempoolError::OperationDropTooSoon(e.added_at, e.attempted_at, e.must_wait) + } None => bail!("unknown proto mempool error"), }) } @@ -223,6 +226,17 @@ impl From for ProtoMempoolError { }, )), }, + MempoolError::OperationDropTooSoon(added_at, attempted_at, must_wait) => { + ProtoMempoolError { + error: Some(mempool_error::Error::OperationDropTooSoon( + OperationDropTooSoon { + added_at, + attempted_at, + must_wait, + }, + )), + } + } } } } diff --git a/crates/pool/src/server/remote/server.rs b/crates/pool/src/server/remote/server.rs index 87c69867b..d8dd3d1c2 100644 --- a/crates/pool/src/server/remote/server.rs +++ b/crates/pool/src/server/remote/server.rs @@ -23,7 +23,7 @@ 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; +use rundler_types::{EntityUpdate, UserOperationId}; use tokio::{sync::mpsc, task::JoinHandle}; use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_util::sync::CancellationToken; @@ -35,21 +35,21 @@ use super::protos::{ debug_dump_reputation_response, debug_set_reputation_response, get_op_by_hash_response, get_ops_response, get_reputation_status_response, get_stake_status_response, op_pool_server::{OpPool, OpPoolServer}, - remove_ops_response, update_entities_response, AddOpRequest, AddOpResponse, AddOpSuccess, - AdminSetTrackingRequest, AdminSetTrackingResponse, AdminSetTrackingSuccess, - DebugClearStateRequest, DebugClearStateResponse, DebugClearStateSuccess, - DebugDumpMempoolRequest, DebugDumpMempoolResponse, DebugDumpMempoolSuccess, - DebugDumpPaymasterBalancesRequest, DebugDumpPaymasterBalancesResponse, + remove_op_by_id_response, remove_ops_response, update_entities_response, AddOpRequest, + AddOpResponse, AddOpSuccess, AdminSetTrackingRequest, AdminSetTrackingResponse, + AdminSetTrackingSuccess, DebugClearStateRequest, DebugClearStateResponse, + DebugClearStateSuccess, DebugDumpMempoolRequest, DebugDumpMempoolResponse, + DebugDumpMempoolSuccess, DebugDumpPaymasterBalancesRequest, DebugDumpPaymasterBalancesResponse, DebugDumpPaymasterBalancesSuccess, DebugDumpReputationRequest, DebugDumpReputationResponse, DebugDumpReputationSuccess, DebugSetReputationRequest, DebugSetReputationResponse, DebugSetReputationSuccess, GetOpByHashRequest, GetOpByHashResponse, GetOpByHashSuccess, GetOpsRequest, GetOpsResponse, GetOpsSuccess, GetReputationStatusRequest, GetReputationStatusResponse, GetReputationStatusSuccess, GetStakeStatusRequest, GetStakeStatusResponse, GetStakeStatusSuccess, GetSupportedEntryPointsRequest, - GetSupportedEntryPointsResponse, MempoolOp, RemoveOpsRequest, RemoveOpsResponse, - RemoveOpsSuccess, ReputationStatus, SubscribeNewHeadsRequest, SubscribeNewHeadsResponse, - UpdateEntitiesRequest, UpdateEntitiesResponse, UpdateEntitiesSuccess, - OP_POOL_FILE_DESCRIPTOR_SET, + GetSupportedEntryPointsResponse, MempoolOp, RemoveOpByIdRequest, RemoveOpByIdResponse, + RemoveOpByIdSuccess, RemoveOpsRequest, RemoveOpsResponse, RemoveOpsSuccess, ReputationStatus, + SubscribeNewHeadsRequest, SubscribeNewHeadsResponse, UpdateEntitiesRequest, + UpdateEntitiesResponse, UpdateEntitiesSuccess, OP_POOL_FILE_DESCRIPTOR_SET, }; use crate::{ mempool::Reputation, @@ -242,6 +242,41 @@ impl OpPool for OpPoolImpl { Ok(Response::new(resp)) } + async fn remove_op_by_id( + &self, + request: Request, + ) -> Result> { + let req = request.into_inner(); + let ep = self.get_entry_point(&req.entry_point)?; + + let resp = match self + .local_pool + .remove_op_by_id( + ep, + UserOperationId { + sender: from_bytes(&req.sender) + .map_err(|e| Status::invalid_argument(format!("Invalid sender: {e}")))?, + nonce: from_bytes(&req.nonce) + .map_err(|e| Status::invalid_argument(format!("Invalid nonce: {e}")))?, + }, + ) + .await + { + Ok(hash) => RemoveOpByIdResponse { + result: Some(remove_op_by_id_response::Result::Success( + RemoveOpByIdSuccess { + hash: hash.map_or(vec![], |h| h.as_bytes().to_vec()), + }, + )), + }, + Err(error) => RemoveOpByIdResponse { + result: Some(remove_op_by_id_response::Result::Failure(error.into())), + }, + }; + + Ok(Response::new(resp)) + } + async fn update_entities( &self, request: Request, diff --git a/crates/pool/src/task.rs b/crates/pool/src/task.rs index f66ba210a..f5bc25d6d 100644 --- a/crates/pool/src/task.rs +++ b/crates/pool/src/task.rs @@ -187,14 +187,14 @@ impl PoolTask { let paymaster_helper = PaymasterHelperContract::new(pool_config.entry_point, Arc::clone(&provider)); - let simulate_validation_tracer = - SimulateValidationTracerImpl::new(Arc::clone(&provider), i_entry_point.clone()); let prechecker = PrecheckerImpl::new( chain_spec, Arc::clone(&provider), i_entry_point.clone(), pool_config.precheck_settings, ); + let simulate_validation_tracer = + SimulateValidationTracerImpl::new(Arc::clone(&provider), i_entry_point.clone()); let simulator = SimulatorImpl::new( Arc::clone(&provider), i_entry_point.address(), diff --git a/crates/provider/Cargo.toml b/crates/provider/Cargo.toml index d4d40c9ec..f8ff285ea 100644 --- a/crates/provider/Cargo.toml +++ b/crates/provider/Cargo.toml @@ -16,6 +16,7 @@ ethers.workspace = true serde.workspace = true tokio.workspace = true thiserror.workspace = true +tracing.workspace = true mockall = {workspace = true, optional = true } diff --git a/crates/provider/src/ethers/entry_point.rs b/crates/provider/src/ethers/entry_point.rs index 5d528b7d0..6b98deb91 100644 --- a/crates/provider/src/ethers/entry_point.rs +++ b/crates/provider/src/ethers/entry_point.rs @@ -29,7 +29,7 @@ use rundler_types::{ i_entry_point::{ExecutionResult, FailedOp, IEntryPoint, SignatureValidationFailed}, shared_types::UserOpsPerAggregator, }, - GasFees, UserOperation, + GasFees, UserOperation, ValidationOutput, }; use rundler_utils::eth::{self, ContractRevertError}; @@ -46,21 +46,38 @@ where self.deref().address() } - async fn simulate_validation( + async fn get_simulate_validation_call( &self, user_op: UserOperation, max_validation_gas: u64, ) -> anyhow::Result { let pvg = user_op.pre_verification_gas; - let tx = self .simulate_validation(user_op) .gas(U256::from(max_validation_gas) + pvg) .tx; - Ok(tx) } + async fn call_simulate_validation( + &self, + user_op: UserOperation, + max_validation_gas: u64, + ) -> anyhow::Result { + let pvg = user_op.pre_verification_gas; + match self + .simulate_validation(user_op) + .gas(U256::from(max_validation_gas) + pvg) + .call() + .await + { + Ok(()) => anyhow::bail!("simulateValidation should always revert"), + Err(ContractError::Revert(revert_data)) => ValidationOutput::decode(revert_data) + .context("entry point should return validation output"), + Err(error) => Err(error).context("call simulation RPC failed")?, + } + } + async fn call_handle_ops( &self, ops_per_aggregator: Vec, diff --git a/crates/provider/src/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index f0b0c90f3..ddf132e0d 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -18,7 +18,7 @@ use ethers::types::{ use mockall::automock; use rundler_types::{ contracts::{i_entry_point::ExecutionResult, shared_types::UserOpsPerAggregator}, - GasFees, UserOperation, + GasFees, UserOperation, ValidationOutput, }; /// Result of an entry point handle ops call @@ -56,13 +56,20 @@ pub trait EntryPoint: Send + Sync + 'static { async fn balance_of(&self, address: Address, block_id: Option) -> anyhow::Result; - /// Call the entry point contract's `simulateValidation` function - async fn simulate_validation( + /// Construct a call for the entry point contract's `simulateValidation` function + async fn get_simulate_validation_call( &self, user_op: UserOperation, max_validation_gas: u64, ) -> anyhow::Result; + /// Call the entry point contract's `simulateValidation` function. + async fn call_simulate_validation( + &self, + user_op: UserOperation, + max_validation_gas: u64, + ) -> anyhow::Result; + /// Call the entry point contract's `simulateHandleOps` function /// with a spoofed state async fn call_spoofed_simulate_op( diff --git a/crates/rpc/src/eth/error.rs b/crates/rpc/src/eth/error.rs index 77dde57b4..e1f5843f8 100644 --- a/crates/rpc/src/eth/error.rs +++ b/crates/rpc/src/eth/error.rs @@ -246,6 +246,9 @@ impl From for EthRpcError { MempoolError::UnknownEntryPoint(a) => { EthRpcError::EntryPointValidationRejected(format!("unknown entry point: {}", a)) } + MempoolError::OperationDropTooSoon(_, _, _) => { + EthRpcError::InvalidParams(value.to_string()) + } } } } diff --git a/crates/rpc/src/eth/mod.rs b/crates/rpc/src/eth/mod.rs index bdbe6a419..3cb50e52b 100644 --- a/crates/rpc/src/eth/mod.rs +++ b/crates/rpc/src/eth/mod.rs @@ -16,6 +16,7 @@ pub(crate) use api::EthApi; pub use api::Settings as EthApiSettings; mod error; +pub(crate) use error::EthRpcError; mod server; use ethers::types::{spoof, Address, H256, U64}; diff --git a/crates/rpc/src/lib.rs b/crates/rpc/src/lib.rs index cf3430999..b44893641 100644 --- a/crates/rpc/src/lib.rs +++ b/crates/rpc/src/lib.rs @@ -34,7 +34,7 @@ mod health; mod metrics; mod rundler; -pub use rundler::RundlerApiClient; +pub use rundler::{RundlerApiClient, Settings as RundlerApiSettings}; mod task; pub use task::{Args as RpcTaskArgs, RpcTask}; diff --git a/crates/rpc/src/rundler.rs b/crates/rpc/src/rundler.rs index c3449eb08..5f11815bf 100644 --- a/crates/rpc/src/rundler.rs +++ b/crates/rpc/src/rundler.rs @@ -14,49 +14,93 @@ use std::sync::Arc; use async_trait::async_trait; -use ethers::types::U256; -use jsonrpsee::{core::RpcResult, proc_macros::rpc, types::error::INTERNAL_ERROR_CODE}; -use rundler_provider::Provider; -use rundler_sim::{FeeEstimator, PrecheckSettings}; -use rundler_types::chain::ChainSpec; +use ethers::types::{Address, H256, U256}; +use jsonrpsee::{ + core::RpcResult, + proc_macros::rpc, + types::error::{INTERNAL_ERROR_CODE, INVALID_REQUEST_CODE}, +}; +use rundler_pool::PoolServer; +use rundler_provider::{EntryPoint, Provider}; +use rundler_sim::{gas, FeeEstimator}; +use rundler_types::{chain::ChainSpec, UserOperation, UserOperationId}; -use crate::error::rpc_err; +use crate::{error::rpc_err, eth::EthRpcError, RpcUserOperation}; + +/// Settings for the `rundler_` API +#[derive(Copy, Clone, Debug)] +pub struct Settings { + /// The priority fee mode to use for calculating required user operation priority fee. + pub priority_fee_mode: gas::PriorityFeeMode, + /// If using a bundle priority fee, the percentage to add to the network/oracle + /// provided value as a safety margin for fast inclusion. + pub bundle_priority_fee_overhead_percent: u64, + /// Max verification gas + pub max_verification_gas: u64, +} #[rpc(client, server, namespace = "rundler")] pub trait RundlerApi { /// Returns the maximum priority fee per gas required by Rundler #[method(name = "maxPriorityFeePerGas")] async fn max_priority_fee_per_gas(&self) -> RpcResult; + + /// Drops a user operation from the local mempool. + /// + /// Requirements: + /// - The user operation must contain a sender/nonce pair this is present in the local mempool. + /// - The user operation must pass entrypoint.simulateValidation. I.e. it must have a valid signature and verificationGasLimit + /// - The user operation must have zero values for: preVerificationGas, callGasLimit, calldata, and maxFeePerGas + /// + /// Returns none if no user operation was found, otherwise returns the hash of the removed user operation. + #[method(name = "dropLocalUserOperation")] + async fn drop_local_user_operation( + &self, + uo: RpcUserOperation, + entry_point: Address, + ) -> RpcResult>; } -pub(crate) struct RundlerApi { +pub(crate) struct RundlerApi { + settings: Settings, fee_estimator: FeeEstimator

, + entry_point: E, + pool_server: PS, } -impl

RundlerApi

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

, - settings: PrecheckSettings, + entry_point: E, + pool_server: PS, + settings: Settings, ) -> Self { Self { + settings, fee_estimator: FeeEstimator::new( chain_spec, provider, settings.priority_fee_mode, settings.bundle_priority_fee_overhead_percent, ), + entry_point, + pool_server, } } } #[async_trait] -impl

RundlerApiServer for RundlerApi

+impl RundlerApiServer for RundlerApi where P: Provider, + E: EntryPoint, + PS: PoolServer, { async fn max_priority_fee_per_gas(&self) -> RpcResult { let (bundle_fees, _) = self @@ -69,4 +113,59 @@ where .required_op_fees(bundle_fees) .max_priority_fee_per_gas) } + + async fn drop_local_user_operation( + &self, + 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: UserOperation = user_op.into(); + let id = UserOperationId { + sender: uo.sender, + nonce: uo.nonce, + }; + + 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, + "Invalid user operation for drop: preVerificationGas, callGasLimit, callData, and maxFeePerGas must be zero", + )); + } + + 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 { + return Err(rpc_err( + INVALID_REQUEST_CODE, + "User operation for drop failed simulateValidation", + )); + } + + // remove the op from the pool + let ret = self + .pool_server + .remove_op_by_id(entry_point, id) + .await + .map_err(|e| { + tracing::info!("Error dropping user operation: {}", e); + EthRpcError::from(e) + })?; + + Ok(ret) + } } diff --git a/crates/rpc/src/task.rs b/crates/rpc/src/task.rs index 90f22d320..bbd686d19 100644 --- a/crates/rpc/src/task.rs +++ b/crates/rpc/src/task.rs @@ -39,7 +39,7 @@ use crate::{ eth::{EthApi, EthApiServer, EthApiSettings}, health::{HealthChecker, SystemApiServer}, metrics::RpcMetricsLogger, - rundler::{RundlerApi, RundlerApiServer}, + rundler::{RundlerApi, RundlerApiServer, Settings as RundlerApiSettings}, types::ApiNamespace, }; @@ -60,6 +60,8 @@ pub struct Args { pub precheck_settings: PrecheckSettings, /// eth_ API settings. pub eth_api_settings: EthApiSettings, + /// rundler_ API settings. + pub rundler_api_settings: RundlerApiSettings, /// Estimation settings. pub estimation_settings: EstimationSettings, /// RPC timeout. @@ -175,7 +177,9 @@ where RundlerApi::new( &self.args.chain_spec, provider.clone(), - self.args.precheck_settings, + entry_point.clone(), + self.pool.clone(), + self.args.rundler_api_settings, ) .into_rpc(), )?, diff --git a/crates/sim/src/simulation/mod.rs b/crates/sim/src/simulation/mod.rs index 6915b0b11..5b6a99b76 100644 --- a/crates/sim/src/simulation/mod.rs +++ b/crates/sim/src/simulation/mod.rs @@ -26,5 +26,3 @@ pub use mempool::MempoolConfig; mod tracer; pub use tracer::{SimulateValidationTracer, SimulateValidationTracerImpl}; - -mod validation_results; diff --git a/crates/sim/src/simulation/simulation.rs b/crates/sim/src/simulation/simulation.rs index 28178c8d4..70e7c34af 100644 --- a/crates/sim/src/simulation/simulation.rs +++ b/crates/sim/src/simulation/simulation.rs @@ -29,8 +29,8 @@ use indexmap::IndexSet; use mockall::automock; use rundler_provider::{AggregatorOut, AggregatorSimOut, Provider}; use rundler_types::{ - contracts::i_entry_point::FailedOp, Entity, EntityType, StorageSlot, UserOperation, - ValidTimeRange, + contracts::i_entry_point::FailedOp, Entity, EntityType, StakeInfo, StorageSlot, UserOperation, + ValidTimeRange, ValidationOutput, ValidationReturnInfo, }; use strum::IntoEnumIterator; @@ -40,7 +40,6 @@ use super::{ parse_combined_tracer_str, AccessInfo, AssociatedSlotsByAddress, SimulateValidationTracer, SimulationTracerOutput, }, - validation_results::{StakeInfo, ValidationOutput, ValidationReturnInfo}, }; use crate::{ types::{ExpectedStorage, ViolationError}, diff --git a/crates/sim/src/simulation/tracer.rs b/crates/sim/src/simulation/tracer.rs index 2d19cb3ba..bbe93eb25 100644 --- a/crates/sim/src/simulation/tracer.rs +++ b/crates/sim/src/simulation/tracer.rs @@ -140,7 +140,7 @@ where ) -> anyhow::Result { let tx = self .entry_point - .simulate_validation(op, max_validation_gas) + .get_simulate_validation_call(op, max_validation_gas) .await?; SimulationTracerOutput::try_from( diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index e7930de9b..fe9b7e8f1 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -44,3 +44,6 @@ pub use user_operation::UserOperationId; mod storage; pub use storage::StorageSlot; + +mod validation_results; +pub use validation_results::{AggregatorInfo, StakeInfo, ValidationOutput, ValidationReturnInfo}; diff --git a/crates/sim/src/simulation/validation_results.rs b/crates/types/src/validation_results.rs similarity index 73% rename from crates/sim/src/simulation/validation_results.rs rename to crates/types/src/validation_results.rs index 3699596f3..695738210 100644 --- a/crates/sim/src/simulation/validation_results.rs +++ b/crates/types/src/validation_results.rs @@ -16,7 +16,8 @@ use ethers::{ abi::{AbiDecode, AbiError}, types::{Address, Bytes, U256}, }; -use rundler_types::{ + +use crate::{ contracts::entry_point::{ValidationResult, ValidationResultWithAggregation}, Timestamp, }; @@ -25,12 +26,17 @@ use rundler_types::{ /// `ValidationResultWithAggregation` from `EntryPoint`, but with named structs /// instead of tuples and with a helper for deserializing. #[derive(Debug)] -pub(crate) struct ValidationOutput { - pub(crate) return_info: ValidationReturnInfo, - pub(crate) sender_info: StakeInfo, - pub(crate) factory_info: StakeInfo, - pub(crate) paymaster_info: StakeInfo, - pub(crate) aggregator_info: Option, +pub struct ValidationOutput { + /// The return info from the validation function + pub return_info: ValidationReturnInfo, + /// The stake info for the sender + pub sender_info: StakeInfo, + /// The stake info for the factory + pub factory_info: StakeInfo, + /// The stake info for the paymaster + pub paymaster_info: StakeInfo, + /// Optional aggregator_info + pub aggregator_info: Option, } impl AbiDecode for ValidationOutput { @@ -82,13 +88,19 @@ impl From for ValidationOutput { } } +/// ValidationReturnInfo from EntryPoint contract #[derive(Debug)] -pub(crate) struct ValidationReturnInfo { - pub(crate) pre_op_gas: U256, - pub(crate) sig_failed: bool, - pub(crate) valid_after: Timestamp, - pub(crate) valid_until: Timestamp, - pub(crate) paymaster_context: Bytes, +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, + /// The time after which the op is valid + pub valid_after: Timestamp, + /// The time until which the op is valid + pub valid_until: Timestamp, + /// The paymaster context + pub paymaster_context: Bytes, } impl From<(U256, U256, bool, u64, u64, Bytes)> for ValidationReturnInfo { @@ -111,10 +123,13 @@ impl From<(U256, U256, bool, u64, u64, Bytes)> for ValidationReturnInfo { } } +/// StakeInfo from EntryPoint contract #[derive(Clone, Copy, Debug)] -pub(crate) struct StakeInfo { - pub(crate) stake: U256, - pub(crate) unstake_delay_sec: U256, +pub struct StakeInfo { + /// The amount of stake + pub stake: U256, + /// The delay for unstaking + pub unstake_delay_sec: U256, } impl From<(U256, U256)> for StakeInfo { @@ -126,10 +141,13 @@ impl From<(U256, U256)> for StakeInfo { } } +/// AggregatorInfo from EntryPoint contract #[derive(Clone, Copy, Debug)] -pub(crate) struct AggregatorInfo { - pub(crate) address: Address, - pub(crate) stake_info: StakeInfo, +pub struct AggregatorInfo { + /// The address of the aggregator + pub address: Address, + /// The stake info for the aggregator + pub stake_info: StakeInfo, } impl From<(Address, (U256, U256))> for AggregatorInfo { diff --git a/docs/architecture/rpc.md b/docs/architecture/rpc.md index 795ec14dd..347f7c497 100644 --- a/docs/architecture/rpc.md +++ b/docs/architecture/rpc.md @@ -108,6 +108,7 @@ Rundler specific methods that are not specified by the ERC-4337 spec. This names | Method | Supported | | ------ | :-----------: | | [`rundler_maxPriorityFeePerGas`](#rundler_maxpriorityfeepergas) | ✅ | +| [`rundler_dropLocalUserOperation`](#rundler_droplocaluseroperation) | ✅ | #### `rundler_maxPriorityFeePerGas` @@ -115,6 +116,69 @@ This method returns the minimum `maxPriorityFeePerGas` that the bundler will acc Users of this method should typically increase their priority fee values by a buffer value in order to handle price fluctuations. +``` +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "rundler_maxPriorityFeePerGas", + "params": [] +} + +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": ["0x..."] // uint256 +} +``` + +#### `rundler_dropLocalUserOperation` + +Drops a user operation from the local mempool for the given sender/nonce. The user must send a signed UO that passes validation and matches the requirements below. + +**NOTE:** there is no guarantee that this method effectively cancels a user operation. If the user operation has been bundled prior to the drop attempt it may still be mined. If the user operation has been sent to the P2P network, it may also be mined by another bundler. + +**Requirements:** + +- Sender and nonce match the UO that is being dropped. +- `preVerificationGas`, `callGasLimit`, `maxFeePerGas` must all be 0. + - This is to ensure this UO is not viable onchain. +- `callData` must be `0x`. + - This is to ensure this UO is not viable onchain. +- If an `initCode` was used on the UO to be dropped, the request must also supply that same `initCode` else `0x`, + - This is required for signature verification. +- `verificationGasLimit` must be high enough to run the account verification step. +- `signature` must be valid on a UO with the above requirements. + +**Notes:** + +- `paymasterAndData` is not required to be `0x`, but there is little use for it here, its recommended to set to `0x`. +- `verificationGasLimit` doesn't require estimation, just set to a high number that is lower than the bundler's max verification gas, i.e. 1M. + +``` +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "rundler_dropLocalUserOperation", + "params": [ + { + ... // UO with the requirements above + }, + "0x..." // entry point address + ] +} + +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": ["0x..."] // hash if UO is dropped, or empty if a UO is not found for the sender/ID +} +``` + + ### `admin_` Namespace Administration methods specific to Rundler. This namespace should not be open to the public. diff --git a/docs/cli.md b/docs/cli.md index 6cc20c4d6..4950bdae6 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -141,6 +141,8 @@ List of command line options for configuring the Pool. - env: *POOL_PAYMASTER_TRACKING_ENABLED* - `--pool.reputation_tracking_enabled`: Boolean field that sets whether the pool server starts with reputation tracking enabled (default: `true`) - env: *POOL_REPUTATION_TRACKING_ENABLED* +- `--pool.drop_min_num_blocks`: The minimum number of blocks that a UO must stay in the mempool before it can be requested to be dropped by the user (default: `10`) + - env: *POOL_DROP_MIN_NUM_BLOCKS* ## Builder Options