Skip to content

Cap gas estimation at the configured per-tx gas limit#4584

Merged
squadgazzz merged 5 commits into
mainfrom
driver/cap-estimate-gas-limit
Jul 3, 2026
Merged

Cap gas estimation at the configured per-tx gas limit#4584
squadgazzz merged 5 commits into
mainfrom
driver/cap-estimate-gas-limit

Conversation

@squadgazzz

@squadgazzz squadgazzz commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Description

When the driver needs to know how much gas a settlement uses, it asks the node via eth_estimateGas. The node answers by running the transaction and binary-searching for the smallest gas that succeeds, and with no ceiling set it searches all the way up to the block gas limit. A solver can return a settlement that burns gas in a loop to make every step of that search maximally expensive. Solver responses are untrusted on the non-colocated path (KYC, no staked COW), so this is a real way to overload the node the driver depends on, and it is worse on high-gas-limit chains.

Two paths ran the estimation with no ceiling:

  • the pre-competition solution simulation (Simulator::gas), run for every solution and re-run every block by resimulate_until_revert,
  • the submission loop (blockchain::Ethereum::estimate_gas), run every block while the settlement sits in the mempool.

No new config is needed. Operators already set tx_gas_limit, and it was already applied in create_access_list, just not in either estimate_gas.

Changes

  • Cap the eth_estimateGas search at the configured tx_gas_limit on both the simulation and submission paths.
  • Make the cap only ever lower a ceiling, never raise one the caller already set.
  • Error out rather than run uncapped if tx_gas_limit does not fit a u64.

How to test

Existing tests. On the competition path, Gas::new already rejects any solution estimated above min(block_limit / 2, tx_gas_limit), so bounding the search by tx_gas_limit returns the same estimate for every accepted solution, and only solutions that would be rejected now fail at the RPC instead of the post-check. On the submission path the submitted tx's gas limit is already <= tx_gas_limit, so a settlement that needs more only shifts from a doomed on-chain revert to an earlier pre-submission abort.

@squadgazzz squadgazzz requested a review from a team as a code owner July 2, 2026 09:36
@claude

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @squadgazzz's task in 2m 22s —— View job


PR Review: Cap gas estimation at the configured per-tx gas limit

  • Read PR diff and context
  • Analyze estimate_gas change
  • Verify tx_gas_limit semantics and re-simulation path
  • Post review

The change itself is correct and cleanly mirrors create_access_list. I left one inline comment flagging what looks like an incomplete fix relative to the stated threat model:

  • The PR caps simulator::Ethereum::estimate_gas (pre-competition solution simulation).
  • But the DoS path the description calls out — "repeated every block during re-simulation" — runs through driver::infra::blockchain::Ethereum::estimate_gas (mod.rs:233), used in the per-block submission loop at mempools.rs:272 (and :148). That one is still uncapped, even though its Ethereum already holds tx_gas_limit.

So the winning solution passes the now-capped pre-comp sim, then the uncapped mempools re-sim still searches up to the block gas limit every block. Worth applying the same cap there to fully close the vector. Details in the inline comment.

Otherwise LGTM — the Gas::new cap reasoning holds, so accepted solutions are unaffected.

Comment thread crates/simulator/src/ethereum/mod.rs Outdated
Comment on lines +117 to +123
// Cap the search at the per-tx gas limit so a rogue solution can't force
// the node to binary-search up to the block gas limit. `Gas::new` rejects
// anything above this cap anyway, so no accepted solution is affected.
let gas_limit = self.inner.tx_gas_limit.try_into().map_err(|err| {
Error::GasPrice(anyhow!("failed to convert gas_limit to u64: {err:?}"))
})?;
let tx = tx.into().with_gas_limit(gas_limit);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This caps the simulator crate's estimate_gas, which is on the pre-competition solution-simulation path (Simulator::gas). But the DoS scenario the PR describes — "burns gas in a loop… repeated every block during re-simulation" — flows through a different, still-uncapped estimate_gas:

  • crates/driver/src/infra/blockchain/mod.rs:233 (driver::infra::blockchain::Ethereum::estimate_gas) applies no gas ceiling.
  • It's called in the per-block submission loop at crates/driver/src/domain/mempools.rs:272 (and pre-submission at mempools.rs:148) — the exact "every block during re-simulation" path.
  • That Ethereum already carries tx_gas_limit (mod.rs:98), so the fix can be applied there too.

