diff --git a/bin/rundler/chain_specs/avax.toml b/bin/rundler/chain_specs/avax.toml new file mode 100644 index 000000000..f8b1001a0 --- /dev/null +++ b/bin/rundler/chain_specs/avax.toml @@ -0,0 +1,7 @@ +name = "Avax" +id = 43114 + +# Intrinsic cost + overhead of non-reentry storage without refund +transaction_intrinsic_gas = "0x5DC0" # 24_000 +# Extra cost of a deploy without refunds +per_user_op_deploy_overhead_gas = "0x4E20" # 20_000 diff --git a/bin/rundler/chain_specs/avax_fuji.toml b/bin/rundler/chain_specs/avax_fuji.toml new file mode 100644 index 000000000..ac1a0c81b --- /dev/null +++ b/bin/rundler/chain_specs/avax_fuji.toml @@ -0,0 +1,4 @@ +base = "avax" + +name = "Avax-Fuji" +id = 43113 diff --git a/bin/rundler/src/cli/builder.rs b/bin/rundler/src/cli/builder.rs index 006907c61..e743399b7 100644 --- a/bin/rundler/src/cli/builder.rs +++ b/bin/rundler/src/cli/builder.rs @@ -11,7 +11,7 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use std::{net::SocketAddr, time::Duration}; +use std::net::SocketAddr; use anyhow::{bail, Context}; use clap::Args; @@ -354,7 +354,6 @@ impl BuilderArgs { bundle_priority_fee_overhead_percent: common.bundle_priority_fee_overhead_percent, priority_fee_mode, sender_args, - eth_poll_interval: Duration::from_millis(common.eth_poll_interval_millis), sim_settings: common.try_into()?, max_blocks_to_wait_for_mine: self.max_blocks_to_wait_for_mine, replacement_fee_percent_increase: self.replacement_fee_percent_increase, @@ -442,7 +441,7 @@ pub async fn run( let task_args = builder_args .to_args( - chain_spec, + chain_spec.clone(), &common_args, Some(format_socket_addr(&builder_args.host, builder_args.port).parse()?), ) @@ -451,7 +450,7 @@ pub async fn run( let pool = connect_with_retries_shutdown( "op pool from builder", &pool_url, - RemotePoolClient::connect, + |url| RemotePoolClient::connect(url, chain_spec.clone()), tokio::signal::ctrl_c(), ) .await?; diff --git a/bin/rundler/src/cli/chain_spec.rs b/bin/rundler/src/cli/chain_spec.rs index 09c9b66a3..f8e0d2bff 100644 --- a/bin/rundler/src/cli/chain_spec.rs +++ b/bin/rundler/src/cli/chain_spec.rs @@ -120,5 +120,7 @@ define_hardcoded_chain_specs!( arbitrum, arbitrum_sepolia, polygon, - polygon_amoy + polygon_amoy, + avax, + avax_fuji ); diff --git a/bin/rundler/src/cli/mod.rs b/bin/rundler/src/cli/mod.rs index 02ea790be..5e5d7c1ec 100644 --- a/bin/rundler/src/cli/mod.rs +++ b/bin/rundler/src/cli/mod.rs @@ -248,17 +248,6 @@ pub struct CommonArgs { )] pre_verification_gas_accept_percent: u64, - /// Interval at which the builder polls an Eth node for new blocks and - /// mined transactions. - #[arg( - long = "eth_poll_interval_millis", - name = "eth_poll_interval_millis", - env = "ETH_POLL_INTERVAL_MILLIS", - default_value = "100", - global = true - )] - pub eth_poll_interval_millis: u64, - #[arg( long = "aws_region", name = "aws_region", diff --git a/bin/rundler/src/cli/pool.rs b/bin/rundler/src/cli/pool.rs index 53c6cf992..53938d27f 100644 --- a/bin/rundler/src/cli/pool.rs +++ b/bin/rundler/src/cli/pool.rs @@ -89,6 +89,27 @@ pub struct PoolArgs { )] pub allowlist_path: Option, + /// Interval at which the pool polls an Eth node for new blocks + #[arg( + long = "pool.chain_poll_interval_millis", + name = "pool.chain_poll_interval_millis", + env = "POOL_CHAIN_POLL_INTERVAL_MILLIS", + default_value = "100", + global = true + )] + pub chain_poll_interval_millis: u64, + + /// The amount of times to retry syncing the chain before giving up and + /// waiting for the next block. + #[arg( + long = "pool.chain_sync_max_retries", + name = "pool.chain_sync_max_retries", + env = "POOL_CHAIN_SYNC_MAX_RETRIES", + default_value = "5", + global = true + )] + pub chain_sync_max_retries: u64, + #[arg( long = "pool.chain_history_size", name = "pool.chain_history_size", @@ -234,7 +255,8 @@ impl PoolArgs { .node_http .clone() .context("pool requires node_http arg")?, - http_poll_interval: Duration::from_millis(common.eth_poll_interval_millis), + chain_poll_interval: Duration::from_millis(self.chain_poll_interval_millis), + chain_max_sync_retries: self.chain_sync_max_retries, pool_configs, remote_address, chain_update_channel_capacity: self.chain_update_channel_capacity.unwrap_or(1024), diff --git a/bin/rundler/src/cli/rpc.rs b/bin/rundler/src/cli/rpc.rs index 3e6cbef1c..7188479f3 100644 --- a/bin/rundler/src/cli/rpc.rs +++ b/bin/rundler/src/cli/rpc.rs @@ -154,7 +154,7 @@ pub async fn run( } = rpc_args; let task_args = rpc_args.to_args( - chain_spec, + chain_spec.clone(), &common_args, (&common_args).try_into()?, (&common_args).into(), @@ -165,7 +165,7 @@ pub async fn run( let pool = connect_with_retries_shutdown( "op pool from rpc", &pool_url, - RemotePoolClient::connect, + |url| RemotePoolClient::connect(url, chain_spec.clone()), tokio::signal::ctrl_c(), ) .await?; diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index ea4def832..656f8be92 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -471,8 +471,7 @@ where let mut context = ProposalContext::::new(); let mut paymasters_to_reject = Vec::::new(); - let ov = UO::gas_overheads(); - let mut gas_spent = ov.transaction_gas_overhead; + let mut gas_spent = self.settings.chain_spec.transaction_intrinsic_gas; let mut constructed_bundle_size = BUNDLE_BYTE_OVERHEAD; for (po, simulation) in ops_with_simulations { let op = po.clone().uo; @@ -1243,7 +1242,7 @@ impl ProposalContext { self.iter_ops_with_simulations() .map(|sim_op| gas::user_operation_gas_limit(chain_spec, &sim_op.op, false)) .fold(U256::zero(), |acc, i| acc + i) - + UO::gas_overheads().transaction_gas_overhead + + chain_spec.transaction_intrinsic_gas } fn iter_ops_with_simulations(&self) -> impl Iterator> + '_ { @@ -1423,7 +1422,7 @@ mod tests { use rundler_sim::MockSimulator; use rundler_types::{ pool::{MockPool, SimulationViolation}, - v0_6::UserOperation, + v0_6::{UserOperation, ENTRY_POINT_INNER_GAS_OVERHEAD}, UserOperation as UserOperationTrait, ValidTimeRange, }; @@ -1444,13 +1443,14 @@ mod tests { }]) .await; - let ov = UserOperation::gas_overheads(); + let cs = ChainSpec::default(); + let expected_gas = math::increase_by_percent( op.pre_verification_gas + op.verification_gas_limit * 2 + op.call_gas_limit - + ov.bundle_transaction_gas_buffer - + ov.transaction_gas_overhead, + + cs.transaction_intrinsic_gas + + ENTRY_POINT_INNER_GAS_OVERHEAD, BUNDLE_TRANSACTION_GAS_OVERHEAD_PERCENT, ); diff --git a/crates/builder/src/task.rs b/crates/builder/src/task.rs index 597d3c99e..39c23c16e 100644 --- a/crates/builder/src/task.rs +++ b/crates/builder/src/task.rs @@ -83,8 +83,6 @@ pub struct Args { pub priority_fee_mode: PriorityFeeMode, /// Sender to be used by the builder pub sender_args: TransactionSenderArgs, - /// RPC node poll interval - pub eth_poll_interval: Duration, /// Operation simulation settings pub sim_settings: SimulationSettings, /// Maximum number of blocks to wait for a transaction to be mined @@ -131,8 +129,7 @@ where P: Pool + Clone, { async fn run(mut self: Box, shutdown_token: CancellationToken) -> anyhow::Result<()> { - let provider = - rundler_provider::new_provider(&self.args.rpc_url, Some(self.args.eth_poll_interval))?; + let provider = rundler_provider::new_provider(&self.args.rpc_url, None)?; let submit_provider = if let TransactionSenderArgs::Raw(args) = &self.args.sender_args { Some(rundler_provider::new_provider(&args.submit_url, None)?) } else { diff --git a/crates/pool/src/chain.rs b/crates/pool/src/chain.rs index d0bafedef..e570ee59e 100644 --- a/crates/pool/src/chain.rs +++ b/crates/pool/src/chain.rs @@ -34,9 +34,10 @@ use tokio::{ select, sync::{broadcast, Semaphore}, task::JoinHandle, + time, }; use tokio_util::sync::CancellationToken; -use tracing::{error, info, warn}; +use tracing::{debug, info, warn}; const MAX_LOAD_OPS_CONCURRENCY: usize = 64; @@ -110,6 +111,7 @@ pub(crate) struct Settings { pub(crate) history_size: u64, pub(crate) poll_interval: Duration, pub(crate) entry_point_addresses: HashMap, + pub(crate) max_sync_retries: u64, } #[derive(Debug)] @@ -201,13 +203,28 @@ impl Chain

{ ) .await; block_hash = hash; - let update = self.sync_to_block(block).await; - match update { - Ok(update) => return update, - Err(error) => { - error!("Failed to update chain at block {block_hash:?}. Will try again at next block. {error:?}"); + + for i in 0..=self.settings.max_sync_retries { + if i > 0 { + ChainMetrics::increment_sync_retries(); + } + + let update = self.sync_to_block(block.clone()).await; + match update { + Ok(update) => return update, + Err(error) => { + debug!("Failed to update chain at block {block_hash:?}: {error:?}"); + } } + + time::sleep(self.settings.poll_interval).await; } + + warn!( + "Failed to update chain at block {:?} after {} retries. Abandoning sync.", + block_hash, self.settings.max_sync_retries + ); + ChainMetrics::increment_sync_abandoned(); } } @@ -665,6 +682,14 @@ impl ChainMetrics { fn increment_total_reorg_depth(depth: u64) { metrics::counter!("op_pool_chain_total_reorg_depth").increment(depth); } + + fn increment_sync_retries() { + metrics::counter!("op_pool_chain_sync_retries").increment(1); + } + + fn increment_sync_abandoned() { + metrics::counter!("op_pool_chain_sync_abandoned").increment(1); + } } #[cfg(test)] @@ -1366,6 +1391,7 @@ mod tests { (ENTRY_POINT_ADDRESS_V0_6, EntryPointVersion::V0_6), (ENTRY_POINT_ADDRESS_V0_7, EntryPointVersion::V0_7), ]), + max_sync_retries: 1, }, ); (chain, controller) diff --git a/crates/pool/src/server/remote/client.rs b/crates/pool/src/server/remote/client.rs index 3d64e4ca7..faf8e13f3 100644 --- a/crates/pool/src/server/remote/client.rs +++ b/crates/pool/src/server/remote/client.rs @@ -13,6 +13,7 @@ use std::{pin::Pin, str::FromStr}; +use anyhow::Context; use ethers::types::{Address, H256}; use futures_util::Stream; use rundler_task::{ @@ -20,6 +21,7 @@ use rundler_task::{ server::{HealthCheck, ServerStatus}, }; use rundler_types::{ + chain::ChainSpec, pool::{ NewHead, PaymasterMetadata, Pool, PoolError, PoolOperation, PoolResult, Reputation, ReputationStatus, StakeStatus, @@ -48,7 +50,7 @@ use super::protos::{ DebugDumpMempoolRequest, DebugDumpPaymasterBalancesRequest, DebugDumpReputationRequest, DebugSetReputationRequest, GetOpsRequest, GetReputationStatusRequest, GetStakeStatusRequest, RemoveOpsRequest, ReputationStatus as ProtoReputationStatus, SubscribeNewHeadsRequest, - SubscribeNewHeadsResponse, UpdateEntitiesRequest, + SubscribeNewHeadsResponse, TryUoFromProto, UpdateEntitiesRequest, }; /// Remote pool client @@ -56,17 +58,19 @@ use super::protos::{ /// Used to submit requests to a remote pool server. #[derive(Debug, Clone)] pub struct RemotePoolClient { + chain_spec: ChainSpec, op_pool_client: OpPoolClient, op_pool_health: HealthClient, } impl RemotePoolClient { /// Connect to a remote pool server, returning a client for submitting requests. - pub async fn connect(url: String) -> anyhow::Result { + pub async fn connect(url: String, chain_spec: ChainSpec) -> anyhow::Result { let op_pool_client = OpPoolClient::connect(url.clone()).await?; let op_pool_health = HealthClient::new(Channel::builder(Uri::from_str(&url)?).connect().await?); Ok(Self { + chain_spec, op_pool_client, op_pool_health, }) @@ -186,7 +190,10 @@ impl Pool for RemotePoolClient { Some(get_ops_response::Result::Success(s)) => s .ops .into_iter() - .map(PoolOperation::try_from) + .map(|proto_uo| { + PoolOperation::try_uo_from_proto(proto_uo, &self.chain_spec) + .context("should convert proto uo to pool operation") + }) .map(|res| res.map_err(PoolError::from)) .collect(), Some(get_ops_response::Result::Failure(f)) => Err(f.try_into()?), @@ -209,9 +216,13 @@ impl Pool for RemotePoolClient { .result; match res { - Some(get_op_by_hash_response::Result::Success(s)) => { - Ok(s.op.map(PoolOperation::try_from).transpose()?) - } + Some(get_op_by_hash_response::Result::Success(s)) => Ok(s + .op + .map(|proto_uo| { + PoolOperation::try_uo_from_proto(proto_uo, &self.chain_spec) + .context("should convert proto uo to pool operation") + }) + .transpose()?), Some(get_op_by_hash_response::Result::Failure(e)) => match e.error { Some(_) => Err(e.try_into()?), None => Err(PoolError::Other(anyhow::anyhow!( @@ -380,7 +391,10 @@ impl Pool for RemotePoolClient { Some(debug_dump_mempool_response::Result::Success(s)) => s .ops .into_iter() - .map(PoolOperation::try_from) + .map(|proto_uo| { + PoolOperation::try_uo_from_proto(proto_uo, &self.chain_spec) + .context("should convert proto uo to pool operation") + }) .map(|res| res.map_err(PoolError::from)) .collect(), Some(debug_dump_mempool_response::Result::Failure(f)) => Err(f.try_into()?), diff --git a/crates/pool/src/server/remote/protos.rs b/crates/pool/src/server/remote/protos.rs index e2f696e0a..f4ba84133 100644 --- a/crates/pool/src/server/remote/protos.rs +++ b/crates/pool/src/server/remote/protos.rs @@ -15,6 +15,7 @@ use anyhow::{anyhow, Context}; use ethers::types::{Address, H256}; use rundler_task::grpc::protos::{from_bytes, ConversionError, ToProtoBytes}; use rundler_types::{ + chain::ChainSpec, pool::{ NewHead as PoolNewHead, PaymasterMetadata as PoolPaymasterMetadata, PoolOperation, Reputation as PoolReputation, ReputationStatus as PoolReputationStatus, @@ -60,10 +61,15 @@ impl From<&v0_6::UserOperation> for UserOperation { } } -impl TryFrom for v0_6::UserOperation { - type Error = ConversionError; +pub trait TryUoFromProto: Sized { + fn try_uo_from_proto(value: T, chain_spec: &ChainSpec) -> Result; +} - fn try_from(op: UserOperationV06) -> Result { +impl TryUoFromProto for v0_6::UserOperation { + fn try_uo_from_proto( + op: UserOperationV06, + _chain_spec: &ChainSpec, + ) -> Result { Ok(v0_6::UserOperation { sender: from_bytes(&op.sender)?, nonce: from_bytes(&op.nonce)?, @@ -107,13 +113,13 @@ impl From<&v0_7::UserOperation> for UserOperation { } } -impl TryFrom for v0_7::UserOperation { - type Error = ConversionError; - - fn try_from(op: UserOperationV07) -> Result { +impl TryUoFromProto for v0_7::UserOperation { + fn try_uo_from_proto( + op: UserOperationV07, + chain_spec: &ChainSpec, + ) -> Result { let mut builder = v0_7::UserOperationBuilder::new( - from_bytes(&op.entry_point)?, - op.chain_id, + chain_spec, v0_7::UserOperationRequiredFields { sender: from_bytes(&op.sender)?, nonce: from_bytes(&op.nonce)?, @@ -144,17 +150,22 @@ impl TryFrom for v0_7::UserOperation { } } -impl TryFrom for UserOperationVariant { - type Error = ConversionError; - - fn try_from(op: UserOperation) -> Result { +impl TryUoFromProto for UserOperationVariant { + fn try_uo_from_proto( + op: UserOperation, + chain_spec: &ChainSpec, + ) -> Result { let op = op .uo .expect("User operation should contain user operation oneof"); match op { - user_operation::Uo::V06(op) => Ok(UserOperationVariant::V0_6(op.try_into()?)), - user_operation::Uo::V07(op) => Ok(UserOperationVariant::V0_7(op.try_into()?)), + user_operation::Uo::V06(op) => Ok(UserOperationVariant::V0_6( + v0_6::UserOperation::try_uo_from_proto(op, chain_spec)?, + )), + user_operation::Uo::V07(op) => Ok(UserOperationVariant::V0_7( + v0_7::UserOperation::try_uo_from_proto(op, chain_spec)?, + )), } } } @@ -349,11 +360,12 @@ impl From<&PoolOperation> for MempoolOp { } pub const MISSING_USER_OP_ERR_STR: &str = "Mempool op should contain user operation"; -impl TryFrom for PoolOperation { - type Error = anyhow::Error; - - fn try_from(op: MempoolOp) -> Result { - let uo = op.uo.context(MISSING_USER_OP_ERR_STR)?.try_into()?; +impl TryUoFromProto for PoolOperation { + fn try_uo_from_proto(op: MempoolOp, chain_spec: &ChainSpec) -> Result { + let uo = UserOperationVariant::try_uo_from_proto( + op.uo.context(MISSING_USER_OP_ERR_STR)?, + chain_spec, + )?; let entry_point = from_bytes(&op.entry_point)?; diff --git a/crates/pool/src/server/remote/server.rs b/crates/pool/src/server/remote/server.rs index 6ebc87002..bd2071bb9 100644 --- a/crates/pool/src/server/remote/server.rs +++ b/crates/pool/src/server/remote/server.rs @@ -24,8 +24,9 @@ use ethers::types::{Address, H256}; use futures_util::StreamExt; use rundler_task::grpc::{metrics::GrpcMetricsLayer, protos::from_bytes}; use rundler_types::{ + chain::ChainSpec, pool::{Pool, Reputation}, - EntityUpdate, UserOperationId, + EntityUpdate, UserOperationId, UserOperationVariant, }; use tokio::{sync::mpsc, task::JoinHandle}; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -51,7 +52,7 @@ use super::protos::{ GetStakeStatusResponse, GetStakeStatusSuccess, GetSupportedEntryPointsRequest, GetSupportedEntryPointsResponse, MempoolOp, RemoveOpByIdRequest, RemoveOpByIdResponse, RemoveOpByIdSuccess, RemoveOpsRequest, RemoveOpsResponse, RemoveOpsSuccess, ReputationStatus, - SubscribeNewHeadsRequest, SubscribeNewHeadsResponse, UpdateEntitiesRequest, + SubscribeNewHeadsRequest, SubscribeNewHeadsResponse, TryUoFromProto, UpdateEntitiesRequest, UpdateEntitiesResponse, UpdateEntitiesSuccess, OP_POOL_FILE_DESCRIPTOR_SET, }; use crate::server::local::LocalPoolHandle; @@ -59,13 +60,13 @@ use crate::server::local::LocalPoolHandle; const MAX_REMOTE_BLOCK_SUBSCRIPTIONS: usize = 32; pub(crate) async fn spawn_remote_mempool_server( - chain_id: u64, + chain_spec: ChainSpec, local_pool: LocalPoolHandle, addr: SocketAddr, shutdown_token: CancellationToken, ) -> anyhow::Result>> { // gRPC server - let pool_impl = OpPoolImpl::new(chain_id, local_pool); + let pool_impl = OpPoolImpl::new(chain_spec, local_pool); let op_pool_server = OpPoolServer::new(pool_impl); let reflection_service = tonic_reflection::server::Builder::configure() .register_encoded_file_descriptor_set(OP_POOL_FILE_DESCRIPTOR_SET) @@ -93,15 +94,15 @@ pub(crate) async fn spawn_remote_mempool_server( } struct OpPoolImpl { - chain_id: u64, + chain_spec: ChainSpec, local_pool: LocalPoolHandle, num_block_subscriptions: Arc, } impl OpPoolImpl { - pub(crate) fn new(chain_id: u64, local_pool: LocalPoolHandle) -> Self { + pub(crate) fn new(chain_spec: ChainSpec, local_pool: LocalPoolHandle) -> Self { Self { - chain_id, + chain_spec, local_pool, num_block_subscriptions: Arc::new(AtomicUsize::new(0)), } @@ -125,7 +126,7 @@ impl OpPool for OpPoolImpl { ) -> Result> { let resp = match self.local_pool.get_supported_entry_points().await { Ok(entry_points) => GetSupportedEntryPointsResponse { - chain_id: self.chain_id, + chain_id: self.chain_spec.id, entry_points: entry_points .into_iter() .map(|ep| ep.as_bytes().to_vec()) @@ -146,9 +147,10 @@ impl OpPool for OpPoolImpl { let proto_op = req .op .ok_or_else(|| Status::invalid_argument("Operation is required in AddOpRequest"))?; - let uo = proto_op.try_into().map_err(|e| { - Status::invalid_argument(format!("Failed to convert to UserOperation: {e}")) - })?; + let uo = + UserOperationVariant::try_uo_from_proto(proto_op, &self.chain_spec).map_err(|e| { + Status::invalid_argument(format!("Failed to convert to UserOperation: {e}")) + })?; let resp = match self.local_pool.add_op(ep, uo).await { Ok(hash) => AddOpResponse { diff --git a/crates/pool/src/task.rs b/crates/pool/src/task.rs index bfe905384..83187926d 100644 --- a/crates/pool/src/task.rs +++ b/crates/pool/src/task.rs @@ -46,8 +46,10 @@ pub struct Args { pub unsafe_mode: bool, /// HTTP URL for the full node. pub http_url: String, - /// Poll interval for full node requests. - pub http_poll_interval: Duration, + /// Interval to poll the chain for updates. + pub chain_poll_interval: Duration, + /// Number of times to retry a block sync at the `chain_poll_interval` before abandoning + pub chain_max_sync_retries: u64, /// Pool configurations. pub pool_configs: Vec, /// Address to bind the remote mempool server to, if any. @@ -75,7 +77,8 @@ impl Task for PoolTask { // create chain let chain_settings = chain::Settings { history_size: self.args.chain_spec.chain_history_size, - poll_interval: self.args.http_poll_interval, + poll_interval: self.args.chain_poll_interval, + max_sync_retries: self.args.chain_max_sync_retries, entry_point_addresses: self .args .pool_configs @@ -85,7 +88,7 @@ impl Task for PoolTask { }; let provider = rundler_provider::new_provider( &self.args.http_url, - Some(self.args.http_poll_interval), + Some(self.args.chain_poll_interval), )?; let chain = Chain::new(provider.clone(), chain_settings); let (update_sender, _) = broadcast::channel(self.args.chain_update_channel_capacity); @@ -133,7 +136,7 @@ impl Task for PoolTask { let remote_handle = match self.args.remote_address { Some(addr) => { spawn_remote_mempool_server( - self.args.chain_spec.id, + self.args.chain_spec.clone(), pool_handle, addr, shutdown_token, diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 6307787ba..931873e33 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -32,4 +32,5 @@ futures-util.workspace = true [dev-dependencies] mockall.workspace = true rundler-provider = { path = "../provider", features = ["test-utils"]} +rundler-sim = { path = "../sim", features = ["test-utils"] } rundler-types= { path = "../types", features = ["test-utils"]} diff --git a/crates/rpc/src/eth/api.rs b/crates/rpc/src/eth/api.rs index 0cd37be96..1a2e4de38 100644 --- a/crates/rpc/src/eth/api.rs +++ b/crates/rpc/src/eth/api.rs @@ -190,7 +190,7 @@ mod tests { types::{Bytes, Log, Transaction}, }; use mockall::predicate::eq; - use rundler_provider::{EntryPoint, MockEntryPointV0_6, MockProvider}; + use rundler_provider::{MockEntryPointV0_6, MockProvider}; use rundler_sim::MockGasEstimator; use rundler_types::{ contracts::v0_6::i_entry_point::{HandleOpsCall, IEntryPointCalls}, @@ -249,7 +249,11 @@ mod tests { #[tokio::test] async fn test_get_user_op_by_hash_mined() { - let ep = Address::random(); + let cs = ChainSpec { + id: 1, + ..Default::default() + }; + let ep = cs.entry_point_address_v0_6; let uo = UserOperation::default(); let hash = uo.hash(ep, 1); let block_number = 1000; @@ -347,12 +351,7 @@ mod tests { .v0_6(EntryPointRouteImpl::new( ep.clone(), gas_estimator, - UserOperationEventProviderV0_6::new( - chain_spec.id, - ep.address(), - provider.clone(), - None, - ), + UserOperationEventProviderV0_6::new(chain_spec.clone(), provider.clone(), None), )) .build(); diff --git a/crates/rpc/src/eth/events/common.rs b/crates/rpc/src/eth/events/common.rs index 29c2edc84..336634693 100644 --- a/crates/rpc/src/eth/events/common.rs +++ b/crates/rpc/src/eth/events/common.rs @@ -22,7 +22,7 @@ use ethers::{ }, }; use rundler_provider::Provider; -use rundler_types::{UserOperation, UserOperationVariant}; +use rundler_types::{chain::ChainSpec, UserOperation, UserOperationVariant}; use rundler_utils::{eth, log::LogOnError}; use super::UserOperationEventProvider; @@ -30,8 +30,7 @@ use crate::types::{RpcUserOperationByHash, RpcUserOperationReceipt}; #[derive(Debug)] pub(crate) struct UserOperationEventProviderImpl { - chain_id: u64, - address: Address, + chain_spec: ChainSpec, provider: Arc

