Skip to content

fix(core): cap estimate_gas ceiling at EIP-7825 per-tx gas limit#3

Draft
op-will wants to merge 5 commits into
mainfrom
op-will/fix_for_eip-7825
Draft

fix(core): cap estimate_gas ceiling at EIP-7825 per-tx gas limit#3
op-will wants to merge 5 commits into
mainfrom
op-will/fix_for_eip-7825

Conversation

@op-will

@op-will op-will commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Problem

On a chain that has activated EIP-7825 (the Osaka per-transaction gas-limit cap of 2^24 = 16,777,216 gas), contender setup aborts during contract deployment with:

server returned an error response: error code -32000: intrinsic gas too high

Root cause

In crates/core/src/test_scenario.rs, the contract-deploy path builds a creation tx with no gas field and calls provider.estimate_gas(...). On an EIP-7825 chain, eth_estimateGas binary-searches using the block gas limit as its upper bound. Because that ceiling exceeds 2^24, the first simulated execution is itself invalid, and the node returns intrinsic gas too high instead of an estimate — so the call fails before any transaction is ever sent.

The anvil-based simulate_setup_cost pass succeeds because anvil does not enforce EIP-7825, which masks the problem until the real deploy.

The same unbounded estimate_gas pattern exists at three sites: contract deploy, setup txs, and update_gas_map (spam txs).

Fix

Add a constant EIP_7825_MAX_TX_GAS_LIMIT = 1 << 24 and a helper cap_gas_for_estimate() that clamps a tx request's gas ceiling to the cap before estimate_gas is called, applied at all three sites.

This keeps the node's binary search inside the valid range. The returned estimate is the true (smaller) gas the tx needs, so the final gas limit is never inflated — on chains without EIP-7825 the behavior is unchanged (the estimate already returns the real value well below 2^24).

Verification

  • cargo build --release — succeeds
  • cargo +nightly fmt --check — clean
  • cargo clippy -p contender_core — clean
  • Live run against an EIP-7825/Osaka-activated devnet: contender setup now deploys the contract and completes; the exact step that previously aborted with intrinsic gas too high succeeds.

jelias2 and others added 5 commits June 5, 2026 14:52
…ts#588)

* feat: add scenario access_list with placeholder resolution

Supersedes flashbots#581. Adds an `access_list` field to FunctionCallDefinition
that accepts {placeholder} strings in `address` and `storageKeys`,
resolved during the loose-to-strict conversion via the existing
templater + DB map.

Coexists with max_priority_fee_per_gas (flashbots#580) and alloy 2.0 (flashbots#561).

* support scenario [env] placeholders in access list fields

* chore: clippy

---------

Co-authored-by: zeroXbrock <2791467+zeroXbrock@users.noreply.github.com>
…shbots#589)

* feat: add spam-stream subcommand for streaming tx specs (draft)

Reads newline-delimited JSON FunctionCallDefinitions from stdin or a
file and spams them via the existing TestScenario pipeline. Reuses
agent pools, rate limiting, nonce management, and receipt tracking.

See docs/stream-mode.md for the design note and scope.

* feat(spam-stream): versioned stdout envelope + drop decoy TestConfig

- Address review feedback on PR flashbots#589:
- #2/#3: emit structured, versioned/tagged JSON envelope on stdout
  (one tx_result event per spec) so the schema can evolve; default
  for spam-stream mode (logs stay on stderr).
- flashbots#5: replace the decoy zero-address TestConfig with a direct
  AgentStore pool, injecting signers into the scenario and syncing
  nonces from the RPC.

* fix(spam-stream): record run, cache gas price, surface backpressure/summary

Address review gaps on the prototype:
- record a real run via insert_run so dumped receipts aren't orphaned
  under run_id 0 (run_txs has a foreign key into runs)
- cache the gas price, refreshing every 6s instead of once per tx
- emit `backpressure` and a terminal `summary` event; track sent vs failed
- reject blob (4844) / setCode (7702) specs up front
- unify the stdin/file reader and drop dead run_id plumbing

* fix(spam-stream): enable nonce sync so sends don't fail with NonceMissing

TestScenario::sync_nonces() is gated on should_sync_nonces (= the
sync_nonces_after_batch param). spam-stream set it false, making its two
explicit sync_nonces() calls silent no-ops, so pool accounts' nonces were
never loaded and prepare_tx_request failed every send with NonceMissing
(surfaced as 'core error'). Stream mode sends one tx at a time and never hits
the post-batch sync path, so enabling this only makes the initial pool-nonce
sync run.

* fix(spam-stream): reclaim nonce on failed send to avoid nonce gaps

prepare_tx_request advances an account's local nonce before the send. When the
send is rejected (e.g. an interop access-list filter rejecting a not-yet-valid
or forged executing message), the tx never enters the mempool but the local
nonce stays advanced, leaving a gap that stalls every later tx from that
account behind it. Roll the nonce back by one on a failed send. The stream
sends serially, so no concurrent send touched the account in between.

* docs(spam-stream): clarify --tps long_help (review flashbots#589)

--tps paces how fast specs are pulled off the input stream; each spec is
sent exactly once, so a one-line input sends a single tx regardless of the
value. Add long_help explaining this, plus tests asserting the help text is
present and the clap arg config is valid (debug_assert).

* feat(spam-stream): accept unit strings for --min-balance (review flashbots#589)

--min-balance now parses unit-value strings ("10 eth", "0.5 ether",
"100 gwei") via util::parse_value, matching the other CLI balance/value
flags; a plain number is still wei. Default expressed as "0.01 ether"
(unchanged 1e16 wei). Tests cover unit parsing, the wei fallback, and the
default round-trip through the value_parser.

* fix(spam-stream): persist seed so --skip-funding works across runs (review flashbots#589)

When --seed was unset, spam-stream generated a fresh random RandSeed each
invocation, so the executor pool's addresses differed every run. Funding the
pool with --min-balance in one run then re-running with --skip-funding hit
"insufficient funds" because the second run derived a different (unfunded)
pool.

Fall back to the persisted seedfile (data_dir/seed) when --seed is unset,
matching spam/setup/campaign. Threads data_dir into spam_stream(). Pool
addresses are now stable across invocations for a given data-dir.

Test: build_pool_agent_store is deterministic per seed (same seed -> same
addresses, different seed -> different). Manually verified against anvil:
run 1 funds the pool + writes the seedfile; run 2 with --skip-funding and no
funder key sends successfully (previously failed with insufficient funds).

* style(spam-stream): cargo fmt test module (CI fmt fix)

---------

Co-authored-by: brock 🤖⚡ <2791467+zeroXbrock@users.noreply.github.com>
* chore: update changelogs

* chore: bump versions
On a chain that enforces EIP-7825 (Osaka), eth_estimateGas binary-searches
using the block gas limit as its upper bound. When that limit exceeds the
2^24 per-tx cap, the first simulated execution is itself invalid and the node
returns "intrinsic gas too high" instead of an estimate, so the call fails
before any tx is sent. This aborts contract deployment during setup.

Clamp the gas ceiling on the estimate request to 2^24 at the three estimate_gas
sites (contract deploy, setup tx, spam tx). The returned estimate is the true
gas the tx needs, so this never inflates the final gas limit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants