From 33161cf873c63d8ef236254adae4eaf2c833003c Mon Sep 17 00:00:00 2001 From: 0xDevNinja Date: Thu, 30 Apr 2026 19:31:34 +0530 Subject: [PATCH 1/5] Enforce EIP-7825 per-tx gas cap on settlement --- .../domain/competition/solution/settlement.rs | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index c2d62f41fa..6bff75ede5 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -428,6 +428,11 @@ pub struct Gas { } impl Gas { + /// EIP-7825 per-transaction gas cap (2^24 - 1) introduced in Fusaka. + /// Any transaction exceeding this will be rejected by the mempool, so a + /// solution requiring more gas can never be settled on chain. + pub const EIP_7825_TX_GAS_CAP: u64 = (1 << 24) - 1; + /// Computes settlement gas parameters given estimates for gas and gas /// price. pub fn new(estimate: eth::Gas, block_limit: eth::Gas) -> Result { @@ -441,7 +446,13 @@ impl Gas { // will not exceed the remaining space in the block next and ignore transactions // whose gas limit exceed the remaining space (without simulating the actual // gas required). - let max_gas = eth::Gas(block_limit.0 / eth::U256::from(2)); + // Additionally cap by the EIP-7825 per-tx gas limit: even if half the + // block limit is higher, the mempool will reject any tx above the cap, + // so the settlement could never be mined. + let max_gas = std::cmp::min( + eth::Gas(block_limit.0 / eth::U256::from(2)), + eth::Gas(eth::U256::from(Self::EIP_7825_TX_GAS_CAP)), + ); if estimate > max_gas { return Err(solution::Error::GasLimitExceeded(estimate, max_gas)); } @@ -467,3 +478,49 @@ impl Gas { self.limit * max_fee_per_gas.into() } } + +#[cfg(test)] +mod tests { + use super::*; + + fn gas(value: u64) -> eth::Gas { + eth::Gas(eth::U256::from(value)) + } + + #[test] + fn rejects_solution_above_eip_7825_cap() { + // 120M block (well above the EIP-7825 cap of 16,777,215). Half the + // block is 60M, but the per-tx cap must still apply. + let block_limit = gas(120_000_000); + let estimate = gas(20_000_000); + let err = Gas::new(estimate, block_limit).unwrap_err(); + match err { + solution::Error::GasLimitExceeded(used, limit) => { + assert_eq!(used, estimate); + assert_eq!(limit, gas(Gas::EIP_7825_TX_GAS_CAP)); + } + other => panic!("unexpected error: {other:?}"), + } + } + + #[test] + fn accepts_solution_at_eip_7825_cap() { + let block_limit = gas(120_000_000); + let estimate = gas(Gas::EIP_7825_TX_GAS_CAP); + let result = Gas::new(estimate, block_limit).unwrap(); + assert_eq!(result.estimate, estimate); + } + + #[test] + fn small_block_limit_still_caps_at_half() { + // On chains with a low block gas limit, the half-block cap is tighter + // than the EIP-7825 cap and must keep applying. + let block_limit = gas(10_000_000); + let estimate = gas(6_000_000); + let err = Gas::new(estimate, block_limit).unwrap_err(); + match err { + solution::Error::GasLimitExceeded(_, limit) => assert_eq!(limit, gas(5_000_000)), + other => panic!("unexpected error: {other:?}"), + } + } +} From 2e4268be6d5c0793ffc33207ff332b5a6d387381 Mon Sep 17 00:00:00 2001 From: 0xDevNinja Date: Mon, 4 May 2026 19:26:21 +0530 Subject: [PATCH 2/5] Address review on EIP-7825 gas cap - Make EIP_7825_TX_GAS_CAP private; no external callers. - Reword test comment for clarity. --- crates/driver/src/domain/competition/solution/settlement.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 6bff75ede5..fb933f8918 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -431,7 +431,7 @@ impl Gas { /// EIP-7825 per-transaction gas cap (2^24 - 1) introduced in Fusaka. /// Any transaction exceeding this will be rejected by the mempool, so a /// solution requiring more gas can never be settled on chain. - pub const EIP_7825_TX_GAS_CAP: u64 = (1 << 24) - 1; + const EIP_7825_TX_GAS_CAP: u64 = (1 << 24) - 1; /// Computes settlement gas parameters given estimates for gas and gas /// price. @@ -489,8 +489,8 @@ mod tests { #[test] fn rejects_solution_above_eip_7825_cap() { - // 120M block (well above the EIP-7825 cap of 16,777,215). Half the - // block is 60M, but the per-tx cap must still apply. + // Block limit (120M) is high enough that half the block (60M) exceeds + // the EIP-7825 per-tx cap (16,777,215). The per-tx cap must win. let block_limit = gas(120_000_000); let estimate = gas(20_000_000); let err = Gas::new(estimate, block_limit).unwrap_err(); From 967f2cdf135c72840ebb5c530c01ec00da99dd06 Mon Sep 17 00:00:00 2001 From: 0xDevNinja Date: Mon, 4 May 2026 19:42:11 +0530 Subject: [PATCH 3/5] Use configured tx_gas_limit knob for settlement cap Replace the hardcoded EIP-7825 constant in Gas::new with the existing per-driver tx_gas_limit config knob. Operators set the cap per chain (EIP-7825's 16,777,215 on Fusaka chains, higher elsewhere) so non-Fusaka chains aren't over-restricted by an L1-only fork rule. --- .../domain/competition/solution/settlement.rs | 67 ++++++++++++------- crates/driver/src/infra/blockchain/mod.rs | 9 +++ crates/driver/src/run.rs | 10 ++- crates/driver/src/tests/setup/solver.rs | 1 + 4 files changed, 62 insertions(+), 25 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index fb933f8918..1d2883fc4b 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -178,7 +178,7 @@ impl Settlement { ) .await?; let price = eth.gas_price().await?; - let gas = Gas::new(gas, eth.block_gas_limit())?; + let gas = Gas::new(gas, eth.block_gas_limit(), eth.tx_gas_limit())?; // Ensure that the solver has sufficient balance for the settlement to be mined // even if the gas price keeps climbing during the tx submission. @@ -428,14 +428,13 @@ pub struct Gas { } impl Gas { - /// EIP-7825 per-transaction gas cap (2^24 - 1) introduced in Fusaka. - /// Any transaction exceeding this will be rejected by the mempool, so a - /// solution requiring more gas can never be settled on chain. - const EIP_7825_TX_GAS_CAP: u64 = (1 << 24) - 1; - /// Computes settlement gas parameters given estimates for gas and gas /// price. - pub fn new(estimate: eth::Gas, block_limit: eth::Gas) -> Result { + pub fn new( + estimate: eth::Gas, + block_limit: eth::Gas, + tx_gas_limit: eth::Gas, + ) -> Result { // We don't allow for solutions to take up more than half of the block's gas // limit. This is to ensure that block producers attempt to include the // settlement transaction in the next block as long as it is reasonably @@ -446,13 +445,11 @@ impl Gas { // will not exceed the remaining space in the block next and ignore transactions // whose gas limit exceed the remaining space (without simulating the actual // gas required). - // Additionally cap by the EIP-7825 per-tx gas limit: even if half the - // block limit is higher, the mempool will reject any tx above the cap, - // so the settlement could never be mined. - let max_gas = std::cmp::min( - eth::Gas(block_limit.0 / eth::U256::from(2)), - eth::Gas(eth::U256::from(Self::EIP_7825_TX_GAS_CAP)), - ); + // Additionally cap by the configured per-tx gas limit. Operators set + // this per chain (e.g. to EIP-7825's 16,777,215 cap on Fusaka chains) + // so the mempool can't reject the settlement for exceeding the per-tx + // ceiling. + let max_gas = std::cmp::min(eth::Gas(block_limit.0 / eth::U256::from(2)), tx_gas_limit); if estimate > max_gas { return Err(solution::Error::GasLimitExceeded(estimate, max_gas)); } @@ -487,40 +484,62 @@ mod tests { eth::Gas(eth::U256::from(value)) } + /// EIP-7825 per-transaction gas cap (2^24 - 1) introduced in Fusaka. + /// Used in tests as a representative value for the configurable + /// `tx_gas_limit` knob on Fusaka chains. + const EIP_7825_TX_GAS_CAP: u64 = (1 << 24) - 1; + #[test] - fn rejects_solution_above_eip_7825_cap() { + fn rejects_solution_above_tx_gas_limit() { // Block limit (120M) is high enough that half the block (60M) exceeds - // the EIP-7825 per-tx cap (16,777,215). The per-tx cap must win. + // the configured per-tx limit (EIP-7825 cap, 16,777,215). The per-tx + // limit must win. let block_limit = gas(120_000_000); + let tx_gas_limit = gas(EIP_7825_TX_GAS_CAP); let estimate = gas(20_000_000); - let err = Gas::new(estimate, block_limit).unwrap_err(); + let err = Gas::new(estimate, block_limit, tx_gas_limit).unwrap_err(); match err { solution::Error::GasLimitExceeded(used, limit) => { assert_eq!(used, estimate); - assert_eq!(limit, gas(Gas::EIP_7825_TX_GAS_CAP)); + assert_eq!(limit, tx_gas_limit); } other => panic!("unexpected error: {other:?}"), } } #[test] - fn accepts_solution_at_eip_7825_cap() { + fn accepts_solution_at_tx_gas_limit() { let block_limit = gas(120_000_000); - let estimate = gas(Gas::EIP_7825_TX_GAS_CAP); - let result = Gas::new(estimate, block_limit).unwrap(); - assert_eq!(result.estimate, estimate); + let tx_gas_limit = gas(EIP_7825_TX_GAS_CAP); + let result = Gas::new(tx_gas_limit, block_limit, tx_gas_limit).unwrap(); + assert_eq!(result.estimate, tx_gas_limit); } #[test] fn small_block_limit_still_caps_at_half() { // On chains with a low block gas limit, the half-block cap is tighter - // than the EIP-7825 cap and must keep applying. + // than the configured per-tx limit and must keep applying. let block_limit = gas(10_000_000); + let tx_gas_limit = gas(EIP_7825_TX_GAS_CAP); let estimate = gas(6_000_000); - let err = Gas::new(estimate, block_limit).unwrap_err(); + let err = Gas::new(estimate, block_limit, tx_gas_limit).unwrap_err(); match err { solution::Error::GasLimitExceeded(_, limit) => assert_eq!(limit, gas(5_000_000)), other => panic!("unexpected error: {other:?}"), } } + + #[test] + fn high_tx_gas_limit_lets_half_block_bind() { + // Non-Fusaka chain: tx_gas_limit configured well above half the block, + // so the half-block cap is the binding limit. + let block_limit = gas(120_000_000); + let tx_gas_limit = gas(100_000_000); + let estimate = gas(70_000_000); + let err = Gas::new(estimate, block_limit, tx_gas_limit).unwrap_err(); + match err { + solution::Error::GasLimitExceeded(_, limit) => assert_eq!(limit, gas(60_000_000)), + other => panic!("unexpected error: {other:?}"), + } + } } diff --git a/crates/driver/src/infra/blockchain/mod.rs b/crates/driver/src/infra/blockchain/mod.rs index 8b3211998f..0dd0f53a46 100644 --- a/crates/driver/src/infra/blockchain/mod.rs +++ b/crates/driver/src/infra/blockchain/mod.rs @@ -91,6 +91,7 @@ struct Inner { current_block: CurrentBlockWatcher, balance_simulator: BalanceSimulator, balance_overrider: Arc, + tx_gas_limit: eth::Gas, } impl Ethereum { @@ -105,6 +106,7 @@ impl Ethereum { addresses: contracts::Addresses, gas: Arc, current_block_args: &shared::current_block::Arguments, + tx_gas_limit: eth::Gas, ) -> Self { let Rpc { web3, chain, args } = rpc; let current_block_stream = current_block_args @@ -132,6 +134,7 @@ impl Ethereum { gas, balance_simulator, balance_overrider, + tx_gas_limit, }), web3, } @@ -190,6 +193,12 @@ impl Ethereum { self.inner.current_block.borrow().gas_limit.into() } + /// Per-transaction gas limit configured for this driver. Operators set + /// this per chain (e.g. EIP-7825's 16,777,215 cap on Fusaka chains). + pub fn tx_gas_limit(&self) -> eth::Gas { + self.inner.tx_gas_limit + } + /// Returns the current [`eth::Ether`] balance of the specified account. pub async fn balance(&self, address: eth::Address) -> Result { self.web3 diff --git a/crates/driver/src/run.rs b/crates/driver/src/run.rs index 90f5d6ce50..83cfe9b00e 100644 --- a/crates/driver/src/run.rs +++ b/crates/driver/src/run.rs @@ -16,6 +16,7 @@ use { }, }, clap::Parser, + eth_domain_types as eth, futures::future::join_all, http_client::HttpClientFactory, shared::arguments::tracing_config, @@ -195,7 +196,14 @@ async fn ethereum( .await .expect("initialize gas price estimator"), ); - Ethereum::new(ethrpc, config.contracts.clone(), gas, current_block_args).await + Ethereum::new( + ethrpc, + config.contracts.clone(), + gas, + current_block_args, + eth::Gas(config.tx_gas_limit), + ) + .await } async fn solvers(config: &config::Config, eth: &Ethereum) -> Vec { diff --git a/crates/driver/src/tests/setup/solver.rs b/crates/driver/src/tests/setup/solver.rs index f18f11c64c..4a3bdf9c38 100644 --- a/crates/driver/src/tests/setup/solver.rs +++ b/crates/driver/src/tests/setup/solver.rs @@ -479,6 +479,7 @@ impl Solver { block_stream_poll_interval: None, node_ws_url: Some(config.blockchain.web3_ws_url.parse().unwrap()), }, + eth_domain_types::Gas(eth_domain_types::U256::from(45_000_000u64)), ) .await; From 20dba823e8a9297c7cfd839fe3b94bb9f975935f Mon Sep 17 00:00:00 2001 From: 0xDevNinja Date: Mon, 4 May 2026 23:47:27 +0530 Subject: [PATCH 4/5] Clarify Mainnet-specific framing of EIP-7825 cap EIP-7825 chose 2^24-1 for Mainnet's Fusaka hardfork; other chains pick their own per-tx limits (e.g. Arbitrum One uses 32M). Reword comments and rename the test const to make the Mainnet scope explicit. --- .../domain/competition/solution/settlement.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 1d2883fc4b..04418dfd57 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -446,7 +446,7 @@ impl Gas { // whose gas limit exceed the remaining space (without simulating the actual // gas required). // Additionally cap by the configured per-tx gas limit. Operators set - // this per chain (e.g. to EIP-7825's 16,777,215 cap on Fusaka chains) + // this per chain (e.g. to EIP-7825's 16,777,215 cap on Mainnet Fusaka) // so the mempool can't reject the settlement for exceeding the per-tx // ceiling. let max_gas = std::cmp::min(eth::Gas(block_limit.0 / eth::U256::from(2)), tx_gas_limit); @@ -484,10 +484,10 @@ mod tests { eth::Gas(eth::U256::from(value)) } - /// EIP-7825 per-transaction gas cap (2^24 - 1) introduced in Fusaka. - /// Used in tests as a representative value for the configurable - /// `tx_gas_limit` knob on Fusaka chains. - const EIP_7825_TX_GAS_CAP: u64 = (1 << 24) - 1; + /// EIP-7825 per-transaction gas cap (2^24 - 1) introduced in Mainnet's + /// Fusaka hardfork. Used in tests as a representative value for the + /// configurable `tx_gas_limit` knob on Mainnet. + const EIP_7825_MAINNET_TX_GAS_CAP: u64 = (1 << 24) - 1; #[test] fn rejects_solution_above_tx_gas_limit() { @@ -495,7 +495,7 @@ mod tests { // the configured per-tx limit (EIP-7825 cap, 16,777,215). The per-tx // limit must win. let block_limit = gas(120_000_000); - let tx_gas_limit = gas(EIP_7825_TX_GAS_CAP); + let tx_gas_limit = gas(EIP_7825_MAINNET_TX_GAS_CAP); let estimate = gas(20_000_000); let err = Gas::new(estimate, block_limit, tx_gas_limit).unwrap_err(); match err { @@ -510,7 +510,7 @@ mod tests { #[test] fn accepts_solution_at_tx_gas_limit() { let block_limit = gas(120_000_000); - let tx_gas_limit = gas(EIP_7825_TX_GAS_CAP); + let tx_gas_limit = gas(EIP_7825_MAINNET_TX_GAS_CAP); let result = Gas::new(tx_gas_limit, block_limit, tx_gas_limit).unwrap(); assert_eq!(result.estimate, tx_gas_limit); } @@ -520,7 +520,7 @@ mod tests { // On chains with a low block gas limit, the half-block cap is tighter // than the configured per-tx limit and must keep applying. let block_limit = gas(10_000_000); - let tx_gas_limit = gas(EIP_7825_TX_GAS_CAP); + let tx_gas_limit = gas(EIP_7825_MAINNET_TX_GAS_CAP); let estimate = gas(6_000_000); let err = Gas::new(estimate, block_limit, tx_gas_limit).unwrap_err(); match err { From 78cbd3b414e66a4857541ddfdc1c5b71595068b6 Mon Sep 17 00:00:00 2001 From: 0xDevNinja Date: Wed, 6 May 2026 11:58:56 +0530 Subject: [PATCH 5/5] Assert clamped limit at tx_gas_limit boundary When estimate == tx_gas_limit, the 2x estimate buffer would otherwise set limit to 2 * tx_gas_limit; the min(max_gas, ...) clamp pins it back to the configured cap. Lock that contract in. --- crates/driver/src/domain/competition/solution/settlement.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/driver/src/domain/competition/solution/settlement.rs b/crates/driver/src/domain/competition/solution/settlement.rs index 04418dfd57..b92766e356 100644 --- a/crates/driver/src/domain/competition/solution/settlement.rs +++ b/crates/driver/src/domain/competition/solution/settlement.rs @@ -513,6 +513,9 @@ mod tests { let tx_gas_limit = gas(EIP_7825_MAINNET_TX_GAS_CAP); let result = Gas::new(tx_gas_limit, block_limit, tx_gas_limit).unwrap(); assert_eq!(result.estimate, tx_gas_limit); + // The 2x buffer would otherwise push limit to 2 * tx_gas_limit; the + // min(max_gas, ...) clamp must keep it at the configured cap. + assert_eq!(result.limit, tx_gas_limit); } #[test]