Net effect: a winning solution now passes the capped pre-competition sim, wins, and then the uncapped mempools re-sim runs eth_estimateGas searching up to the block limit every block anyway — so the described attack vector isn't fully closed. Consider applying the same cap to the driver's estimate_gas.

Fix this →

@jmg-duarte jmg-duarte left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread crates/simulator/src/ethereum/mod.rs Outdated
{
let tx = tx.into();
// Cap the search at the per-tx gas limit so a rogue solution can't force
// the node to binary-search up to the block gas limit. `Gas::new` rejects

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gas simulation is done with binary search? where can i learn more about this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request caps the transaction gas limit search in the simulator to prevent rogue solutions from forcing a binary search up to the block gas limit. The reviewer correctly identified that unconditionally setting the gas limit overwrites any pre-existing lower gas limit on the transaction request, and provided an actionable suggestion to cap it by taking the minimum of the existing gas limit and the configured transaction gas limit instead.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread crates/simulator/src/ethereum/mod.rs Outdated
@squadgazzz squadgazzz marked this pull request as draft July 2, 2026 09:49
@squadgazzz squadgazzz marked this pull request as ready for review July 2, 2026 10:47
@claude

claude Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @squadgazzz's task in 2m 51s —— View job


PR Review: Cap gas estimation at the configured per-tx gas limit

The follow-up commit (0743e0a) addresses both earlier review points — the submission-path estimate_gas in the driver is now capped (closing the "re-simulated every block" vector I flagged), and the simulator's cap correctly takes min with any caller-set ceiling per Gemini's note. The core logic is sound.

I left 2 inline comments, both on crates/driver/src/infra/blockchain/mod.rs:

  • Behavior change on the re-sim path (medium): capping makes the node return a JSON-RPC error (gas required exceeds allowance) when a settlement needs more than tx_gas_limit, and is_revert() classifies that as a revert. On the pre-submission check (mempools.rs:147) a winning solution whose gas drifts just above tx_gas_limit between competition and submission would now abort as a false SimulationRevert, whereas before it would still submit. Worth confirming this doesn't fire spuriously where tx_gas_limit is close to typical settlement gas.
  • Minor consistency: the driver uses unwrap_or(u64::MAX) on conversion failure (silently disabling the cap), while the simulator returns an error. Suggest mirroring the simulator's try_into()?.

Otherwise LGTM.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces gas limit capping based on the per-transaction gas limit in both the blockchain driver and the Ethereum simulator. This change prevents rogue solutions from forcing expensive binary searches up to the block gas limit. There are no critical issues or feedback to address.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread crates/driver/src/infra/blockchain/mod.rs
// Cap the search at the per-tx gas limit so a rogue solution can't force
// the node to binary-search up to the block gas limit while we re-check
// the settlement on every block during submission.
let gas_limit = u64::try_from(self.inner.tx_gas_limit.0).unwrap_or(u64::MAX);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this silently falls back to u64::MAX on conversion failure, which disables the cap entirely — on the exact path this PR is meant to harden. The simulator version (simulator/src/ethereum/mod.rs:121) instead returns an error. tx_gas_limit is a U256 operator config that realistically fits in u64, so this is unlikely to bite, but the two implementations diverging is a small footgun. Consider mirroring the simulator's try_into().map_err(..)? here for consistency.

@MartinquaXD

Copy link
Copy Markdown
Contributor

Looks like reth (and probably all major nodes) already pick a reasonable maximum gas limit to avoid this issue: https://github.com/paradigmxyz/reth/blob/00aa375238e8f03b3e824dad57f47ade6b05bec7/crates/rpc/rpc-eth-api/src/helpers/estimate.rs#L102.

@squadgazzz

Copy link
Copy Markdown
Contributor Author

Looks like reth (and probably all major nodes) already pick a reasonable maximum gas limit to avoid this issue: paradigmxyz/reth@00aa375/crates/rpc/rpc-eth-api/src/helpers/estimate.rs#L102.

Nice, thanks! But does it still make sense to be on the safer side? Looks like zero cost from our side.

Comment thread crates/simulator/src/ethereum/mod.rs Outdated
@squadgazzz squadgazzz enabled auto-merge July 3, 2026 09:54
@squadgazzz squadgazzz added this pull request to the merge queue Jul 3, 2026
Merged via the queue into main with commit 574144a Jul 3, 2026
20 checks passed
@squadgazzz squadgazzz deleted the driver/cap-estimate-gas-limit branch July 3, 2026 10:13
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants