From d1bfebb558fd3184ce63946a042ad8b567c10675 Mon Sep 17 00:00:00 2001 From: Alex Miao Date: Mon, 2 Oct 2023 21:09:32 -0700 Subject: [PATCH 1/3] fix(gas): amortize evm transaction fixed cost over bundle --- crates/builder/Cargo.toml | 1 + crates/builder/src/bundle_proposer.rs | 26 +++++++++++++++----------- crates/sim/src/gas/gas.rs | 22 ++++++++++++++-------- crates/sim/src/precheck.rs | 2 +- 4 files changed, 31 insertions(+), 20 deletions(-) diff --git a/crates/builder/Cargo.toml b/crates/builder/Cargo.toml index 872db3942..833a6be58 100644 --- a/crates/builder/Cargo.toml +++ b/crates/builder/Cargo.toml @@ -48,6 +48,7 @@ mockall = {workspace = true, optional = true } mockall.workspace = true rundler-pool = { path = "../pool", features = ["test-utils"] } rundler-provider = { path = "../provider", features = ["test-utils"] } +rundler-sim = { path = "../sim", features = ["test-utils"] } [build-dependencies] tonic-build.workspace = true diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index 5976e686a..ac9dcdbc0 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -45,6 +45,8 @@ const TIME_RANGE_BUFFER: Duration = Duration::from_secs(60); const BUNDLE_TRANSACTION_GAS_OVERHEAD_BUFFER: u64 = 5000; /// Extra buffer percent to add on the bundle transaction gas estimate to be sure it will be enough const BUNDLE_TRANSACTION_GAS_OVERHEAD_PERCENT: u64 = 5; +/// The fixed gas overhead for any EVM transaction +const EVM_TRANSACTION_GAS_OVERHEAD: u64 = 21000; #[derive(Debug, Default)] pub(crate) struct Bundle { @@ -538,7 +540,7 @@ where let mut gas_left = U256::from(self.settings.max_bundle_gas); let mut ops_in_bundle = Vec::new(); for op in ops { - let gas = gas::user_operation_gas_limit(&op.uo, self.settings.chain_id); + let gas = gas::user_operation_gas_limit(&op.uo, self.settings.chain_id, false); if gas_left < gas { self.emit(BuilderEvent::skipped_op( self.builder_index, @@ -740,9 +742,10 @@ impl ProposalContext { fn get_total_gas_limit(&self, chain_id: u64) -> U256 { self.iter_ops() - .map(|op| gas::user_operation_gas_limit(op, chain_id)) + .map(|op| gas::user_operation_gas_limit(op, chain_id, false)) .fold(U256::zero(), |acc, c| acc + c) + BUNDLE_TRANSACTION_GAS_OVERHEAD_BUFFER + + EVM_TRANSACTION_GAS_OVERHEAD } fn iter_ops_with_simulations(&self) -> impl Iterator + '_ { @@ -786,7 +789,8 @@ mod tests { op.pre_verification_gas + op.verification_gas_limit * 2 + op.call_gas_limit - + BUNDLE_TRANSACTION_GAS_OVERHEAD_BUFFER, + + BUNDLE_TRANSACTION_GAS_OVERHEAD_BUFFER + + EVM_TRANSACTION_GAS_OVERHEAD, BUNDLE_TRANSACTION_GAS_OVERHEAD_PERCENT, ); @@ -909,7 +913,7 @@ mod tests { let max_priority_fee_per_gas = U256::from(50); let op1 = op_with_sender_and_fees(address(1), 2054.into(), 54.into()); let op2 = op_with_sender_and_fees(address(2), 2055.into(), 55.into()); - let bundle = make_bundle( + let bundle = mock_make_bundle( vec![ MockOp { op: op1.clone(), @@ -943,7 +947,7 @@ mod tests { let max_priority_fee_per_gas = U256::from(50); let op1 = op_with_sender_and_fees(address(1), 1054.into(), 55.into()); let op2 = op_with_sender_and_fees(address(2), 1055.into(), 55.into()); - let bundle = make_bundle( + let bundle = mock_make_bundle( vec![ MockOp { op: op1.clone(), @@ -993,7 +997,7 @@ mod tests { let op_b_aggregated_sig = 21; let aggregator_a_signature = 101; let aggregator_b_signature = 102; - let bundle = make_bundle( + let bundle = mock_make_bundle( vec![ MockOp { op: unaggregated_op.clone(), @@ -1098,7 +1102,7 @@ mod tests { let op6 = op_with_sender_factory(address(6), address(4)); let deposit = parse_units("1", "ether").unwrap().into(); - let bundle = make_bundle( + let bundle = mock_make_bundle( vec![ MockOp { op: op1.clone(), @@ -1159,7 +1163,7 @@ mod tests { let op4 = op_with_sender_call_gas_limit(address(4), U256::from(10_000_000)); let deposit = parse_units("1", "ether").unwrap().into(); - let bundle = make_bundle( + let bundle = mock_make_bundle( vec![ MockOp { op: op1.clone(), @@ -1198,7 +1202,7 @@ mod tests { assert_eq!( bundle.gas_estimate, U256::from(math::increase_by_percent( - 10_000_000 + BUNDLE_TRANSACTION_GAS_OVERHEAD_BUFFER, + 10_000_000 + BUNDLE_TRANSACTION_GAS_OVERHEAD_BUFFER + EVM_TRANSACTION_GAS_OVERHEAD, BUNDLE_TRANSACTION_GAS_OVERHEAD_PERCENT )) ); @@ -1216,7 +1220,7 @@ mod tests { } async fn simple_make_bundle(mock_ops: Vec) -> Bundle { - make_bundle( + mock_make_bundle( mock_ops, vec![], vec![HandleOpsOut::Success], @@ -1227,7 +1231,7 @@ mod tests { .await } - async fn make_bundle( + async fn mock_make_bundle( mock_ops: Vec, mock_aggregators: Vec, mock_handle_ops_call_results: Vec, diff --git a/crates/sim/src/gas/gas.rs b/crates/sim/src/gas/gas.rs index 470988075..c15896cae 100644 --- a/crates/sim/src/gas/gas.rs +++ b/crates/sim/src/gas/gas.rs @@ -38,7 +38,6 @@ struct GasOverheads { per_user_op_word: U256, zero_byte: U256, non_zero_byte: U256, - bundle_size: U256, } impl Default for GasOverheads { @@ -49,7 +48,6 @@ impl Default for GasOverheads { per_user_op_word: 4.into(), zero_byte: 4.into(), non_zero_byte: 16.into(), - bundle_size: 1.into(), } } } @@ -74,7 +72,7 @@ pub async fn calc_pre_verification_gas( provider: Arc