, event_block_distance: Option, _f_type: PhantomData, @@ -50,11 +49,9 @@ pub(crate) trait EntryPointFilters: Send + Sync + 'static { tx_receipt: TransactionReceipt, ) -> RpcUserOperationReceipt; - fn get_user_operations_from_tx_data( - tx_data: Bytes, - address: Address, - chain_id: u64, - ) -> Vec; + fn get_user_operations_from_tx_data(tx_data: Bytes, chain_spec: &ChainSpec) -> Vec; + + fn address(chain_spec: &ChainSpec) -> Address; } #[async_trait::async_trait] @@ -96,10 +93,10 @@ where .context("tx.to should be present on transaction containing user operation event")?; // Find first op matching the hash - let user_operation = if self.address == to { - F::get_user_operations_from_tx_data(tx.input, self.address, self.chain_id) + let user_operation = if F::address(&self.chain_spec) == to { + F::get_user_operations_from_tx_data(tx.input, &self.chain_spec) .into_iter() - .find(|op| op.hash(to, self.chain_id) == hash) + .find(|op| op.hash(to, self.chain_spec.id) == hash) .context("matching user operation should be found in tx data")? } else { self.trace_find_user_operation(transaction_hash, hash) @@ -167,14 +164,12 @@ where F: EntryPointFilters, { pub(crate) fn new( - chain_id: u64, - address: Address, + chain_spec: ChainSpec, provider: Arc

