From e7e330049c958ddb27615c676a36b1e05ce27595 Mon Sep 17 00:00:00 2001 From: Vid Kersic Date: Tue, 10 Oct 2023 19:37:35 +0200 Subject: [PATCH] feat: bundler conformance to latest specs, handle reputation --- .github/workflows/ci.yml | 4 +- Cargo.lock | 46 +++- README.md | 2 +- bin/silius/src/bundler.rs | 1 - bin/silius/src/cli/args.rs | 4 - crates/grpc/src/builder.rs | 15 +- crates/grpc/src/proto.rs | 20 ++ crates/grpc/src/protos/types/types.proto | 6 + crates/grpc/src/protos/uopool/uopool.proto | 13 ++ crates/grpc/src/uopool.rs | 51 +++-- crates/primitives/Cargo.toml | 1 + crates/primitives/src/consts.rs | 30 ++- crates/primitives/src/reputation.rs | 43 +++- crates/primitives/src/sanity.rs | 15 +- crates/primitives/src/simulation.rs | 2 +- crates/primitives/src/uopool.rs | 2 +- crates/primitives/src/user_operation.rs | 17 +- crates/primitives/src/utils.rs | 38 +++- crates/rpc/src/debug.rs | 96 +++++++-- crates/rpc/src/debug_api.rs | 55 ++++- crates/rpc/src/error.rs | 112 +++++----- crates/uopool/src/database/mempool.rs | 147 +++++++++++-- crates/uopool/src/database/reputation.rs | 59 +++-- crates/uopool/src/database/tables.rs | 16 +- crates/uopool/src/memory/mempool.rs | 80 ++++++- crates/uopool/src/memory/reputation.rs | 49 ++--- crates/uopool/src/mempool.rs | 10 + crates/uopool/src/reputation.rs | 13 +- crates/uopool/src/uopool.rs | 66 +++++- crates/uopool/src/utils.rs | 5 +- crates/uopool/src/validate/sanity/entities.rs | 130 +++++++++++ crates/uopool/src/validate/sanity/mod.rs | 3 +- .../uopool/src/validate/sanity/paymaster.rs | 14 +- crates/uopool/src/validate/sanity/sender.rs | 45 +++- .../uopool/src/validate/sanity/sender_uos.rs | 104 --------- .../src/validate/sanity/unstaked_entities.rs | 204 ++++++++++++++++++ .../validate/simulation_trace/call_stack.rs | 2 + crates/uopool/src/validate/validator.rs | 40 ++-- docker-compose.yml | 4 +- tests/src/simulation_tests.rs | 32 ++- 40 files changed, 1193 insertions(+), 403 deletions(-) create mode 100644 crates/uopool/src/validate/sanity/entities.rs delete mode 100644 crates/uopool/src/validate/sanity/sender_uos.rs create mode 100644 crates/uopool/src/validate/sanity/unstaked_entities.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d71ec2e2..8476afdd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,7 +84,7 @@ jobs: - uses: actions/checkout@v3 with: repository: eth-infinitism/bundler-spec-tests - ref: 'e193753db1910fb6d0ee2661d96a8d8f79d6c7d8' + ref: 'bbd61f21e95ed1290678fcbfd9551b1502c81fe9' submodules: true - uses: actions/checkout@v3 with: @@ -94,7 +94,7 @@ jobs: - run: pip install jq yq - - run: pdm install && git submodule update --init --recursive && cd @account-abstraction && yarn && yarn compile && cd ../spec && yarn && yarn build + - run: pdm install && git submodule update --init --recursive && cd @account-abstraction && git fetch --all --tags && git checkout tags/v0.6.0 -b v0.6.0 && yarn && yarn compile && cd ../spec && yarn && yarn build - uses: actions/download-artifact@v3 with: diff --git a/Cargo.lock b/Cargo.lock index e876ef13..29f2d500 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -371,6 +371,15 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "array-init" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23589ecb866b460d3a0f1278834750268c607e8e28a1b982c907219f3178cd72" +dependencies = [ + "nodrop", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -3399,6 +3408,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + [[package]] name = "md-5" version = "0.10.6" @@ -3529,6 +3544,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "nom" version = "7.1.3" @@ -3779,7 +3800,7 @@ dependencies = [ "cfg-if", "libc", "redox_syscall 0.3.5", - "smallvec", + "smallvec 1.11.1", "windows-targets", ] @@ -5105,6 +5126,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-hex" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca37e3e4d1b39afd7ff11ee4e947efae85adfddf4841787bfa47c470e96dc26d" +dependencies = [ + "array-init", + "serde", + "smallvec 0.6.14", +] + [[package]] name = "serde_derive" version = "1.0.188" @@ -5393,6 +5425,7 @@ dependencies = [ "lazy_static", "rustc-hex", "serde", + "serde-hex", "serde_json", "ssz_rs", "ssz_rs_derive", @@ -5486,6 +5519,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + [[package]] name = "smallvec" version = "1.11.1" @@ -6225,7 +6267,7 @@ dependencies = [ "once_cell", "regex", "sharded-slab", - "smallvec", + "smallvec 1.11.1", "thread_local", "tracing", "tracing-core", diff --git a/README.md b/README.md index c0847614..9a3d480c 100644 --- a/README.md +++ b/README.md @@ -109,7 +109,7 @@ make lint make test ``` -Official [bundler spec tests](https://github.com/eth-infinitism/bundler-spec-tests) developed by the [eth-infinitism](https://github.com/eth-infinitism/) team are also included in the repo's CI pipeline (commit: [e193753db1910fb6d0ee2661d96a8d8f79d6c7d8](https://github.com/eth-infinitism/bundler-spec-tests/tree/e193753db1910fb6d0ee2661d96a8d8f79d6c7d8)). You can find more information on how to run tests [here](https://github.com/eth-infinitism/bundler-spec-tests). Make sure your contribution doesn't break the tests! +Official [bundler spec tests](https://github.com/eth-infinitism/bundler-spec-tests) developed by the [eth-infinitism](https://github.com/eth-infinitism/) team are also included in the repo's CI pipeline (commit: [bbd61f21e95ed1290678fcbfd9551b1502c81fe9](https://github.com/eth-infinitism/bundler-spec-tests/tree/bbd61f21e95ed1290678fcbfd9551b1502c81fe9)). You can find more information on how to run tests [here](https://github.com/eth-infinitism/bundler-spec-tests). Make sure your contribution doesn't break the tests! ## Contact diff --git a/bin/silius/src/bundler.rs b/bin/silius/src/bundler.rs index da6f48ba..93667dab 100644 --- a/bin/silius/src/bundler.rs +++ b/bin/silius/src/bundler.rs @@ -162,7 +162,6 @@ where chain_id, args.max_verification_gas, args.min_stake, - args.min_unstake_delay, args.min_priority_fee_per_gas, args.whitelist, args.uopool_mode, diff --git a/bin/silius/src/cli/args.rs b/bin/silius/src/cli/args.rs index 429b9e57..8bccd252 100644 --- a/bin/silius/src/cli/args.rs +++ b/bin/silius/src/cli/args.rs @@ -77,10 +77,6 @@ pub struct UoPoolArgs { #[clap(long, value_parser=parse_u256, default_value = "1")] pub min_stake: U256, - /// Minimum unstake delay for entities. - #[clap(long, value_parser=parse_u256, default_value = "0")] - pub min_unstake_delay: U256, - /// Minimum priority fee per gas. #[clap(long, value_parser=parse_u256, default_value = "0")] pub min_priority_fee_per_gas: U256, diff --git a/crates/grpc/src/builder.rs b/crates/grpc/src/builder.rs index 2a22f29b..1375b408 100644 --- a/crates/grpc/src/builder.rs +++ b/crates/grpc/src/builder.rs @@ -1,4 +1,3 @@ -use crate::uopool::{GAS_INCREASE_PERC, MAX_UOS_PER_UNSTAKED_SENDER}; use ethers::{ providers::Middleware, types::{Address, H256, U256}, @@ -7,9 +6,12 @@ use eyre::format_err; use futures_util::StreamExt; use silius_contracts::EntryPoint; use silius_primitives::{ + consts::reputation::{ + BAN_SLACK, MIN_INCLUSION_RATE_DENOMINATOR, MIN_UNSTAKE_DELAY, THROTTLING_SLACK, + }, get_address, provider::BlockStream, - reputation::{ReputationEntry, BAN_SLACK, MIN_INCLUSION_RATE_DENOMINATOR, THROTTLING_SLACK}, + reputation::ReputationEntry, Chain, UserOperation, }; use silius_uopool::{ @@ -35,7 +37,6 @@ where chain: Chain, max_verification_gas: U256, min_stake: U256, - min_unstake_delay: U256, min_priority_fee_per_gas: U256, whitelist: Vec
, mempool: MempoolBox, @@ -57,7 +58,6 @@ where chain: Chain, max_verification_gas: U256, min_stake: U256, - min_unstake_delay: U256, min_priority_fee_per_gas: U256, whitelist: Vec
, mempool: P, @@ -73,7 +73,7 @@ where THROTTLING_SLACK, BAN_SLACK, min_stake, - min_unstake_delay, + MIN_UNSTAKE_DELAY.into(), ); for addr in whitelist.iter() { reputation.add_whitelist(addr); @@ -86,7 +86,6 @@ where chain, max_verification_gas, min_stake, - min_unstake_delay, min_priority_fee_per_gas, whitelist, mempool, @@ -199,8 +198,6 @@ where self.chain, self.max_verification_gas, self.min_priority_fee_per_gas, - MAX_UOS_PER_UNSTAKED_SENDER, - GAS_INCREASE_PERC.into(), ) } else { StandardUserOperationValidator::new_canonical( @@ -208,8 +205,6 @@ where self.chain, self.max_verification_gas, self.min_priority_fee_per_gas, - MAX_UOS_PER_UNSTAKED_SENDER, - GAS_INCREASE_PERC.into(), ) }; diff --git a/crates/grpc/src/proto.rs b/crates/grpc/src/proto.rs index 07823071..2b7313ac 100644 --- a/crates/grpc/src/proto.rs +++ b/crates/grpc/src/proto.rs @@ -315,6 +315,26 @@ pub mod types { Self::from(value.0) } } + + impl From for StakeInfo { + fn from(value: silius_primitives::reputation::StakeInfo) -> Self { + Self { + address: Some(value.address.into()), + stake: value.stake.as_u64(), + unstake_delay: value.unstake_delay.as_u64(), + } + } + } + + impl From for silius_primitives::reputation::StakeInfo { + fn from(value: StakeInfo) -> Self { + Self { + address: value.address.unwrap_or_default().into(), + stake: value.stake.into(), + unstake_delay: value.unstake_delay.into(), + } + } + } } pub mod uopool { diff --git a/crates/grpc/src/protos/types/types.proto b/crates/grpc/src/protos/types/types.proto index d3428127..260f1c27 100644 --- a/crates/grpc/src/protos/types/types.proto +++ b/crates/grpc/src/protos/types/types.proto @@ -88,3 +88,9 @@ message Log{ H256 transaction_hash = 8; PbU256 log_index = 9; } + +message StakeInfo { + H160 address = 1; + uint64 stake = 2; + uint64 unstake_delay = 3; +} diff --git a/crates/grpc/src/protos/uopool/uopool.proto b/crates/grpc/src/protos/uopool/uopool.proto index 15b07606..4a2f04fb 100644 --- a/crates/grpc/src/protos/uopool/uopool.proto +++ b/crates/grpc/src/protos/uopool/uopool.proto @@ -103,6 +103,16 @@ message GetUserOperationReceiptResponse{ string reason = 10; } +message GetStakeInfoRequest { + types.H160 addr = 1; + types.H160 ep = 2; +} + +message GetStakeInfoResponse { + types.StakeInfo info = 1; + bool is_staked = 2; +} + service UoPool { rpc Add(AddRequest) returns (AddResponse); rpc Remove(RemoveRequest) returns (google.protobuf.Empty); @@ -112,9 +122,12 @@ service UoPool { rpc GetSortedUserOperations(GetSortedRequest) returns (GetSortedResponse); rpc GetUserOperationByHash(UserOperationHashRequest) returns (GetUserOperationByHashResponse); rpc GetUserOperationReceipt(UserOperationHashRequest) returns (GetUserOperationReceiptResponse); + rpc GetStakeInfo(GetStakeInfoRequest) returns (GetStakeInfoResponse); // debug rpc GetAll(GetAllRequest) returns (GetAllResponse); + rpc ClearMempool(google.protobuf.Empty) returns (google.protobuf.Empty); + rpc ClearReputation(google.protobuf.Empty) returns (google.protobuf.Empty); rpc Clear(google.protobuf.Empty) returns (google.protobuf.Empty); rpc GetAllReputation(GetAllReputationRequest) returns (GetAllReputationResponse); rpc SetReputation(SetReputationRequest) returns (SetReputationResponse); diff --git a/crates/grpc/src/uopool.rs b/crates/grpc/src/uopool.rs index 83366224..5e23b311 100644 --- a/crates/grpc/src/uopool.rs +++ b/crates/grpc/src/uopool.rs @@ -26,9 +26,6 @@ use std::fmt::{Debug, Display}; use std::{net::SocketAddr, sync::Arc, time::Duration}; use tonic::{Code, Request, Response, Status}; -pub const MAX_UOS_PER_UNSTAKED_SENDER: usize = 4; -pub const GAS_INCREASE_PERC: u64 = 10; - type StandardUserPool = UserOperationPool, P, R, E>; @@ -81,17 +78,7 @@ where let res = { let uopool = self.get_uopool(&ep)?; - match uopool.validate_user_operation(&uo).await { - Ok(res) => res, - Err(err) => { - return Ok(Response::new(AddResponse { - res: AddResult::NotAdded as i32, - data: serde_json::to_string(&err).map_err(|err| { - Status::internal(format!("Failed to serialize error: {err}")) - })?, - })) - } - } + uopool.validate_user_operation(&uo).await }; let mut uopool = self.get_uopool(&ep)?; @@ -279,6 +266,20 @@ where })) } + async fn clear_mempool(&self, _req: Request<()>) -> Result, Status> { + self.uopools.iter_mut().for_each(|uopool| { + uopool.uopool().clear_mempool(); + }); + Ok(Response::new(())) + } + + async fn clear_reputation(&self, _req: Request<()>) -> Result, Status> { + self.uopools.iter_mut().for_each(|uopool| { + uopool.uopool().clear_reputation(); + }); + Ok(Response::new(())) + } + async fn clear(&self, _req: Request<()>) -> Result, Status> { self.uopools.iter_mut().for_each(|uopool| { uopool.uopool().clear(); @@ -322,6 +323,26 @@ where Ok(res) } + + async fn get_stake_info( + &self, + req: Request, + ) -> Result, Status> { + let req = req.into_inner(); + + let ep = parse_addr(req.ep)?; + let addr = parse_addr(req.addr)?; + let uopool = self.get_uopool(&ep)?; + + let res = uopool + .get_stake_info(&addr) + .await + .map_err(|e| tonic::Status::internal(format!("Get stake info internal error: {e}")))?; + Ok(Response::new(GetStakeInfoResponse { + info: Some(res.stake_info.into()), + is_staked: res.is_staked, + })) + } } #[allow(clippy::too_many_arguments)] @@ -334,7 +355,6 @@ pub async fn uopool_service_run( chain: Chain, max_verification_gas: U256, min_stake: U256, - min_unstake_delay: U256, min_priority_fee_per_gas: U256, whitelist: Vec
, upool_mode: UoPoolMode, @@ -363,7 +383,6 @@ where chain, max_verification_gas, min_stake, - min_unstake_delay, min_priority_fee_per_gas, whitelist.clone(), DatabaseMempool::new(env.clone()), diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index eaa307ec..64ba0fc2 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -20,6 +20,7 @@ futures-util = { workspace = true } lazy_static = "1.4.0" rustc-hex = "^2.0.1" serde = "1" +serde-hex = "0.1.0" serde_json = { workspace = true } ssz_rs = "0.8.0" ssz_rs_derive = "0.8.0" diff --git a/crates/primitives/src/consts.rs b/crates/primitives/src/consts.rs index 991df1f9..77024036 100644 --- a/crates/primitives/src/consts.rs +++ b/crates/primitives/src/consts.rs @@ -10,7 +10,7 @@ pub mod rpc_error_codes { pub const PAYMASTER: i32 = -32501; pub const OPCODE: i32 = -32502; pub const EXPIRATION: i32 = -32503; - pub const ENTITY_BANNED: i32 = -32504; + pub const ENTITY_BANNED_OR_THROTTLED: i32 = -32504; pub const STAKE_TOO_LOW: i32 = -32505; pub const SIGNATURE_AGGREGATOR: i32 = -32506; pub const SIGNATURE: i32 = -32507; @@ -25,14 +25,36 @@ pub mod entities { pub const NUMBER_LEVELS: usize = 3; pub const FACTORY: &str = "factory"; - pub const ACCOUNT: &str = "account"; + pub const SENDER: &str = "account"; pub const PAYMASTER: &str = "paymaster"; pub const FACTORY_LEVEL: usize = 0; - pub const ACCOUNT_LEVEL: usize = 1; + pub const SENDER_LEVEL: usize = 1; pub const PAYMASTER_LEVEL: usize = 2; - pub const LEVEL_TO_ENTITY: [&str; NUMBER_LEVELS] = [FACTORY, ACCOUNT, PAYMASTER]; + pub const LEVEL_TO_ENTITY: [&str; NUMBER_LEVELS] = [FACTORY, SENDER, PAYMASTER]; +} + +/// Reputation +/// https://github.com/eth-infinitism/account-abstraction/blob/develop/eip/EIPS/eip-aa-rules.md#constants +pub mod reputation { + pub const MIN_UNSTAKE_DELAY: u64 = 86400; + // pub const MIN_STAKE_VALUE - Adjustable per chain value, Equivalent to ~$1000 in native tokens + pub const SAME_SENDER_MEMPOOL_COUNT: usize = 4; + pub const SAME_UNSTAKED_ENTITY_MEMPOOL_COUNT: usize = 10; + pub const THROTTLED_ENTITY_MEMPOOL_COUNT: usize = 4; + pub const THROTTLED_ENTITY_LIVE_BLOCKS: usize = 4; + pub const THROTTLED_ENTITY_BUNDLE_COUNT: usize = 4; + pub const MIN_INCLUSION_RATE_DENOMINATOR: u64 = 10; + pub const INCLUSION_RATE_FACTOR: u64 = 10; + pub const THROTTLING_SLACK: u64 = 10; + pub const BAN_SLACK: u64 = 50; +} + +/// User opereation mempool +pub mod uopool { + pub const GAS_INCREASE_PERC: u64 = 10; + pub const LATEST_SCAN_DEPTH: u64 = 1000; } /// Builder JSON-RPC Endpoints diff --git a/crates/primitives/src/reputation.rs b/crates/primitives/src/reputation.rs index 02b0ad87..76f62176 100644 --- a/crates/primitives/src/reputation.rs +++ b/crates/primitives/src/reputation.rs @@ -1,3 +1,4 @@ +use super::utils::{as_checksum_addr, as_hex_string, as_u64}; use educe::Educe; use ethers::{ prelude::{EthAbiCodec, EthAbiType}, @@ -5,18 +6,12 @@ use ethers::{ }; use serde::{Deserialize, Serialize}; -pub type ReputationStatus = u8; - -pub const MIN_INCLUSION_RATE_DENOMINATOR: u64 = 10; -pub const THROTTLING_SLACK: u64 = 10; -pub const BAN_SLACK: u64 = 50; - -// If the paymaster is throttle, maximum amount in one bundle is 1. -pub const THROTTLED_MAX_INCLUDE: u64 = 1; +pub type ReputationStatus = u64; /// All possible reputation statuses #[derive(Default, Clone, Educe, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] #[educe(Debug)] +#[serde(rename_all = "lowercase")] pub enum Status { #[default] OK, @@ -62,8 +57,11 @@ impl From for Status { #[educe(Debug)] pub struct ReputationEntry { pub address: Address, + #[serde(rename = "opsSeen", serialize_with = "as_hex_string")] pub uo_seen: u64, + #[serde(rename = "opsIncluded", serialize_with = "as_hex_string")] pub uo_included: u64, + #[serde(default, serialize_with = "as_hex_string")] pub status: ReputationStatus, } @@ -71,8 +69,11 @@ pub struct ReputationEntry { #[derive(Clone, Copy, Default, Educe, Eq, PartialEq, Serialize, Deserialize)] #[educe(Debug)] pub struct StakeInfo { + #[serde(rename = "addr", serialize_with = "as_checksum_addr")] pub address: Address, + #[serde(serialize_with = "as_u64")] pub stake: U256, + #[serde(rename = "unstakeDelaySec", serialize_with = "as_u64")] pub unstake_delay: U256, // seconds } @@ -82,21 +83,41 @@ impl StakeInfo { } } +/// Stake info response for RPC +#[derive(Clone, Copy, Default, Educe, Eq, PartialEq, Serialize, Deserialize)] +#[educe(Debug)] +pub struct StakeInfoResponse { + #[serde(rename = "stakeInfo")] + pub stake_info: StakeInfo, + #[serde(rename = "isStaked")] + pub is_staked: bool, +} + /// Error object for reputation +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum ReputationError { EntityBanned { address: Address, - title: String, + entity: String, + }, + ThrottledLimit { + address: Address, + entity: String, + }, + UnstakedEntityVerification { + address: Address, + entity: String, + message: String, }, StakeTooLow { address: Address, - title: String, + entity: String, min_stake: U256, min_unstake_delay: U256, }, UnstakeDelayTooLow { address: Address, - title: String, + entity: String, min_stake: U256, min_unstake_delay: U256, }, diff --git a/crates/primitives/src/sanity.rs b/crates/primitives/src/sanity.rs index 7ab0f573..ff85e785 100644 --- a/crates/primitives/src/sanity.rs +++ b/crates/primitives/src/sanity.rs @@ -1,3 +1,4 @@ +use crate::reputation::ReputationError; use ethers::{ providers::MiddlewareError, types::{Address, Bytes, U256}, @@ -5,7 +6,7 @@ use ethers::{ use serde::{Deserialize, Serialize}; /// Error object for sanity check -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum SanityCheckError { SenderOrInitCode { sender: Address, @@ -45,6 +46,12 @@ pub enum SanityCheckError { sender: Address, message: String, }, + EntityVerification { + entity: String, + address: Address, + message: String, + }, + Reputation(ReputationError), Validation { message: String, }, @@ -56,6 +63,12 @@ pub enum SanityCheckError { }, } +impl From for SanityCheckError { + fn from(err: ReputationError) -> Self { + SanityCheckError::Reputation(err) + } +} + impl From for SanityCheckError { fn from(err: M) -> Self { SanityCheckError::MiddlewareError { diff --git a/crates/primitives/src/simulation.rs b/crates/primitives/src/simulation.rs index 652a4943..f5939367 100644 --- a/crates/primitives/src/simulation.rs +++ b/crates/primitives/src/simulation.rs @@ -40,7 +40,7 @@ lazy_static! { } /// Error object for simulation -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum SimulationCheckError { Signature {}, Expiration { diff --git a/crates/primitives/src/uopool.rs b/crates/primitives/src/uopool.rs index 51b7d450..9233e85b 100644 --- a/crates/primitives/src/uopool.rs +++ b/crates/primitives/src/uopool.rs @@ -10,7 +10,7 @@ pub enum Mode { Unsafe, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum ValidationError { Sanity(SanityCheckError), Simulation(SimulationCheckError), diff --git a/crates/primitives/src/user_operation.rs b/crates/primitives/src/user_operation.rs index 66572c91..3e5896cd 100644 --- a/crates/primitives/src/user_operation.rs +++ b/crates/primitives/src/user_operation.rs @@ -1,4 +1,4 @@ -use super::utils::as_checksum; +use super::utils::{as_checksum_addr, as_checksum_bytes, get_address}; use ethers::{ abi::AbiEncode, prelude::{EthAbiCodec, EthAbiType}, @@ -31,13 +31,14 @@ use std::{ #[serde(rename_all = "camelCase")] pub struct UserOperation { /// Sender of the user operation - #[serde(serialize_with = "as_checksum")] + #[serde(serialize_with = "as_checksum_addr")] pub sender: Address, /// Nonce (anti replay protection) pub nonce: U256, /// Init code for the account (needed if account not yet deployed and needs to be created) + #[serde(serialize_with = "as_checksum_bytes")] pub init_code: Bytes, /// The data that is passed to the sender during the main execution call @@ -161,6 +162,14 @@ impl UserOperation { self } + /// Gets the entities (optionally if present) involved in the user operation + pub fn get_entities(&self) -> (Address, Option
, Option
) { + let sender = self.sender; + let factory = get_address(&self.init_code); + let paymaster = get_address(&self.paymaster_and_data); + (sender, factory, paymaster) + } + /// Creates random user operation (for testing purposes) #[cfg(feature = "test-utils")] pub fn random() -> Self { @@ -273,7 +282,7 @@ impl From for UserOperationUnsigned { pub struct UserOperationReceipt { #[serde(rename = "userOpHash")] pub user_operation_hash: UserOperationHash, - #[serde(serialize_with = "as_checksum")] + #[serde(serialize_with = "as_checksum_addr")] pub sender: Address, pub nonce: U256, #[serde(skip_serializing_if = "Option::is_none")] @@ -292,7 +301,7 @@ pub struct UserOperationReceipt { #[serde(rename_all = "camelCase")] pub struct UserOperationByHash { pub user_operation: UserOperation, - #[serde(serialize_with = "as_checksum")] + #[serde(serialize_with = "as_checksum_addr")] pub entry_point: Address, pub transaction_hash: H256, pub block_hash: H256, diff --git a/crates/primitives/src/utils.rs b/crates/primitives/src/utils.rs index 413dc539..e4ceec1e 100644 --- a/crates/primitives/src/utils.rs +++ b/crates/primitives/src/utils.rs @@ -1,13 +1,47 @@ -use ethers::{types::Address, utils::to_checksum}; +use ethers::{ + types::{Address, Bytes, U256}, + utils::{hex, to_checksum}, +}; /// Converts address to checksum address -pub fn as_checksum(val: &Address, s: S) -> Result +pub fn as_checksum_addr(val: &Address, s: S) -> Result where S: serde::Serializer, { s.serialize_str(&to_checksum(val, None)) } +/// Converts bytes to checksum (first 20 bytes are address) +pub fn as_checksum_bytes(val: &Bytes, s: S) -> Result +where + S: serde::Serializer, +{ + let mut str = hex::encode_prefixed(val); + s.serialize_str(if val.len() >= 20 { + let addr = Address::from_slice(&val[0..20]); + str.replace_range(0..42, &to_checksum(&addr, None)); + &str + } else { + &str + }) +} + +/// Serializes U256 as u64 +pub fn as_u64(val: &U256, s: S) -> Result +where + S: serde::Serializer, +{ + s.serialize_str(&val.as_u64().to_string()) +} + +/// Serializes u64 as hex string +pub fn as_hex_string(val: &u64, s: S) -> Result +where + S: serde::Serializer, +{ + serde_hex::SerHex::::serialize(val, s) +} + /// If possible, parses address from the first 20 bytes pub fn get_address(buf: &[u8]) -> Option
{ if buf.len() >= 20 { diff --git a/crates/rpc/src/debug.rs b/crates/rpc/src/debug.rs index 35c8e8f9..abc7a1b5 100644 --- a/crates/rpc/src/debug.rs +++ b/crates/rpc/src/debug.rs @@ -1,4 +1,7 @@ -use crate::{debug_api::DebugApiServer, error::JsonRpcError}; +use crate::{ + debug_api::{DebugApiServer, ResponseSuccess}, + error::JsonRpcError, +}; use async_trait::async_trait; use ethers::types::{Address, H256}; use jsonrpsee::{ @@ -7,10 +10,13 @@ use jsonrpsee::{ }; use silius_grpc::{ bundler_client::BundlerClient, uo_pool_client::UoPoolClient, GetAllReputationRequest, - GetAllRequest, Mode as GrpcMode, SetModeRequest, SetReputationRequest, SetReputationResult, + GetAllRequest, GetStakeInfoRequest, Mode as GrpcMode, SetModeRequest, SetReputationRequest, + SetReputationResult, }; use silius_primitives::{ - bundler::DEFAULT_BUNDLE_INTERVAL, reputation::ReputationEntry, BundlerMode, UserOperation, + bundler::DEFAULT_BUNDLE_INTERVAL, + reputation::{ReputationEntry, StakeInfoResponse}, + BundlerMode, UserOperation, }; use tonic::Request; @@ -26,8 +32,42 @@ impl DebugApiServer for DebugApiServerImpl { /// /// /// # Returns - /// * `RpcResult<()>` - None if no error - async fn clear_state(&self) -> RpcResult<()> { + /// * `RpcResult` - Ok + async fn clear_mempool(&self) -> RpcResult { + let mut uopool_grpc_client = self.uopool_grpc_client.clone(); + + uopool_grpc_client + .clear_mempool(Request::new(())) + .await + .map_err(JsonRpcError::from)? + .into_inner(); + + Ok(ResponseSuccess::Ok) + } + + /// Clears the bundler reputation + /// + /// + /// # Returns + /// * `RpcResult` - Ok + async fn clear_reputation(&self) -> RpcResult { + let mut uopool_grpc_client = self.uopool_grpc_client.clone(); + + uopool_grpc_client + .clear_reputation(Request::new(())) + .await + .map_err(JsonRpcError::from)? + .into_inner(); + + Ok(ResponseSuccess::Ok) + } + + /// Clears the bundler mempool and reputation + /// + /// + /// # Returns + /// * `RpcResult` - Ok + async fn clear_state(&self) -> RpcResult { let mut uopool_grpc_client = self.uopool_grpc_client.clone(); uopool_grpc_client @@ -36,7 +76,7 @@ impl DebugApiServer for DebugApiServerImpl { .map_err(JsonRpcError::from)? .into_inner(); - Ok(()) + Ok(ResponseSuccess::Ok) } /// Sending an [GetAllRequest](GetAllRequest) to the UoPool gRPC server @@ -73,8 +113,12 @@ impl DebugApiServer for DebugApiServerImpl { /// * `entry_point: Address` - The address of the entry point. /// /// # Returns - /// * `RpcResult<()>` - None if no error - async fn set_reputation(&self, entries: Vec, ep: Address) -> RpcResult<()> { + /// * `RpcResult` - Ok + async fn set_reputation( + &self, + entries: Vec, + ep: Address, + ) -> RpcResult { let mut uopool_grpc_client = self.uopool_grpc_client.clone(); let req = Request::new(SetReputationRequest { @@ -89,7 +133,7 @@ impl DebugApiServer for DebugApiServerImpl { .into_inner(); if res.res == SetReputationResult::SetReputation as i32 { - return Ok(()); + return Ok(ResponseSuccess::Ok); } Err(ErrorObjectOwned::owned( @@ -128,8 +172,8 @@ impl DebugApiServer for DebugApiServerImpl { /// * `mode: BundlerMode` - The [BundlingMode](BundlingMode) to be set. /// /// # Returns - /// * `RpcResult<()>` - None if no error - async fn set_bundling_mode(&self, mode: BundlerMode) -> RpcResult<()> { + /// * `RpcResult` - Ok + async fn set_bundling_mode(&self, mode: BundlerMode) -> RpcResult { let mut bundler_grpc_client = self.bundler_grpc_client.clone(); let req = Request::new(SetModeRequest { @@ -138,7 +182,7 @@ impl DebugApiServer for DebugApiServerImpl { }); match bundler_grpc_client.set_bundler_mode(req).await { - Ok(_) => Ok(()), + Ok(_) => Ok(ResponseSuccess::Ok), Err(s) => Err(JsonRpcError::from(s).into()), } } @@ -163,4 +207,32 @@ impl DebugApiServer for DebugApiServerImpl { Err(s) => Err(JsonRpcError::from(s).into()), } } + + /// Returns the stake info of the given address. + /// + /// # Arguments + /// * `address: Address` - The address of the entity. + /// * `entry_point: Address` - The address of the entry point. + /// + /// # Returns + /// * `RpcResult` - Stake info of the entity. + async fn get_stake_status(&self, addr: Address, ep: Address) -> RpcResult { + let mut uopool_grpc_client = self.uopool_grpc_client.clone(); + + let req = Request::new(GetStakeInfoRequest { + addr: Some(addr.into()), + ep: Some(ep.into()), + }); + + match uopool_grpc_client.get_stake_info(req).await { + Ok(res) => Ok({ + let res = res.into_inner(); + StakeInfoResponse { + stake_info: res.info.expect("Must return stake info").into(), + is_staked: res.is_staked, + } + }), + Err(s) => Err(JsonRpcError::from(s).into()), + } + } } diff --git a/crates/rpc/src/debug_api.rs b/crates/rpc/src/debug_api.rs index c251dff8..7993e9ac 100644 --- a/crates/rpc/src/debug_api.rs +++ b/crates/rpc/src/debug_api.rs @@ -1,7 +1,17 @@ pub use crate::debug::DebugApiServerImpl; use ethers::types::{Address, H256}; use jsonrpsee::{core::RpcResult, proc_macros::rpc}; -use silius_primitives::{reputation::ReputationEntry, BundlerMode, UserOperation}; +use serde::{Deserialize, Serialize}; +use silius_primitives::{ + reputation::{ReputationEntry, StakeInfoResponse}, + BundlerMode, UserOperation, +}; + +#[derive(Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum ResponseSuccess { + Ok, +} /// The ERC-4337 `debug` namespace RPC methods trait #[rpc(server, namespace = "debug_bundler")] @@ -10,9 +20,25 @@ pub trait DebugApi { /// /// /// # Returns - /// * `RpcResult<()>` - None + /// * `RpcResult` - Ok + #[method(name = "clearMempool")] + async fn clear_mempool(&self) -> RpcResult; + + /// Clears the bundler reputation + /// + /// + /// # Returns + /// * `RpcResult` - Ok + #[method(name = "clearReputation")] + async fn clear_reputation(&self) -> RpcResult; + + /// Clears the bundler mempool and reputation + /// + /// + /// # Returns + /// * `RpcResult` - Ok #[method(name = "clearState")] - async fn clear_state(&self) -> RpcResult<()>; + async fn clear_state(&self) -> RpcResult; /// Get all [UserOperations](UserOperation) of the mempool /// @@ -31,13 +57,13 @@ pub trait DebugApi { /// * `entry_point: Address` - The address of the entry point. /// /// # Returns - /// * `RpcResult<()>` - None + /// * `RpcResult` - Ok #[method(name = "setReputation")] async fn set_reputation( &self, reputation_entries: Vec, entry_point: Address, - ) -> RpcResult<()>; + ) -> RpcResult; /// Return the all of [ReputationEntries](ReputationEntry) in the mempool. /// @@ -55,9 +81,9 @@ pub trait DebugApi { /// * `mode: BundlerMode` - The [BundlingMode](BundlingMode) to be set. /// /// # Returns - /// * `RpcResult<()>` - None + /// * `RpcResult` - Ok #[method(name = "setBundlingMode")] - async fn set_bundling_mode(&self, mode: BundlerMode) -> RpcResult<()>; + async fn set_bundling_mode(&self, mode: BundlerMode) -> RpcResult; /// Immediately send the current bundle of user operations. /// This is useful for testing or in situations where waiting for the next scheduled bundle is not desirable. @@ -67,4 +93,19 @@ pub trait DebugApi { /// * `RpcResult` - The hash of the bundle that was sent. #[method(name = "sendBundleNow")] async fn send_bundle_now(&self) -> RpcResult; + + /// Returns the stake info of the given address. + /// + /// # Arguments + /// * `address: Address` - The address of the entity. + /// * `entry_point: Address` - The address of the entry point. + /// + /// # Returns + /// * `RpcResult` - Stake info of the entity. + #[method(name = "getStakeStatus")] + async fn get_stake_status( + &self, + address: Address, + entry_point: Address, + ) -> RpcResult; } diff --git a/crates/rpc/src/error.rs b/crates/rpc/src/error.rs index 6118ace0..7ff7035e 100644 --- a/crates/rpc/src/error.rs +++ b/crates/rpc/src/error.rs @@ -1,10 +1,9 @@ -use ethers::abi::AbiEncode; use jsonrpsee::types::{error::ErrorCode, ErrorObject, ErrorObjectOwned}; use serde_json::json; use silius_primitives::{ consts::rpc_error_codes::{ - ENTITY_BANNED, EXECUTION, EXPIRATION, OPCODE, SANITY_CHECK, SIGNATURE, STAKE_TOO_LOW, - VALIDATION, + ENTITY_BANNED_OR_THROTTLED, EXECUTION, EXPIRATION, OPCODE, SANITY_CHECK, SIGNATURE, + STAKE_TOO_LOW, VALIDATION, }, reputation::ReputationError, sanity::SanityCheckError, @@ -112,9 +111,62 @@ impl From for JsonRpcError { ), SanityCheckError::SenderVerification { sender, message } => ErrorObject::owned( SANITY_CHECK, - format!("Sender {sender} {message}",), + format!("Sender {sender:?} {message}",), None::, ), + SanityCheckError::EntityVerification { entity, address, message } => { + ErrorObject::owned( + OPCODE, + format!("Entity {entity} with {address:?} {message}",), + None::, + ) + }, + SanityCheckError::Reputation(err) => + match err { + ReputationError::EntityBanned { entity, address } => { + ErrorObject::owned( + ENTITY_BANNED_OR_THROTTLED, + format!("Entity {entity} with {address:?} is banned",), + None::, + ) + }, + ReputationError::ThrottledLimit { address, entity } => { + ErrorObject::owned( + ENTITY_BANNED_OR_THROTTLED, + format!("Limit for throttled entity {entity} with {address:?} exceeded",), + None::, + ) + } + ReputationError::UnstakedEntityVerification { entity, address, message } => { + ErrorObject::owned( + STAKE_TOO_LOW, + format!("Unstaked entity {entity} with {address:?} {message}",), + None::, + ) + }, + ReputationError::StakeTooLow { entity, address, min_stake, min_unstake_delay } => { + ErrorObject::owned( + STAKE_TOO_LOW, + format!("Entity {entity} with {address} has too low stake: min stake {min_stake}, min unstake delay {min_unstake_delay}",), + None::, + ) + }, + ReputationError::UnstakeDelayTooLow { entity, address, min_stake, min_unstake_delay } => { + ErrorObject::owned( + STAKE_TOO_LOW, + format!("Entity {entity} with {address} has too low unstake delay: min stake {min_stake}, min unstake delay {min_unstake_delay}",), + None::, + ) + }, + ReputationError::UnknownError { message } => { + ErrorObject::owned( + SANITY_CHECK, + message, + None::, + ) + } + } + , SanityCheckError::Validation { message } => { ErrorObject::owned( VALIDATION, @@ -207,58 +259,6 @@ impl From for JsonRpcError { } } -impl From for JsonRpcError { - /// Convert a [ReputationError](ReputationError) to a [JsonRpcError](JsonRpcError). - fn from(err: ReputationError) -> Self { - JsonRpcError( - match err { - ReputationError::EntityBanned { address, title } => ErrorObject::owned( - ENTITY_BANNED, - format!("{title} with address {address} is banned",), - Some(json!({ - title: address.to_string(), - })), - ), - ReputationError::StakeTooLow { - address, - title, - min_stake, - min_unstake_delay, - } => ErrorObject::owned( - STAKE_TOO_LOW, - format!( - "{title} with address {address} stake is lower than {min_stake}", - ), - Some(json!({ - title: address.to_string(), - "minimumStake": AbiEncode::encode_hex(min_stake), - "minimumUnstakeDelay": AbiEncode::encode_hex(min_unstake_delay), - })), - ), - ReputationError::UnstakeDelayTooLow { - address, - title, - min_stake, - min_unstake_delay, - } => ErrorObject::owned( - STAKE_TOO_LOW, - format!( - "{title} with address {address} unstake delay is lower than {min_unstake_delay}", - ), - Some(json!({ - title: address.to_string(), - "minimumStake": AbiEncode::encode_hex(min_stake), - "minimumUnstakeDelay": AbiEncode::encode_hex(min_unstake_delay), - })), - ), - ReputationError::UnknownError { message } => { - ErrorObject::owned(ErrorCode::InternalError.code(), message, None::) - } - } - ) - } -} - impl From for JsonRpcError { /// Convert a [ValidationError](ValidationError) to a [JsonRpcError](JsonRpcError). fn from(err: ValidationError) -> Self { diff --git a/crates/uopool/src/database/mempool.rs b/crates/uopool/src/database/mempool.rs index 8eb3453b..d9696c11 100644 --- a/crates/uopool/src/database/mempool.rs +++ b/crates/uopool/src/database/mempool.rs @@ -1,13 +1,14 @@ use super::env::Env; use super::{ env::DBError, - tables::{CodeHashes, UserOperations, UserOperationsBySender}, + tables::{CodeHashes, UserOperations, UserOperationsByEntity, UserOperationsBySender}, utils::{WrapAddress, WrapUserOperation, WrapUserOperationHash}, }; use crate::mempool::Mempool; use ethers::types::{Address, U256}; +use reth_db::cursor::DbDupCursorRO; use reth_db::{ - cursor::{DbCursorRO, DbDupCursorRO}, + cursor::DbCursorRO, database::Database, mdbx::EnvironmentKind, transaction::{DbTx, DbTxMut}, @@ -53,9 +54,17 @@ impl Mempool for DatabaseMempool { let uo_hash_wrap: WrapUserOperationHash = hash.into(); let uo_wrap: WrapUserOperation = uo.clone().into(); + let (sender, factory, paymaster) = uo.get_entities(); + + tx.put::(uo_hash_wrap.clone(), uo_wrap.clone())?; + tx.put::(sender.into(), uo_hash_wrap.clone())?; + if let Some(factory) = factory { + tx.put::(factory.into(), uo_hash_wrap.clone())?; + } + if let Some(paymaster) = paymaster { + tx.put::(paymaster.into(), uo_hash_wrap)?; + } - tx.put::(uo_hash_wrap, uo_wrap.clone())?; - tx.put::(uo.sender.into(), uo_wrap)?; tx.commit()?; Ok(hash) } @@ -91,10 +100,21 @@ impl Mempool for DatabaseMempool { .tx() .and_then(|tx| { let mut cursor = tx.cursor_dup_read::()?; - let res: Vec = cursor - .walk_dup(Some(sender_wrap.clone()), Some(Address::default().into()))? - .map(|a| a.map(|(_, v)| v.into())) - .collect::, _>>()?; + // https://github.com/ralexstokes/reth/blob/ebd5d3c1a2645119330f1dbdd759c995c4f0947c/crates/stages/src/trie/mod.rs#L242 + let mut curr = + cursor.seek_by_key_subkey(sender_wrap.clone(), Address::default().into())?; + + let mut v: Vec = vec![]; + while let Some(uo_hash) = curr { + v.push(uo_hash); + curr = cursor.next_dup()?.map(|(_, v)| v); + } + + let res: Vec = v + .iter() + .filter_map(|uo_hash| tx.get::(uo_hash.clone()).ok()) + .filter_map(|uo_wrap| uo_wrap.map(|uo| uo.into())) + .collect(); tx.commit()?; Ok(res) }) @@ -104,21 +124,55 @@ impl Mempool for DatabaseMempool { /// Gets the number of [UserOperations](UserOperation) from the mempool database given a sender [Address]. /// /// # Arguments - /// * `sender` - The sender [Address](Address). + /// * `addr` - The sender [Address](Address). /// /// # Returns /// * `usize` - The number of [UserOperations](UserOperation) from the given sender. - fn get_number_by_sender(&self, sender: &Address) -> usize { - let sender_wrap: WrapAddress = (*sender).into(); + fn get_number_by_sender(&self, addr: &Address) -> usize { + let addr_wrap: WrapAddress = (*addr).into(); self.env .tx() .and_then(|tx| { let mut cursor = tx.cursor_dup_read::()?; - let res = cursor - .walk_dup(Some(sender_wrap.clone()), Some(Address::default().into()))? - .count(); + let mut curr = + cursor.seek_by_key_subkey(addr_wrap.clone(), Address::default().into())?; + + let mut c: usize = 0; + while curr.is_some() { + c += 1; + curr = cursor.next_dup()?.map(|(_, v)| v); + } + tx.commit()?; - Ok(res) + Ok(c) + }) + .unwrap_or(0) + } + + /// Gets the number of [UserOperations](UserOperation) from the mempool database given a entity [Address]. + /// + /// # Arguments + /// * `addr` - The entity [Address](Address). + /// + /// # Returns + /// * `usize` - The number of [UserOperations](UserOperation) from the given entity. + fn get_number_by_entity(&self, addr: &Address) -> usize { + let addr_wrap: WrapAddress = (*addr).into(); + self.env + .tx() + .and_then(|tx| { + let mut cursor = tx.cursor_dup_read::()?; + let mut curr = + cursor.seek_by_key_subkey(addr_wrap.clone(), Address::default().into())?; + + let mut c: usize = 0; + while curr.is_some() { + c += 1; + curr = cursor.next_dup()?.map(|(_, v)| v); + } + + tx.commit()?; + Ok(c) }) .unwrap_or(0) } @@ -153,12 +207,17 @@ impl Mempool for DatabaseMempool { .tx() .and_then(|tx| { let mut cursor = tx.cursor_dup_read::()?; - let res: Vec = cursor - .walk_dup(Some(uo_hash_wrap), Some(Address::default().into()))? - .map(|a| a.map(|(_, v)| v.into())) - .collect::, _>>()?; + let mut curr = + cursor.seek_by_key_subkey(uo_hash_wrap.clone(), Address::default().into())?; + + let mut v: Vec = vec![]; + while let Some(ch) = curr { + v.push(ch.into()); + curr = cursor.next_dup()?.map(|(_, v)| v); + } + tx.commit()?; - Ok(res) + Ok(v) }) .unwrap_or_else(|_| vec![]) } @@ -203,10 +262,21 @@ impl Mempool for DatabaseMempool { let uo_hash_wrap: WrapUserOperationHash = (*uo_hash).into(); let tx = self.env.tx_mut()?; - if let Some(uo) = tx.get::(uo_hash_wrap.clone())? { + if let Some(uo_wrap) = tx.get::(uo_hash_wrap.clone())? { + let uo: UserOperation = uo_wrap.into(); + let (sender, factory, paymaster) = uo.get_entities(); + tx.delete::(uo_hash_wrap.clone(), None)?; - tx.delete::(uo.0.sender.into(), Some(uo))?; - tx.delete::(uo_hash_wrap, None)?; + tx.delete::(sender.into(), Some(uo_hash_wrap.clone()))?; + tx.delete::(uo_hash_wrap.clone(), None)?; + + if let Some(factory) = factory { + tx.delete::(factory.into(), Some(uo_hash_wrap.clone()))?; + } + if let Some(paymaster) = paymaster { + tx.delete::(paymaster.into(), Some(uo_hash_wrap))?; + } + tx.commit()?; Ok(()) } else { @@ -214,6 +284,36 @@ impl Mempool for DatabaseMempool { } } + /// Removes all [UserOperations](UserOperation) by entity + /// + /// # Arguments + /// * `entity` - The [Address](Address) of the entity + /// + /// # Returns + /// * `Ok(())` - If the [UserOperations](UserOperation) were removed + /// * `Err(eyre::Error)` - If the [UserOperations](UserOperation) could not be removed + fn remove_by_entity(&mut self, entity: &Address) -> Result<(), Self::Error> { + let entity_wrap: WrapAddress = (*entity).into(); + + let tx = self.env.tx()?; + let mut cursor = tx.cursor_dup_read::()?; + let mut curr = cursor.seek_by_key_subkey(entity_wrap.clone(), Address::default().into())?; + + let mut v: Vec = vec![]; + while let Some(uo_hash) = curr { + v.push(uo_hash); + curr = cursor.next_dup()?.map(|(_, v)| v); + } + + tx.commit()?; + + for uo_hash_wrap in v { + self.remove(&uo_hash_wrap.into())?; + } + + Ok(()) + } + /// Sorts the [UserOperations](UserOperation) by `max_priority_fee_per_gas` and `nonce` /// /// # Returns @@ -268,6 +368,7 @@ impl Mempool for DatabaseMempool { .and_then(|tx| { tx.clear::()?; tx.clear::()?; + tx.clear::()?; tx.commit() }) .expect("Clear database failed"); diff --git a/crates/uopool/src/database/reputation.rs b/crates/uopool/src/database/reputation.rs index 53bd307e..40f5aaaa 100644 --- a/crates/uopool/src/database/reputation.rs +++ b/crates/uopool/src/database/reputation.rs @@ -111,7 +111,7 @@ impl Reputation for DatabaseReputation { /// #Returns /// * `Ok(ReputationEntry)` if the operation was successful /// * `Err(Self::Error)` if the operation failed - fn get(&mut self, addr: &Address) -> Result { + fn get(&self, addr: &Address) -> Result { let addr_wrap: WrapAddress = (*addr).into(); let tx = self.env.tx()?; @@ -119,7 +119,10 @@ impl Reputation for DatabaseReputation { tx.commit()?; if let Some(ent) = res { - Ok(ent.into()) + Ok(ReputationEntry { + status: self.get_status(addr)?, + ..ent.into() + }) } else { let ent = ReputationEntry { address: *addr, @@ -128,10 +131,6 @@ impl Reputation for DatabaseReputation { status: Status::OK.into(), }; - let tx = self.env.tx_mut()?; - tx.put::((*addr).into(), ent.clone().into())?; - tx.commit()?; - Ok(ent) } } @@ -301,13 +300,13 @@ impl Reputation for DatabaseReputation { Some(ent) => { let ent: ReputationEntry = ent.into(); - let min_expected_included = ent.uo_seen / self.min_inclusion_denominator; - if min_expected_included <= ent.uo_included + self.throttling_slack { - Status::OK.into() - } else if min_expected_included <= ent.uo_included + self.ban_slack { + let max_seen = ent.uo_seen / self.min_inclusion_denominator; + if max_seen > ent.uo_included + self.ban_slack { + Status::BANNED.into() + } else if max_seen > ent.uo_included + self.throttling_slack { Status::THROTTLED.into() } else { - Status::BANNED.into() + Status::OK.into() } } None => Status::OK.into(), @@ -343,7 +342,7 @@ impl Reputation for DatabaseReputation { /// Verify the stake information of an entity /// /// # Arguments - /// * `title` - The entity's name + /// * `entity` - The entity type /// * `info` - The entity's [stake information](StakeInfo) /// /// # Returns @@ -353,41 +352,26 @@ impl Reputation for DatabaseReputation { /// * `Err(ReputationError::UnknownError)` if an unknown error occurred /// * `Err(ReputationError::StakeTooLow)` if the entity's stake is too low /// * `Err(ReputationError::UnstakeDelayTooLow)` if unstakes too early - fn verify_stake(&self, title: &str, info: Option) -> Result<(), ReputationError> { + fn verify_stake(&self, entity: &str, info: Option) -> Result<(), ReputationError> { if let Some(info) = info { if self.is_whitelist(&info.address) { return Ok(()); } - let tx = self.env.tx().map_err(|_| ReputationError::UnknownError { - message: "database error".into(), - })?; - let res = tx - .get::(info.address.into()) - .map_err(|_| ReputationError::UnknownError { - message: "database error".into(), - })?; - if let Some(ent) = res { - let ent: ReputationEntry = ent.into(); - if Status::from(ent.status) == Status::BANNED { - return Err(ReputationError::EntityBanned { - address: info.address, - title: title.to_string(), - }); - } - } - let err = if info.stake < self.min_stake { ReputationError::StakeTooLow { address: info.address, - title: title.to_string(), + entity: entity.to_string(), min_stake: self.min_stake, min_unstake_delay: self.min_unstake_delay, } - } else if info.unstake_delay < self.min_unstake_delay { + } else if info.unstake_delay < U256::from(2) + // TODO: remove this when spec tests are updated!!!! + /* self.min_unstake_delay */ + { ReputationError::UnstakeDelayTooLow { address: info.address, - title: title.to_string(), + entity: entity.to_string(), min_stake: self.min_stake, min_unstake_delay: self.min_unstake_delay, } @@ -429,7 +413,12 @@ impl Reputation for DatabaseReputation { let mut c = tx.cursor_read::()?; let res: Vec = c .walk(Some(WrapAddress::default()))? - .map(|a| a.map(|(_, v)| v.into())) + .map(|a| { + a.map(|(_, v)| ReputationEntry { + status: self.get_status(&v.0.address).unwrap_or_default(), + ..v.into() + }) + }) .collect::, _>>()?; tx.commit()?; Ok(res) diff --git a/crates/uopool/src/database/tables.rs b/crates/uopool/src/database/tables.rs index e9bf96a9..7c8b0037 100644 --- a/crates/uopool/src/database/tables.rs +++ b/crates/uopool/src/database/tables.rs @@ -9,9 +9,14 @@ table!( ); table!( - /// Stores the user operations by sender + /// Stores the hashes of user operations by sender /// Benefit for merklization is that hashed addresses/keys are sorted. - ( UserOperationsBySender ) WrapAddress | WrapUserOperation + ( UserOperationsBySender ) WrapAddress | WrapUserOperationHash +); + +table!( + /// Stores the hashes of user operations by involved entities + ( UserOperationsByEntity ) WrapAddress | WrapUserOperationHash ); dupsort!( @@ -25,9 +30,10 @@ table!( ); /// Tables that should be present inside database -pub const TABLES: [(TableType, &str); 4] = [ +pub const TABLES: [(TableType, &str); 5] = [ (TableType::Table, UserOperations::const_name()), (TableType::DupSort, UserOperationsBySender::const_name()), + (TableType::DupSort, UserOperationsByEntity::const_name()), (TableType::DupSort, CodeHashes::const_name()), (TableType::Table, EntitiesReputation::const_name()), ]; @@ -35,3 +41,7 @@ pub const TABLES: [(TableType, &str); 4] = [ impl DupSort for UserOperationsBySender { type SubKey = WrapAddress; } + +impl DupSort for UserOperationsByEntity { + type SubKey = WrapAddress; +} diff --git a/crates/uopool/src/memory/mempool.rs b/crates/uopool/src/memory/mempool.rs index e8cc084b..507f3c7c 100644 --- a/crates/uopool/src/memory/mempool.rs +++ b/crates/uopool/src/memory/mempool.rs @@ -16,6 +16,9 @@ pub struct MemoryMempool { /// A [Hashmap](std::collections::HashMap) of [UserOperationHash](UserOperationHash) to [Vec] of /// [CodeHash](CodeHash) for lookups by [UserOperationHash](UserOperationHash) code_hashes_by_user_operation: HashMap>, // user_operation_hash -> (contract_address -> code_hash) + /// A [Hashmap](std::collections::HashMap) of [Address] to [HashSet] of + /// [UserOperationHash](UserOperationHash) for lookups by entity + user_operations_by_entity: HashMap>, // entity -> user_operations } impl Mempool for MemoryMempool { @@ -42,11 +45,24 @@ impl Mempool for MemoryMempool { chain_id: &U256, ) -> eyre::Result { let uo_hash = uo.hash(ep, chain_id); + let (sender, factory, paymaster) = uo.get_entities(); self.user_operations_by_sender - .entry(uo.sender) + .entry(sender) .or_default() .insert(uo_hash); + if let Some(factory) = factory { + self.user_operations_by_entity + .entry(factory) + .or_default() + .insert(uo_hash); + } + if let Some(paymaster) = paymaster { + self.user_operations_by_entity + .entry(paymaster) + .or_default() + .insert(uo_hash); + } self.user_operations.insert(uo_hash, uo); Ok(uo_hash) @@ -99,6 +115,21 @@ impl Mempool for MemoryMempool { }; } + /// Gets the number of [UserOperation](UserOperation)s by entity + /// + /// # Arguments + /// * `addr` - The [Address](Address) of the sender + /// + /// # Returns + /// * `usize` - The number of [UserOperations](UserOperation) if they exist. Otherwise, 0. + fn get_number_by_entity(&self, addr: &Address) -> usize { + return if let Some(uos_by_entity) = self.user_operations_by_entity.get(addr) { + uos_by_entity.len() + } else { + 0 + }; + } + /// Gets [CodeHash](CodeHash) by [UserOperationHash](UserOperationHash) /// /// # Arguments @@ -161,13 +192,35 @@ impl Mempool for MemoryMempool { return Err(eyre::eyre!("User operation not found")); } + let (sender, factory, paymaster) = uo.get_entities(); + self.user_operations.remove(uo_hash); - if let Some(uos) = self.user_operations_by_sender.get_mut(&uo.sender) { + if let Some(uos) = self.user_operations_by_sender.get_mut(&sender) { uos.remove(uo_hash); if uos.is_empty() { - self.user_operations_by_sender.remove(&uo.sender); + self.user_operations_by_sender.remove(&sender); + } + } + + if let Some(factory) = factory { + if let Some(uos) = self.user_operations_by_entity.get_mut(&factory) { + uos.remove(uo_hash); + + if uos.is_empty() { + self.user_operations_by_entity.remove(&factory); + } + } + } + + if let Some(paymaster) = paymaster { + if let Some(uos) = self.user_operations_by_entity.get_mut(&paymaster) { + uos.remove(uo_hash); + + if uos.is_empty() { + self.user_operations_by_entity.remove(&paymaster); + } } } @@ -176,6 +229,26 @@ impl Mempool for MemoryMempool { Ok(()) } + /// Removes a [UserOperation](UserOperation) by its entity + /// + /// # Arguments + /// * `entity` - The [Address](Address) of the entity + /// + /// # Returns + /// * `Ok(())` - If the [UserOperation](UserOperation) was removed + /// * `Err(eyre::Error)` - If the [UserOperation](UserOperation) could not be removed + fn remove_by_entity(&mut self, entity: &Address) -> eyre::Result<()> { + let uos = self.user_operations_by_entity.get(entity).cloned(); + + if let Some(uos) = uos { + for uo_hash in uos { + self.remove(&uo_hash)?; + } + } + + Ok(()) + } + /// Sorts the [UserOperations](UserOperation) by `max_priority_fee_per_gas` and `nonce` /// /// # Returns @@ -208,6 +281,7 @@ impl Mempool for MemoryMempool { self.user_operations.clear(); self.user_operations_by_sender.clear(); self.code_hashes_by_user_operation.clear(); + self.user_operations_by_entity.clear(); } } diff --git a/crates/uopool/src/memory/reputation.rs b/crates/uopool/src/memory/reputation.rs index 52a71318..69af7b29 100644 --- a/crates/uopool/src/memory/reputation.rs +++ b/crates/uopool/src/memory/reputation.rs @@ -79,9 +79,12 @@ impl Reputation for MemoryReputation { /// # Returns /// * `Ok(ReputationEntry)` if the address exists /// * `Err(ReputationError::NotFound)` if the address does not exist - fn get(&mut self, addr: &Address) -> Result { + fn get(&self, addr: &Address) -> Result { if let Some(ent) = self.entities.get(addr) { - return Ok(ent.clone()); + return Ok(ReputationEntry { + status: self.get_status(addr)?, + ..ent.clone() + }); } let ent = ReputationEntry { @@ -91,8 +94,6 @@ impl Reputation for MemoryReputation { status: Status::OK.into(), }; - self.entities.insert(*addr, ent.clone()); - Ok(ent) } @@ -226,13 +227,13 @@ impl Reputation for MemoryReputation { Ok(match self.entities.get(addr) { Some(ent) => { - let min_expected_included = ent.uo_seen / self.min_inclusion_denominator; - if min_expected_included <= ent.uo_included + self.throttling_slack { - Status::OK.into() - } else if min_expected_included <= ent.uo_included + self.ban_slack { + let max_seen = ent.uo_seen / self.min_inclusion_denominator; + if max_seen > ent.uo_included + self.ban_slack { + Status::BANNED.into() + } else if max_seen > ent.uo_included + self.throttling_slack { Status::THROTTLED.into() } else { - Status::BANNED.into() + Status::OK.into() } } _ => Status::OK.into(), @@ -260,7 +261,7 @@ impl Reputation for MemoryReputation { /// Verify the stake information of an entity /// /// # Arguments - /// * `title` - The entity's name + /// * `entity` - The entity type /// * `info` - The entity's [stake information](StakeInfo) /// /// # Returns @@ -268,32 +269,26 @@ impl Reputation for MemoryReputation { /// * `Err(ReputationError::EntityBanned)` if the entity is banned /// * `Err(ReputationError::StakeTooLow)` if the entity's stake is too low /// * `Err(ReputationError::UnstakeDelayTooLow)` if unstakes too early - fn verify_stake(&self, title: &str, info: Option) -> Result<(), ReputationError> { + fn verify_stake(&self, entity: &str, info: Option) -> Result<(), ReputationError> { if let Some(info) = info { if self.is_whitelist(&info.address) { return Ok(()); } - if let Some(ent) = self.entities.get(&info.address) { - if Status::from(ent.status) == Status::BANNED { - return Err(ReputationError::EntityBanned { - address: info.address, - title: title.to_string(), - }); - } - } - let err = if info.stake < self.min_stake { ReputationError::StakeTooLow { address: info.address, - title: title.to_string(), + entity: entity.to_string(), min_stake: self.min_stake, min_unstake_delay: self.min_unstake_delay, } - } else if info.unstake_delay < self.min_unstake_delay { + } else if info.unstake_delay < U256::from(2) + // TODO: remove this when spec tests are updated!!!! + /* self.min_unstake_delay */ + { ReputationError::UnstakeDelayTooLow { address: info.address, - title: title.to_string(), + entity: entity.to_string(), min_stake: self.min_stake, min_unstake_delay: self.min_unstake_delay, } @@ -327,7 +322,13 @@ impl Reputation for MemoryReputation { /// # Returns /// * All [reputation entries](ReputationEntries) fn get_all(&self) -> Self::ReputationEntries { - self.entities.values().cloned().collect() + self.entities + .values() + .map(|ent| ReputationEntry { + status: self.get_status(&ent.address).unwrap_or_default(), + ..ent.clone() + }) + .collect() } /// Clear all [reputation entries](ReputationEntries) diff --git a/crates/uopool/src/mempool.rs b/crates/uopool/src/mempool.rs index beac8d56..88e715a3 100644 --- a/crates/uopool/src/mempool.rs +++ b/crates/uopool/src/mempool.rs @@ -94,6 +94,10 @@ where self.inner.read().get_number_by_sender(addr) } + fn get_number_by_entity(&self, addr: &Address) -> usize { + self.inner.read().get_number_by_entity(addr) + } + fn get_prev_by_sender(&self, uo: &UserOperation) -> Option { self.inner.read().get_prev_by_sender(uo) } @@ -109,6 +113,10 @@ where fn remove(&mut self, uo_hash: &UserOperationHash) -> Result<(), Self::Error> { self.inner.write().remove(uo_hash) } + + fn remove_by_entity(&mut self, entity: &Address) -> Result<(), Self::Error> { + self.inner.write().remove_by_entity(entity) + } } pub fn mempool_id(ep: &Address, chain_id: &U256) -> MempoolId { @@ -133,6 +141,7 @@ pub trait Mempool: Debug { fn get(&self, uo_hash: &UserOperationHash) -> Result, Self::Error>; fn get_all_by_sender(&self, addr: &Address) -> Self::UserOperations; fn get_number_by_sender(&self, addr: &Address) -> usize; + fn get_number_by_entity(&self, addr: &Address) -> usize; fn get_prev_by_sender(&self, uo: &UserOperation) -> Option { self.get_all_by_sender(&uo.sender) .into_iter() @@ -147,6 +156,7 @@ pub trait Mempool: Debug { ) -> Result<(), Self::Error>; fn get_code_hashes(&self, uo_hash: &UserOperationHash) -> Self::CodeHashes; fn remove(&mut self, uo_hash: &UserOperationHash) -> Result<(), Self::Error>; + fn remove_by_entity(&mut self, entity: &Address) -> Result<(), Self::Error>; // Get UserOperations sorted by max_priority_fee_per_gas without dup sender fn get_sorted(&self) -> Result; fn get_all(&self) -> Self::UserOperations; diff --git a/crates/uopool/src/reputation.rs b/crates/uopool/src/reputation.rs index d975ea5c..5b8ee426 100644 --- a/crates/uopool/src/reputation.rs +++ b/crates/uopool/src/reputation.rs @@ -61,9 +61,10 @@ where self.inner.write().clear() } - fn get(&mut self, addr: &Address) -> Result { - self.inner.write().get(addr) + fn get(&self, addr: &Address) -> Result { + self.inner.read().get(addr) } + fn get_status(&self, addr: &Address) -> Result { self.inner.read().get_status(addr) } @@ -126,8 +127,8 @@ where self.inner.write().update_hourly() } - fn verify_stake(&self, title: &str, info: Option) -> Result<(), ReputationError> { - self.inner.read().verify_stake(title, info) + fn verify_stake(&self, entity: &str, info: Option) -> Result<(), ReputationError> { + self.inner.read().verify_stake(entity, info) } } @@ -147,7 +148,7 @@ pub trait Reputation: Debug { min_unstake_delay: U256, ); fn set(&mut self, addr: &Address) -> Result<(), Self::Error>; - fn get(&mut self, addr: &Address) -> Result; + fn get(&self, addr: &Address) -> Result; fn increment_seen(&mut self, addr: &Address) -> Result<(), Self::Error>; fn increment_included(&mut self, addr: &Address) -> Result<(), Self::Error>; fn update_hourly(&mut self) -> Result<(), Self::Error>; @@ -159,7 +160,7 @@ pub trait Reputation: Debug { fn is_blacklist(&self, addr: &Address) -> bool; fn get_status(&self, addr: &Address) -> Result; fn update_handle_ops_reverted(&mut self, addr: &Address) -> Result<(), Self::Error>; - fn verify_stake(&self, title: &str, info: Option) -> Result<(), ReputationError>; + fn verify_stake(&self, entity: &str, info: Option) -> Result<(), ReputationError>; // Try to get the reputation status from a sequence of bytes which the first 20 bytes should be the address // This is useful in getting the reputation directly from paymaster_and_data field and init_code field in user operation. diff --git a/crates/uopool/src/uopool.rs b/crates/uopool/src/uopool.rs index a767a43d..6fd950df 100644 --- a/crates/uopool/src/uopool.rs +++ b/crates/uopool/src/uopool.rs @@ -20,8 +20,10 @@ use silius_contracts::{ EntryPoint, }; use silius_primitives::{ + consts::reputation::THROTTLED_ENTITY_BUNDLE_COUNT, get_address, - reputation::{ReputationEntry, Status, THROTTLED_MAX_INCLUDE}, + reputation::{ReputationEntry, ReputationError, StakeInfo, StakeInfoResponse, Status}, + sanity::SanityCheckError, simulation::{CodeHash, SimulationCheckError}, uopool::{AddError, ValidationError}, Chain, UserOperation, UserOperationByHash, UserOperationGasEstimation, UserOperationHash, @@ -34,8 +36,6 @@ use tracing::trace; pub type VecUo = Vec; pub type VecCh = Vec; -const LATEST_SCAN_DEPTH: u64 = 1000; - /// The alternative mempool pool implementation that provides functionalities to add, remove, validate, and serves data requests from the [RPC API](EthApiServer). /// Architecturally, the [UoPool](UoPool) is the backend service managed by the [UoPoolService](UoPoolService) and serves requests from the [RPC API](EthApiServer). pub struct UoPool, P, R, E> @@ -134,6 +134,22 @@ where self.reputation.set_entities(reputation) } + /// Batch clears the [Mempool](Mempool). + /// + /// # Returns + /// `()` - Returns nothing + pub fn clear_mempool(&mut self) { + self.mempool.clear(); + } + + /// Batch clears the [Reputation](Reputation). + /// + /// # Returns + /// `()` - Returns nothing + pub fn clear_reputation(&mut self) { + self.reputation.clear(); + } + /// Batch clears the [Mempool](Mempool) and [Reputation](Reputation). /// /// # Returns @@ -179,8 +195,21 @@ where pub async fn add_user_operation( &mut self, uo: UserOperation, - res: UserOperationValidationOutcome, + res: Result, ) -> Result { + let res = match res { + Ok(res) => res, + Err(err) => { + if let ValidationError::Sanity(SanityCheckError::Reputation( + ReputationError::EntityBanned { address, entity: _ }, + )) = err.clone() + { + self.remove_user_operation_by_entity(&address); + } + return Err(AddError::Verification(err)); + } + }; + if let Some(uo_hash) = res.prev_hash { self.remove_user_operation(&uo_hash); } @@ -297,10 +326,10 @@ where })?; continue; } - (Status::THROTTLED, _) if p_c > THROTTLED_MAX_INCLUDE => { + (Status::THROTTLED, _) if p_c > THROTTLED_ENTITY_BUNDLE_COUNT => { continue; } - (_, Status::THROTTLED) if f_c > THROTTLED_MAX_INCLUDE => { + (_, Status::THROTTLED) if f_c > THROTTLED_ENTITY_BUNDLE_COUNT => { continue; } _ => (), @@ -598,6 +627,11 @@ where None } + pub fn remove_user_operation_by_entity(&mut self, entity: &Address) -> Option<()> { + self.mempool.remove_by_entity(entity).ok(); + None + } + /// Removes multiple [UserOperations](UserOperation) from the [UserOperationQueue](UserOperationQueue) given an array of [UserOperationHash](UserOperationHash). /// /// # Arguments @@ -610,4 +644,24 @@ where self.remove_user_operation(&uo_hash); } } + + /// Gets the [StakeInfoResponse](StakeInfoResponse) for entity + /// + /// # Arguments + /// * `addr` - The address of the entity. + /// + /// # Returns + /// `Result` - Stake info of the entity. + pub async fn get_stake_info(&self, addr: &Address) -> eyre::Result { + let info = self.entry_point.get_deposit_info(addr).await?; + let stake_info = StakeInfo { + address: *addr, + stake: U256::from(info.stake), + unstake_delay: U256::from(info.unstake_delay_sec), + }; + Ok(StakeInfoResponse { + stake_info, + is_staked: self.reputation.verify_stake("", Some(stake_info)).is_ok(), + }) + } } diff --git a/crates/uopool/src/utils.rs b/crates/uopool/src/utils.rs index 7afbcc09..bd3177b6 100644 --- a/crates/uopool/src/utils.rs +++ b/crates/uopool/src/utils.rs @@ -162,9 +162,8 @@ pub mod tests { use crate::{mempool::Mempool, Reputation}; use ethers::types::{Address, Bytes, H256, U256}; use silius_primitives::{ - reputation::{ - ReputationEntry, Status, BAN_SLACK, MIN_INCLUSION_RATE_DENOMINATOR, THROTTLING_SLACK, - }, + consts::reputation::{BAN_SLACK, MIN_INCLUSION_RATE_DENOMINATOR, THROTTLING_SLACK}, + reputation::{ReputationEntry, Status}, UserOperation, UserOperationHash, }; use std::fmt::Debug; diff --git a/crates/uopool/src/validate/sanity/entities.rs b/crates/uopool/src/validate/sanity/entities.rs new file mode 100644 index 00000000..0e47925f --- /dev/null +++ b/crates/uopool/src/validate/sanity/entities.rs @@ -0,0 +1,130 @@ +use crate::{ + mempool::Mempool, + reputation::Reputation as Rep, + uopool::{VecCh, VecUo}, + validate::{SanityCheck, SanityHelper}, +}; +use ethers::{providers::Middleware, types::Address}; +use silius_primitives::{ + consts::{ + entities::{FACTORY, PAYMASTER, SENDER}, + reputation::THROTTLED_ENTITY_MEMPOOL_COUNT, + }, + reputation::{ReputationEntry, ReputationError, Status}, + sanity::SanityCheckError, + UserOperation, +}; +use std::fmt::Debug; + +pub struct Entities; + +impl Entities { + /// Gets the status for entity. + fn get_status( + &self, + addr: &Address, + helper: &SanityHelper, + ) -> Result + where + P: Mempool + Send + Sync, + R: Rep, Error = E> + Send + Sync, + E: Debug, + { + Ok(Status::from(helper.reputation.get_status(addr).map_err( + |_| SanityCheckError::UnknownError { + message: "Failed to retrieve reputation status".into(), + }, + )?)) + } + + /// [SREP-020]: A BANNED address is not allowed into the mempool. + fn check_banned( + &self, + entity: &str, + addr: &Address, + status: &Status, + ) -> Result<(), SanityCheckError> { + if *status == Status::BANNED { + return Err(ReputationError::EntityBanned { + entity: entity.to_string(), + address: *addr, + } + .into()); + } + + Ok(()) + } + + /// [SREP-030]: A THROTTLED address is limited to THROTTLED_ENTITY_MEMPOOL_COUNT entries in the mempool. + fn check_throttled( + &self, + entity: &str, + addr: &Address, + status: &Status, + helper: &SanityHelper, + ) -> Result<(), SanityCheckError> + where + P: Mempool + Send + Sync, + R: Rep, Error = E> + Send + Sync, + E: Debug, + { + if *status == Status::THROTTLED + && (helper.mempool.get_number_by_sender(addr) + + helper.mempool.get_number_by_entity(addr)) + >= THROTTLED_ENTITY_MEMPOOL_COUNT + { + return Err(ReputationError::ThrottledLimit { + entity: entity.to_string(), + address: *addr, + } + .into()); + } + + Ok(()) + } +} + +#[async_trait::async_trait] +impl SanityCheck for Entities +where + P: Mempool + Send + Sync, + R: Rep, Error = E> + Send + Sync, + E: Debug, +{ + /// The [check_user_operation] method implementation that performs the sanity check for the staked entities. + /// + /// # Arguments + /// `uo` - The user operation to be checked. + /// `helper` - The [sanity check helper](SanityHelper) that contains the necessary data to perform the sanity check. + /// + /// # Returns + /// None if the sanity check is successful, otherwise a [SanityCheckError] is returned. + async fn check_user_operation( + &self, + uo: &UserOperation, + helper: &SanityHelper, + ) -> Result<(), SanityCheckError> { + let (sender, factory, paymaster) = uo.get_entities(); + + // sender + let status = self.get_status(&sender, helper)?; + self.check_banned(SENDER, &sender, &status)?; + self.check_throttled(SENDER, &sender, &status, helper)?; + + // factory + if let Some(factory) = factory { + let status = self.get_status(&factory, helper)?; + self.check_banned(FACTORY, &factory, &status)?; + self.check_throttled(FACTORY, &factory, &status, helper)?; + } + + // paymaster + if let Some(paymaster) = paymaster { + let status = self.get_status(&paymaster, helper)?; + self.check_banned(PAYMASTER, &paymaster, &status)?; + self.check_throttled(PAYMASTER, &paymaster, &status, helper)?; + } + + Ok(()) + } +} diff --git a/crates/uopool/src/validate/sanity/mod.rs b/crates/uopool/src/validate/sanity/mod.rs index 6a1a631e..14b3e064 100644 --- a/crates/uopool/src/validate/sanity/mod.rs +++ b/crates/uopool/src/validate/sanity/mod.rs @@ -1,7 +1,8 @@ //! Sanity module performs call gas limit, verification gas limit, max priority fee, paymaster verification, sender vericiation, and UserOperation type checks pub mod call_gas; +pub mod entities; pub mod max_fee; pub mod paymaster; pub mod sender; -pub mod sender_uos; +pub mod unstaked_entities; pub mod verification_gas; diff --git a/crates/uopool/src/validate/sanity/paymaster.rs b/crates/uopool/src/validate/sanity/paymaster.rs index e96493b6..b35f5c42 100644 --- a/crates/uopool/src/validate/sanity/paymaster.rs +++ b/crates/uopool/src/validate/sanity/paymaster.rs @@ -6,10 +6,7 @@ use crate::{ }; use ethers::{providers::Middleware, types::U256}; use silius_primitives::{ - get_address, - reputation::{ReputationEntry, Status}, - sanity::SanityCheckError, - UserOperation, + get_address, reputation::ReputationEntry, sanity::SanityCheckError, UserOperation, }; use std::fmt::Debug; @@ -49,14 +46,7 @@ where message: "Couldn't retrieve deposit info from entry point".into(), })?; - if U256::from(deposit_info.deposit) >= uo.max_fee_per_gas - && Status::from(helper.reputation.get_status(&addr).map_err(|_| { - SanityCheckError::UnknownError { - message: "Failed to retrieve reputation status for paymaster" - .into(), - } - })?) != Status::BANNED - { + if U256::from(deposit_info.deposit) >= uo.max_fee_per_gas { return Ok(()); } } diff --git a/crates/uopool/src/validate/sanity/sender.rs b/crates/uopool/src/validate/sanity/sender.rs index 094d9277..5669414e 100644 --- a/crates/uopool/src/validate/sanity/sender.rs +++ b/crates/uopool/src/validate/sanity/sender.rs @@ -1,21 +1,27 @@ use crate::{ mempool::Mempool, uopool::{VecCh, VecUo}, + utils::calculate_valid_gas, validate::{SanityCheck, SanityHelper}, Reputation, }; use ethers::providers::Middleware; -use silius_primitives::{reputation::ReputationEntry, sanity::SanityCheckError, UserOperation}; +use silius_primitives::{ + consts::uopool::GAS_INCREASE_PERC, reputation::ReputationEntry, sanity::SanityCheckError, + UserOperation, +}; +use std::fmt::Debug; -pub struct SenderOrInitCode; +pub struct Sender; #[async_trait::async_trait] -impl SanityCheck for SenderOrInitCode +impl SanityCheck for Sender where P: Mempool + Send + Sync, R: Reputation, Error = E> + Send + Sync, + E: Debug, { - /// The [check_user_operation] method implementation that performs the check whether the [UserOperation](UserOperation) is a deployment or a transaction. + /// The [check_user_operation] method implementation that performs the check for the sender of the [UserOperation](UserOperation). /// /// # Arguments /// `uo` - The [UserOperation](UserOperation) to be checked. @@ -33,6 +39,8 @@ where .eth_client() .get_code(uo.sender, None) .await?; + + // check if sender or init code if (code.is_empty() && uo.init_code.is_empty()) || (!code.is_empty() && !uo.init_code.is_empty()) { @@ -41,6 +49,35 @@ where init_code: uo.init_code.clone(), }); } + + // check if prev user operation exists + if helper.mempool.get_number_by_sender(&uo.sender) == 0 { + return Ok(()); + } + + let uo_prev = helper + .mempool + .get_all_by_sender(&uo.sender) + .iter() + .find(|uo_prev| uo_prev.nonce == uo.nonce) + .cloned(); + + if let Some(uo_prev) = uo_prev { + if uo.max_fee_per_gas + < calculate_valid_gas(uo_prev.max_fee_per_gas, GAS_INCREASE_PERC.into()) + || uo.max_priority_fee_per_gas + < calculate_valid_gas( + uo_prev.max_priority_fee_per_gas, + GAS_INCREASE_PERC.into(), + ) + { + return Err(SanityCheckError::SenderVerification { + sender: uo.sender, + message: "couldn't replace user operation (gas increase too low)".into(), + }); + } + } + Ok(()) } } diff --git a/crates/uopool/src/validate/sanity/sender_uos.rs b/crates/uopool/src/validate/sanity/sender_uos.rs deleted file mode 100644 index a9726bd1..00000000 --- a/crates/uopool/src/validate/sanity/sender_uos.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::{ - mempool::Mempool, - uopool::{VecCh, VecUo}, - utils::calculate_valid_gas, - validate::{SanityCheck, SanityHelper}, - Reputation, -}; -use ethers::{providers::Middleware, types::U256}; -use silius_primitives::{ - consts::entities::ACCOUNT, - reputation::{ReputationEntry, StakeInfo}, - sanity::SanityCheckError, - UserOperation, -}; -use std::fmt::Debug; - -pub struct SenderUos { - pub max_uos_per_unstaked_sender: usize, - pub gas_increase_perc: U256, -} - -#[async_trait::async_trait] -impl SanityCheck for SenderUos -where - P: Mempool + Send + Sync, - R: Reputation, Error = E> + Send + Sync, - E: Debug, -{ - /// The [check_user_operation] method implementation that performs the sanity check on the [UserOperation](UserOperation) sender. - /// - /// # Arguments - /// `uo` - The [UserOperation](UserOperation) to be checked. - /// `helper` - The [sanity check helper](SanityHelper) that contains the necessary data to perform the sanity check. - /// - /// # Returns - /// Nothing if the sanity check is successful, otherwise a [SanityCheckError](SanityCheckError) is returned. - async fn check_user_operation( - &self, - uo: &UserOperation, - helper: &SanityHelper, - ) -> Result<(), SanityCheckError> { - if helper.mempool.get_number_by_sender(&uo.sender) == 0 { - return Ok(()); - } - - let uo_prev = helper - .mempool - .get_all_by_sender(&uo.sender) - .iter() - .find(|uo_prev| uo_prev.nonce == uo.nonce) - .cloned(); - - match uo_prev { - Some(uo_prev) => { - if uo.max_fee_per_gas - >= calculate_valid_gas(uo_prev.max_fee_per_gas, self.gas_increase_perc) - && uo.max_priority_fee_per_gas - >= calculate_valid_gas( - uo_prev.max_priority_fee_per_gas, - self.gas_increase_perc, - ) - { - return Ok(()); - } else { - Err(SanityCheckError::SenderVerification { - sender: uo.sender, - message: "couldn't replace user operation (gas increase too low)".into(), - }) - } - } - None => { - if helper.mempool.get_number_by_sender(&uo.sender) - >= self.max_uos_per_unstaked_sender - { - let info = helper - .entry_point - .get_deposit_info(&uo.sender) - .await - .map_err(|_| SanityCheckError::UnknownError { - message: "Couldn't retrieve deposit info from entry point".to_string(), - })?; - match helper.reputation.verify_stake( - ACCOUNT, - Some(StakeInfo { - address: uo.sender, - stake: U256::from(info.stake), - unstake_delay: U256::from(info.unstake_delay_sec), - }), - ) { - Ok(_) => {} - Err(_) => { - return Err(SanityCheckError::SenderVerification { - sender: uo.sender, - message: "has too many user operations in the mempool".into(), - }); - } - } - } - - Ok(()) - } - } - } -} diff --git a/crates/uopool/src/validate/sanity/unstaked_entities.rs b/crates/uopool/src/validate/sanity/unstaked_entities.rs new file mode 100644 index 00000000..7c68a1ac --- /dev/null +++ b/crates/uopool/src/validate/sanity/unstaked_entities.rs @@ -0,0 +1,204 @@ +use crate::{ + mempool::Mempool, + reputation::Reputation as Rep, + uopool::{VecCh, VecUo}, + validate::{SanityCheck, SanityHelper}, +}; +use ethers::{ + providers::Middleware, + types::{Address, U256}, +}; +use silius_primitives::{ + consts::{ + entities::{FACTORY, PAYMASTER, SENDER}, + reputation::{ + INCLUSION_RATE_FACTOR, SAME_SENDER_MEMPOOL_COUNT, SAME_UNSTAKED_ENTITY_MEMPOOL_COUNT, + }, + }, + reputation::{ReputationEntry, ReputationError, StakeInfo}, + sanity::SanityCheckError, + UserOperation, +}; +use std::{cmp, fmt::Debug}; + +pub struct UnstakedEntities; + +impl UnstakedEntities { + /// Gets the deposit info for entity. + async fn get_stake<'a, M: Middleware, P, R, E>( + &self, + addr: &Address, + helper: &SanityHelper<'a, M, P, R, E>, + ) -> Result + where + P: Mempool + Send + Sync, + R: Rep, Error = E> + Send + Sync, + E: Debug, + { + let info = helper + .entry_point + .get_deposit_info(addr) + .await + .map_err(|_| SanityCheckError::UnknownError { + message: "Couldn't retrieve deposit info from entry point".to_string(), + })?; + + Ok(StakeInfo { + address: *addr, + stake: U256::from(info.stake), + unstake_delay: U256::from(info.unstake_delay_sec), + }) + } + + /// Gets the reputation entry for entity. + fn get_entity( + &self, + addr: &Address, + helper: &SanityHelper, + ) -> Result + where + P: Mempool + Send + Sync, + R: Rep, Error = E> + Send + Sync, + E: Debug, + { + helper + .reputation + .get(addr) + .map_err(|_| SanityCheckError::UnknownError { + message: "Failed to retrieve reputation entry".into(), + }) + } + + /// Calculates allowed number of user operations + fn calculate_allowed_user_operations(entity: ReputationEntry) -> u64 { + if entity.uo_seen == 0 { + SAME_UNSTAKED_ENTITY_MEMPOOL_COUNT as u64 + } else { + SAME_UNSTAKED_ENTITY_MEMPOOL_COUNT as u64 + + ((entity.uo_included as f64 / entity.uo_seen as f64) + * INCLUSION_RATE_FACTOR as f64) as u64 + + cmp::min(entity.uo_included, 10000) + } + } +} + +#[async_trait::async_trait] +impl SanityCheck for UnstakedEntities +where + P: Mempool + Send + Sync, + R: Rep, Error = E> + Send + Sync, + E: Debug, +{ + /// The [check_user_operation] method implementation that performs the sanity check for the unstaked entities. + /// + /// # Arguments + /// `uo` - The user operation to be checked. + /// `helper` - The [sanity check helper](SanityHelper) that contains the necessary data to perform the sanity check. + /// + /// # Returns + /// None if the sanity check is successful, otherwise a [SanityCheckError] is returned. + async fn check_user_operation( + &self, + uo: &UserOperation, + helper: &SanityHelper, + ) -> Result<(), SanityCheckError> { + let (sender, factory, paymaster) = uo.get_entities(); + + // sender + // [STO-040] + if helper.mempool.get_number_by_entity(&sender) > 0 { + return Err(SanityCheckError::EntityVerification { + entity: SENDER.to_string(), + address: sender, + message: + "is used as a different entity in another UserOperation currently in mempool" + .to_string(), + }); + } + + let sender_stake = self.get_stake(&sender, helper).await?; + if helper + .reputation + .verify_stake(SENDER, Some(sender_stake)) + .is_err() + { + // [UREP-010] + if helper.mempool.get_number_by_sender(&uo.sender) >= SAME_SENDER_MEMPOOL_COUNT { + return Err(ReputationError::UnstakedEntityVerification { + entity: SENDER.to_string(), + address: uo.sender, + message: "has too many user operations in the mempool".into(), + } + .into()); + } + } + + // factory + if let Some(factory) = factory { + // [STO-040] + if helper.mempool.get_number_by_sender(&factory) > 0 { + return Err(SanityCheckError::EntityVerification { + entity: FACTORY.to_string(), + address: factory, + message: + "is used as a sender entity in another UserOperation currently in mempool" + .to_string(), + }); + } + + let factory_stake = self.get_stake(&factory, helper).await?; + if helper + .reputation + .verify_stake(FACTORY, Some(factory_stake)) + .is_err() + { + // [UREP-020] + let entity = self.get_entity(&factory, helper)?; + let uos_allowed = Self::calculate_allowed_user_operations(entity); + if helper.mempool.get_number_by_entity(&factory) as u64 >= uos_allowed { + return Err(ReputationError::UnstakedEntityVerification { + entity: FACTORY.to_string(), + address: factory, + message: "has too many user operations in the mempool".into(), + } + .into()); + } + } + } + + // paymaster + if let Some(paymaster) = paymaster { + // [STO-040] + if helper.mempool.get_number_by_sender(&paymaster) > 0 { + return Err(SanityCheckError::EntityVerification { + entity: PAYMASTER.to_string(), + address: paymaster, + message: + "is used as a sender entity in another UserOperation currently in mempool" + .to_string(), + }); + } + + let paymaster_stake = self.get_stake(&paymaster, helper).await?; + if helper + .reputation + .verify_stake(PAYMASTER, Some(paymaster_stake)) + .is_err() + { + // [UREP-020] + let entity = self.get_entity(&paymaster, helper)?; + let uos_allowed = Self::calculate_allowed_user_operations(entity); + if helper.mempool.get_number_by_entity(&paymaster) as u64 >= uos_allowed { + return Err(ReputationError::UnstakedEntityVerification { + entity: PAYMASTER.to_string(), + address: paymaster, + message: "has too many user operations in the mempool".into(), + } + .into()); + } + } + } + + Ok(()) + } +} diff --git a/crates/uopool/src/validate/simulation_trace/call_stack.rs b/crates/uopool/src/validate/simulation_trace/call_stack.rs index f91c3dff..6cedd97e 100644 --- a/crates/uopool/src/validate/simulation_trace/call_stack.rs +++ b/crates/uopool/src/validate/simulation_trace/call_stack.rs @@ -156,6 +156,8 @@ where })?; let context = validate_paymaster_return.context; + // [EREP-050] + // This will be removed in the future if !context.is_empty() && helper .reputation diff --git a/crates/uopool/src/validate/validator.rs b/crates/uopool/src/validate/validator.rs index 352c183f..2b5deb48 100644 --- a/crates/uopool/src/validate/validator.rs +++ b/crates/uopool/src/validate/validator.rs @@ -1,7 +1,7 @@ use super::{ sanity::{ - call_gas::CallGas, max_fee::MaxFee, paymaster::Paymaster, sender::SenderOrInitCode, - sender_uos::SenderUos, verification_gas::VerificationGas, + call_gas::CallGas, entities::Entities, max_fee::MaxFee, paymaster::Paymaster, + sender::Sender, unstaked_entities::UnstakedEntities, verification_gas::VerificationGas, }, simulation::{signature::Signature, timestamp::Timestamp}, simulation_trace::{ @@ -17,7 +17,7 @@ use crate::{ mempool::{Mempool, MempoolBox}, reputation::ReputationBox, uopool::{VecCh, VecUo}, - Reputation, + Reputation as Rep, }; use enumset::EnumSet; use ethers::{ @@ -39,7 +39,7 @@ use std::fmt::{Debug, Display}; pub struct StandardUserOperationValidator where P: Mempool + Send + Sync, - R: Reputation, Error = E> + Send + Sync, + R: Rep, Error = E> + Send + Sync, { /// The [EntryPoint](EntryPoint) object. entry_point: EntryPoint, @@ -56,7 +56,7 @@ where impl StandardUserOperationValidator where P: Mempool + Send + Sync, - R: Reputation, Error = E> + Send + Sync, + R: Rep, Error = E> + Send + Sync, E: Debug + Display, { /// Creates a new [StandardUserOperationValidator](StandardUserOperationValidator). @@ -85,7 +85,7 @@ where /// `chain` - A [EIP-155](https://eips.ethereum.org/EIPS/eip-155) chain ID. /// `max_verification_gas` - max verification gas that bundler would accept for one user operation /// `min_priority_fee_per_gas` - min priority fee per gas that bundler would accept for one user operation - /// `max_uos_per_unstaked_sender` - max user operations that bundler would accept from one sender + /// `max_uos_per_sender` - max user operations that bundler would accept from one sender /// `gas_increase_perc` - gas increase percentage that bundler would accept for overwriting one user operation /// /// # Returns @@ -95,23 +95,19 @@ where chain: Chain, max_verification_gas: U256, min_priority_fee_per_gas: U256, - max_uos_per_unstaked_sender: usize, - gas_increase_perc: U256, ) -> Self { Self::new(entry_point.clone(), chain) - .with_sanity_check(SenderOrInitCode) + .with_sanity_check(Sender) .with_sanity_check(VerificationGas { max_verification_gas, }) - .with_sanity_check(Paymaster) .with_sanity_check(CallGas) .with_sanity_check(MaxFee { min_priority_fee_per_gas, }) - .with_sanity_check(SenderUos { - max_uos_per_unstaked_sender, - gas_increase_perc, - }) + .with_sanity_check(Paymaster) + .with_sanity_check(Entities) + .with_sanity_check(UnstakedEntities) .with_simulation_check(Signature) .with_simulation_check(Timestamp) .with_simulation_trace_check(Gas) @@ -133,7 +129,7 @@ where /// `chain` - A [EIP-155](https://eips.ethereum.org/EIPS/eip-155) chain ID. /// `max_verification_gas` - max verification gas that bundler would accept for one user operation /// `min_priority_fee_per_gas` - min priority fee per gas that bundler would accept for one user operation - /// `max_uos_per_unstaked_sender` - max user operations that bundler would accept from one sender + /// `max_uos_per_sender` - max user operations that bundler would accept from one sender /// `gas_increase_perc` - gas increase percentage that bundler would accept for overwriting one user operation /// /// # Returns @@ -143,23 +139,19 @@ where chain: Chain, max_verification_gas: U256, min_priority_fee_per_gas: U256, - max_uos_per_unstaked_sender: usize, - gas_increase_perc: U256, ) -> Self { Self::new(entry_point.clone(), chain) - .with_sanity_check(SenderOrInitCode) + .with_sanity_check(Sender) .with_sanity_check(VerificationGas { max_verification_gas, }) - .with_sanity_check(Paymaster) .with_sanity_check(CallGas) .with_sanity_check(MaxFee { min_priority_fee_per_gas, }) - .with_sanity_check(SenderUos { - max_uos_per_unstaked_sender, - gas_increase_perc, - }) + .with_sanity_check(Paymaster) + .with_sanity_check(Entities) + .with_sanity_check(UnstakedEntities) } /// Simulates validation of a [UserOperation](UserOperation) via the [simulate_validation](crate::entry_point::EntryPoint::simulate_validation) method of the [entry_point](crate::entry_point::EntryPoint). @@ -259,7 +251,7 @@ impl UserOperationValidator for StandardUserOperationValidator where P: Mempool + Send + Sync, - R: Reputation, Error = E> + Send + Sync, + R: Rep, Error = E> + Send + Sync, E: Debug + Display, { /// Validates a [UserOperation](UserOperation) via the [simulate_validation](crate::entry_point::EntryPoint::simulate_validation) method of the [entry_point](crate::entry_point::EntryPoint). diff --git a/docker-compose.yml b/docker-compose.yml index 3c959be4..a746860c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,5 +33,5 @@ services: - "3001" - --ws.api - eth,debug,web3 - -- eth-client-proxy-address - -- http://127.0.0.1:8545 + - --eth-client-proxy-address + - http://127.0.0.1:8545 diff --git a/tests/src/simulation_tests.rs b/tests/src/simulation_tests.rs index 12ee8822..1f945a63 100644 --- a/tests/src/simulation_tests.rs +++ b/tests/src/simulation_tests.rs @@ -20,16 +20,17 @@ use ethers::{ types::{Bytes, U256}, }; use silius_contracts::EntryPoint; -use silius_primitives::consts::entities::{ACCOUNT, FACTORY, PAYMASTER}; +use silius_primitives::consts::entities::{FACTORY, PAYMASTER, SENDER}; use silius_primitives::reputation::ReputationEntry; use silius_primitives::simulation::SimulationCheckError; use silius_primitives::uopool::ValidationError; use silius_primitives::{Chain, UserOperation}; use silius_uopool::validate::sanity::call_gas::CallGas; +use silius_uopool::validate::sanity::entities::Entities; use silius_uopool::validate::sanity::max_fee::MaxFee; use silius_uopool::validate::sanity::paymaster::Paymaster; -use silius_uopool::validate::sanity::sender::SenderOrInitCode; -use silius_uopool::validate::sanity::sender_uos::SenderUos; +use silius_uopool::validate::sanity::sender::Sender; +use silius_uopool::validate::sanity::unstaked_entities::UnstakedEntities; use silius_uopool::validate::sanity::verification_gas::VerificationGas; use silius_uopool::validate::simulation::signature::Signature; use silius_uopool::validate::simulation::timestamp::Timestamp; @@ -43,23 +44,20 @@ use silius_uopool::validate::{ UserOperationValidationOutcome, UserOperationValidator, UserOperationValidatorMode, }; use silius_uopool::{ - mempool_id, MemoryMempool, MemoryReputation, Mempool, MempoolBox, Reputation, ReputationBox, - UoPool, VecCh, VecUo, + mempool_id, MemoryMempool, MemoryReputation, Mempool, MempoolBox, Reputation as Rep, + ReputationBox, UoPool, VecCh, VecUo, }; use std::collections::HashMap; use std::fmt::Debug; use std::ops::Deref; use std::sync::Arc; -const MAX_UOS_PER_UNSTAKED_SENDER: usize = 4; -const GAS_INCREASE_PERC: u64 = 10; - struct TestContext where M: Middleware + 'static, V: UserOperationValidator + 'static, P: Mempool + Send + Sync, - R: Reputation, Error = E> + Send + Sync, + R: Rep, Error = E> + Send + Sync, E: Debug, { pub client: Arc, @@ -128,19 +126,17 @@ async fn setup() -> eyre::Result { let c = Chain::from(chain_id); let validator = StandardUserOperationValidator::new(entry_point2, c.clone()) - .with_sanity_check(SenderOrInitCode {}) + .with_sanity_check(Sender) .with_sanity_check(VerificationGas { max_verification_gas: U256::from(3000000_u64), }) - .with_sanity_check(Paymaster {}) - .with_sanity_check(CallGas {}) + .with_sanity_check(CallGas) .with_sanity_check(MaxFee { min_priority_fee_per_gas: U256::from(1u64), }) - .with_sanity_check(SenderUos { - max_uos_per_unstaked_sender: MAX_UOS_PER_UNSTAKED_SENDER, - gas_increase_perc: GAS_INCREASE_PERC.into(), - }) + .with_sanity_check(Paymaster) + .with_sanity_check(Entities) + .with_sanity_check(UnstakedEntities) .with_simulation_check(Signature) .with_simulation_check(Timestamp) .with_simulation_trace_check(Gas) @@ -449,7 +445,7 @@ async fn fail_with_bad_opcode_in_validation() -> eyre::Result<()> { .await; assert!(matches!( res, - Err(ValidationError::Simulation(SimulationCheckError::Opcode { entity, opcode })) if entity==ACCOUNT && opcode == "BLOCKHASH" + Err(ValidationError::Simulation(SimulationCheckError::Opcode { entity, opcode })) if entity==SENDER && opcode == "BLOCKHASH" )); Ok(()) @@ -473,7 +469,7 @@ async fn fail_if_create_too_many() -> eyre::Result<()> { .await; assert!(matches!( res, - Err(ValidationError::Simulation(SimulationCheckError::Opcode { entity, opcode })) if entity==ACCOUNT && opcode == "CREATE2" + Err(ValidationError::Simulation(SimulationCheckError::Opcode { entity, opcode })) if entity==SENDER && opcode == "CREATE2" )); Ok(())