Skip to content

Commit

Permalink
feat(builder): only include paymaster gas if there is a postOp
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-miao committed Oct 9, 2023
1 parent 440c096 commit f2e1857
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 108 deletions.
41 changes: 34 additions & 7 deletions crates/builder/src/bundle_proposer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// If not, see https://www.gnu.org/licenses/.

use std::{
cmp,
collections::{HashMap, HashSet},
mem,
sync::Arc,
Expand All @@ -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};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Item = &OpWithSimulation> + '_ {
Expand All @@ -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::*;
Expand Down
114 changes: 14 additions & 100 deletions crates/sim/src/gas/gas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -94,44 +94,21 @@ pub async fn calc_pre_verification_gas<P: Provider>(
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<Item = &'a UserOperation>,
{
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<bool>,
) -> 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,
Expand Down Expand Up @@ -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<bool>,
) -> 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
Expand Down Expand Up @@ -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);
}
}
2 changes: 1 addition & 1 deletion crates/sim/src/precheck.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ impl<P: Provider, E: EntryPoint> PrecheckerImpl<P, E> {
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,
Expand Down
4 changes: 4 additions & 0 deletions crates/sim/src/simulation/simulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ pub struct SimulationSuccess {
pub accessed_addresses: HashSet<Address>,
/// 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 {
Expand Down Expand Up @@ -503,6 +505,7 @@ where
pre_op_gas,
valid_after,
valid_until,
paymaster_context,
..
} = return_info;
Ok(SimulationSuccess {
Expand All @@ -516,6 +519,7 @@ where
account_is_staked,
accessed_addresses,
expected_storage: tracer_out.expected_storage,
requires_post_op: !paymaster_context.is_empty(),
})
}
}
Expand Down

0 comments on commit f2e1857

Please sign in to comment.