Skip to content

Commit

Permalink
feat(builder): limit bundle size by max gas limit
Browse files Browse the repository at this point in the history
  • Loading branch information
dancoombs committed Aug 15, 2023
1 parent 622c9c5 commit 6e7cf2e
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 19 deletions.
94 changes: 76 additions & 18 deletions src/builder/bundle_proposer.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::{
cmp,
collections::{HashMap, HashSet},
mem,
sync::Arc,
Expand Down Expand Up @@ -30,15 +29,16 @@ use crate::{
simulation::{SimulationError, SimulationSuccess, Simulator},
types::{
Entity, EntityType, EntryPointLike, ExpectedStorage, HandleOpsOut, ProviderLike,
Timestamp, UserOperation, OP_BEDROCK_CHAIN_IDS,
Timestamp, UserOperation,
},
},
};

/// 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);

const BUNDLE_TRANSACTION_GAS_OVERHEAD_PERCENT: u64 = 100;
const MAX_BUNDLE_GAS_LIMIT: u64 = 20_000_000;
const BUNDLE_TRANSACTION_GAS_OVERHEAD_PERCENT: u64 = 50;

#[derive(Debug, Default)]
pub struct Bundle {
Expand Down Expand Up @@ -114,6 +114,9 @@ where
self.fee_estimator.required_bundle_fees(required_fees)
)?;

// Limit the amount of gas in the bundle
let ops = self.limit_gas_in_bundle(ops);

// Determine fees required for ops to be included in a bundle, and filter out ops that don't
// meet the requirements.
let required_op_fees = self.fee_estimator.required_op_fees(bundle_fees);
Expand Down Expand Up @@ -479,6 +482,20 @@ where
Ok(())
}

fn limit_gas_in_bundle(&self, ops: Vec<OpFromPool>) -> Vec<OpFromPool> {
let mut gas_left = U256::from(MAX_BUNDLE_GAS_LIMIT);
let mut ops_in_bundle = Vec::new();
for op in ops {
let gas = op.op.total_execution_gas_limit(self.chain_id);
if gas_left < gas {
break;
}
gas_left -= gas;
ops_in_bundle.push(op);
}
ops_in_bundle
}

fn emit(&self, event: BuilderEvent) {
let _ = self.event_sender.send(WithEntryPoint {
entry_point: self.entry_point.address(),
Expand Down Expand Up @@ -680,22 +697,8 @@ impl ProposalContext {
}

fn get_total_gas(&self, chain_id: u64) -> U256 {
// On some chains the L1 gas fee is charged via pre_verification_gas
// in the bundle, but is not part of the gas limit of the transaction.
// Thus, including it can cause the transaction to exceed the maximum limit
// of a block. This workaround caps the pre_verification_gas at 100k during
// bundle gas estimation.
let max_pre_verification_gas = if OP_BEDROCK_CHAIN_IDS.contains(&chain_id) {
U256::from(100_000)
} else {
U256::MAX
};
self.iter_ops()
.map(|op| {
cmp::min(op.pre_verification_gas, max_pre_verification_gas)
+ op.verification_gas_limit
+ op.call_gas_limit
})
.map(|op| op.total_execution_gas_limit(chain_id))
.fold(U256::zero(), |acc, c| acc + c)
}

Expand Down Expand Up @@ -1108,6 +1111,53 @@ mod tests {
);
}

#[tokio::test]
async fn test_bundle_gas_limit() {
let op1 = op_with_sender_call_gas_limit(address(1), U256::from(10_000_000));
let op2 = op_with_sender_call_gas_limit(address(2), U256::from(10_000_000));
let op3 = op_with_sender_call_gas_limit(address(3), U256::from(10_000_000));
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(
vec![
MockOp {
op: op1.clone(),
simulation_result: Box::new(|| Ok(SimulationSuccess::default())),
},
MockOp {
op: op2.clone(),
simulation_result: Box::new(|| Ok(SimulationSuccess::default())),
},
MockOp {
op: op3.clone(),
simulation_result: Box::new(|| Ok(SimulationSuccess::default())),
},
MockOp {
op: op4.clone(),
simulation_result: Box::new(|| Ok(SimulationSuccess::default())),
},
],
vec![],
vec![HandleOpsOut::Success],
vec![deposit, deposit, deposit],
U256::zero(),
U256::zero(),
)
.await;

assert_eq!(bundle.rejected_entities, vec![]);
assert_eq!(bundle.rejected_ops, vec![]);
assert_eq!(
bundle.ops_per_aggregator,
vec![UserOpsPerAggregator {
user_ops: vec![op1, op2],
..Default::default()
}]
);
assert_eq!(bundle.gas_estimate, U256::from(30_000_000));
}

struct MockOp {
op: UserOperation,
simulation_result:
Expand Down Expand Up @@ -1277,4 +1327,12 @@ mod tests {
..Default::default()
}
}

fn op_with_sender_call_gas_limit(sender: Address, call_gas_limit: U256) -> UserOperation {
UserOperation {
sender,
call_gas_limit,
..Default::default()
}
}
}
2 changes: 1 addition & 1 deletion src/cli/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ pub struct BuilderArgs {
long = "builder.max_bundle_size",
name = "builder.max_bundle_size",
env = "BUILDER_MAX_BUNDLE_SIZE",
default_value = "64"
default_value = "128"
)]
max_bundle_size: u64,

Expand Down
16 changes: 16 additions & 0 deletions src/common/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ impl UserOperation {
let max_gas = self.call_gas_limit + self.verification_gas_limit + self.pre_verification_gas;
max_gas * self.max_fee_per_gas
}

pub fn total_execution_gas_limit(&self, chain_id: u64) -> U256 {
// On some chains the L1 gas fee is charged via pre_verification_gas
// but this not part of the execution gas of the transaction.
// This workaround caps the pre_verification_gas at 100k during
// the execution gas calculation.
let max = if OP_BEDROCK_CHAIN_IDS.contains(&chain_id) {
U256::from(100_000)
} else {
U256::MAX
};

std::cmp::min(max, self.pre_verification_gas)
+ self.call_gas_limit
+ self.verification_gas_limit
}
}

#[derive(Display, Debug, Clone, Ord, Copy, Eq, PartialEq, EnumIter, PartialOrd, Deserialize)]
Expand Down

0 comments on commit 6e7cf2e

Please sign in to comment.