, event_block_distance: Option, ) -> Self { Self { - chain_id, - address, + chain_spec, provider, event_block_distance, _f_type: PhantomData, @@ -190,7 +185,7 @@ where }; let filter = Filter::new() - .address(self.address) + .address(F::address(&self.chain_spec)) .event(&F::UserOperationEventFilter::abi_signature()) .from_block(from_block) .to_block(to_block) @@ -240,16 +235,13 @@ where .to .as_ref() .and_then(|to| to.as_address()) - .filter(|to| **to == self.address) + .filter(|to| **to == F::address(&self.chain_spec)) { // check if the user operation is in the call frame - if let Some(uo) = F::get_user_operations_from_tx_data( - call_frame.input, - self.address, - self.chain_id, - ) - .into_iter() - .find(|op| op.hash(*to, self.chain_id) == user_op_hash) + if let Some(uo) = + F::get_user_operations_from_tx_data(call_frame.input, &self.chain_spec) + .into_iter() + .find(|op| op.hash(*to, self.chain_spec.id) == user_op_hash) { return Ok(Some(uo)); } diff --git a/crates/rpc/src/eth/events/v0_6.rs b/crates/rpc/src/eth/events/v0_6.rs index 803e436c4..ff09d985e 100644 --- a/crates/rpc/src/eth/events/v0_6.rs +++ b/crates/rpc/src/eth/events/v0_6.rs @@ -17,6 +17,7 @@ use ethers::{ types::{Address, Bytes, Log, TransactionReceipt, H256}, }; use rundler_types::{ + chain::ChainSpec, contracts::v0_6::i_entry_point::{ IEntryPointCalls, UserOperationEventFilter, UserOperationRevertReasonFilter, }, @@ -79,11 +80,7 @@ impl EntryPointFilters for EntryPointFiltersV0_6 { } } - fn get_user_operations_from_tx_data( - tx_data: Bytes, - _address: Address, - _chain_id: u64, - ) -> Vec { + fn get_user_operations_from_tx_data(tx_data: Bytes, _chain_spec: &ChainSpec) -> Vec { let entry_point_calls = match IEntryPointCalls::decode(tx_data) { Ok(entry_point_calls) => entry_point_calls, Err(_) => return vec![], @@ -101,4 +98,8 @@ impl EntryPointFilters for EntryPointFiltersV0_6 { _ => vec![], } } + + fn address(chain_spec: &ChainSpec) -> Address { + chain_spec.entry_point_address_v0_6 + } } diff --git a/crates/rpc/src/eth/events/v0_7.rs b/crates/rpc/src/eth/events/v0_7.rs index ffa1fbeab..b43d5cf1b 100644 --- a/crates/rpc/src/eth/events/v0_7.rs +++ b/crates/rpc/src/eth/events/v0_7.rs @@ -17,6 +17,7 @@ use ethers::{ types::{Address, Bytes, Log, TransactionReceipt, H256}, }; use rundler_types::{ + chain::ChainSpec, contracts::v0_7::i_entry_point::{ IEntryPointCalls, UserOperationEventFilter, UserOperationRevertReasonFilter, }, @@ -79,11 +80,7 @@ impl EntryPointFilters for EntryPointFiltersV0_7 { } } - fn get_user_operations_from_tx_data( - tx_data: Bytes, - address: Address, - chain_id: u64, - ) -> Vec { + fn get_user_operations_from_tx_data(tx_data: Bytes, chain_spec: &ChainSpec) -> Vec { let entry_point_calls = match IEntryPointCalls::decode(tx_data) { Ok(entry_point_calls) => entry_point_calls, Err(_) => return vec![], @@ -93,20 +90,20 @@ impl EntryPointFilters for EntryPointFiltersV0_7 { IEntryPointCalls::HandleOps(handle_ops_call) => handle_ops_call .ops .into_iter() - .map(|op| op.unpack(address, chain_id)) + .map(|op| op.unpack(chain_spec)) .collect(), IEntryPointCalls::HandleAggregatedOps(handle_aggregated_ops_call) => { handle_aggregated_ops_call .ops_per_aggregator .into_iter() - .flat_map(|ops| { - ops.user_ops - .into_iter() - .map(|op| op.unpack(address, chain_id)) - }) + .flat_map(|ops| ops.user_ops.into_iter().map(|op| op.unpack(chain_spec))) .collect() } _ => vec![], } } + + fn address(chain_spec: &ChainSpec) -> Address { + chain_spec.entry_point_address_v0_7 + } } diff --git a/crates/rpc/src/eth/server.rs b/crates/rpc/src/eth/server.rs index dcb0fef89..8e485a924 100644 --- a/crates/rpc/src/eth/server.rs +++ b/crates/rpc/src/eth/server.rs @@ -38,7 +38,7 @@ where "eth_sendUserOperation", EthApi::send_user_operation( self, - UserOperationVariant::from_rpc(op, entry_point, self.chain_spec.id), + UserOperationVariant::from_rpc(op, &self.chain_spec), entry_point, ), ) diff --git a/crates/rpc/src/rundler.rs b/crates/rpc/src/rundler.rs index 68ac3c49b..d61967f5a 100644 --- a/crates/rpc/src/rundler.rs +++ b/crates/rpc/src/rundler.rs @@ -139,7 +139,7 @@ where user_op: RpcUserOperation, entry_point: Address, ) -> EthResult> { - let uo = UserOperationVariant::from_rpc(user_op, entry_point, self.chain_spec.id); + let uo = UserOperationVariant::from_rpc(user_op, &self.chain_spec); let id = uo.id(); if uo.pre_verification_gas() != U256::zero() diff --git a/crates/rpc/src/task.rs b/crates/rpc/src/task.rs index 1e1a2771c..25d4df4b5 100644 --- a/crates/rpc/src/task.rs +++ b/crates/rpc/src/task.rs @@ -129,8 +129,7 @@ where ), ), UserOperationEventProviderV0_6::new( - self.args.chain_spec.id, - self.args.chain_spec.entry_point_address_v0_6, + self.args.chain_spec.clone(), provider.clone(), self.args .eth_api_settings @@ -157,8 +156,7 @@ where ), ), UserOperationEventProviderV0_7::new( - self.args.chain_spec.id, - self.args.chain_spec.entry_point_address_v0_7, + self.args.chain_spec.clone(), provider.clone(), self.args .eth_api_settings diff --git a/crates/rpc/src/types/mod.rs b/crates/rpc/src/types/mod.rs index 0ede03d43..293503807 100644 --- a/crates/rpc/src/types/mod.rs +++ b/crates/rpc/src/types/mod.rs @@ -16,6 +16,7 @@ use ethers::{ utils::to_checksum, }; use rundler_types::{ + chain::ChainSpec, pool::{Reputation, ReputationStatus}, v0_6::UserOperation as UserOperationV0_6, v0_7::UserOperation as UserOperationV0_7, @@ -46,7 +47,7 @@ pub enum ApiNamespace { /// Conversion trait for RPC types adding the context of the entry point and chain id pub(crate) trait FromRpc { - fn from_rpc(rpc: R, entry_point: Address, chain_id: u64) -> Self; + fn from_rpc(rpc: R, chain_spec: &ChainSpec) -> Self; } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -116,13 +117,13 @@ impl From for RpcUserOperation { } impl FromRpc for UserOperationVariant { - fn from_rpc(op: RpcUserOperation, entry_point: Address, chain_id: u64) -> Self { + fn from_rpc(op: RpcUserOperation, chain_spec: &ChainSpec) -> Self { match op { RpcUserOperation::V0_6(op) => { - UserOperationVariant::V0_6(UserOperationV0_6::from_rpc(op, entry_point, chain_id)) + UserOperationVariant::V0_6(UserOperationV0_6::from_rpc(op, chain_spec)) } RpcUserOperation::V0_7(op) => { - UserOperationVariant::V0_7(UserOperationV0_7::from_rpc(op, entry_point, chain_id)) + UserOperationVariant::V0_7(UserOperationV0_7::from_rpc(op, chain_spec)) } } } diff --git a/crates/rpc/src/types/v0_6.rs b/crates/rpc/src/types/v0_6.rs index 750df5349..68fa7f834 100644 --- a/crates/rpc/src/types/v0_6.rs +++ b/crates/rpc/src/types/v0_6.rs @@ -13,6 +13,7 @@ use ethers::types::{Address, Bytes, U256}; use rundler_types::{ + chain::ChainSpec, v0_6::{UserOperation, UserOperationOptionalGas}, GasEstimate, }; @@ -56,7 +57,7 @@ impl From for RpcUserOperation { } impl FromRpc for UserOperation { - fn from_rpc(def: RpcUserOperation, _entry_point: Address, _chain_id: u64) -> Self { + fn from_rpc(def: RpcUserOperation, _chain_spec: &ChainSpec) -> Self { UserOperation { sender: def.sender.into(), nonce: def.nonce, diff --git a/crates/rpc/src/types/v0_7.rs b/crates/rpc/src/types/v0_7.rs index 4dfd6ffee..ccd9ced6c 100644 --- a/crates/rpc/src/types/v0_7.rs +++ b/crates/rpc/src/types/v0_7.rs @@ -13,6 +13,7 @@ use ethers::types::{Address, Bytes, H256, U128, U256}; use rundler_types::{ + chain::ChainSpec, v0_7::{ UserOperation, UserOperationBuilder, UserOperationOptionalGas, UserOperationRequiredFields, }, @@ -88,10 +89,9 @@ impl From for RpcUserOperation { } impl FromRpc for UserOperation { - fn from_rpc(def: RpcUserOperation, entry_point: Address, chain_id: u64) -> Self { + fn from_rpc(def: RpcUserOperation, chain_spec: &ChainSpec) -> Self { let mut builder = UserOperationBuilder::new( - entry_point, - chain_id, + chain_spec, UserOperationRequiredFields { sender: def.sender, nonce: def.nonce, diff --git a/crates/sim/src/estimation/v0_7.rs b/crates/sim/src/estimation/v0_7.rs index e021d1527..50ff1d22d 100644 --- a/crates/sim/src/estimation/v0_7.rs +++ b/crates/sim/src/estimation/v0_7.rs @@ -28,7 +28,7 @@ use rundler_types::{ }, entry_point_simulations::ENTRYPOINTSIMULATIONS_DEPLOYED_BYTECODE, }, - v0_7::{UserOperation, UserOperationOptionalGas}, + v0_7::{UserOperation, UserOperationBuilder, UserOperationOptionalGas}, GasEstimate, }; use rundler_utils::{eth, math}; @@ -85,8 +85,7 @@ where let full_op = op .clone() .into_user_operation_builder( - self.entry_point.address(), - self.chain_spec.id, + &self.chain_spec, settings.max_call_gas.into(), settings.max_verification_gas.into(), settings.max_paymaster_verification_gas.into(), @@ -175,7 +174,9 @@ where let call_gas_estimator = CallGasEstimatorImpl::new( entry_point.clone(), settings, - CallGasEstimatorSpecializationV07, + CallGasEstimatorSpecializationV07 { + chain_spec: chain_spec.clone(), + }, ); Self { chain_spec, @@ -262,7 +263,7 @@ where let get_op_with_limit = |op: UserOperation, args: GetOpWithLimitArgs| { let GetOpWithLimitArgs { gas, fee } = args; - op.into_builder() + UserOperationBuilder::from_uo(op, &self.chain_spec) .verification_gas_limit(gas) .max_fee_per_gas(fee) .max_priority_fee_per_gas(fee) @@ -307,7 +308,7 @@ where let get_op_with_limit = |op: UserOperation, args: GetOpWithLimitArgs| { let GetOpWithLimitArgs { gas, fee } = args; - op.into_builder() + UserOperationBuilder::from_uo(op, &self.chain_spec) .max_fee_per_gas(fee) .max_priority_fee_per_gas(fee) .paymaster_verification_gas_limit(gas) @@ -367,8 +368,8 @@ where Ok(gas::estimate_pre_verification_gas( &self.chain_spec, &self.entry_point, - &optional_op.max_fill(self.entry_point.address(), self.chain_spec.id), - &optional_op.random_fill(self.entry_point.address(), self.chain_spec.id), + &optional_op.max_fill(&self.chain_spec), + &optional_op.random_fill(&self.chain_spec), gas_price, ) .await?) @@ -409,7 +410,9 @@ where /// Implementation of functions that specialize the call gas estimator to the /// v0.7 entry point. #[derive(Debug)] -pub struct CallGasEstimatorSpecializationV07; +pub struct CallGasEstimatorSpecializationV07 { + chain_spec: ChainSpec, +} impl CallGasEstimatorSpecialization for CallGasEstimatorSpecializationV07 { type UO = UserOperation; @@ -432,7 +435,7 @@ impl CallGasEstimatorSpecialization for CallGasEstimatorSpecializationV07 { } fn get_op_with_no_call_gas(&self, op: Self::UO) -> Self::UO { - op.into_builder() + UserOperationBuilder::from_uo(op, &self.chain_spec) .call_gas_limit(U128::zero()) .max_fee_per_gas(U128::zero()) .build() diff --git a/crates/sim/src/gas/gas.rs b/crates/sim/src/gas/gas.rs index 6c6167241..a1a35d31b 100644 --- a/crates/sim/src/gas/gas.rs +++ b/crates/sim/src/gas/gas.rs @@ -50,7 +50,7 @@ pub async fn estimate_pre_verification_gas< random_op: &UO, gas_price: U256, ) -> anyhow::Result { - let static_gas = full_op.calc_static_pre_verification_gas(true); + let static_gas = full_op.calc_static_pre_verification_gas(chain_spec, true); if !chain_spec.calldata_pre_verification_gas { return Ok(static_gas); } @@ -74,7 +74,7 @@ pub async fn calc_required_pre_verification_gas< op: &UO, base_fee: U256, ) -> anyhow::Result { - let static_gas = op.calc_static_pre_verification_gas(true); + let static_gas = op.calc_static_pre_verification_gas(chain_spec, true); if !chain_spec.calldata_pre_verification_gas { return Ok(static_gas); } @@ -152,7 +152,7 @@ pub fn user_operation_pre_verification_execution_gas_limit( // but this not part of the EXECUTION gas limit of the transaction. // In such cases we only consider the static portion of the pre_verification_gas in the gas limit. if chain_spec.calldata_pre_verification_gas { - uo.calc_static_pre_verification_gas(include_fixed_gas_overhead) + uo.calc_static_pre_verification_gas(chain_spec, include_fixed_gas_overhead) } else { uo.pre_verification_gas() } @@ -170,7 +170,7 @@ pub fn user_operation_pre_verification_gas_limit( // but this not part of the execution TOTAL limit of the transaction. // In such cases we only consider the static portion of the pre_verification_gas in the gas limit. if chain_spec.calldata_pre_verification_gas && !chain_spec.include_l1_gas_in_gas_limit { - uo.calc_static_pre_verification_gas(include_fixed_gas_overhead) + uo.calc_static_pre_verification_gas(chain_spec, include_fixed_gas_overhead) } else { uo.pre_verification_gas() } diff --git a/crates/task/src/grpc/protos.rs b/crates/task/src/grpc/protos.rs index 8d4013786..bb7966f2f 100644 --- a/crates/task/src/grpc/protos.rs +++ b/crates/task/src/grpc/protos.rs @@ -27,6 +27,9 @@ pub enum ConversionError { /// Invalid enum value, does not map to a valid enum variant #[error("Invalid enum value {0}")] InvalidEnumValue(i32), + /// Other error + #[error(transparent)] + Other(#[from] anyhow::Error), } /// Convert proto bytes into a type that implements `FromProtoBytes`. diff --git a/crates/types/src/chain.rs b/crates/types/src/chain.rs index a232c457c..b1db68f28 100644 --- a/crates/types/src/chain.rs +++ b/crates/types/src/chain.rs @@ -43,6 +43,21 @@ pub struct ChainSpec { pub deposit_transfer_overhead: U256, /// The maximum size of a transaction in bytes pub max_transaction_size_bytes: usize, + /// Intrinsic gas cost for a transaction + pub transaction_intrinsic_gas: U256, + /// Per user operation gas cost for v0.6 + pub per_user_op_v0_6_gas: U256, + /// Per user operation gas cost for v0.7 + pub per_user_op_v0_7_gas: U256, + /// Per user operation deploy gas cost overhead, to capture + /// deploy costs that are not metered by the entry point + pub per_user_op_deploy_overhead_gas: U256, + /// Gas cost for a user operation word in a bundle transaction + pub per_user_op_word_gas: U256, + /// Gas cost for a zero byte in calldata + pub calldata_zero_byte_gas: U256, + /// Gas cost for a non-zero byte in calldata + pub calldata_non_zero_byte_gas: U256, /* * Gas estimation @@ -134,7 +149,14 @@ impl Default for ChainSpec { id: 0, entry_point_address_v0_6: Address::from_str(ENTRY_POINT_ADDRESS_V6_0).unwrap(), entry_point_address_v0_7: Address::from_str(ENTRY_POINT_ADDRESS_V7_0).unwrap(), - deposit_transfer_overhead: U256::from(30000), + deposit_transfer_overhead: U256::from(30_000), + transaction_intrinsic_gas: U256::from(21_000), + per_user_op_v0_6_gas: U256::from(18_300), + per_user_op_v0_7_gas: U256::from(19_500), + per_user_op_deploy_overhead_gas: U256::from(0), + per_user_op_word_gas: U256::from(4), + calldata_zero_byte_gas: U256::from(4), + calldata_non_zero_byte_gas: U256::from(16), eip1559_enabled: true, calldata_pre_verification_gas: false, l1_gas_oracle_contract_type: L1GasOracleContractType::default(), diff --git a/crates/types/src/user_operation/mod.rs b/crates/types/src/user_operation/mod.rs index fc319d553..1a9594c6a 100644 --- a/crates/types/src/user_operation/mod.rs +++ b/crates/types/src/user_operation/mod.rs @@ -23,7 +23,7 @@ pub mod v0_6; /// User Operation types for Entry Point v0.7 pub mod v0_7; -use crate::Entity; +use crate::{chain::ChainSpec, Entity}; /// A user op must be valid for at least this long into the future to be included. pub const TIME_RANGE_BUFFER: Duration = Duration::from_secs(60); @@ -138,7 +138,11 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { fn pre_verification_gas(&self) -> U256; /// Calculate the static portion of the pre-verification gas for this user operation - fn calc_static_pre_verification_gas(&self, include_fixed_gas_overhead: bool) -> U256; + fn calc_static_pre_verification_gas( + &self, + chain_spec: &ChainSpec, + include_fixed_gas_overhead: bool, + ) -> U256; /// Clear the signature field of the user op /// @@ -148,9 +152,6 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { /// Abi encode size of the user operation fn abi_encoded_size(&self) -> usize; - /// Return the gas overheads for this user operation type - fn gas_overheads() -> GasOverheads; - /// Calculate the size of the user operation in single UO bundle in bytes fn single_uo_bundle_size_bytes(&self) -> usize { self.abi_encoded_size() + BUNDLE_BYTE_OVERHEAD + USER_OP_OFFSET_WORD_SIZE @@ -278,13 +279,17 @@ impl UserOperation for UserOperationVariant { } } - fn calc_static_pre_verification_gas(&self, include_fixed_gas_overhead: bool) -> U256 { + fn calc_static_pre_verification_gas( + &self, + chain_spec: &ChainSpec, + include_fixed_gas_overhead: bool, + ) -> U256 { match self { UserOperationVariant::V0_6(op) => { - op.calc_static_pre_verification_gas(include_fixed_gas_overhead) + op.calc_static_pre_verification_gas(chain_spec, include_fixed_gas_overhead) } UserOperationVariant::V0_7(op) => { - op.calc_static_pre_verification_gas(include_fixed_gas_overhead) + op.calc_static_pre_verification_gas(chain_spec, include_fixed_gas_overhead) } } } @@ -316,15 +321,6 @@ impl UserOperation for UserOperationVariant { UserOperationVariant::V0_7(op) => op.abi_encoded_size(), } } - - /// Return the gas overheads for this user operation type - fn gas_overheads() -> GasOverheads { - match Self::entry_point_version() { - EntryPointVersion::V0_6 => GasOverheads::v0_6(), - EntryPointVersion::V0_7 => GasOverheads::v0_7(), - EntryPointVersion::Unspecified => unreachable!(), - } - } } impl UserOperationVariant { @@ -399,61 +395,27 @@ pub struct UserOpsPerAggregator { pub signature: Bytes, } -/// Gas overheads for user operations used in calculating the pre-verification gas. See: https://github.com/eth-infinitism/bundler/blob/main/packages/sdk/src/calcPreVerificationGas.ts -#[derive(Clone, Copy, Debug)] -pub struct GasOverheads { - /// The Entrypoint requires a gas buffer for the bundle to account for the gas spent outside of the major steps in the processing of UOs - pub bundle_transaction_gas_buffer: U256, - /// The fixed gas overhead for any EVM transaction - pub transaction_gas_overhead: U256, - per_user_op: U256, - per_user_op_word: U256, - zero_byte: U256, - non_zero_byte: U256, -} - -impl GasOverheads { - /// Gas overheads for entry point v0.6 - pub fn v0_6() -> Self { - Self { - bundle_transaction_gas_buffer: 5_000.into(), - transaction_gas_overhead: 21_000.into(), - per_user_op: 18_300.into(), - per_user_op_word: 4.into(), - zero_byte: 4.into(), - non_zero_byte: 16.into(), - } - } - - /// Gas overheads for entry point v0.7 - pub fn v0_7() -> Self { - Self { - bundle_transaction_gas_buffer: 5_000.into(), - transaction_gas_overhead: 21_000.into(), - per_user_op: 19_500.into(), - per_user_op_word: 4.into(), - zero_byte: 4.into(), - non_zero_byte: 16.into(), - } - } -} - -pub(crate) fn op_calldata_gas_cost(uo: UO, ov: &GasOverheads) -> U256 { +pub(crate) fn op_calldata_gas_cost( + uo: UO, + zero_byte_cost: U256, + non_zero_byte_cost: U256, + per_word_cost: U256, +) -> U256 { let encoded_op = uo.encode(); let length_in_words = (encoded_op.len() + 31) >> 5; // ceil(encoded_op.len() / 32) let call_data_cost: U256 = encoded_op .iter() .map(|&x| { if x == 0 { - ov.zero_byte + zero_byte_cost } else { - ov.non_zero_byte + non_zero_byte_cost } }) .reduce(|a, b| a + b) .unwrap_or_default(); - call_data_cost + ov.per_user_op + ov.per_user_op_word * length_in_words + call_data_cost + per_word_cost * length_in_words } /// Calculates the size a byte array padded to the next largest multiple of 32 diff --git a/crates/types/src/user_operation/v0_6.rs b/crates/types/src/user_operation/v0_6.rs index a8abe5157..a3262a4e1 100644 --- a/crates/types/src/user_operation/v0_6.rs +++ b/crates/types/src/user_operation/v0_6.rs @@ -20,15 +20,17 @@ use rand::{self, RngCore}; use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; -use super::{ - GasOverheads, UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant, -}; +use super::{UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant}; pub use crate::contracts::v0_6::i_entry_point::{UserOperation, UserOpsPerAggregator}; use crate::{ + chain::ChainSpec, entity::{Entity, EntityType}, EntryPointVersion, }; +/// Gas overhead required by the entry point contract for the inner call +pub const ENTRY_POINT_INNER_GAS_OVERHEAD: U256 = U256([5_000, 0, 0, 0]); + /// Number of bytes in the fixed size portion of an ABI encoded user operation /// sender = 32 bytes /// nonce = 32 bytes @@ -136,14 +138,27 @@ impl UserOperationTrait for UserOperation { } fn required_pre_execution_buffer(&self) -> U256 { - self.verification_gas_limit + U256::from(5_000) - } - - fn calc_static_pre_verification_gas(&self, include_fixed_gas_overhead: bool) -> U256 { - let ov = GasOverheads::v0_6(); - super::op_calldata_gas_cost(self.clone(), &ov) + self.verification_gas_limit + ENTRY_POINT_INNER_GAS_OVERHEAD + } + + fn calc_static_pre_verification_gas( + &self, + chain_spec: &ChainSpec, + include_fixed_gas_overhead: bool, + ) -> U256 { + super::op_calldata_gas_cost( + self.clone(), + chain_spec.calldata_zero_byte_gas, + chain_spec.calldata_non_zero_byte_gas, + chain_spec.per_user_op_word_gas, + ) + chain_spec.per_user_op_v0_6_gas + + (if self.factory().is_some() { + chain_spec.per_user_op_deploy_overhead_gas + } else { + U256::zero() + }) + (if include_fixed_gas_overhead { - ov.transaction_gas_overhead + chain_spec.transaction_intrinsic_gas } else { 0.into() }) @@ -160,11 +175,6 @@ impl UserOperationTrait for UserOperation { + super::byte_array_abi_len(&self.paymaster_and_data) + super::byte_array_abi_len(&self.signature) } - - /// Return the gas overheads for this user operation type - fn gas_overheads() -> GasOverheads { - GasOverheads::v0_6() - } } impl UserOperation { diff --git a/crates/types/src/user_operation/v0_7.rs b/crates/types/src/user_operation/v0_7.rs index f1c01afc3..3ba212e04 100644 --- a/crates/types/src/user_operation/v0_7.rs +++ b/crates/types/src/user_operation/v0_7.rs @@ -20,10 +20,11 @@ use rand::RngCore; use super::{UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant}; use crate::{ - contracts::v0_7::shared_types::PackedUserOperation, Entity, EntryPointVersion, GasOverheads, + chain::ChainSpec, contracts::v0_7::shared_types::PackedUserOperation, Entity, EntryPointVersion, }; -const ENTRY_POINT_INNER_GAS_OVERHEAD: U256 = U256([10_000, 0, 0, 0]); +/// Gas overhead required by the entry point contract for the inner call +pub const ENTRY_POINT_INNER_GAS_OVERHEAD: U256 = U256([10_000, 0, 0, 0]); /// Number of bytes in the fixed size portion of an ABI encoded user operation /// sender = 32 bytes @@ -188,11 +189,20 @@ impl UserOperationTrait for UserOperation { U256::from(self.verification_gas_limit) + U256::from(self.paymaster_verification_gas_limit) } - fn calc_static_pre_verification_gas(&self, include_fixed_gas_overhead: bool) -> U256 { - let ov = GasOverheads::v0_7(); + fn calc_static_pre_verification_gas( + &self, + chain_spec: &ChainSpec, + include_fixed_gas_overhead: bool, + ) -> U256 { self.calldata_gas_cost + + chain_spec.per_user_op_v0_7_gas + + (if self.factory.is_some() { + chain_spec.per_user_op_deploy_overhead_gas + } else { + 0.into() + }) + (if include_fixed_gas_overhead { - ov.transaction_gas_overhead + chain_spec.transaction_intrinsic_gas } else { 0.into() }) @@ -228,11 +238,6 @@ impl UserOperationTrait for UserOperation { + super::byte_array_abi_len(&self.packed.paymaster_and_data) + super::byte_array_abi_len(&self.packed.signature) } - - /// Return the gas overheads for this user operation type - fn gas_overheads() -> GasOverheads { - GasOverheads::v0_7() - } } impl UserOperation { @@ -245,34 +250,6 @@ impl UserOperation { pub fn packed(&self) -> &PackedUserOperation { &self.packed } - - /// Returns a builder with the same values as this user operation. Should be - /// used instead of mutating this user operation directly when updating - /// fields. - pub fn into_builder(self) -> UserOperationBuilder { - UserOperationBuilder { - entry_point: self.entry_point, - chain_id: self.chain_id, - required: UserOperationRequiredFields { - sender: self.sender, - nonce: self.nonce, - call_data: self.call_data, - call_gas_limit: self.call_gas_limit, - verification_gas_limit: self.verification_gas_limit, - pre_verification_gas: self.pre_verification_gas, - max_priority_fee_per_gas: self.max_priority_fee_per_gas, - max_fee_per_gas: self.max_fee_per_gas, - signature: self.signature, - }, - factory: self.factory, - factory_data: self.factory_data, - paymaster: self.paymaster, - paymaster_verification_gas_limit: self.paymaster_verification_gas_limit, - paymaster_post_op_gas_limit: self.paymaster_post_op_gas_limit, - paymaster_data: self.paymaster_data, - packed_uo: None, - } - } } impl From for UserOperation { @@ -363,13 +340,12 @@ pub struct UserOperationOptionalGas { impl UserOperationOptionalGas { /// Fill in the optional and dummy fields of the user operation with values /// that will cause the maximum possible calldata gas cost. - pub fn max_fill(&self, entry_point: Address, chain_id: u64) -> UserOperation { + pub fn max_fill(&self, chain_spec: &ChainSpec) -> UserOperation { let max_4 = U128::from(u32::MAX); let max_8 = U128::from(u64::MAX); let mut builder = UserOperationBuilder::new( - entry_point, - chain_id, + chain_spec, UserOperationRequiredFields { sender: self.sender, nonce: self.nonce, @@ -410,10 +386,9 @@ impl UserOperationOptionalGas { // /// Note that this will slightly overestimate the calldata gas needed as it uses /// the worst case scenario for the unknown gas values and paymaster_and_data. - pub fn random_fill(&self, entry_point: Address, chain_id: u64) -> UserOperation { + pub fn random_fill(&self, chain_spec: &ChainSpec) -> UserOperation { let mut builder = UserOperationBuilder::new( - entry_point, - chain_id, + chain_spec, UserOperationRequiredFields { sender: self.sender, nonce: self.nonce, @@ -449,12 +424,11 @@ impl UserOperationOptionalGas { /// Fill in the optional fields of the user operation with default values if unset pub fn into_user_operation_builder( self, - entry_point: Address, - chain_id: u64, + chian_spec: &ChainSpec, max_call_gas: U128, max_verification_gas: U128, max_paymaster_verification_gas: U128, - ) -> UserOperationBuilder { + ) -> UserOperationBuilder<'_> { // If unset or zero, default these to gas limits from settings // Cap their values to the gas limits from settings let cgl = super::default_if_none_or_equal(self.call_gas_limit, max_call_gas, U128::zero()); @@ -475,8 +449,7 @@ impl UserOperationOptionalGas { ); let mut builder = UserOperationBuilder::new( - entry_point, - chain_id, + chian_spec, UserOperationRequiredFields { sender: self.sender, nonce: self.nonce, @@ -545,10 +518,9 @@ impl From for UserOperationOptionalGas { /// Builder for UserOperation /// /// Used to create a v0.7 while ensuring all required fields and grouped fields are present -pub struct UserOperationBuilder { - // required fields for hash - entry_point: Address, - chain_id: u64, +pub struct UserOperationBuilder<'a> { + // chain spec + chain_spec: &'a ChainSpec, // required fields required: UserOperationRequiredFields, @@ -585,12 +557,11 @@ pub struct UserOperationRequiredFields { pub signature: Bytes, } -impl UserOperationBuilder { +impl<'a> UserOperationBuilder<'a> { /// Creates a new builder - pub fn new(entry_point: Address, chain_id: u64, required: UserOperationRequiredFields) -> Self { + pub fn new(chain_spec: &'a ChainSpec, required: UserOperationRequiredFields) -> Self { Self { - entry_point, - chain_id, + chain_spec, required, factory: None, factory_data: Bytes::new(), @@ -602,6 +573,31 @@ impl UserOperationBuilder { } } + /// Creates a builder from an existing UO + pub fn from_uo(uo: UserOperation, chain_spec: &'a ChainSpec) -> Self { + Self { + chain_spec, + required: UserOperationRequiredFields { + sender: uo.sender, + nonce: uo.nonce, + call_data: uo.call_data, + call_gas_limit: uo.call_gas_limit, + verification_gas_limit: uo.verification_gas_limit, + pre_verification_gas: uo.pre_verification_gas, + max_priority_fee_per_gas: uo.max_priority_fee_per_gas, + max_fee_per_gas: uo.max_fee_per_gas, + signature: uo.signature, + }, + factory: uo.factory, + factory_data: uo.factory_data, + paymaster: uo.paymaster, + paymaster_verification_gas_limit: uo.paymaster_verification_gas_limit, + paymaster_post_op_gas_limit: uo.paymaster_post_op_gas_limit, + paymaster_data: uo.paymaster_data, + packed_uo: None, + } + } + /// Sets the factory and factory data pub fn factory(mut self, factory: Address, factory_data: Bytes) -> Self { self.factory = Some(factory); @@ -693,8 +689,8 @@ impl UserOperationBuilder { paymaster_post_op_gas_limit: self.paymaster_post_op_gas_limit, paymaster_data: self.paymaster_data, signature: self.required.signature, - entry_point: self.entry_point, - chain_id: self.chain_id, + entry_point: self.chain_spec.entry_point_address_v0_7, + chain_id: self.chain_spec.id, hash: H256::zero(), packed: PackedUserOperation::default(), calldata_gas_cost: U256::zero(), @@ -703,8 +699,17 @@ impl UserOperationBuilder { let packed = self .packed_uo .unwrap_or_else(|| pack_user_operation(uo.clone())); - let hash = hash_packed_user_operation(&packed, self.entry_point, self.chain_id); - let calldata_gas_cost = super::op_calldata_gas_cost(packed.clone(), &GasOverheads::v0_7()); + let hash = hash_packed_user_operation( + &packed, + self.chain_spec.entry_point_address_v0_7, + self.chain_spec.id, + ); + let calldata_gas_cost = super::op_calldata_gas_cost( + packed.clone(), + self.chain_spec.calldata_zero_byte_gas, + self.chain_spec.calldata_non_zero_byte_gas, + self.chain_spec.per_user_op_word_gas, + ); UserOperation { hash, @@ -759,14 +764,9 @@ fn pack_user_operation(uo: UserOperation) -> PackedUserOperation { } } -fn unpack_user_operation( - puo: PackedUserOperation, - entry_point: Address, - chain_id: u64, -) -> UserOperation { +fn unpack_user_operation(puo: PackedUserOperation, chain_spec: &ChainSpec) -> UserOperation { let mut builder = UserOperationBuilder::new( - entry_point, - chain_id, + chain_spec, UserOperationRequiredFields { sender: puo.sender, nonce: puo.nonce, @@ -850,8 +850,8 @@ fn concat_128(a: [u8; 16], b: [u8; 16]) -> [u8; 32] { impl PackedUserOperation { /// Unpacks the user operation to its offchain representation - pub fn unpack(self, entry_point: Address, chain_id: u64) -> UserOperation { - unpack_user_operation(self.clone(), entry_point, chain_id) + pub fn unpack(self, chain_spec: &ChainSpec) -> UserOperation { + unpack_user_operation(self.clone(), chain_spec) } fn heap_size(&self) -> usize { @@ -869,9 +869,9 @@ mod tests { #[test] fn test_pack_unpack() { + let cs = ChainSpec::default(); let builder = UserOperationBuilder::new( - Address::zero(), - 1, + &cs, UserOperationRequiredFields { sender: Address::zero(), nonce: 0.into(), @@ -887,16 +887,16 @@ mod tests { let uo = builder.build(); let packed = uo.clone().pack(); - let unpacked = packed.unpack(Address::zero(), 1); + let unpacked = packed.unpack(&cs); assert_eq!(uo, unpacked); } #[test] fn test_pack_unpack_2() { + let cs = ChainSpec::default(); let builder = UserOperationBuilder::new( - Address::zero(), - 1, + &cs, UserOperationRequiredFields { sender: Address::zero(), nonce: 0.into(), @@ -920,7 +920,7 @@ mod tests { let uo = builder.build(); let packed = uo.clone().pack(); - let unpacked = packed.unpack(Address::zero(), 1); + let unpacked = packed.unpack(&cs); assert_eq!(uo, unpacked); } @@ -928,8 +928,10 @@ mod tests { #[test] fn test_hash() { // From https://sepolia.etherscan.io/tx/0x51c1f40ce6e997a54b39a0eb783e472c2afa4ed3f2f11f97986f7f3a347b9d50 - let entry_point = Address::from_str("0x0000000071727De22E5E9d8BAf0edAc6f37da032").unwrap(); - let chain_id = 11155111; + let cs = ChainSpec { + id: 11155111, + ..Default::default() + }; let puo = PackedUserOperation { sender: Address::from_str("0xb292Cf4a8E1fF21Ac27C4f94071Cd02C022C414b").unwrap(), @@ -952,21 +954,18 @@ mod tests { let hash = H256::from_str("0xe486401370d145766c3cf7ba089553214a1230d38662ae532c9b62eb6dadcf7e") .unwrap(); - let uo = puo.unpack(entry_point, chain_id); - assert_eq!(uo.hash(entry_point, chain_id), hash); + let uo = puo.unpack(&cs); + assert_eq!(uo.hash(cs.entry_point_address_v0_7, cs.id), hash); } #[test] fn test_builder() { - let entry_point = Address::zero(); - let chain_id = 1; - let factory_address = Address::random(); let paymaster_address = Address::random(); + let cs = ChainSpec::default(); let uo = UserOperationBuilder::new( - entry_point, - chain_id, + &cs, UserOperationRequiredFields { sender: Address::zero(), nonce: 0.into(), diff --git a/docs/cli.md b/docs/cli.md index 194f76985..31a2eb310 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -57,8 +57,6 @@ See [chain spec](./architecture/chain_spec.md) for a detailed description of cha - `--aws_region`: AWS region. (default: `us-east-1`). - env: *AWS_REGION* - (*Only required if using other AWS features*) -- `--eth_poll_interval_millis`: Interval at which the builder polls an RPC node for new blocks and mined transactions (default: `100`) - - env: *ETH_POLL_INTERVAL_MILLIS* - `--unsafe`: Flag for unsafe bundling mode. When set Rundler will skip checking simulation rules (and any `debug_traceCall`). (default: `false`). - env: *UNSAFE* - `--mempool_config_path`: Path to the mempool configuration file. (example: `mempool-config.json`, `s3://my-bucket/mempool-config.json`) @@ -145,6 +143,10 @@ List of command line options for configuring the Pool. - 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* +- `--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* - `--pool.chain_history_size`: Size of the chain history - env: *POOL_CHAIN_HISTORY_SIZE* - `--pool.paymaster_tracking_enabled`: Boolean field that sets whether the pool server starts with paymaster tracking enabled (default: `true`)