diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..0f096eff0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,117 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Rundler is a high-performance, modular Rust implementation of an ERC-4337 bundler for Account Abstraction. It's built by Alchemy and designed for cloud-scale deployments with a focus on reliability and performance. + +## Development Commands + +### Building and Testing +```bash +# Build the project +make build +cargo build --all --all-features + +# Run unit tests +make test-unit +cargo nextest run --locked --workspace --all-features --no-fail-fast + +# Run all tests (unit + spec tests) +make test + +# Run ERC-4337 spec tests (v0.6 and v0.7) +make test-spec-integrated +make test-spec-integrated-v0_6 # v0.6 only +make test-spec-integrated-v0_7 # v0.7 only + +# Run spec tests in modular mode +make test-spec-modular +``` + +### Code Quality +```bash +# Format code (requires nightly Rust) +make fmt +cargo +nightly fmt + +# Lint code +make lint +cargo clippy --all --all-features --tests -- -D warnings + +# Clean build artifacts +make clean +cargo clean +``` + +### Running Locally +```bash +# Run full node (RPC + Pool + Builder in single process) +cargo run node + +# Run individual components in distributed mode +cargo run rpc # RPC server only +cargo run pool # Pool server only +cargo run builder # Builder server only +``` + +## Architecture + +Rundler consists of 3 main modular components: + +1. **RPC Server** (`crates/rpc/`): Implements ERC-4337 RPC methods (`eth_*`, `debug_*`, `rundler_*` namespaces) +2. **Pool** (`crates/pool/`): User Operation mempool with validation, simulation, and chain reorg handling +3. **Builder** (`crates/builder/`): Bundle construction, transaction submission, and mining monitoring + +### Communication Patterns +- **RPC → Pool**: Submits user operations via `eth_sendUserOperation` +- **RPC → Builder**: Debug namespace for manual bundling control +- **Builder ↔ Pool**: Bundle coordination and operation status updates + +### Key Supporting Crates +- `crates/sim/`: Gas estimation and operation simulation +- `crates/provider/`: Ethereum provider abstractions with Alloy +- `crates/types/`: Core type definitions +- `crates/contracts/`: Smart contract bindings and utilities +- `crates/signer/`: Transaction signing (local keys, AWS KMS) + +## Configuration + +### Environment Variables +Most CLI options can be set via environment variables. Key ones: +- `NODE_HTTP`: Ethereum RPC endpoint (required) +- `NETWORK`: Predefined network (dev, ethereum, optimism, etc.) +- `CHAIN_SPEC`: Path to custom chain specification TOML +- `RUST_LOG`: Log level control + +### Chain Specifications +Chain configs are in `bin/rundler/chain_specs/` with network-specific settings for gas estimation, fee calculation, and protocol parameters. + +## Entry Point Support + +- **v0.6**: ERC-4337 v0.6 specification (can be disabled with `--disable_entry_point_v0_6`) +- **v0.7**: ERC-4337 v0.7 specification (can be disabled with `--disable_entry_point_v0_7`) + +Both versions are supported simultaneously by default. + +## Prerequisites + +- Rust 1.87+ with nightly for formatting +- Docker (for spec tests) +- PDM (Python dependency manager for spec tests) +- Protobuf compiler (protoc) +- Buf (protobuf linting) +- Foundry ^0.3.0 (contract compilation) + +## Testing Setup + +For spec tests, first install frameworks: +```bash +cd test/spec-tests/v0_6/bundler-spec-tests && pdm install && pdm run update-deps +cd test/spec-tests/v0_7/bundler-spec-tests && pdm install && pdm run update-deps +``` + +## Workspace Structure + +This is a Cargo workspace with the main binary in `bin/rundler/` and library crates in `crates/`. The architecture is designed for both monolithic and distributed deployment modes. \ No newline at end of file diff --git a/bin/rundler/src/cli/builder.rs b/bin/rundler/src/cli/builder.rs index 821311af0..1d59ceaea 100644 --- a/bin/rundler/src/cli/builder.rs +++ b/bin/rundler/src/cli/builder.rs @@ -300,6 +300,7 @@ impl BuilderArgs { .verification_gas_limit_efficiency_reject_threshold, chain_spec, assigner_max_ops_per_request: self.assigner_max_ops_per_request, + revert_check_call_type: common.revert_check_call_type, }) } diff --git a/bin/rundler/src/cli/mod.rs b/bin/rundler/src/cli/mod.rs index 30f5d2e87..077699b88 100644 --- a/bin/rundler/src/cli/mod.rs +++ b/bin/rundler/src/cli/mod.rs @@ -44,7 +44,7 @@ use reth_tasks::TaskManager; use rpc::RpcCliArgs; use rundler_provider::{ AlloyEntryPointV0_6, AlloyEntryPointV0_7, AlloyEvmProvider, AlloyNetworkConfig, DAGasOracle, - DAGasOracleSync, EntryPointProvider, EvmProvider, FeeEstimator, Providers, + DAGasOracleSync, EntryPointProvider, EvmProvider, FeeEstimator, Providers, RevertCheckCallType, }; use rundler_sim::{ EstimationSettings, MempoolConfigs, PrecheckSettings, SimulationSettings, MIN_CALL_GAS_LIMIT, @@ -376,14 +376,6 @@ pub struct CommonArgs { )] pre_verification_gas_accept_percent: u32, - #[arg( - long = "execution_gas_limit_efficiency_reject_threshold", - name = "execution_gas_limit_efficiency_reject_threshold", - env = "EXECUTION_GAS_LIMIT_EFFICIENCY_REJECT_THRESHOLD", - default_value = "0.0" - )] - pub execution_gas_limit_efficiency_reject_threshold: f64, - #[arg( long = "verification_gas_limit_efficiency_reject_threshold", name = "verification_gas_limit_efficiency_reject_threshold", @@ -545,6 +537,23 @@ pub struct CommonArgs { value_parser = ValueParser::new(parse_key_val) )] pub aggregator_options: Vec<(String, String)>, + + #[arg( + long = "revert_check_call_type", + name = "revert_check_call_type", + env = "REVERT_CHECK_CALL_TYPE", + value_parser = ValueParser::new(parse_revert_check_call_type) + )] + pub revert_check_call_type: Option, +} + +fn parse_revert_check_call_type(s: &str) -> Result { + match s { + "eth_call" => Ok(RevertCheckCallType::EthCall), + "eth_simulateV1" => Ok(RevertCheckCallType::EthSimulateV1), + "debug_trace_call" => Ok(RevertCheckCallType::DebugTraceCall), + _ => Err(anyhow::anyhow!("invalid revert check mode: {}, must be one of eth_call, eth_simulate_v1, debug_trace_call", s)), + } } impl CommonArgs { diff --git a/bin/rundler/src/cli/pool.rs b/bin/rundler/src/cli/pool.rs index da2f92294..7ef6ef6c0 100644 --- a/bin/rundler/src/cli/pool.rs +++ b/bin/rundler/src/cli/pool.rs @@ -223,12 +223,11 @@ impl PoolArgs { reputation_tracking_enabled: self.reputation_tracking_enabled, drop_min_num_blocks: self.drop_min_num_blocks, da_gas_tracking_enabled, - execution_gas_limit_efficiency_reject_threshold: common - .execution_gas_limit_efficiency_reject_threshold, verification_gas_limit_efficiency_reject_threshold: common .verification_gas_limit_efficiency_reject_threshold, max_time_in_pool: self.max_time_in_pool_secs.map(Duration::from_secs), max_expected_storage_slots: common.max_expected_storage_slots.unwrap_or(usize::MAX), + revert_check_call_type: common.revert_check_call_type, }; let mut pool_configs = vec![]; diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index 9be07f6b8..edca55259 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -31,7 +31,8 @@ use metrics_derive::Metrics; use mockall::automock; use rundler_provider::{ BundleHandler, DAGasOracleSync, DAGasProvider, EntryPoint, EvmProvider, FeeEstimator, - HandleOpsOut, ProvidersWithEntryPointT, + HandleOpRevert, HandleOpsOut, ProvidersWithEntryPointT, RevertCheckCallType, + SimulationProvider, }; use rundler_sim::{SimulationError, SimulationResult, Simulator, ViolationError}; use rundler_types::{ @@ -158,6 +159,12 @@ pub(crate) struct Settings { pub(crate) max_expected_storage_slots: usize, pub(crate) verification_gas_limit_efficiency_reject_threshold: f64, pub(crate) submission_proxy: Option>, + pub(crate) revert_check_call_type: Option, +} + +enum SecondSimulationError { + SimulationError(SimulationError), + RevertCheckError(HandleOpRevert), } #[async_trait] @@ -548,29 +555,58 @@ where block_hash: B256, ) -> Option<( PoolOperationWithSponsoredDAGas, - Result, + Result, )> { let _timer_guard = CustomTimerGuard::new(self.metrics.op_simulation_ms.clone()); let op_hash = op.op.uo.hash(); - // Simulate - let result = self - .bundle_providers - .simulator() - .simulate_validation( - op.op.uo.clone().into(), - op.op.perms.trusted, - block_hash, - Some(op.op.expected_code_hash), + // Check if we need to run revert check + let should_revert_check = self.settings.revert_check_call_type.is_some() + && self.settings.chain_spec.monad_min_reserve_balance.is_some() + && op.op.sender_is_7702 + // Don't run revert check if checking for post op and the revert check is eth call, as that won't ever revert + || (op.op.uo.paymaster_post_op_gas_limit() > 0 && self.settings.revert_check_call_type != Some(RevertCheckCallType::EthCall)); + + // Run both simulations in parallel + let (revert_check_result, validation_result) = if should_revert_check { + tokio::join!( + self.ep_providers + .entry_point() + .simulate_handle_op_revert_check( + op.op.uo.clone().into(), + block_hash.into(), + self.settings.revert_check_call_type.unwrap(), + ), + self.bundle_providers.simulator().simulate_validation( + op.op.uo.clone().into(), + op.op.perms.trusted, + block_hash, + Some(op.op.expected_code_hash), + ) ) - .await; - let result = match result { + } else { + // Only run validation if no revert check needed + let validation = self + .bundle_providers + .simulator() + .simulate_validation( + op.op.uo.clone().into(), + op.op.perms.trusted, + block_hash, + Some(op.op.expected_code_hash), + ) + .await; + (Ok(Ok(0)), validation) + }; + + // Handle validation result + let result = match validation_result { Ok(success) => (op, Ok(success)), Err(error) => match error { SimulationError { violation_error: ViolationError::Violations(_), entity_infos: _, - } => (op, Err(error)), + } => return Some((op, Err(SecondSimulationError::SimulationError(error)))), SimulationError { violation_error: ViolationError::Other(error), entity_infos: _, @@ -587,6 +623,30 @@ where }, }; + // Handle revert check result + if should_revert_check { + match revert_check_result { + Ok(Ok(_)) => {} + // allow execution reverts through + Ok(Err(HandleOpRevert::ExecutionRevert { data: _, reason: _ })) => {} + Ok(Err(e)) => { + return Some((result.0, Err(SecondSimulationError::RevertCheckError(e)))) + } + Err(e) => { + self.emit(BuilderEvent::skipped_op( + self.builder_tag.clone(), + op_hash, + SkipReason::Other { + reason: Arc::new(format!( + "Failed to run revert check: {e:?}, skipping" + )), + }, + )); + return None; + } + } + } + Some(result) } @@ -596,7 +656,7 @@ where gas_price: u128, ops_with_simulations: Vec<( PoolOperationWithSponsoredDAGas, - Result, + Result, )>, mut balances_by_paymaster: HashMap, ) -> ProposalContext<::UO> { @@ -621,7 +681,7 @@ where let op = po.op.clone().uo; let simulation = match simulation { Ok(simulation) => simulation, - Err(error) => { + Err(SecondSimulationError::SimulationError(error)) => { self.emit(BuilderEvent::rejected_op( self.builder_tag.clone(), op.hash(), @@ -640,6 +700,15 @@ where } continue; } + Err(SecondSimulationError::RevertCheckError(e)) => { + self.emit(BuilderEvent::rejected_op( + self.builder_tag.clone(), + op.hash(), + OpRejectionReason::FailedRevertCheck { error: e }, + )); + context.rejected_ops.push((op.into(), po.op.entity_infos)); + continue; + } }; // filter time range @@ -4154,6 +4223,7 @@ mod tests { max_expected_storage_slots: MAX_EXPECTED_STORAGE_SLOTS, verification_gas_limit_efficiency_reject_threshold: 0.5, submission_proxy, + revert_check_call_type: None, }, event_sender, ); diff --git a/crates/builder/src/bundle_sender.rs b/crates/builder/src/bundle_sender.rs index 6a69e24b1..0715a2f5c 100644 --- a/crates/builder/src/bundle_sender.rs +++ b/crates/builder/src/bundle_sender.rs @@ -852,6 +852,10 @@ where async fn process_revert(&self, tx_hash: B256) -> anyhow::Result<()> { warn!("Bundle transaction {tx_hash:?} reverted onchain"); + if !self.chain_spec.rpc_debug_trace_transaction_enabled { + warn!("Debug trace transaction is not enabled, skipping trace"); + return Ok(()); + } let trace_options = GethDebugTracingOptions::new_tracer( GethDebugTracerType::BuiltInTracer(GethDebugBuiltInTracerType::CallTracer), diff --git a/crates/builder/src/emit.rs b/crates/builder/src/emit.rs index c12020427..8e32e4f20 100644 --- a/crates/builder/src/emit.rs +++ b/crates/builder/src/emit.rs @@ -14,7 +14,7 @@ use std::{fmt::Display, sync::Arc}; use alloy_primitives::{Address, B256, U256}; -use rundler_provider::TransactionRequest; +use rundler_provider::{HandleOpRevert, TransactionRequest}; use rundler_sim::SimulationError; use rundler_types::{GasFees, ValidTimeRange}; use rundler_utils::strs; @@ -194,6 +194,8 @@ pub enum SkipReason { pub enum OpRejectionReason { /// Operation failed its 2nd validation simulation attempt FailedRevalidation { error: SimulationError }, + /// Operation failed its revert check + FailedRevertCheck { error: HandleOpRevert }, /// Operation reverted during bundle formation simulation with message FailedInBundle { message: Arc }, /// Operation's storage slot condition was not met diff --git a/crates/builder/src/task.rs b/crates/builder/src/task.rs index 65e364687..44be2d1ff 100644 --- a/crates/builder/src/task.rs +++ b/crates/builder/src/task.rs @@ -22,6 +22,7 @@ use alloy_primitives::{Address, B256}; use anyhow::Context; use rundler_provider::{ AlloyNetworkConfig, EntryPoint, Providers as ProvidersT, ProvidersWithEntryPointT, + RevertCheckCallType, }; use rundler_signer::{SignerManager, SigningScheme}; use rundler_sim::{ @@ -91,6 +92,8 @@ pub struct Args { pub verification_gas_limit_efficiency_reject_threshold: f64, /// Maximum ops requested from mempool pub assigner_max_ops_per_request: u64, + /// Revert check call type + pub revert_check_call_type: Option, } /// Builder settings @@ -417,6 +420,7 @@ where .args .verification_gas_limit_efficiency_reject_threshold, submission_proxy: submission_proxy.cloned(), + revert_check_call_type: self.args.revert_check_call_type, }; let transaction_sender = self diff --git a/crates/contracts/src/v0_7.rs b/crates/contracts/src/v0_7.rs index 4e4c362ea..2860b0744 100644 --- a/crates/contracts/src/v0_7.rs +++ b/crates/contracts/src/v0_7.rs @@ -132,6 +132,8 @@ sol!( error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner); + error PostOpReverted(bytes returnData); + error SignatureValidationFailed(address aggregator); function handleOps( diff --git a/crates/pool/proto/op_pool/op_pool.proto b/crates/pool/proto/op_pool/op_pool.proto index c8a500256..9b71bb5fb 100644 --- a/crates/pool/proto/op_pool/op_pool.proto +++ b/crates/pool/proto/op_pool/op_pool.proto @@ -646,6 +646,8 @@ message MempoolError { UseUnsupportedEIP use_unsupported_eip = 20; AggregatorError aggregator = 21; Invalid7702AuthSignature invalid_7702_auth_signature = 22; + ExecutionRevert execution_revert = 23; + PostOpRevert post_op_revert = 24; } } @@ -721,6 +723,16 @@ message UseUnsupportedEIP { string eip_name = 1; } +message ExecutionRevert { + bytes data = 1; + string reason = 2; +} + +message PostOpRevert { + bytes data = 1; + string reason = 2; +} + // PRECHECK VIOLATIONS message PrecheckViolationError { oneof violation { diff --git a/crates/pool/src/mempool/mod.rs b/crates/pool/src/mempool/mod.rs index b4909055c..4ceb9b989 100644 --- a/crates/pool/src/mempool/mod.rs +++ b/crates/pool/src/mempool/mod.rs @@ -21,6 +21,7 @@ mod size; mod paymaster; pub(crate) use paymaster::{PaymasterConfig, PaymasterTracker}; +use rundler_provider::RevertCheckCallType; mod uo_pool; use std::{ @@ -173,16 +174,14 @@ pub struct PoolConfig { pub drop_min_num_blocks: u64, /// Reject user operations with gas limit efficiency below this threshold. /// Gas limit efficiency is defined as the ratio of the gas limit to the gas used. - /// This applies to the execution gas limit. - pub execution_gas_limit_efficiency_reject_threshold: f64, - /// Reject user operations with gas limit efficiency below this threshold. - /// Gas limit efficiency is defined as the ratio of the gas limit to the gas used. /// This applies to the verification gas limit. pub verification_gas_limit_efficiency_reject_threshold: f64, /// Maximum time a UO is allowed in the pool before being dropped pub max_time_in_pool: Option, /// The maximum number of storage slots that can be expected to be used by a user operation during validation pub max_expected_storage_slots: usize, + /// Whether to enable the revert check and the mode to use + pub revert_check_call_type: Option, } /// Origin of an operation. diff --git a/crates/pool/src/mempool/uo_pool.rs b/crates/pool/src/mempool/uo_pool.rs index 3a9ad5fe8..71a169502 100644 --- a/crates/pool/src/mempool/uo_pool.rs +++ b/crates/pool/src/mempool/uo_pool.rs @@ -13,22 +13,21 @@ use std::{collections::HashSet, sync::Arc}; -use alloy_primitives::{utils::format_units, Address, Bytes, B256, U256}; +use alloy_primitives::{utils::format_units, Address, B256}; use anyhow::Context; -use futures::TryFutureExt; use itertools::Itertools; use metrics::{Counter, Gauge, Histogram}; use metrics_derive::Metrics; use parking_lot::RwLock; use rundler_provider::{ - DAGasOracleSync, EvmProvider, FeeEstimator, ProvidersWithEntryPointT, SimulationProvider, - StateOverride, + DAGasOracleSync, EvmProvider, FeeEstimator, HandleOpRevert, ProvidersWithEntryPointT, + SimulationProvider, }; use rundler_sim::{MempoolConfig, Prechecker, Simulator}; use rundler_types::{ pool::{ - MempoolError, PaymasterMetadata, PoolOperation, PreconfInfo, Reputation, ReputationStatus, - StakeStatus, + InnerRevert, MempoolError, PaymasterMetadata, PoolOperation, PreconfInfo, Reputation, + ReputationStatus, SimulationViolation, StakeStatus, }, Entity, EntityUpdate, EntityUpdateType, EntryPointVersion, GasFees, UserOperation, UserOperationId, UserOperationPermissions, UserOperationVariant, @@ -158,77 +157,47 @@ where self.ep_specific_metrics.removed_entities.increment(1); } - async fn check_execution_gas_limit_efficiency( - &self, - op: UserOperationVariant, - block_hash: B256, - ) -> MempoolResult<()> { - // Check call gas limit efficiency only if needed - if self.config.execution_gas_limit_efficiency_reject_threshold > 0.0 { - // Node clients set base_fee to 0 during eth_call. - // Geth: https://github.com/ethereum/go-ethereum/blob/a5fe7353cff959d6fcfcdd9593de19056edb9bdb/internal/ethapi/api.go#L1202 - // Reth: https://github.com/paradigmxyz/reth/blob/4d3b35dbd24c3a5c6b1a4f7bd86b1451e8efafcc/crates/rpc/rpc-eth-api/src/helpers/call.rs#L1098 - // Arb-geth: https://github.com/OffchainLabs/go-ethereum/blob/54adef6e3fbea263e770c578047fd38842b8e17f/internal/ethapi/api.go#L1126 - let gas_price = op.gas_price(0); - - if gas_price == 0 { - // Can't calculate efficiency without gas price, fail open. - return Ok(()); - } - - let execution_gas_limit = match &op { - // For v0.6 only use the call gas limit as post op gas limit is always set to VGL*2 - // whether or not the UO is using a post op. Can cause the efficiency check to fail. - UserOperationVariant::V0_6(op) => op.call_gas_limit(), - UserOperationVariant::V0_7(op) => op.execution_gas_limit(), - }; - if execution_gas_limit == 0 { - return Ok(()); // No call gas limit, not useful, but not a failure here. - } - - let sim_result = self - .ep_providers - .entry_point() - .simulate_handle_op( - op.into(), - Address::ZERO, - Bytes::new(), - block_hash.into(), - StateOverride::default(), - ) - .await; - match sim_result { - Err(e) => { - tracing::error!("Failed to simulate handle op for gas limit efficiency check, failing open: {:?}", e); - } - Ok(Err(e)) => { - tracing::debug!( - "Validation error during gas limit efficiency check, failing open: {:?}", - e - ); - } - Ok(Ok(execution_res)) => { - let total_gas_used: u128 = (execution_res.paid / U256::from(gas_price)) - .try_into() - .context("total gas used should fit in u128")?; + async fn check_revert(&self, op: UserOperationVariant, block_hash: B256) -> MempoolResult<()> { + let Some(revert_check_call_type) = self.config.revert_check_call_type else { + return Ok(()); + }; - let execution_gas_used = total_gas_used - execution_res.pre_op_gas; + let sim_result = self + .ep_providers + .entry_point() + .simulate_handle_op_revert_check(op.into(), block_hash.into(), revert_check_call_type) + .await + .context("Failed to simulate handle op with revert check")?; - let execution_gas_efficiency = - execution_gas_used as f64 / execution_gas_limit as f64; - if execution_gas_efficiency - < self.config.execution_gas_limit_efficiency_reject_threshold - { - return Err(MempoolError::ExecutionGasLimitEfficiencyTooLow( - self.config.execution_gas_limit_efficiency_reject_threshold, - execution_gas_efficiency, - )); - } + match sim_result { + Ok(_) => Ok(()), + Err(HandleOpRevert::ExecutionRevert { data, reason }) => { + Err(MempoolError::ExecutionRevert(InnerRevert { data, reason })) + } + Err(HandleOpRevert::PostOpRevert { data, reason }) => { + Err(MempoolError::PostOpRevert(InnerRevert { data, reason })) + } + Err(HandleOpRevert::TransactionRevert(data)) => { + if let Some(monad_min_reserve_balance) = + self.config.chain_spec.monad_min_reserve_balance + { + Err(MempoolError::ExecutionRevert(InnerRevert { + data, + reason: Some( + format!("Execution reverted, possibly due to min reserve balance below threshold of {}", monad_min_reserve_balance), + ), + })) + } else { + Err(MempoolError::ExecutionRevert(InnerRevert { + data, + reason: Some("unknown revert during execution simulation".to_string()), + })) } } + Err(HandleOpRevert::ValidationRevert(r)) => Err(MempoolError::SimulationViolation( + SimulationViolation::ValidationRevert(r), + )), } - - Ok(()) } fn update_preconfirmed_uos( @@ -623,15 +592,18 @@ where .await?; // Only let ops with successful simulations through - // Run simulation and call gas limit efficiency check in parallel - let sim_fut = self - .pool_providers - .simulator() - .simulate_validation(versioned_op, perms.trusted, block_hash, None) - .map_err(Into::into); - let execution_gas_check_future = - self.check_execution_gas_limit_efficiency(op.clone(), block_hash); - let (sim_result, _) = tokio::try_join!(sim_fut, execution_gas_check_future)?; + // Run simulation and revert check in parallel + let sim_fut = self.pool_providers.simulator().simulate_validation( + versioned_op, + perms.trusted, + block_hash, + None, + ); + let revert_check_future = self.check_revert(op.clone(), block_hash); + let (sim_result, revert_check_result) = tokio::join!(sim_fut, revert_check_future); + // Handle errors in this order + let sim_result = sim_result?; + let _ = revert_check_result?; // Check if op has more than the maximum allowed expected storage slots let expected_slots = sim_result.expected_storage.num_slots(); @@ -1038,7 +1010,7 @@ struct UoPoolMetrics { mod tests { use std::{collections::HashMap, str::FromStr, vec}; - use alloy_primitives::{address, bytes, uint, Address, Bytes, Log as PrimitiveLog, LogData}; + use alloy_primitives::{address, bytes, Address, Bytes, Log as PrimitiveLog, LogData, U256}; use alloy_rpc_types_eth::TransactionReceipt as AlloyTransactionReceipt; use alloy_serde::WithOtherFields; use alloy_signer::SignerSync; @@ -1047,9 +1019,9 @@ mod tests { use mockall::Sequence; use rundler_contracts::v0_6::IEntryPoint::UserOperationEvent as UserOperationEventV06; use rundler_provider::{ - AnyReceiptEnvelope, DepositInfo, EntryPoint, ExecutionResult, Log, MockDAGasOracleSync, - MockEntryPointV0_6, MockEvmProvider, MockFeeEstimator, ProvidersWithEntryPoint, - ReceiptWithBloom, TransactionReceipt, + AnyReceiptEnvelope, DepositInfo, EntryPoint, Log, MockDAGasOracleSync, MockEntryPointV0_6, + MockEvmProvider, MockFeeEstimator, ProvidersWithEntryPoint, ReceiptWithBloom, + TransactionReceipt, }; use rundler_sim::{ MockPrechecker, MockSimulator, PrecheckError, PrecheckReturn, PrecheckSettings, @@ -2043,20 +2015,10 @@ mod tests { ..Default::default() }); - let mut ep = MockEntryPointV0_6::new(); - ep.expect_simulate_handle_op().returning(|_, _, _, _, _| { - Ok(Ok(ExecutionResult { - pre_op_gas: 100_000, // used 50K of 500K verification gas (used 50K PVG) - paid: uint!(110_000_U256), - target_success: true, - ..Default::default() - })) - }); - let pool = create_pool_with_entry_point_config( config, vec![op.clone()], - ep, + MockEntryPointV0_6::new(), MempoolConfig::default(), ); let ret = pool @@ -2073,52 +2035,9 @@ mod tests { } } - #[tokio::test] - async fn test_call_gas_limit_reject() { - let mut config = default_config(); - config.execution_gas_limit_efficiency_reject_threshold = 0.25; - - let op = create_op_from_op_v0_6(UserOperationRequiredFields { - call_gas_limit: 50_000, - max_fee_per_gas: 1, - max_priority_fee_per_gas: 1, - ..Default::default() - }); - - let mut ep = MockEntryPointV0_6::new(); - ep.expect_simulate_handle_op().returning(|_, _, _, _, _| { - Ok(Ok(ExecutionResult { - pre_op_gas: 50_000, - paid: uint!(60_000_U256), // call gas used is 10K - target_success: true, - ..Default::default() - })) - }); - - let pool = create_pool_with_entry_point_config( - config, - vec![op.clone()], - ep, - MempoolConfig::default(), - ); - let ret = pool - .add_operation(OperationOrigin::Local, op.op, default_perms()) - .await; - let actual_eff = 10_000_f64 / 50_000_f64; - - match ret.err().unwrap() { - MempoolError::ExecutionGasLimitEfficiencyTooLow(eff, actual) => { - assert_eq!(eff, 0.25); - assert_eq!(actual, actual_eff); - } - _ => panic!("Expected ExecutionGasLimitEfficiencyTooLow error"), - } - } - #[tokio::test] async fn test_gas_price_zero_fail_open() { - let mut config = default_config(); - config.execution_gas_limit_efficiency_reject_threshold = 0.25; + let config = default_config(); let op = create_op_from_op_v0_6(UserOperationRequiredFields { call_gas_limit: 50_000, @@ -2464,6 +2383,7 @@ mod tests { fn default_config() -> PoolConfig { PoolConfig { + // disable revert check by default chain_spec: ChainSpec::default(), entry_point: Address::random(), entry_point_version: EntryPointVersion::V0_6, @@ -2482,10 +2402,10 @@ mod tests { paymaster_cache_length: 100, reputation_tracking_enabled: true, drop_min_num_blocks: 10, - execution_gas_limit_efficiency_reject_threshold: 0.0, verification_gas_limit_efficiency_reject_threshold: 0.0, max_time_in_pool: None, max_expected_storage_slots: usize::MAX, + revert_check_call_type: None, } } diff --git a/crates/pool/src/server/remote/error.rs b/crates/pool/src/server/remote/error.rs index 1e5abb62b..8d50875d3 100644 --- a/crates/pool/src/server/remote/error.rs +++ b/crates/pool/src/server/remote/error.rs @@ -16,7 +16,8 @@ use anyhow::{bail, Context}; use rundler_task::grpc::protos::{from_bytes, ToProtoBytes}; use rundler_types::{ pool::{ - MempoolError, NeedsStakeInformation, PoolError, PrecheckViolation, SimulationViolation, + InnerRevert, MempoolError, NeedsStakeInformation, PoolError, PrecheckViolation, + SimulationViolation, }, Opcode, StorageSlot, Timestamp, ValidationRevert, ViolationOpCode, }; @@ -27,14 +28,15 @@ use super::protos::{ AggregatorMismatch, AssociatedStorageDuringDeploy, AssociatedStorageIsAlternateSender, CallGasLimitTooLow, CallHadValue, CalledBannedEntryPointMethod, CodeHashChanged, DidNotRevert, DiscardedOnInsertError, Eip7702Disabled, Entity, EntityThrottledError, EntityType, - EntryPointRevert, ExecutionGasLimitEfficiencyTooLow, ExistingSenderWithInitCode, - FactoryCalledCreate2Twice, FactoryIsNotContract, FactoryMustBeEmpty, Invalid7702AuthSignature, - InvalidAccountSignature, InvalidPaymasterSignature, InvalidSignature, InvalidStorageAccess, - InvalidTimeRange, MaxFeePerGasTooLow, MaxOperationsReachedError, MaxPriorityFeePerGasTooLow, + EntryPointRevert, ExecutionGasLimitEfficiencyTooLow, ExecutionRevert, + ExistingSenderWithInitCode, FactoryCalledCreate2Twice, FactoryIsNotContract, + FactoryMustBeEmpty, Invalid7702AuthSignature, InvalidAccountSignature, + InvalidPaymasterSignature, InvalidSignature, InvalidStorageAccess, InvalidTimeRange, + MaxFeePerGasTooLow, MaxOperationsReachedError, MaxPriorityFeePerGasTooLow, MempoolError as ProtoMempoolError, MultipleRolesViolation, NotStaked, OperationAlreadyKnownError, OperationDropTooSoon, OperationRevert, OutOfGas, OverMaxCost, PanicRevert, PaymasterBalanceTooLow, PaymasterDepositTooLow, PaymasterIsNotContract, - PreVerificationGasTooLow, PrecheckViolationError as ProtoPrecheckViolationError, + PostOpRevert, PreVerificationGasTooLow, PrecheckViolationError as ProtoPrecheckViolationError, ReplacementUnderpricedError, SenderAddressUsedAsAlternateEntity, SenderFundsTooLow, SenderIsNotContractAndNoInitCode, SimulationViolationError as ProtoSimulationViolationError, TooManyExpectedStorageSlots, TotalGasLimitTooHigh, UnintendedRevert, @@ -144,6 +146,26 @@ impl TryFrom for MempoolError { Some(mempool_error::Error::Invalid7702AuthSignature(e)) => { MempoolError::Invalid7702AuthSignature(e.reason) } + Some(mempool_error::Error::ExecutionRevert(e)) => { + MempoolError::ExecutionRevert(InnerRevert { + data: e.data.into(), + reason: if e.reason.is_empty() { + None + } else { + Some(e.reason) + }, + }) + } + Some(mempool_error::Error::PostOpRevert(e)) => { + MempoolError::PostOpRevert(InnerRevert { + data: e.data.into(), + reason: if e.reason.is_empty() { + None + } else { + Some(e.reason) + }, + }) + } None => bail!("unknown proto mempool error"), }) } @@ -278,6 +300,18 @@ impl From for ProtoMempoolError { Invalid7702AuthSignature { reason: msg }, )), }, + MempoolError::ExecutionRevert(r) => ProtoMempoolError { + error: Some(mempool_error::Error::ExecutionRevert(ExecutionRevert { + data: r.data.to_proto_bytes(), + reason: r.reason.unwrap_or_default(), + })), + }, + MempoolError::PostOpRevert(r) => ProtoMempoolError { + error: Some(mempool_error::Error::PostOpRevert(PostOpRevert { + data: r.data.to_proto_bytes(), + reason: r.reason.unwrap_or_default(), + })), + }, } } } diff --git a/crates/provider/src/alloy/entry_point/mod.rs b/crates/provider/src/alloy/entry_point/mod.rs index e27fbc406..fb1129031 100644 --- a/crates/provider/src/alloy/entry_point/mod.rs +++ b/crates/provider/src/alloy/entry_point/mod.rs @@ -12,11 +12,21 @@ // If not, see https://www.gnu.org/licenses/. use alloy_consensus::{transaction::SignableTransaction, TxEnvelope, TypedTransaction}; -use alloy_primitives::{address, Address, Bytes, Signature, U256}; -use alloy_provider::network::TransactionBuilder7702; +use alloy_json_rpc::{ErrorPayload, RpcError}; +use alloy_primitives::{address, Address, Bytes, Log, LogData, Signature, U256}; +use alloy_provider::{ext::DebugApi, network::TransactionBuilder7702}; use alloy_rlp::Encodable; -use alloy_rpc_types_eth::TransactionRequest; +use alloy_rpc_types_eth::{ + simulate::{SimBlock, SimulatePayload}, + BlockId, TransactionRequest, +}; +use alloy_rpc_types_trace::geth::{ + CallConfig, GethDebugBuiltInTracerType, GethDebugTracerType, GethDebugTracingOptions, GethTrace, +}; use rundler_types::authorization::Eip7702Auth; +use rundler_utils::json_rpc; + +use crate::{AlloyProvider, ProviderResult, RevertCheckCallType}; pub(crate) mod v0_6; pub(crate) mod v0_7; @@ -75,3 +85,129 @@ fn max_bundle_transaction_data( } } } + +struct SimulateResult { + gas_used: u128, + success: bool, + data: Bytes, + logs: Vec, +} + +async fn simulate_transaction( + provider: &P, + tx: TransactionRequest, + block_id: BlockId, + revert_check_call_type: RevertCheckCallType, +) -> ProviderResult { + match revert_check_call_type { + RevertCheckCallType::EthCall => simulate_with_eth_call(provider, tx, block_id).await, + RevertCheckCallType::EthSimulateV1 => { + simulate_with_simulate_v1(provider, tx, block_id).await + } + RevertCheckCallType::DebugTraceCall => { + simulate_with_debug_trace_call(provider, tx, block_id).await + } + } +} + +async fn simulate_with_simulate_v1( + provider: &P, + tx: TransactionRequest, + block_id: BlockId, +) -> ProviderResult { + let payload = SimulatePayload { + block_state_calls: vec![SimBlock { + block_overrides: None, + state_overrides: None, + calls: vec![tx], + }], + trace_transfers: false, + validation: false, + return_full_transactions: false, + }; + + let result = provider.simulate(&payload).block_id(block_id).await?; + if result.len() != 1 { + return Err(anyhow::anyhow!("expected 1 simulated block, got {}", result.len()).into()); + } + if result[0].calls.len() != 1 { + return Err(anyhow::anyhow!("expected 1 call, got {}", result[0].calls.len()).into()); + } + let result_call = &result[0].calls[0]; + + Ok(SimulateResult { + gas_used: result_call.gas_used.into(), + success: result_call.status, + data: result_call.return_data.clone(), + logs: result_call + .logs + .clone() + .into_iter() + .map(|log| log.inner) + .collect(), + }) +} + +async fn simulate_with_debug_trace_call( + provider: &P, + tx: TransactionRequest, + block_id: BlockId, +) -> ProviderResult { + let trace_options = GethDebugTracingOptions::new_tracer(GethDebugTracerType::BuiltInTracer( + GethDebugBuiltInTracerType::CallTracer, + )) + .with_call_config(CallConfig::default().with_log()); + + let trace = provider + .debug_trace_call(tx.into(), block_id, trace_options.into()) + .await?; + + let GethTrace::CallTracer(call_frame) = trace else { + return Err(anyhow::anyhow!("expected call tracer, got {:?}", trace).into()); + }; + + Ok(SimulateResult { + gas_used: call_frame.gas_used.try_into().unwrap_or(u128::MAX), + success: call_frame.error.is_none(), + data: call_frame.output.unwrap_or_default(), + logs: call_frame + .logs + .iter() + .filter_map(|log| { + let address = log.address?; + let topics = log.clone().topics?; + let data = log.clone().data?; + let data = LogData::new(topics, data)?; + Some(Log { address, data }) + }) + .collect(), + }) +} + +async fn simulate_with_eth_call( + provider: &P, + tx: TransactionRequest, + block_id: BlockId, +) -> ProviderResult { + match provider.call(tx.into()).block(block_id).await { + Ok(data) => Ok(SimulateResult { + gas_used: 0, + success: false, + data, + logs: vec![], + }), + Err(RpcError::ErrorResp(ErrorPayload { + code: json_rpc::INTERNAL_ERROR_CODE, + message, + data, + })) if json_rpc::check_execution_reverted(&message) => Ok(SimulateResult { + gas_used: 0, + success: false, + data: data + .and_then(|data| data.to_string().parse::().ok()) + .unwrap_or_default(), + logs: vec![], + }), + Err(e) => Err(e.into()), + } +} diff --git a/crates/provider/src/alloy/entry_point/v0_6.rs b/crates/provider/src/alloy/entry_point/v0_6.rs index 3906080c2..53fed22b8 100644 --- a/crates/provider/src/alloy/entry_point/v0_6.rs +++ b/crates/provider/src/alloy/entry_point/v0_6.rs @@ -19,14 +19,16 @@ use alloy_rpc_types_eth::{ state::{AccountOverride, StateOverride}, BlockId, }; -use alloy_sol_types::{ContractError as SolContractError, SolInterface}; +use alloy_sol_types::{ + ContractError as SolContractError, Revert, SolError, SolEvent, SolInterface, +}; use alloy_transport::TransportError; use anyhow::Context; use rundler_contracts::v0_6::{ DepositInfo as DepositInfoV0_6, GetEntryPointBalances, IAggregator, IEntryPoint::{ ExecutionResult as ExecutionResultV0_6, FailedOp, IEntryPointCalls, IEntryPointErrors, - IEntryPointInstance, + IEntryPointInstance, UserOperationRevertReason, }, UserOperation as ContractUserOperation, UserOpsPerAggregator as UserOpsPerAggregatorV0_6, }; @@ -43,8 +45,8 @@ use tracing::instrument; use crate::{ AggregatorOut, AggregatorSimOut, AlloyProvider, BlockHashOrNumber, BundleHandler, DAGasOracle, DAGasProvider, DepositInfo, EntryPoint, EntryPointProvider as EntryPointProviderTrait, - ExecutionResult, HandleOpsOut, ProviderResult, SignatureAggregator, SimulationProvider, - TransactionRequest, + ExecutionResult, HandleOpRevert, HandleOpsOut, ProviderResult, RevertCheckCallType, + SignatureAggregator, SimulationProvider, TransactionRequest, }; /// Entry point provider for v0.6 @@ -261,7 +263,7 @@ where &self.i_entry_point, ops_per_aggregator, sender_eoa, - gas_limit, + Some(gas_limit), gas_fees, proxy, self.chain_spec.id, @@ -290,7 +292,7 @@ where &self.i_entry_point, ops_per_aggregator, sender_eoa, - gas_limit, + Some(gas_limit), gas_fees, proxy, self.chain_spec.id, @@ -503,6 +505,91 @@ where .await } + #[instrument(skip_all)] + async fn simulate_handle_op_revert_check( + &self, + op: Self::UO, + block_id: BlockId, + revert_check_call_type: RevertCheckCallType, + ) -> ProviderResult> { + let mut state_override = StateOverride::default(); + let da_gas = op + .pre_verification_da_gas_limit(&self.chain_spec, Some(1)) + .try_into() + .unwrap_or(u64::MAX); + + if let Some(authorization) = op.authorization_tuple() { + authorization_utils::apply_7702_overrides( + &mut state_override, + op.sender(), + authorization.address, + ); + } + + let tx = self + .i_entry_point + .simulateHandleOp(op.into(), Address::ZERO, Bytes::new()) + .block(block_id) + .gas(self.max_simulate_handle_op_gas.saturating_add(da_gas)) + .state(state_override) + .into_transaction_request() + .inner; + + let sim_result = super::simulate_transaction( + self.i_entry_point.provider(), + tx, + block_id, + revert_check_call_type, + ) + .await?; + + if !sim_result.success { + let revert = Self::decode_simulate_handle_ops_revert(&sim_result.data)?; + let Err(revert) = revert else { + return Err(anyhow::anyhow!("unexpected execution result in revert").into()); + }; + + match revert { + ValidationRevert::EntryPoint(e) => { + if e.contains("AA50") { + return Ok(Err(HandleOpRevert::PostOpRevert { + reason: Some(e), + data: Bytes::new(), + })); + } else { + return Ok(Err(HandleOpRevert::ValidationRevert( + ValidationRevert::EntryPoint(e), + ))); + } + } + ValidationRevert::Unknown(data) => { + return Ok(Err(HandleOpRevert::TransactionRevert(data))); + } + _ => return Ok(Err(HandleOpRevert::ValidationRevert(revert))), + } + } + + for log in &sim_result.logs { + if log.topics().is_empty() || log.address != *self.i_entry_point.address() { + continue; + } + + if log.topics()[0] == UserOperationRevertReason::SIGNATURE_HASH { + let revert_reason = UserOperationRevertReason::decode_log(log) + .map(|l| l.data) + .context("failed to decode user operation revert reason")?; + return Ok(Err(HandleOpRevert::ExecutionRevert { + reason: Revert::abi_decode(&revert_reason.revertReason) + .ok() + .map(|r| r.reason), + data: revert_reason.revertReason, + })); + } + } + + Ok(Ok(sim_result.gas_used)) + } + fn decode_simulate_handle_ops_revert( payload: &Bytes, ) -> ProviderResult> { @@ -553,7 +640,7 @@ fn get_handle_ops_call( entry_point: &IEntryPointInstance, ops_per_aggregator: Vec>, sender_eoa: Address, - gas_limit: u64, + gas_limit: Option, gas_fees: GasFees, proxy: Option
, chain_id: u64, @@ -594,10 +681,12 @@ fn get_handle_ops_call( txn_request = txn_request .from(sender_eoa) - .gas_limit(gas_limit) .max_fee_per_gas(gas_fees.max_fee_per_gas) .max_priority_fee_per_gas(gas_fees.max_priority_fee_per_gas); + if let Some(gas_limit) = gas_limit { + txn_request = txn_request.gas_limit(gas_limit); + } if !eip7702_auth_list.is_empty() { txn_request = txn_request.with_authorization_list(eip7702_auth_list); } diff --git a/crates/provider/src/alloy/entry_point/v0_7.rs b/crates/provider/src/alloy/entry_point/v0_7.rs index ff496d3fa..cb69201cc 100644 --- a/crates/provider/src/alloy/entry_point/v0_7.rs +++ b/crates/provider/src/alloy/entry_point/v0_7.rs @@ -19,13 +19,16 @@ use alloy_rpc_types_eth::{ state::{AccountOverride, StateOverride}, BlockId, }; -use alloy_sol_types::{ContractError as SolContractError, SolInterface, SolValue}; +use alloy_sol_types::{ + ContractError as SolContractError, Revert, SolError, SolEvent, SolInterface, SolValue, +}; use alloy_transport::TransportError; use anyhow::Context; use rundler_contracts::v0_7::{ DepositInfo as DepositInfoV0_7, GetEntryPointBalances, IAggregator, IEntryPoint::{ FailedOp, FailedOpWithRevert, IEntryPointCalls, IEntryPointErrors, IEntryPointInstance, + PostOpRevertReason, PostOpReverted, UserOperationRevertReason, }, IEntryPointSimulations::{ self, ExecutionResult as ExecutionResultV0_7, IEntryPointSimulationsInstance, @@ -47,8 +50,8 @@ use tracing::instrument; use crate::{ AggregatorOut, AggregatorSimOut, AlloyProvider, BlockHashOrNumber, BundleHandler, DAGasOracle, DAGasProvider, DepositInfo, EntryPoint, EntryPointProvider as EntryPointProviderTrait, - ExecutionResult, HandleOpsOut, ProviderResult, SignatureAggregator, SimulationProvider, - TransactionRequest, + ExecutionResult, HandleOpRevert, HandleOpsOut, ProviderResult, RevertCheckCallType, + SignatureAggregator, SimulationProvider, TransactionRequest, }; /// Entry point provider for v0.7 @@ -57,7 +60,7 @@ pub struct EntryPointProvider { i_entry_point: IEntryPointInstance, da_gas_oracle: D, max_verification_gas: u64, - max_simulate_handle_ops_gas: u64, + max_simulate_handle_op_gas: u64, max_gas_estimation_gas: u64, max_aggregation_gas: u64, chain_spec: ChainSpec, @@ -71,7 +74,7 @@ where pub fn new( chain_spec: ChainSpec, max_verification_gas: u64, - max_simulate_handle_ops_gas: u64, + max_simulate_handle_op_gas: u64, max_gas_estimation_gas: u64, max_aggregation_gas: u64, provider: AP, @@ -84,7 +87,7 @@ where ), da_gas_oracle, max_verification_gas, - max_simulate_handle_ops_gas, + max_simulate_handle_op_gas, max_gas_estimation_gas, max_aggregation_gas, chain_spec, @@ -302,7 +305,7 @@ where &self.i_entry_point, ops_per_aggregator, sender_eoa, - gas_limit, + Some(gas_limit), gas_fees, proxy, self.chain_spec.id, @@ -355,7 +358,7 @@ where &self.i_entry_point, ops_per_aggregator, sender_eoa, - gas_limit, + Some(gas_limit), gas_fees, proxy, self.chain_spec.id, @@ -386,6 +389,9 @@ where IEntryPointErrors::SignatureValidationFailed(failure) => { HandleOpsOut::SignatureValidationFailed(failure.aggregator) } + IEntryPointErrors::PostOpReverted(PostOpReverted { returnData }) => { + HandleOpsOut::Revert(returnData) + } }; Some(ret) } else { @@ -527,7 +533,7 @@ where ) -> ProviderResult> { simulate_handle_op_inner( &self.chain_spec, - self.max_simulate_handle_ops_gas, + self.max_simulate_handle_op_gas, &self.i_entry_point, op, target, @@ -562,6 +568,86 @@ where .await } + #[instrument(skip_all)] + async fn simulate_handle_op_revert_check( + &self, + op: Self::UO, + block_id: BlockId, + revert_check_call_type: RevertCheckCallType, + ) -> ProviderResult> { + let da_gas = op + .pre_verification_da_gas_limit(&self.chain_spec, Some(1)) + .try_into() + .unwrap_or(u64::MAX); + let mut state_override = StateOverride::default(); + + add_simulations_override(&mut state_override, *self.i_entry_point.address()); + add_authorization_tuple(op.sender(), op.authorization_tuple(), &mut state_override); + + let ep_simulations = IEntryPointSimulations::new( + *self.i_entry_point.address(), + self.i_entry_point.provider(), + ); + + let tx = ep_simulations + .simulateHandleOp(op.pack(), Address::ZERO, Bytes::new()) + .block(block_id) + .gas(self.max_simulate_handle_op_gas.saturating_add(da_gas)) + .state(state_override) + .into_transaction_request() + .inner; + + let sim_result = super::simulate_transaction( + self.i_entry_point.provider(), + tx, + block_id, + revert_check_call_type, + ) + .await?; + + if !sim_result.success { + let revert = decode_validation_revert(&sim_result.data); + if let ValidationRevert::Unknown(data) = revert { + return Ok(Err(HandleOpRevert::TransactionRevert(data))); + } + return Ok(Err(HandleOpRevert::ValidationRevert(revert))); + } + + for log in &sim_result.logs { + if log.topics().is_empty() || log.address != *self.i_entry_point.address() { + continue; + } + + if log.topics()[0] == UserOperationRevertReason::SIGNATURE_HASH { + let revert_reason = UserOperationRevertReason::decode_log(log) + .map(|l| l.data) + .context("failed to decode user operation revert reason")?; + return Ok(Err(HandleOpRevert::ExecutionRevert { + reason: Revert::abi_decode(&revert_reason.revertReason) + .ok() + .map(|r| r.reason), + data: revert_reason.revertReason, + })); + } else if log.topics()[0] == PostOpRevertReason::SIGNATURE_HASH { + let revert_reason = PostOpRevertReason::decode_log(log) + .map(|l| l.data) + .context("failed to decode post op revert reason")?; + + let revert_reason = PostOpReverted::abi_decode(&revert_reason.revertReason) + .context("failed to decode post op revert reason")?; + + return Ok(Err(HandleOpRevert::PostOpRevert { + reason: Revert::abi_decode(&revert_reason.returnData) + .ok() + .map(|r| r.reason), + data: revert_reason.returnData, + })); + } + } + + Ok(Ok(sim_result.gas_used)) + } + fn decode_simulate_handle_ops_revert( revert_data: &Bytes, ) -> ProviderResult> { @@ -599,7 +685,7 @@ fn get_handle_ops_call( entry_point: &IEntryPointInstance, ops_per_aggregator: Vec>, sender_eoa: Address, - gas_limit: u64, + gas_limit: Option, gas_fees: GasFees, proxy: Option
, chain_id: u64, @@ -640,10 +726,12 @@ fn get_handle_ops_call( txn_request = txn_request .from(sender_eoa) - .gas_limit(gas_limit) .max_fee_per_gas(gas_fees.max_fee_per_gas) .max_priority_fee_per_gas(gas_fees.max_priority_fee_per_gas); + if let Some(gas_limit) = gas_limit { + txn_request = txn_request.gas_limit(gas_limit); + } if !authorization_list.is_empty() { txn_request = txn_request.with_authorization_list(authorization_list); } @@ -666,6 +754,10 @@ pub fn decode_validation_revert(err_bytes: &Bytes) -> ValidationRevert { f.aggregator )) } + SolContractError::CustomError(IEntryPointErrors::PostOpReverted(f)) => { + // This should never happen + ValidationRevert::Unknown(f.returnData) + } SolContractError::Revert(r) => r.into(), SolContractError::Panic(p) => p.into(), } diff --git a/crates/provider/src/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index 9dcbd1eb2..99cfedc30 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -87,6 +87,29 @@ pub struct ExecutionResult { pub target_result: Bytes, } +/// Error type for handle op simulation +#[derive(Clone, Debug)] +pub enum HandleOpRevert { + /// The operation validation reverted + ValidationRevert(ValidationRevert), + /// The operation execution reverted + ExecutionRevert { + /// The revert data + data: Bytes, + /// String reason for the revert, if available + reason: Option, + }, + /// The post-operation reverted + PostOpRevert { + /// The revert data + data: Bytes, + /// String reason for the revert, if available + reason: Option, + }, + /// The transaction revert for unknown reason + TransactionRevert(Bytes), +} + /// Trait for interacting with an entry point contract. #[async_trait::async_trait] #[auto_impl::auto_impl(&, &mut, Rc, Arc, Box)] @@ -196,6 +219,17 @@ pub trait DAGasProvider: Send + Sync { ) -> ProviderResult<(u128, DAGasData, DAGasBlockData)>; } +/// Mode for checking for reverts during simulation +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum RevertCheckCallType { + /// Use eth_call to check for reverts + EthCall, + /// Use eth_simulateV1 to check for reverts + EthSimulateV1, + /// Use debug_traceCall to check for reverts + DebugTraceCall, +} + /// Trait for simulating user operations on an entry point contract #[async_trait::async_trait] #[auto_impl::auto_impl(&, &mut, Rc, Arc, Box)] @@ -236,6 +270,15 @@ pub trait SimulationProvider: Send + Sync { state_override: StateOverride, ) -> ProviderResult>; + /// Simulate a full user operation (with signature validation) and check for reverts. + /// Returns a result with the gas used if the operation succeeded, or the revert data if it failed. + async fn simulate_handle_op_revert_check( + &self, + op: Self::UO, + block_id: BlockId, + revert_check_call_type: RevertCheckCallType, + ) -> ProviderResult>; + /// Decode the revert data from a call to `simulateHandleOps` fn decode_simulate_handle_ops_revert( revert_data: &Bytes, diff --git a/crates/provider/src/traits/test_utils.rs b/crates/provider/src/traits/test_utils.rs index f3242c4e9..9e7718466 100644 --- a/crates/provider/src/traits/test_utils.rs +++ b/crates/provider/src/traits/test_utils.rs @@ -30,8 +30,9 @@ use super::error::ProviderResult; use crate::{ AggregatorOut, Block, BlockHashOrNumber, BundleHandler, DAGasOracle, DAGasOracleSync, DAGasProvider, DepositInfo, EntryPoint, EntryPointProvider, EvmCall, - EvmProvider as EvmProviderTrait, ExecutionResult, FeeEstimator, HandleOpsOut, RpcRecv, RpcSend, - SignatureAggregator, SimulationProvider, Transaction, TransactionReceipt, TransactionRequest, + EvmProvider as EvmProviderTrait, ExecutionResult, FeeEstimator, HandleOpRevert, HandleOpsOut, + RevertCheckCallType, RpcRecv, RpcSend, SignatureAggregator, SimulationProvider, Transaction, + TransactionReceipt, TransactionRequest, }; mockall::mock! { @@ -185,6 +186,12 @@ mockall::mock! { block_id: BlockId, state_override: StateOverride, ) -> ProviderResult>; + async fn simulate_handle_op_revert_check( + &self, + op: v0_6::UserOperation, + block_id: BlockId, + revert_check_call_type: RevertCheckCallType, + ) -> ProviderResult>; fn decode_simulate_handle_ops_revert( revert_data: &Bytes, ) -> ProviderResult>; @@ -289,6 +296,12 @@ mockall::mock! { block_id: BlockId, state_override: StateOverride, ) -> ProviderResult>; + async fn simulate_handle_op_revert_check( + &self, + op: v0_7::UserOperation, + block_id: BlockId, + revert_check_call_type: RevertCheckCallType, + ) -> ProviderResult>; fn decode_simulate_handle_ops_revert( revert_data: &Bytes, ) -> ProviderResult>; diff --git a/crates/rpc/src/eth/error.rs b/crates/rpc/src/eth/error.rs index 6a7b3086e..3782ef74f 100644 --- a/crates/rpc/src/eth/error.rs +++ b/crates/rpc/src/eth/error.rs @@ -136,7 +136,11 @@ pub enum EthRpcError { #[error("{0}")] ExecutionReverted(String), #[error("execution reverted")] - ExecutionRevertedWithBytes(ExecutionRevertedWithBytesData), + ExecutionRevertedWithData(ExecutionRevertedWithDataData), + #[error("post op reverted: {0}")] + PostOpReverted(String), + #[error("post op reverted")] + PostOpRevertedWithData(ExecutionRevertedWithDataData), #[error("operation rejected by mempool: {0}")] OperationRejected(String), } @@ -264,8 +268,8 @@ pub struct UnsupportedAggregatorData { #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] -pub struct ExecutionRevertedWithBytesData { - pub revert_data: Bytes, +pub struct ExecutionRevertedWithDataData { + pub data: Bytes, } impl From for EthRpcError { @@ -317,6 +321,20 @@ impl From for EthRpcError { | MempoolError::TooManyExpectedStorageSlots(_, _) | MempoolError::Invalid7702AuthSignature(_) | MempoolError::EIPNotSupported(_) => Self::InvalidParams(value.to_string()), + MempoolError::ExecutionRevert(r) => { + if let Some(reason) = r.reason { + Self::ExecutionReverted(reason) + } else { + Self::ExecutionRevertedWithData(ExecutionRevertedWithDataData { data: r.data }) + } + } + MempoolError::PostOpRevert(r) => { + if let Some(reason) = r.reason { + Self::PostOpReverted(reason) + } else { + Self::PostOpRevertedWithData(ExecutionRevertedWithDataData { data: r.data }) + } + } } } } @@ -425,8 +443,13 @@ impl From for ErrorObjectOwned { | EthRpcError::AggregatorError(_) | EthRpcError::AggregatorMismatch(_, _) => rpc_err(SIGNATURE_CHECK_FAILED_CODE, msg), EthRpcError::PrecheckFailed(_) => rpc_err(CALL_EXECUTION_FAILED_CODE, msg), - EthRpcError::ExecutionReverted(_) => rpc_err(EXECUTION_REVERTED, msg), - EthRpcError::ExecutionRevertedWithBytes(data) => { + EthRpcError::ExecutionReverted(_) | EthRpcError::PostOpReverted(_) => { + rpc_err(EXECUTION_REVERTED, msg) + } + EthRpcError::ExecutionRevertedWithData(data) => { + rpc_err_with_data(EXECUTION_REVERTED, msg, data) + } + EthRpcError::PostOpRevertedWithData(data) => { rpc_err_with_data(EXECUTION_REVERTED, msg, data) } EthRpcError::ValidationRevert(data) => { @@ -508,7 +531,7 @@ impl From for EthRpcError { Self::ExecutionReverted(message) } GasEstimationError::RevertInCallWithBytes(b) => { - Self::ExecutionRevertedWithBytes(ExecutionRevertedWithBytesData { revert_data: b }) + Self::ExecutionRevertedWithData(ExecutionRevertedWithDataData { data: b }) } error @ GasEstimationError::GasUsedTooLarge => { Self::EntryPointValidationRejected(error.to_string()) diff --git a/crates/rpc/src/eth/events/common.rs b/crates/rpc/src/eth/events/common.rs index a81caf1fa..ea4e1c9ef 100644 --- a/crates/rpc/src/eth/events/common.rs +++ b/crates/rpc/src/eth/events/common.rs @@ -335,6 +335,11 @@ where tx_hash: B256, user_op_hash: B256, ) -> anyhow::Result> { + if !self.chain_spec.rpc_debug_trace_transaction_enabled { + tracing::warn!("Debug trace transaction is not enabled, skipping trace"); + return Ok(None); + } + // initial call wasn't to an entrypoint, so we need to trace the transaction to find the user operation let trace_options = GethDebugTracingOptions { tracer: Some(GethDebugTracerType::BuiltInTracer( diff --git a/crates/types/src/chain.rs b/crates/types/src/chain.rs index 723f80364..161bc52ef 100644 --- a/crates/types/src/chain.rs +++ b/crates/types/src/chain.rs @@ -141,6 +141,18 @@ pub struct ChainSpec { /// Size of the chain history to keep to handle reorgs pub chain_history_size: u64, + /* + * Node RPC + */ + /// True if the node RPC supports debug_traceTransaction + pub rpc_debug_trace_transaction_enabled: bool, + + /* + * Network specific behavior + */ + /// Monad minimum reserve balance for EIP-7702 delegated accounts in MON + pub monad_min_reserve_balance: Option, + /* * Contracts */ @@ -206,6 +218,8 @@ impl Default for ChainSpec { flashbots_relay_url: None, bloxroute_enabled: false, chain_history_size: 64, + rpc_debug_trace_transaction_enabled: true, + monad_min_reserve_balance: None, signature_aggregators: Arc::new(ContractRegistry::default()), submission_proxies: Arc::new(ContractRegistry::default()), } diff --git a/crates/types/src/pool/error.rs b/crates/types/src/pool/error.rs index 793b810b8..d9cd6d270 100644 --- a/crates/types/src/pool/error.rs +++ b/crates/types/src/pool/error.rs @@ -11,7 +11,9 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use alloy_primitives::{Address, U256}; +use std::fmt::Display; + +use alloy_primitives::{Address, Bytes, U256}; use crate::{ validation_results::ValidationRevert, Entity, EntityType, StorageSlot, Timestamp, @@ -41,6 +43,25 @@ impl From for PoolError { } } +/// Inner revert data +#[derive(Debug, Clone)] +pub struct InnerRevert { + /// Revert data + pub data: Bytes, + /// Revert reason + pub reason: Option, +} + +impl Display for InnerRevert { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + self.reason.as_ref().unwrap_or(&self.data.to_string()) + ) + } +} + /// Mempool error type. #[derive(Debug, thiserror::Error)] pub enum MempoolError { @@ -109,6 +130,12 @@ pub enum MempoolError { /// Use unsupported EIP #[error("{0} is not supported")] EIPNotSupported(String), + /// Execution reverted + #[error("Execution reverted: {0}")] + ExecutionRevert(InnerRevert), + /// Post op reverted + #[error("Post op reverted: {0}")] + PostOpRevert(InnerRevert), } /// Precheck violation enumeration diff --git a/crates/utils/src/json_rpc.rs b/crates/utils/src/json_rpc.rs new file mode 100644 index 000000000..29b1d813a --- /dev/null +++ b/crates/utils/src/json_rpc.rs @@ -0,0 +1,22 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +//! JSON-RPC utilities + +/// The error code for internal errors in JSON-RPC responses +pub const INTERNAL_ERROR_CODE: i64 = -32603; + +/// Check if a JSON-RPC response indicates an execution revert +pub fn check_execution_reverted(message: &str) -> bool { + message == "execution reverted" +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 404d75ffb..a959cfe75 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -25,6 +25,7 @@ pub mod cache; pub mod emit; pub mod eth; pub mod guard_timer; +pub mod json_rpc; pub mod log; pub mod math; pub mod random; diff --git a/docs/cli.md b/docs/cli.md index 547d4dbdd..10aa5ae2e 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -20,152 +20,152 @@ These options are common to all subcommands and can be used globally: See [chain spec](./architecture/chain_spec.md) for a detailed description of chain spec derivation from these options. - `--network`: Network to look up a hardcoded chain spec. (default: None) - - env: *NETWORK* + - env: _NETWORK_ - `--chain_spec`: Path to a chain spec TOML file. - - env: *CHAIN_SPEC* + - env: _CHAIN_SPEC_ - (env only): Chain specification overrides. - - env: *CHAIN_** + - env: \*CHAIN\_\*\* ### Rundler Common - `--node_http`: EVM Node HTTP URL to use. (**REQUIRED**) - - env: *NODE_HTTP* + - env: _NODE_HTTP_ - `--max_verification_gas`: Maximum verification gas. (default: `5000000`). - - env: *MAX_VERIFICATION_GAS* + - env: _MAX_VERIFICATION_GAS_ - `--max_uo_cost`: Maximum cost of a UO that the mempool will accept. Optional, defaults to MAX (default: `None`). - - env: *MAX_UO_COST* + - env: _MAX_UO_COST_ - `--min_stake_value`: Minimum stake value. (default: `1000000000000000000`). - - env: *MIN_STAKE_VALUE* + - env: _MIN_STAKE_VALUE_ - `--min_unstake_delay`: Minimum unstake delay. (default: `86400`). - - env: *MIN_UNSTAKE_DELAY* + - env: _MIN_UNSTAKE_DELAY_ - `--tracer_timeout`: The timeout used for custom javascript tracers, the string must be in a valid parseable format that can be used in the `ParseDuration` function on an ethereum node. See Docs [Here](https://pkg.go.dev/time#ParseDuration). (default: `15s`) - - env: *TRACER_TIMEOUT* + - env: _TRACER_TIMEOUT_ - `--enable_unsafe_fallback`: If set, allows the simulation code to fallback to an unsafe simulation if there is a tracer error. (default: `false`) - - env: *ENABLE_UNSAFE_FALLBACK* + - env: _ENABLE_UNSAFE_FALLBACK_ - `--user_operation_event_block_distance`: Number of blocks to search when calling `eth_getUserOperationByHash`/`eth_getUserOperationReceipt`. (default: all blocks) - - env: *USER_OPERATION_EVENT_BLOCK_DISTANCE* + - env: _USER_OPERATION_EVENT_BLOCK_DISTANCE_ - `--user_operation_event_block_distance_fallback`: Number of blocks to search when falling back during `eth_getUserOperationByHash`/`eth_getUserOperationReceipt` upon initial failure using `user_operation_event_block_distance`. (default: None) - - env: *USER_OPERATION_EVENT_BLOCK_DISTANCE_FALLBACK* + - env: _USER_OPERATION_EVENT_BLOCK_DISTANCE_FALLBACK_ - `--verification_estimation_gas_fee`: The gas fee to use during verification estimation. (default: `1000000000000` 10K gwei). - - env: *VERIFICATION_ESTIMATION_GAS_FEE* + - env: _VERIFICATION_ESTIMATION_GAS_FEE_ - See [RPC documentation](./architecture/rpc.md#verificationGasLimit-estimation) for details. - `--bundle_base_fee_overhead_percent`: bundle transaction base fee overhead over network pending value. (default: `27`). - - env: *BUNDLE_BASE_FEE_OVERHEAD_PERCENT* + - env: _BUNDLE_BASE_FEE_OVERHEAD_PERCENT_ - `--bundle_priority_fee_overhead_percent`: bundle transaction priority fee overhead over network value. (default: `0`). - - env: *BUNDLE_PRIORITY_FEE_OVERHEAD_PERCENT* + - env: _BUNDLE_PRIORITY_FEE_OVERHEAD_PERCENT_ - `--priority_fee_mode_kind`: Priority fee mode kind. Possible values are `base_fee_percent` and `priority_fee_increase_percent`. (default: `priority_fee_increase_percent`). - options: ["base_fee_percent", "priority_fee_increase_percent"] - - env: *PRIORITY_FEE_MODE_KIND* + - env: _PRIORITY_FEE_MODE_KIND_ - `--priority_fee_mode_value`: Priority fee mode value. (default: `0`). - - env: *PRIORITY_FEE_MODE_VALUE* + - env: _PRIORITY_FEE_MODE_VALUE_ - `--base_fee_accept_percent`: Percentage of the current network fees a user operation must have in order to be accepted into the mempool. (default: `100`). - - env: *BASE_FEE_ACCEPT_PERCENT* + - env: _BASE_FEE_ACCEPT_PERCENT_ - `--pre_verification_gas_accept_percent`: Percentage of the required PVG that a user operation must have in order to be accepted into the mempool. Only applies if there is dynamic PVG, else the full amount is required. (default: `50`) - - env: *PRE_VERIFICATION_GAS_ACCEPT_PERCENT* -- `--execution_gas_limit_efficiency_reject_threshold`: The ratio of execution gas used to gas limit under which to reject UOs upon entry to the mempool (default: `0.0` disabled) - - env: *EXECUTION_GAS_LIMIT_EFFICIENCY_REJECT_THRESHOLD* + - env: _PRE_VERIFICATION_GAS_ACCEPT_PERCENT_ - `--verification_gas_limit_efficiency_reject_threshold`: The ratio of verification gas used to gas limit under which to reject UOs upon entry to the mempool (default: `0.0` disabled) - - env: *VERIFICATION_GAS_LIMIT_EFFICIENCY_REJECT_THRESHOLD* + - env: _VERIFICATION_GAS_LIMIT_EFFICIENCY_REJECT_THRESHOLD_ - `--verification_gas_allowed_error_pct`: The allowed error percentage during verification gas estimation. (default: 15) - - env: *VERIFICATION_GAS_ALLOWED_ERROR_PCT* + - env: _VERIFICATION_GAS_ALLOWED_ERROR_PCT_ - `--call_gas_allowed_error_pct`: The allowed error percentage during call gas estimation. (default: 15) - - env: *CALL_GAS_ALLOWED_ERROR_PCT* + - env: _CALL_GAS_ALLOWED_ERROR_PCT_ - `--max_gas_estimation_gas`: The gas limit to use during the call to the gas estimation binary search helper functions. (default: 550M) - - env: *MAX_GAS_ESTIMATION_GAS* + - env: _MAX_GAS_ESTIMATION_GAS_ - `--max_gas_estimation_rounds`: The maximum amount of remote RPC calls to make during gas estimation while attempting to converge to the error percentage. (default: 3) - - env: *MAX_GAS_ESTIMATION_ROUNDS* + - env: _MAX_GAS_ESTIMATION_ROUNDS_ - `--aws_region`: AWS region. (default: `us-east-1`). - - env: *AWS_REGION* - - (*Only required if using other AWS features*) + - env: _AWS_REGION_ + - (_Only required if using other AWS features_) - `--unsafe`: Flag for unsafe bundling mode. When set Rundler will skip checking simulation rules (and any `debug_traceCall`). (default: `false`). - - env: *UNSAFE* + - env: _UNSAFE_ - `--mempool_config_path`: Path to the mempool configuration file. (example: `mempool-config.json`, `s3://my-bucket/mempool-config.json`). (default: `None`) - - This path can either be a local file path or an S3 url. If using an S3 url, Make sure your machine has access to this file. - - env: *MEMPOOL_CONFIG_PATH* + - This path can either be a local file path or an S3 url. If using an S3 url, Make sure your machine has access to this file. + - env: _MEMPOOL_CONFIG_PATH_ - See [here](./architecture/pool.md#alternative-mempools-in-preview) for details. - `--entry_point_builders_path`: Path to the entry point builders configuration file (example: `builders.json`, `s3://my-bucket/builders.json`). (default: `None`) - This path can either be a local file path or an S3 url. If using an S3 url, Make sure your machine has access to this file. - - env: *ENTRY_POINT_BUILDERS_PATH* + - env: _ENTRY_POINT_BUILDERS_PATH_ - NOTE: most deployments can ignore this and use the settings below. - See [here](./architecture/builder.md#custom) for details. - `--disable_entry_point_v0_6`: Disable entry point v0.6 support. (default: `false`). - - env: *DISABLE_ENTRY_POINT_V0_6* + - env: _DISABLE_ENTRY_POINT_V0_6_ - `--num_builders_v0_6`: The number of bundle builders to run on entry point v0.6 (default: `1`) - - env: *NUM_BUILDERS_V0_6* + - env: _NUM_BUILDERS_V0_6_ - NOTE: ignored if `entry_point_builders_path` is set - `--disable_entry_point_v0_7`: Disable entry point v0.7 support. (default: `false`). - - env: *DISABLE_ENTRY_POINT_V0_7* + - env: _DISABLE_ENTRY_POINT_V0_7_ - `--num_builders_v0_7`: The number of bundle builders to run on entry point v0.7 (default: `1`) - - env: *NUM_BUILDERS_V0_7* + - env: _NUM_BUILDERS_V0_7_ - NOTE: ignored if `entry_point_builders_path` is set - `--da_gas_tracking_enabled`: Enable the DA gas tracking feature of the mempool (default: `false`) - - env: *DA_GAS_TRACKING_ENABLED* + - env: _DA_GAS_TRACKING_ENABLED_ - `--max_expected_storage_slots`: Optionally set the maximum number of expected storage slots to submit with a conditional transaction. (default: `None`) - - env: *MAX_EXPECTED_STORAGE_SLOTS* + - env: _MAX_EXPECTED_STORAGE_SLOTS_ - `--enabled_aggregators`: List of enabled aggregators. - - env: *ENABLED_AGGREGATORS* + - env: _ENABLED_AGGREGATORS_ - Types: see [aggregator.rs](../bin/rundler/src/cli/aggregator.rs) - `--aggregator_options`: List of aggregator specific options - - env: *ENABLED_AGGREGATORS* + - env: _ENABLED_AGGREGATORS_ - List of KEY=VALUE delimited by ',': i.e. `ENABLED_AGGREGATORS="KEY1=VALUE1,KEY2=VALUE2"` - Options: see [aggregator.rs](../bin/rundler/src/cli/aggregator.rs) - `--provider_client_timeout_seconds`: Timeout in seconds of external provider RPC requests (default: `10`) - - env: *PROVIDER_CLIENT_TIMEOUT_SECONDS* + - env: _PROVIDER_CLIENT_TIMEOUT_SECONDS_ - `--provider_rate_limit_retry_enabled`: Enable retries on rate limit errors - with default backoff settings (default: `false`) - - env: *PROVIDER_RATE_LIMIT_RETRY_ENABLED* + - env: _PROVIDER_RATE_LIMIT_RETRY_ENABLED_ - `--provider_consistency_retry_enabled`: Enable retries on block consistency errors - with default backoff settings (default: `false`) - - env: *PROVIDER_CONSISTENCY_RETRY_ENABLED* + - env: _PROVIDER_CONSISTENCY_RETRY_ENABLED_ +- `--revert_check_call_type`: Enable revert checking with the specified call type. Options: [eth_simulateV1, debug_traceCall, eth_call] (default: `None`) + - eng: _REVERT_CHECK_CALL_TYPE_ ## Metrics Options Options for the metrics server: - `--metrics.port`: Port to listen on for metrics requests. default: `8080`. - - env: *METRICS_PORT* + - env: _METRICS_PORT_ - `--metrics.host`: Host to listen on for metrics requests. default: `0.0.0.0`. - - env: *METRICS_HOST* + - env: _METRICS_HOST_ - `--metrics.tags`: Tags for metrics in the format `key1=value1,key2=value2,...`. - - env: *METRICS_TAGS* + - env: _METRICS_TAGS_ - `--metrics.sample_interval_millis`: Sample interval to use for sampling metrics. default: `1000`. - - env: *METRICS_SAMPLE_INTERVAL_MILLIS* + - env: _METRICS_SAMPLE_INTERVAL_MILLIS_ ## Logging Options Options for logging: - `RUST_LOG` environment variable is used for controlling log level see: [env_logger](https://docs.rs/env_logger/0.10.1/env_logger/#enabling-logging). -Only `level` is supported. + Only `level` is supported. - `--log.file`: Log file. If not provided, logs will be written to stdout. - - env: *LOG_FILE* + - env: _LOG_FILE_ - `--log.json`: If set, logs will be written in JSON format. - - env: *LOG_JSON* - - `--log.otlp_grpc_endpoint`: If set, tracing spans will be forwarded to the provided gRPC OTLP endpoint. - - env: *LOG_OTLP_GRPC_ENDPOINT* + - env: _LOG_JSON_ +- `--log.otlp_grpc_endpoint`: If set, tracing spans will be forwarded to the provided gRPC OTLP endpoint. +- env: _LOG_OTLP_GRPC_ENDPOINT_ ## RPC Options List of command line options for configuring the RPC API. -- `--rpc.port`: Port to listen on for JSON-RPC requests (default: `3000`) - - env: *RPC_PORT* -- `--rpc.host`: Host to listen on for JSON-RPC requests (default: `0.0.0.0`) - - env: *RPC_HOST* -- `--rpc.api`: Which APIs to expose over the RPC interface (default: `eth,rundler`) - - env: *RPC_API* -- `--rpc.timeout_seconds`: Timeout for RPC requests (default: `20`) - - env: *RPC_TIMEOUT_SECONDS* -- `--rpc.max_connections`: Maximum number of concurrent connections (default: `100`) - - env: *RPC_MAX_CONNECTIONS* +- `--rpc.port`: Port to listen on for JSON-RPC requests (default: `3000`) + - env: _RPC_PORT_ +- `--rpc.host`: Host to listen on for JSON-RPC requests (default: `0.0.0.0`) + - env: _RPC_HOST_ +- `--rpc.api`: Which APIs to expose over the RPC interface (default: `eth,rundler`) + - env: _RPC_API_ +- `--rpc.timeout_seconds`: Timeout for RPC requests (default: `20`) + - env: _RPC_TIMEOUT_SECONDS_ +- `--rpc.max_connections`: Maximum number of concurrent connections (default: `100`) + - env: _RPC_MAX_CONNECTIONS_ - `--rpc.corsdomain`: Enable the cors functionality on the server (default: None and therefore corsdomain is disabled). - - env: *RPC_CORSDOMAIN* -- `--rpc.pool_url`: Pool URL for RPC (default: `http://localhost:50051`) - - env: *RPC_POOL_URL* - - *Only required when running in distributed mode* -- `--rpc.builder_url`: Builder URL for RPC (default: `http://localhost:50052`) - - env: *RPC_BUILDER_URL* - - *Only required when running in distributed mode* + - env: _RPC_CORSDOMAIN_ +- `--rpc.pool_url`: Pool URL for RPC (default: `http://localhost:50051`) + - env: _RPC_POOL_URL_ + - _Only required when running in distributed mode_ +- `--rpc.builder_url`: Builder URL for RPC (default: `http://localhost:50052`) + - env: _RPC_BUILDER_URL_ + - _Only required when running in distributed mode_ - `--rpc.permissions_enabled`: True if user operation permissions are enabled on the RPC API (default: `false`) - - env: *RPC_PERMISSIONS_ENABLED + - env: \*RPC_PERMISSIONS_ENABLED - **NOTE: Do not enable this on a public API - for internal, trusted connections only.** ## Pool Options @@ -173,113 +173,114 @@ List of command line options for configuring the RPC API. List of command line options for configuring the Pool. - `--pool.port`: Port to listen on for gRPC requests (default: `50051`) - - env: *POOL_PORT* - - *Only required when running in distributed mode* + - env: _POOL_PORT_ + - _Only required when running in distributed mode_ - `--pool.host`: Host to listen on for gRPC requests (default: `127.0.0.1`) - - env: *POOL_HOST* - - *Only required when running in distributed mode* + - env: _POOL_HOST_ + - _Only required when running in distributed mode_ - `--pool.max_size_in_bytes`: Maximum size in bytes for the pool (default: `500000000`, `0.5 GB`) - - env: *POOL_MAX_SIZE_IN_BYTES* + - env: _POOL_MAX_SIZE_IN_BYTES_ - `--pool.same_sender_mempool_count`: Maximum number of user operations for an unstaked sender (default: `4`) - - env: *POOL_SAME_SENDER_MEMPOOL_COUNT* + - env: _POOL_SAME_SENDER_MEMPOOL_COUNT_ - `--pool.min_replacement_fee_increase_percentage`: Minimum replacement fee increase percentage (default: `10`) - - env: *POOL_MIN_REPLACEMENT_FEE_INCREASE_PERCENTAGE* + - env: _POOL_MIN_REPLACEMENT_FEE_INCREASE_PERCENTAGE_ - `--pool.blocklist_path`: Path to a blocklist file (e.g `blocklist.json`, `s3://my-bucket/blocklist.json`) - - env: *POOL_BLOCKLIST_PATH* - - This path can either be a local file path or an S3 url. If using an S3 url, Make sure your machine has access to this file. + - env: _POOL_BLOCKLIST_PATH_ + - This path can either be a local file path or an S3 url. If using an S3 url, Make sure your machine has access to this file. - See [here](./architecture/pool.md#allowlistblocklist) for details. - `--pool.allowlist_path`: Path to an allowlist file (e.g `allowlist.json`, `s3://my-bucket/allowlist.json`) - - env: *POOL_ALLOWLIST_PATH* - - This path can either be a local file path or an S3 url. If using an S3 url, Make sure your machine has access to this file. + - env: _POOL_ALLOWLIST_PATH_ + - This path can either be a local file path or an S3 url. If using an S3 url, Make sure your machine has access to this file. - See [here](./architecture/pool.md#allowlistblocklist) for details. - `--pool.chain_poll_interval_millis`: Interval at which the pool polls an Eth node for new blocks (default: `100`) - - env: *POOL_CHAIN_POLL_INTERVAL_MILLIS* + - env: _POOL_CHAIN_POLL_INTERVAL_MILLIS_ - `--pool.chain_sync_max_retries`: The amount of times to retry syncing the chain before giving up and waiting for the next block (default: `5`) - - env: *POOL_CHAIN_SYNC_MAX_RETRIES* + - env: _POOL_CHAIN_SYNC_MAX_RETRIES_ - `--pool.paymaster_tracking_enabled`: Boolean field that sets whether the pool server starts with paymaster tracking enabled (default: `true`) - - env: *POOL_PAYMASTER_TRACKING_ENABLED* + - env: _POOL_PAYMASTER_TRACKING_ENABLED_ - `--pool.paymaster_cache_length`: Length of the paymaster cache (default: `10_000`) - - env: *POOL_PAYMASTER_CACHE_LENGTH* + - env: _POOL_PAYMASTER_CACHE_LENGTH_ - `--pool.reputation_tracking_enabled`: Boolean field that sets whether the pool server starts with reputation tracking enabled (default: `true`) - - env: *POOL_REPUTATION_TRACKING_ENABLED* + - 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* + - env: _POOL_DROP_MIN_NUM_BLOCKS_ - `--pool.max_time_in_pool_secs`: The maximum amount of time a UO is allowed to be in the mempool, in seconds. (default: `None`) - - env: *POOL_MAX_TIME_IN_POOL_SECS* + - env: _POOL_MAX_TIME_IN_POOL_SECS_ +- `--pool.disable_revert_check`: Disable the pool's revert check. (default: `false`) + - env: _POOL_DISABLE_REVERT_CHECK_ ## Builder Options List of command line options for configuring the Builder. - `--builder.port`: Port to listen on for gRPC requests (default: `50052`) - - env: *BUILDER_PORT* - - *Only required when running in distributed mode* + - env: _BUILDER_PORT_ + - _Only required when running in distributed mode_ - `--builder.host`: Host to listen on for gRPC requests (default: `127.0.0.1`) - - env: *BUILDER_HOST* - - *Only required when running in distributed mode* + - env: _BUILDER_HOST_ + - _Only required when running in distributed mode_ - `--builder.max_bundle_size`: Maximum number of ops to include in one bundle (default: `128`) - - env: *BUILDER_MAX_BUNDLE_SIZE* + - env: _BUILDER_MAX_BUNDLE_SIZE_ - `--builder.max_blocks_to_wait_for_mine`: After submitting a bundle transaction, the maximum number of blocks to wait for that transaction to mine before trying to resend with higher gas fees (default: `2`) - - env: *BUILDER_MAX_BLOCKS_TO_WAIT_FOR_MINE* + - env: _BUILDER_MAX_BLOCKS_TO_WAIT_FOR_MINE_ - `--builder.replacement_fee_percent_increase`: Percentage amount to increase gas fees when retrying a transaction after it failed to mine (default: `10`) - - env: *BUILDER_REPLACEMENT_FEE_PERCENT_INCREASE* + - env: _BUILDER_REPLACEMENT_FEE_PERCENT_INCREASE_ - `--builder.max_cancellation_fee_increases`: Maximum number of cancellation fee increases to attempt (default: `15`) - - env: *BUILDER_MAX_CANCELLATION_FEE_INCREASES* + - env: _BUILDER_MAX_CANCELLATION_FEE_INCREASES_ - `--builder.max_replacement_underpriced_blocks`: The maximum number of blocks to wait in a replacement underpriced state before issuing a cancellation transaction (default: `20`) - - env: *BUILDER_MAX_REPLACEMENT_UNDERPRICED_BLOCKS* + - env: _BUILDER_MAX_REPLACEMENT_UNDERPRICED_BLOCKS_ - `--builder.sender`: Choice of what sender type to use for transaction submission. (default: `raw`, options: `raw`, `flashbots`, `polygon_bloxroute`) - - env: *BUILDER_SENDER* + - env: _BUILDER_SENDER_ - `--builder.submit_url`: Only used if builder.sender == "raw." If present, the URL of the ETH provider that will be used to send transactions. Defaults to the value of `node_http`. - - env: *BUILDER_SUBMIT_URL* + - env: _BUILDER_SUBMIT_URL_ - `--builder.use_conditional_rpc`: Only used if builder.sender == "raw." Use `eth_sendRawTransactionConditional` when submitting. (default: `false`) - - env: *BUILDER_USE_CONDITIONAL_RPC* + - env: _BUILDER_USE_CONDITIONAL_RPC_ - `--builder.flashbots_relay_builders`: Only used if builder.sender == "flashbots." Additional builders to send bundles to through the Flashbots relay RPC (comma-separated). List of builders that the Flashbots RPC supports can be found [here](https://docs.flashbots.net/flashbots-auction/advanced/rpc-endpoint#eth_sendprivatetransaction). (default: `flashbots`) - - env: *BUILDER_FLASHBOTS_RELAY_BUILDERS* + - env: _BUILDER_FLASHBOTS_RELAY_BUILDERS_ - `--builder.flashbots_relay_auth_key`: Only used/required if builder.sender == "flashbots." Authorization key to use with the flashbots relay. See [here](https://docs.flashbots.net/flashbots-auction/advanced/rpc-endpoint#authentication) for more info. (default: None) - - env: *BUILDER_FLASHBOTS_RELAY_AUTH_KEY* + - env: _BUILDER_FLASHBOTS_RELAY_AUTH_KEY_ - `--builder.bloxroute_auth_header`: Only used/required if builder.sender == "polygon_bloxroute." If using the bloxroute transaction sender on Polygon, this is the auth header to supply with the requests. (default: None) - - env: *BUILDER_BLOXROUTE_AUTH_HEADER* + - env: _BUILDER_BLOXROUTE_AUTH_HEADER_ - `--builder.pool_url`: If running in distributed mode, the URL of the pool server to use. - - env: *BUILDER_POOL_URL* - - *Only required when running in distributed mode* + - env: _BUILDER_POOL_URL_ + - _Only required when running in distributed mode_ ## Signer Options - `--signer.private_keys`: Private keys to use for signing transactions, separated by `,` - - env: *SIGNER_PRIVATE_KEYS* + - env: _SIGNER_PRIVATE_KEYS_ - `--signer.mnemonic`: Mnemonic to use for signing transactions - - env: *SIGNER_MNEMONIC* -- `--signer.aws_kms_key_ids`: AWS KMS key IDs to use for signing transactions, separated by `,`. - - env: *SIGNER_AWS_KMS_KEY_IDS* + - env: _SIGNER_MNEMONIC_ +- `--signer.aws_kms_key_ids`: AWS KMS key IDs to use for signing transactions, separated by `,`. + - env: _SIGNER_AWS_KMS_KEY_IDS_ - To enable signer locking see `SIGNER_ENABLE_KMS_LOCKING`. - `--signer.aws_kms_grouped_keys`: AWS KMS key ids grouped to keys in `aws_kms_key_ids` Separated by `,`. Groups are made based on the number of signers required. There must be enough signers to make a full group for every entry in `aws_kms_key_ids`. - - env: *SIGNER_AWS_KMS_GROUPED_KEYS* + - env: _SIGNER_AWS_KMS_GROUPED_KEYS_ - `--signer.enable_kms_locking`: True if keys should be locked before use. Only applies to keys in `aws_kms_key_ids`. - - env: *SIGNER_ENABLE_KMS_LOCKING* + - env: _SIGNER_ENABLE_KMS_LOCKING_ - `--signer.redis_uri`: Redis URI to use for KMS leasing (default: `""`) - - env: *SIGNER_REDIS_URI* - -*Only required when SIGNER_ENABLE_KMS_LOCKING is set* + - env: _SIGNER_REDIS_URI_ -_Only required when SIGNER_ENABLE_KMS_LOCKING is set_ - `--signer.redis_lock_ttl_millis`: Redis lock TTL in milliseconds (default: `60000`) - - env: *SIGNER_REDIS_LOCK_TTL_MILLIS* - - *Only required when SIGNER_ENABLE_KMS_LOCKING is set* + - env: _SIGNER_REDIS_LOCK_TTL_MILLIS_ + - _Only required when SIGNER_ENABLE_KMS_LOCKING is set_ - `--signer.enable_kms_funding`: Whether to enable kms funding from `aws_kms_key_ids` to the key ids in `aws_kms_key_groups`. (default: `false`) - - env: *SIGNER_ENABLE_KMS_FUNDING* + - env: _SIGNER_ENABLE_KMS_FUNDING_ - `--signer.fund_below`: If KMS funding is enabled, this is the signer balance value below which to trigger a funding event - - env: *SIGNER_FUND_BELOW* + - env: _SIGNER_FUND_BELOW_ - `--signer.fund_to`: If KMS funding is enabled, this is the signer balance to fund to during a funding event - - env: *SIGNER_FUND_TO* + - env: _SIGNER_FUND_TO_ - `--signer.funding_txn_poll_interval_ms`: During funding, this is the poll interval for transaction status (default: `1000`) - - env: *SIGNER_FUNDING_TXN_POLL_INTERVAL_MS* + - env: _SIGNER_FUNDING_TXN_POLL_INTERVAL_MS_ - `--signer.funding_txn_poll_max_retries`: During funding, this is the maximum amount of time to poll for transaction status before abandoning (default: `20`) - - env: *SIGNER_FUNDING_TXN_POLL_MAX_RETRIES* + - env: _SIGNER_FUNDING_TXN_POLL_MAX_RETRIES_ - `--signer.funding_txn_priority_fee_multiplier`: During funding, this is the multiplier to apply to the network priority fee (default: `2.0`) - - env: *SIGNER_FUNDING_TXN_PRIORITY_FEE_MULTIPLIER* + - env: _SIGNER_FUNDING_TXN_PRIORITY_FEE_MULTIPLIER_ - `--signer.funding_txn_base_fee_multiplier`: During funding, this is the multiplier to apply to the network base fee (default: `2.0`) - - env: *SIGNER_FUNDING_TXN_BASE_FEE_MULTIPLIER* + - env: _SIGNER_FUNDING_TXN_BASE_FEE_MULTIPLIER_ ### Signing schemes -Rundler supports multiple ways to sign bundle transactions. In configuration precedence order: +Rundler supports multiple ways to sign bundle transactions. In configuration precedence order: 1. KMS locked master key with funded sub-keys: `--signer.enable_kms_funding` 2. Private keys: `--signer.private_keys` @@ -297,8 +298,8 @@ Locking uses Redis and thus a Redis URL must be provided to Rundler for key leas If `--signer.enable_kms_funding` is set this scheme will be enabled. It will look for subkeys in the following precedence order: 1. `aws_kms_grouped_keys`: Must have enough signers to make a full group for each `aws_kms_key_ids`. Group size is based on number of signers requested. - - If locking is enabled, once a funding KMS key is locked, the corresponding group is used for all subkeys. - - Else, the first group is always used + - If locking is enabled, once a funding KMS key is locked, the corresponding group is used for all subkeys. + - Else, the first group is always used 2. `private_keys`: Private keys for the subkeys. The same list applies regardless of which KMS key is locked. 3. `mnemonic`: Supports a `mnemonic` from which multiple subkeys can be derived. The same `mnemonic` applies regardless of which KMS key is locked diff --git a/test/spec-tests/local/docker-compose.yml b/test/spec-tests/local/docker-compose.yml index f910af4e0..54894f35b 100644 --- a/test/spec-tests/local/docker-compose.yml +++ b/test/spec-tests/local/docker-compose.yml @@ -1,14 +1,13 @@ services: geth: - image: ethereum/client-go:release-1.14 - ports: [ '8545:8545' ] + image: ethereum/client-go:release-1.16 + ports: ["8545:8545"] command: --verbosity 1 --http.vhosts '*,localhost,host.docker.internal' --http --http.api eth,net,web3,debug --http.corsdomain '*' --http.addr "0.0.0.0" - --networkid 1337 --dev --dev.period 0 --allow-insecure-unlock