From f2e1857ae938e9604212cf2720aae0272e846031 Mon Sep 17 00:00:00 2001 From: Alex Miao Date: Thu, 5 Oct 2023 19:15:53 -0700 Subject: [PATCH] feat(builder): only include paymaster gas if there is a postOp --- crates/builder/src/bundle_proposer.rs | 41 +++++++-- crates/sim/src/gas/gas.rs | 114 +++--------------------- crates/sim/src/precheck.rs | 2 +- crates/sim/src/simulation/simulation.rs | 4 + 4 files changed, 53 insertions(+), 108 deletions(-) diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index c23f7db0b..94132fdec 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -12,6 +12,7 @@ // If not, see https://www.gnu.org/licenses/. use std::{ + cmp, collections::{HashMap, HashSet}, mem, sync::Arc, @@ -29,8 +30,10 @@ use mockall::automock; use rundler_pool::{PoolOperation, PoolServer}; use rundler_provider::{EntryPoint, HandleOpsOut, Provider}; use rundler_sim::{ - gas, ExpectedStorage, FeeEstimator, PriorityFeeMode, SimulationError, SimulationSuccess, - Simulator, + gas::{ + self, user_operation_gas_limit, user_operation_pre_verification_gas_limit, GasOverheads, + }, + ExpectedStorage, FeeEstimator, PriorityFeeMode, SimulationError, SimulationSuccess, Simulator, }; use rundler_types::{Entity, EntityType, GasFees, Timestamp, UserOperation, UserOpsPerAggregator}; use rundler_utils::{emit::WithEntryPoint, math}; @@ -409,7 +412,7 @@ where // sum up the gas needed for all the ops in the bundle // and apply an overhead multiplier let gas = math::increase_by_percent( - context.get_total_gas_limit(self.settings.chain_id), + context.get_bundle_gas_limit(self.settings.chain_id), BUNDLE_TRANSACTION_GAS_OVERHEAD_PERCENT, ); let handle_ops_out = self @@ -536,7 +539,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, false); + let gas = gas::user_operation_gas_limit(&op.uo, self.settings.chain_id, false, None); if gas_left < gas { self.emit(BuilderEvent::skipped_op( self.builder_index, @@ -736,8 +739,32 @@ impl ProposalContext { .collect() } - fn get_total_gas_limit(&self, chain_id: u64) -> U256 { - gas::bundle_gas_limit(self.iter_ops(), chain_id) + fn get_bundle_gas_limit(&self, chain_id: u64) -> U256 { + let ov = GasOverheads::default(); + let mut max_gas = U256::zero(); + let mut gas_spent = U256::zero(); + for op_with_sim in self.iter_ops_with_simulations() { + let op = &op_with_sim.op; + let post_exec_req_gas = op + .paymaster() + .map_or(ov.bundle_transaction_gas_buffer, |_| { + cmp::max(op.verification_gas_limit, ov.bundle_transaction_gas_buffer) + }); + let required_gas = gas_spent + + user_operation_pre_verification_gas_limit(op, chain_id, false) + + op.verification_gas_limit * 2 + + op.call_gas_limit + + post_exec_req_gas; + max_gas = cmp::max(required_gas, max_gas); + gas_spent += user_operation_gas_limit( + op, + chain_id, + false, + Some(op_with_sim.simulation.requires_post_op), + ); + } + + max_gas + ov.transaction_gas_overhead } fn iter_ops_with_simulations(&self) -> impl Iterator + '_ { @@ -757,7 +784,7 @@ mod tests { use ethers::{types::H160, utils::parse_units}; use rundler_pool::MockPoolServer; use rundler_provider::{AggregatorSimOut, MockEntryPoint, MockProvider}; - use rundler_sim::{gas::GasOverheads, MockSimulator, SimulationViolation}; + use rundler_sim::{MockSimulator, SimulationViolation}; use rundler_types::ValidTimeRange; use super::*; diff --git a/crates/sim/src/gas/gas.rs b/crates/sim/src/gas/gas.rs index fa0393184..646df51e7 100644 --- a/crates/sim/src/gas/gas.rs +++ b/crates/sim/src/gas/gas.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::{cmp, sync::Arc}; +use std::sync::Arc; use ethers::{ abi::AbiEncode, @@ -94,44 +94,21 @@ 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(); - let mut max_gas = U256::zero(); - let mut gas_spent = U256::zero(); - for op in iter_ops { - let post_exec_req_gas = op - .paymaster() - .map_or(ov.bundle_transaction_gas_buffer, |_| { - cmp::max(op.verification_gas_limit, ov.bundle_transaction_gas_buffer) - }); - let required_gas = gas_spent - + user_operation_pre_verification_gas_limit(op, chain_id, false) - + op.verification_gas_limit * 2 - + op.call_gas_limit - + post_exec_req_gas; - max_gas = cmp::max(required_gas, max_gas); - gas_spent += user_operation_gas_limit(op, chain_id, false); - } - - max_gas + ov.transaction_gas_overhead -} - /// 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, assume_single_op_bundle: bool, + paymaster_post_op: Option, ) -> U256 { user_operation_pre_verification_gas_limit(uo, chain_id, assume_single_op_bundle) + uo.call_gas_limit - + uo.verification_gas_limit * verification_gas_limit_multiplier(uo, assume_single_op_bundle) + + uo.verification_gas_limit + * verification_gas_limit_multiplier(uo, assume_single_op_bundle, paymaster_post_op) } -fn user_operation_pre_verification_gas_limit( +/// Returns the static pre-verification gas cost of a user operation +pub fn user_operation_pre_verification_gas_limit( uo: &UserOperation, chain_id: u64, include_fixed_gas_overhead: bool, @@ -179,13 +156,15 @@ fn calc_static_pre_verification_gas(op: &UserOperation, include_fixed_gas_overhe }) } -fn verification_gas_limit_multiplier(uo: &UserOperation, assume_single_op_bundle: bool) -> u64 { - // If using a paymaster, we need to account for potentially 2 postOp - // calls (even though it won't be called). - // Else the entrypoint expects the gas for 1 postOp call that - // uses verification_gas_limit plus the actual verification call +fn verification_gas_limit_multiplier( + uo: &UserOperation, + assume_single_op_bundle: bool, + paymaster_post_op: Option, +) -> u64 { + // If using a paymaster that has a postOp, we need to account for potentially 2 postOp calls which can each use up to verification_gas_limit gas. + // otherwise the entrypoint expects the gas for 1 postOp call that uses verification_gas_limit plus the actual verification call // we only add the additional verification_gas_limit only if we know for sure that this is a single op bundle, which what we do to get a worst-case upper bound - if uo.paymaster().is_some() { + if uo.paymaster().is_some() && paymaster_post_op.map_or(true, |x| x) { 3 } else if assume_single_op_bundle { 2 @@ -329,68 +308,3 @@ const NON_EIP_1559_CHAIN_IDS: &[u64] = &[ fn is_known_non_eip_1559_chain(chain_id: u64) -> bool { NON_EIP_1559_CHAIN_IDS.contains(&chain_id) } - -#[cfg(test)] -mod tests { - use ethers::types::Bytes; - - use super::*; - - fn create_test_op_with_gas( - pre_verification_gas: U256, - call_gas_limit: U256, - verification_gas_limit: U256, - with_paymaster: bool, - ) -> UserOperation { - UserOperation { - pre_verification_gas, - call_gas_limit, - verification_gas_limit, - paymaster_and_data: if with_paymaster { - Bytes::from(vec![0; 20]) - } else { - Default::default() - }, - ..Default::default() - } - } - - #[tokio::test] - async fn test_bundle_gas_limit() { - let op1 = create_test_op_with_gas(100_000.into(), 100_000.into(), 1_000_000.into(), false); - let op2 = create_test_op_with_gas(100_000.into(), 100_000.into(), 200_000.into(), false); - let ops = vec![op1.clone(), op2.clone()]; - let chain_id = 1; - let gas_limit = bundle_gas_limit(ops.iter(), chain_id); - - // The gas requirement in the first user operation dominates and determines the expected gas limit - let expected_gas_limit = op1.pre_verification_gas - + op1.verification_gas_limit * 2 - + op1.call_gas_limit - + 21_000 - + 5_000; - - assert_eq!(gas_limit, expected_gas_limit); - } - - #[tokio::test] - async fn test_bundle_gas_limit_with_paymaster_op() { - let op1 = create_test_op_with_gas(100_000.into(), 100_000.into(), 1_000_000.into(), true); // has paymaster - let op2 = create_test_op_with_gas(100_000.into(), 100_000.into(), 200_000.into(), false); - let ops = vec![op1.clone(), op2.clone()]; - let chain_id = 1; - let gas_limit = bundle_gas_limit(ops.iter(), chain_id); - - // The gas requirement in the second user operation dominates and determines the expected gas limit - let expected_gas_limit = op1.pre_verification_gas - + op1.verification_gas_limit * 3 - + op1.call_gas_limit - + op2.pre_verification_gas - + op2.verification_gas_limit * 2 - + op2.call_gas_limit - + 21_000 - + 5_000; - - assert_eq!(gas_limit, expected_gas_limit); - } -} diff --git a/crates/sim/src/precheck.rs b/crates/sim/src/precheck.rs index 74fddf1f2..ca2177981 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, true); + let total_gas_limit = gas::user_operation_gas_limit(op, chain_id, true, None); if total_gas_limit > max_total_execution_gas { violations.push(PrecheckViolation::TotalGasLimitTooHigh( total_gas_limit, diff --git a/crates/sim/src/simulation/simulation.rs b/crates/sim/src/simulation/simulation.rs index 6a3e533d4..c4acfcb60 100644 --- a/crates/sim/src/simulation/simulation.rs +++ b/crates/sim/src/simulation/simulation.rs @@ -71,6 +71,8 @@ pub struct SimulationSuccess { pub accessed_addresses: HashSet
, /// Expected storage values for all accessed slots during validation pub expected_storage: ExpectedStorage, + /// Whether the operation requires a post-op + pub requires_post_op: bool, } impl SimulationSuccess { @@ -503,6 +505,7 @@ where pre_op_gas, valid_after, valid_until, + paymaster_context, .. } = return_info; Ok(SimulationSuccess { @@ -516,6 +519,7 @@ where account_is_staked, accessed_addresses, expected_storage: tracer_out.expected_storage, + requires_post_op: !paymaster_context.is_empty(), }) } }