, chain_id: u64, ) -> anyhow::Result { - let static_gas = calc_static_pre_verification_gas(full_op); + let static_gas = calc_static_pre_verification_gas(full_op, true); let dynamic_gas = match chain_id { _ if ARBITRUM_CHAIN_IDS.contains(&chain_id) => { provider @@ -95,12 +93,16 @@ pub async fn calc_pre_verification_gas( } /// Returns the gas limit for the user operation that applies to bundle transaction's limit -pub fn user_operation_gas_limit(uo: &UserOperation, chain_id: u64) -> U256 { +pub fn user_operation_gas_limit( + uo: &UserOperation, + chain_id: u64, + include_fixed_gas_overhead: bool, +) -> U256 { // On some chains (OP bedrock, Arbitrum) the L1 gas fee is charged via pre_verification_gas // 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. let pvg = if OP_BEDROCK_CHAIN_IDS.contains(&chain_id) | ARBITRUM_CHAIN_IDS.contains(&chain_id) { - calc_static_pre_verification_gas(uo) + calc_static_pre_verification_gas(uo, include_fixed_gas_overhead) } else { uo.pre_verification_gas }; @@ -115,7 +117,7 @@ pub fn user_operation_max_gas_cost(uo: &UserOperation) -> U256 { * (uo.pre_verification_gas + uo.call_gas_limit + uo.verification_gas_limit * mul) } -fn calc_static_pre_verification_gas(op: &UserOperation) -> U256 { +fn calc_static_pre_verification_gas(op: &UserOperation, include_fixed_gas_overhead: bool) -> U256 { let ov = GasOverheads::default(); let encoded_op = op.clone().encode(); let length_in_words = encoded_op.len() / 32; // size of packed user op is always a multiple of 32 bytes @@ -131,10 +133,14 @@ fn calc_static_pre_verification_gas(op: &UserOperation) -> U256 { .reduce(|a, b| a + b) .unwrap_or_default(); - ov.fixed / ov.bundle_size - + call_data_cost + call_data_cost + ov.per_user_op + ov.per_user_op_word * length_in_words + + (if include_fixed_gas_overhead { + ov.fixed + } else { + 0.into() + }) } fn verification_gas_limit_multiplier(uo: &UserOperation) -> u64 { diff --git a/crates/sim/src/precheck.rs b/crates/sim/src/precheck.rs index 95acec561..eef954f71 100644 --- a/crates/sim/src/precheck.rs +++ b/crates/sim/src/precheck.rs @@ -180,7 +180,7 @@ impl PrecheckerImpl { max_verification_gas, )); } - let total_gas_limit = gas::user_operation_gas_limit(op, chain_id); + let total_gas_limit = gas::user_operation_gas_limit(op, chain_id, false); if total_gas_limit > max_total_execution_gas { violations.push(PrecheckViolation::TotalGasLimitTooHigh( total_gas_limit, From 10c13ed9ac003b26540502ed733db5d021b99c3e Mon Sep 17 00:00:00 2001 From: Alex Miao Date: Tue, 3 Oct 2023 11:39:52 -0700 Subject: [PATCH 2/3] fix(gas): refactor bundle gas calculation --- crates/builder/src/bundle_proposer.rs | 16 +++--------- crates/sim/src/gas/gas.rs | 35 +++++++++++++++++++++------ crates/sim/src/precheck.rs | 2 +- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index ac9dcdbc0..48cd3230d 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -41,12 +41,8 @@ use crate::emit::{BuilderEvent, OpRejectionReason, SkipReason}; /// A user op must be valid for at least this long into the future to be included. const TIME_RANGE_BUFFER: Duration = Duration::from_secs(60); -/// Entrypoint requires a buffer over the user operation gas limits in the bundle transaction -const BUNDLE_TRANSACTION_GAS_OVERHEAD_BUFFER: u64 = 5000; /// Extra buffer percent to add on the bundle transaction gas estimate to be sure it will be enough const BUNDLE_TRANSACTION_GAS_OVERHEAD_PERCENT: u64 = 5; -/// The fixed gas overhead for any EVM transaction -const EVM_TRANSACTION_GAS_OVERHEAD: u64 = 21000; #[derive(Debug, Default)] pub(crate) struct Bundle { @@ -741,11 +737,7 @@ impl ProposalContext { } fn get_total_gas_limit(&self, chain_id: u64) -> U256 { - self.iter_ops() - .map(|op| gas::user_operation_gas_limit(op, chain_id, false)) - .fold(U256::zero(), |acc, c| acc + c) - + BUNDLE_TRANSACTION_GAS_OVERHEAD_BUFFER - + EVM_TRANSACTION_GAS_OVERHEAD + gas::bundle_gas_limit(self.iter_ops(), chain_id) } fn iter_ops_with_simulations(&self) -> impl Iterator + '_ { @@ -789,8 +781,8 @@ mod tests { op.pre_verification_gas + op.verification_gas_limit * 2 + op.call_gas_limit - + BUNDLE_TRANSACTION_GAS_OVERHEAD_BUFFER - + EVM_TRANSACTION_GAS_OVERHEAD, + + U256::from(5_000) + + U256::from(21_000), BUNDLE_TRANSACTION_GAS_OVERHEAD_PERCENT, ); @@ -1202,7 +1194,7 @@ mod tests { assert_eq!( bundle.gas_estimate, U256::from(math::increase_by_percent( - 10_000_000 + BUNDLE_TRANSACTION_GAS_OVERHEAD_BUFFER + EVM_TRANSACTION_GAS_OVERHEAD, + 10_000_000 + 5_000 + 21_000, BUNDLE_TRANSACTION_GAS_OVERHEAD_PERCENT )) ); diff --git a/crates/sim/src/gas/gas.rs b/crates/sim/src/gas/gas.rs index c15896cae..3988a7ccd 100644 --- a/crates/sim/src/gas/gas.rs +++ b/crates/sim/src/gas/gas.rs @@ -33,7 +33,8 @@ use super::polygon::Polygon; // see: https://github.com/eth-infinitism/bundler/blob/main/packages/sdk/src/calcPreVerificationGas.ts #[derive(Clone, Copy, Debug)] struct GasOverheads { - fixed: U256, + bundle_transaction_gas_buffer: U256, + transaction_gas_overhead: U256, per_user_op: U256, per_user_op_word: U256, zero_byte: U256, @@ -43,8 +44,9 @@ struct GasOverheads { impl Default for GasOverheads { fn default() -> Self { Self { - fixed: 21000.into(), - per_user_op: 18300.into(), + bundle_transaction_gas_buffer: 5_000.into(), // Entrypoint requires a buffer over the user operation gas limits in the bundle transaction + transaction_gas_overhead: 21_000.into(), // The fixed gas overhead for any EVM transaction + per_user_op: 18_300.into(), per_user_op_word: 4.into(), zero_byte: 4.into(), non_zero_byte: 16.into(), @@ -72,7 +74,7 @@ pub async fn calc_pre_verification_gas( provider: Arc

, chain_id: u64, ) -> anyhow::Result { - let static_gas = calc_static_pre_verification_gas(full_op, true); + let static_gas = calc_static_pre_verification_gas(full_op, GasOverheads::default(), true); let dynamic_gas = match chain_id { _ if ARBITRUM_CHAIN_IDS.contains(&chain_id) => { provider @@ -92,6 +94,20 @@ pub async fn calc_pre_verification_gas( Ok(static_gas + dynamic_gas) } +/// Compute the gas limit for the bundle composed of the given user operations +pub fn bundle_gas_limit<'a, I>(iter_ops: I, chain_id: u64) -> U256 +where + I: Iterator, +{ + let ov = GasOverheads::default(); + iter_ops + .map(|op| user_operation_gas_limit(op, chain_id, false)) + .fold( + ov.bundle_transaction_gas_buffer + ov.transaction_gas_overhead, + |acc, c| acc + c, + ) +} + /// Returns the gas limit for the user operation that applies to bundle transaction's limit pub fn user_operation_gas_limit( uo: &UserOperation, @@ -102,7 +118,7 @@ pub fn user_operation_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. let pvg = if OP_BEDROCK_CHAIN_IDS.contains(&chain_id) | ARBITRUM_CHAIN_IDS.contains(&chain_id) { - calc_static_pre_verification_gas(uo, include_fixed_gas_overhead) + calc_static_pre_verification_gas(uo, GasOverheads::default(), include_fixed_gas_overhead) } else { uo.pre_verification_gas }; @@ -117,8 +133,11 @@ pub fn user_operation_max_gas_cost(uo: &UserOperation) -> U256 { * (uo.pre_verification_gas + uo.call_gas_limit + uo.verification_gas_limit * mul) } -fn calc_static_pre_verification_gas(op: &UserOperation, include_fixed_gas_overhead: bool) -> U256 { - let ov = GasOverheads::default(); +fn calc_static_pre_verification_gas( + op: &UserOperation, + ov: GasOverheads, + include_fixed_gas_overhead: bool, +) -> U256 { let encoded_op = op.clone().encode(); let length_in_words = encoded_op.len() / 32; // size of packed user op is always a multiple of 32 bytes let call_data_cost: U256 = encoded_op @@ -137,7 +156,7 @@ fn calc_static_pre_verification_gas(op: &UserOperation, include_fixed_gas_overhe + ov.per_user_op + ov.per_user_op_word * length_in_words + (if include_fixed_gas_overhead { - ov.fixed + ov.transaction_gas_overhead } else { 0.into() }) diff --git a/crates/sim/src/precheck.rs b/crates/sim/src/precheck.rs index eef954f71..74fddf1f2 100644 --- a/crates/sim/src/precheck.rs +++ b/crates/sim/src/precheck.rs @@ -180,7 +180,7 @@ impl PrecheckerImpl { max_verification_gas, )); } - let total_gas_limit = gas::user_operation_gas_limit(op, chain_id, false); + let total_gas_limit = gas::user_operation_gas_limit(op, chain_id, true); if total_gas_limit > max_total_execution_gas { violations.push(PrecheckViolation::TotalGasLimitTooHigh( total_gas_limit, From 7bb3809abd9be85c705632960638ca18a9e83780 Mon Sep 17 00:00:00 2001 From: Alex Miao Date: Wed, 4 Oct 2023 14:56:29 -0700 Subject: [PATCH 3/3] fix(gas): refactor gas overheads --- crates/builder/src/bundle_proposer.rs | 7 ++++--- crates/sim/src/gas/gas.rs | 27 ++++++++++++--------------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index 48cd3230d..c23f7db0b 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -757,7 +757,7 @@ mod tests { use ethers::{types::H160, utils::parse_units}; use rundler_pool::MockPoolServer; use rundler_provider::{AggregatorSimOut, MockEntryPoint, MockProvider}; - use rundler_sim::{MockSimulator, SimulationViolation}; + use rundler_sim::{gas::GasOverheads, MockSimulator, SimulationViolation}; use rundler_types::ValidTimeRange; use super::*; @@ -777,12 +777,13 @@ mod tests { }]) .await; + let ov = GasOverheads::default(); let expected_gas = math::increase_by_percent( op.pre_verification_gas + op.verification_gas_limit * 2 + op.call_gas_limit - + U256::from(5_000) - + U256::from(21_000), + + ov.bundle_transaction_gas_buffer + + ov.transaction_gas_overhead, BUNDLE_TRANSACTION_GAS_OVERHEAD_PERCENT, ); diff --git a/crates/sim/src/gas/gas.rs b/crates/sim/src/gas/gas.rs index 3988a7ccd..50fcdd771 100644 --- a/crates/sim/src/gas/gas.rs +++ b/crates/sim/src/gas/gas.rs @@ -28,13 +28,13 @@ use tokio::try_join; use super::polygon::Polygon; -// 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 +/// 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)] -struct GasOverheads { - bundle_transaction_gas_buffer: U256, - transaction_gas_overhead: U256, +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, @@ -44,8 +44,8 @@ struct GasOverheads { impl Default for GasOverheads { fn default() -> Self { Self { - bundle_transaction_gas_buffer: 5_000.into(), // Entrypoint requires a buffer over the user operation gas limits in the bundle transaction - transaction_gas_overhead: 21_000.into(), // The fixed gas overhead for any EVM transaction + 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(), @@ -74,7 +74,7 @@ pub async fn calc_pre_verification_gas( provider: Arc

, chain_id: u64, ) -> anyhow::Result { - let static_gas = calc_static_pre_verification_gas(full_op, GasOverheads::default(), true); + let static_gas = calc_static_pre_verification_gas(full_op, true); let dynamic_gas = match chain_id { _ if ARBITRUM_CHAIN_IDS.contains(&chain_id) => { provider @@ -118,7 +118,7 @@ pub fn user_operation_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. let pvg = if OP_BEDROCK_CHAIN_IDS.contains(&chain_id) | ARBITRUM_CHAIN_IDS.contains(&chain_id) { - calc_static_pre_verification_gas(uo, GasOverheads::default(), include_fixed_gas_overhead) + calc_static_pre_verification_gas(uo, include_fixed_gas_overhead) } else { uo.pre_verification_gas }; @@ -133,11 +133,8 @@ pub fn user_operation_max_gas_cost(uo: &UserOperation) -> U256 { * (uo.pre_verification_gas + uo.call_gas_limit + uo.verification_gas_limit * mul) } -fn calc_static_pre_verification_gas( - op: &UserOperation, - ov: GasOverheads, - include_fixed_gas_overhead: bool, -) -> U256 { +fn calc_static_pre_verification_gas(op: &UserOperation, include_fixed_gas_overhead: bool) -> U256 { + let ov = GasOverheads::default(); let encoded_op = op.clone().encode(); let length_in_words = encoded_op.len() / 32; // size of packed user op is always a multiple of 32 bytes let call_data_cost: U256 = encoded_op