Skip to content

Commit f5d45c6

Browse files
jakehobbsdancoombs
andauthored
fix(builder): fix EIP-7623 intrinsic gas handling (#1268)
Co-authored-by: Dan <dan.coombs@alchemy.com>
1 parent b4c94f2 commit f5d45c6

File tree

6 files changed

+203
-34
lines changed

6 files changed

+203
-34
lines changed

bin/rundler/chain_specs/base.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ min_max_priority_fee_per_gas = 1000000
1212
block_gas_limit = 140000000
1313
transaction_gas_limit = 16777216
1414
eip7702_enabled = true
15+
eip7623_enabled = true
1516
flashblocks_enabled = true

bin/rundler/chain_specs/optimism.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ min_max_priority_fee_per_gas = 100000
1010
max_transaction_size_bytes = 90000
1111
block_gas_limit = 30000000
1212
eip7702_enabled = true
13+
eip7623_enabled = true

crates/builder/src/bundle_proposer.rs

Lines changed: 176 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1732,7 +1732,10 @@ impl<UO: UserOperation> ProposalContext<UO> {
17321732
}
17331733

17341734
fn get_bundle_gas_limit_inner(&self, chain_spec: &ChainSpec, include_da_gas: bool) -> u128 {
1735-
let mut gas_limit = rundler_types::bundle_shared_gas(chain_spec);
1735+
let mut authorization_gas = 0_u128;
1736+
let mut standard_gas_limit = 0_u128;
1737+
let mut calldata_floor_gas_limit = 0_u128;
1738+
let mut da_gas_limit = 0_u128;
17361739

17371740
// Per aggregator fixed gas
17381741
for agg in self.groups_by_aggregator.keys() {
@@ -1743,36 +1746,45 @@ impl<UO: UserOperation> ProposalContext<UO> {
17431746
// this should be checked prior to calling this function
17441747
panic!("BUG: aggregator {agg:?} not found in chain spec");
17451748
};
1746-
gas_limit += agg.costs().execution_fixed_gas;
1749+
standard_gas_limit = standard_gas_limit.saturating_add(agg.costs().execution_fixed_gas);
17471750

17481751
// NOTE: this assumes that the DA cost for the aggregated signature is covered by the gas limits
17491752
// from the UOs (on chains that have DA gas in gas limit). This is enforced during fee check phase.
17501753
}
17511754

1752-
// per UO gas, bundle_size == None to signal to exclude shared gas
1753-
gas_limit += self
1754-
.iter_ops_with_simulations()
1755-
.map(|sim_op| {
1756-
if include_da_gas {
1757-
sim_op.op.bundle_gas_limit(chain_spec, None) + sim_op.sponsored_da_gas
1758-
} else {
1759-
sim_op.op.bundle_computation_gas_limit(chain_spec, None)
1760-
}
1761-
})
1762-
.sum::<u128>();
1755+
for sim_op in self.iter_ops_with_simulations() {
1756+
let op_authorization_gas = sim_op.op.authorization_gas_limit();
1757+
authorization_gas = authorization_gas.saturating_add(op_authorization_gas);
17631758

1764-
let calldata_floor_gas_limit = self.bundle_overhead_bytes(chain_spec)
1765-
* chain_spec.calldata_floor_non_zero_byte_gas()
1766-
+ self
1767-
.iter_ops_with_simulations()
1768-
.map(|sim_op| sim_op.op.calldata_floor_gas_limit())
1769-
.sum::<u128>();
1759+
// EIP-7623 calculations require this to be done without the EIP-7702 intrinsic gas. Its added later in the final calculation.
1760+
standard_gas_limit = standard_gas_limit.saturating_add(
1761+
sim_op.op.bundle_computation_gas_limit(chain_spec, None) - op_authorization_gas,
1762+
);
1763+
1764+
calldata_floor_gas_limit =
1765+
calldata_floor_gas_limit.saturating_add(sim_op.op.calldata_floor_gas_limit());
17701766

1771-
if calldata_floor_gas_limit > gas_limit {
1772-
return calldata_floor_gas_limit;
1767+
if include_da_gas {
1768+
da_gas_limit = da_gas_limit.saturating_add(
1769+
sim_op.op.pre_verification_da_gas_limit(chain_spec, None)
1770+
+ sim_op.sponsored_da_gas,
1771+
);
1772+
}
17731773
}
17741774

1775-
gas_limit
1775+
calldata_floor_gas_limit = calldata_floor_gas_limit.saturating_add(
1776+
self.bundle_overhead_bytes(chain_spec)
1777+
.saturating_mul(chain_spec.calldata_floor_non_zero_byte_gas()),
1778+
);
1779+
1780+
// EIP-7623 maximum between standard and calldata floor
1781+
let execution_gas_limit = standard_gas_limit.max(calldata_floor_gas_limit);
1782+
1783+
chain_spec
1784+
.transaction_intrinsic_gas()
1785+
.saturating_add(authorization_gas)
1786+
.saturating_add(da_gas_limit)
1787+
.saturating_add(execution_gas_limit)
17761788
}
17771789

17781790
fn iter_ops_with_simulations(&self) -> impl Iterator<Item = &OpWithSimulation<UO>> + '_ {
@@ -3084,6 +3096,148 @@ mod tests {
30843096
assert_eq!(gas_limit, expected_gas_limit);
30853097
}
30863098

3099+
#[tokio::test]
3100+
async fn test_bundle_gas_limit_calldata_floor_with_authorization() {
3101+
// Enable EIP-7623 so the calldata floor is non-zero
3102+
let cs = ChainSpec {
3103+
eip7623_enabled: true,
3104+
..Default::default()
3105+
};
3106+
3107+
// Create an op with an authorization tuple
3108+
let op = UserOperationBuilder::new(
3109+
&cs,
3110+
UserOperationRequiredFields {
3111+
// Use small gas limits so the calldata floor wins over execution gas
3112+
pre_verification_gas: 1_000,
3113+
call_gas_limit: 1_000,
3114+
verification_gas_limit: 1_000,
3115+
// Large calldata to ensure the calldata floor exceeds execution gas
3116+
call_data: Bytes::from(vec![1u8; 4096]),
3117+
..Default::default()
3118+
},
3119+
)
3120+
.authorization_tuple(rundler_types::authorization::Eip7702Auth::new_dummy(
3121+
cs.id,
3122+
address(1),
3123+
))
3124+
.build();
3125+
3126+
let auth_gas = op.authorization_gas_limit();
3127+
assert!(auth_gas > 0, "op must have authorization gas");
3128+
3129+
let mut groups_by_aggregator = LinkedHashMap::new();
3130+
groups_by_aggregator.insert(
3131+
Address::ZERO,
3132+
AggregatorGroup {
3133+
ops_with_simulations: vec![OpWithSimulation {
3134+
op: op.clone(),
3135+
simulation: SimulationResult {
3136+
requires_post_op: false,
3137+
..Default::default()
3138+
},
3139+
sponsored_da_gas: 0,
3140+
}],
3141+
signature: Default::default(),
3142+
},
3143+
);
3144+
let context = ProposalContext {
3145+
groups_by_aggregator,
3146+
rejected_ops: vec![],
3147+
entity_updates: BTreeMap::new(),
3148+
bundle_expected_storage: BundleExpectedStorage::default(),
3149+
};
3150+
3151+
let gas_limit = context.get_bundle_gas_limit(&cs);
3152+
3153+
// Verify the calldata floor path is being taken by checking that
3154+
// the gas limit is higher than what execution gas alone would give
3155+
let execution_gas = op.bundle_gas_limit(&cs, None) + rundler_types::bundle_shared_gas(&cs);
3156+
let calldata_floor = context.bundle_overhead_bytes(&cs)
3157+
* cs.calldata_floor_non_zero_byte_gas()
3158+
+ op.calldata_floor_gas_limit();
3159+
assert!(
3160+
calldata_floor > execution_gas,
3161+
"calldata floor ({calldata_floor}) must exceed execution gas ({execution_gas}) for this test to be valid"
3162+
);
3163+
3164+
// The gas limit must include transaction intrinsic and authorization gas
3165+
// even when calldata floor wins.
3166+
assert_eq!(
3167+
gas_limit,
3168+
cs.transaction_intrinsic_gas() + calldata_floor + auth_gas
3169+
);
3170+
}
3171+
3172+
#[tokio::test]
3173+
async fn test_bundle_gas_limit_authorization_delta_is_empty_account_cost() {
3174+
let cs = ChainSpec::default();
3175+
let required = UserOperationRequiredFields {
3176+
pre_verification_gas: 100_000,
3177+
call_gas_limit: 100_000,
3178+
verification_gas_limit: 100_000,
3179+
..Default::default()
3180+
};
3181+
3182+
let op_without_authorization = UserOperationBuilder::new(&cs, required.clone()).build();
3183+
let op_with_authorization = UserOperationBuilder::new(&cs, required)
3184+
.authorization_tuple(rundler_types::authorization::Eip7702Auth::new_dummy(
3185+
cs.id,
3186+
address(1),
3187+
))
3188+
.build();
3189+
3190+
assert_eq!(op_without_authorization.authorization_gas_limit(), 0);
3191+
assert_eq!(op_with_authorization.authorization_gas_limit(), 25_000);
3192+
3193+
let mut groups_without_authorization = LinkedHashMap::new();
3194+
groups_without_authorization.insert(
3195+
Address::ZERO,
3196+
AggregatorGroup {
3197+
ops_with_simulations: vec![OpWithSimulation {
3198+
op: op_without_authorization,
3199+
simulation: SimulationResult::default(),
3200+
sponsored_da_gas: 0,
3201+
}],
3202+
signature: Default::default(),
3203+
},
3204+
);
3205+
let context_without_authorization = ProposalContext {
3206+
groups_by_aggregator: groups_without_authorization,
3207+
rejected_ops: vec![],
3208+
entity_updates: BTreeMap::new(),
3209+
bundle_expected_storage: BundleExpectedStorage::default(),
3210+
};
3211+
3212+
let mut groups_with_authorization = LinkedHashMap::new();
3213+
groups_with_authorization.insert(
3214+
Address::ZERO,
3215+
AggregatorGroup {
3216+
ops_with_simulations: vec![OpWithSimulation {
3217+
op: op_with_authorization,
3218+
simulation: SimulationResult::default(),
3219+
sponsored_da_gas: 0,
3220+
}],
3221+
signature: Default::default(),
3222+
},
3223+
);
3224+
let context_with_authorization = ProposalContext {
3225+
groups_by_aggregator: groups_with_authorization,
3226+
rejected_ops: vec![],
3227+
entity_updates: BTreeMap::new(),
3228+
bundle_expected_storage: BundleExpectedStorage::default(),
3229+
};
3230+
3231+
let gas_without_authorization = context_without_authorization.get_bundle_gas_limit(&cs);
3232+
let gas_with_authorization = context_with_authorization.get_bundle_gas_limit(&cs);
3233+
3234+
assert_eq!(
3235+
gas_with_authorization - gas_without_authorization,
3236+
25_000,
3237+
"authorization should add only EIP-7702 empty account intrinsic gas"
3238+
);
3239+
}
3240+
30873241
#[tokio::test]
30883242
async fn test_post_op_revert() {
30893243
let op1 = op_with_sender(address(1));

crates/builder/src/bundle_sender.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
use std::{pin::Pin, sync::Arc, time::Duration};
1515

16-
use alloy_primitives::{Address, B256};
16+
use alloy_primitives::{Address, B256, hex};
1717
use anyhow::{Context, bail};
1818
use async_trait::async_trait;
1919
use futures::Stream;
@@ -701,7 +701,6 @@ where
701701
}
702702
Err(e) => bail!("Failed to make bundle: {e:?}"),
703703
};
704-
705704
let Some(bundle_tx) = self.get_bundle_tx(nonce, bundle).await? else {
706705
self.emit(BuilderEvent::formed_bundle(
707706
self.builder_tag.clone(),
@@ -798,11 +797,26 @@ where
798797
}
799798
Err(TransactionTrackerError::Other(e)) => {
800799
error!("Failed to send bundle with unexpected error: {e:?}");
800+
if Self::is_intrinsic_gas_too_low_error(&e) {
801+
let tx_bytes = tx.input.input().map_or_else(
802+
|| String::from("0x"),
803+
|data| format!("0x{}", hex::encode(data)),
804+
);
805+
error!(
806+
"Bundle transaction bytes for intrinsic gas too low: tx_bytes={tx_bytes}"
807+
);
808+
}
801809
Err(e)
802810
}
803811
}
804812
}
805813

814+
fn is_intrinsic_gas_too_low_error(error: &anyhow::Error) -> bool {
815+
format!("{error:#}")
816+
.to_lowercase()
817+
.contains("intrinsic gas too low")
818+
}
819+
806820
/// Builds a bundle and returns some metadata and the transaction to send
807821
/// it, or `None` if there are no valid operations available.
808822
async fn get_bundle_tx(

crates/types/src/user_operation/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,8 +412,7 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static {
412412
/// Returns the gas limit for the authorization
413413
fn authorization_gas_limit(&self) -> u128 {
414414
if self.authorization_tuple().is_some() {
415-
alloy_eips::eip7702::constants::PER_AUTH_BASE_COST as u128
416-
+ alloy_eips::eip7702::constants::PER_EMPTY_ACCOUNT_COST as u128
415+
alloy_eips::eip7702::constants::PER_EMPTY_ACCOUNT_COST as u128
417416
} else {
418417
0
419418
}

docs/developing.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,21 @@ git submodule update --init --recursive
1212

1313
2. Install prerequisites
1414

15-
* [Rust/Cargo](https://www.rust-lang.org/tools/install): 1.85 or higher with nightly
16-
* [Cocogitto](https://github.com/cocogitto/cocogitto): Commit linting
17-
* [Docker](https://docs.docker.com/engine/install/): Run spec tests
18-
* [PDM](https://pdm.fming.dev/latest/#installation): Run spec tests
19-
* [Protoc](https://grpc.io/docs/protoc-installation/): Compile protobuf
20-
* [Buf](https://buf.build/docs/installation): Protobuf linting
21-
* [Foundry ^0.3.0](https://book.getfoundry.sh/getting-started/installation): Compile contracts
15+
- [Rust/Cargo](https://www.rust-lang.org/tools/install): 1.85 or higher with nightly
16+
- [Cocogitto](https://github.com/cocogitto/cocogitto): Commit linting
17+
- [Docker](https://docs.docker.com/engine/install/): Run spec tests
18+
- [PDM](https://pdm.fming.dev/latest/#installation): Run spec tests
19+
- [Protoc](https://grpc.io/docs/protoc-installation/): Compile protobuf
20+
- [Buf](https://buf.build/docs/cli/installation/): Protobuf linting
21+
- [Foundry ^0.3.0](https://book.getfoundry.sh/getting-started/installation): Compile contracts
2222

2323
## Build & Test
2424

2525
Rundler contains a `Makefile` to simplify common build/test commands
2626

2727
```
2828
# build rundler
29-
$ make build
29+
$ make build
3030
3131
# run unit tests
3232
$ make test-unit

0 commit comments

Comments
 (0)