Skip to content

fix(l1): bump EIP-1559 fees on same-nonce poster retries - #34

Open
stephenctw wants to merge 7 commits into
mainfrom
fix/poster-replacement-fee-bump
Open

fix(l1): bump EIP-1559 fees on same-nonce poster retries#34
stephenctw wants to merge 7 commits into
mainfrom
fix/poster-replacement-fee-bump

Conversation

@stephenctw

Copy link
Copy Markdown
Collaborator

Summary

Fixes same-nonce batch-poster retries that could rebroadcast underpriced EIP-1559 replacements when the fee market was flat. The poster now tracks in-flight fees per wallet nonce and floors each re-estimate against a shared ≥10% replacement bump (max fee ×1.1 +1; priority ×2), matching the mempool flusher’s rule.

  • Hoist bumped_replacement_fees into l1::eip1559 and add fees_for_nonce so poster + flusher share one replacement policy.
  • On successful broadcast, record fees; on failed send, leave the floor unchanged; prune entries once Latest advances past the nonce.
  • Cover bump math, mixed-component floors, Latest prune, and failed-send non-record behavior with unit/Anvil tests.

Test plan

  • cargo test -p sequencer --lib eip1559
  • cargo test -p sequencer --lib submitter::poster
  • Confirm flusher still compiles/uses shared bumped_replacement_fees (cargo test -p sequencer --lib recovery::flusher)
  • Anvil-backed poster replacement test (submit_batches_replacement_clears_ten_percent_bump)

@stephenctw
stephenctw requested a review from GCdePaula August 12, 2026 02:14
@stephenctw

Copy link
Copy Markdown
Collaborator Author

@jplgarcia FYI

@stephenctw stephenctw self-assigned this Aug 13, 2026
Track in-flight fees and floor re-estimates to the shared ≥10%
replacement rule so flat markets cannot underprice replacements.
Add mixed-component fees_for_nonce coverage, seeded replacement floor,
Latest prune, and failed-send non-record assertions.
@stephenctw
stephenctw force-pushed the fix/poster-replacement-fee-bump branch from 9e084bc to 77a4a61 Compare August 18, 2026 11:55
A 200ms sleep raced the first spawn_blocking SQLite write on loaded CI,
so the assertion still saw the default log_gas_price of 0.
Grow max_fee and priority by the same ×1.1+1 factor and clamp tip ≤ cap
so poster retries cannot wedge on ErrTipAboveFeeCap. Only the blocking
(Latest) nonce is re-escalated; suffix txs keep their original hash.
When a same-nonce send is rejected as replacement-underpriced, immediately
raise the stored head-nonce floor from the attempted fees so the next tick
self-corrects instead of repeating the same underpriced pair.
@stephenctw

stephenctw commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Fixes the permanent poster wedge from asymmetric replacement bumps; limits re-escalation to the head nonce; adds geth underpriced fast-path floor raise.

@stephenctw
stephenctw force-pushed the fix/poster-replacement-fee-bump branch from 812e198 to b09dbc1 Compare August 20, 2026 13:08
@jplgarcia

Copy link
Copy Markdown

Thanks for the fix @stephenctw . I validated this against the current PR head with a real pending transaction on Anvil (--no-mining).

The fee-bump logic itself is correct: the retry uses the same nonce and raises both EIP-1559 values by at least 10%:

  • maxFeePerGas: 2000000001 -> 2200000002
  • maxPriorityFeePerGas: 1 -> 2

However, the poster still does not reach eth_sendRawTransaction in the real replacement path.

The failure occurs earlier, during gas estimation:

eth_estimateGas(
  from=<poster>,
  nonce=0,
  maxFeePerGas=2200000002,
  maxPriorityFeePerGas=2,
  block="pending"
) -> -32003 "nonce too low"

At that point, nonce 0 is already pending (latest=0, pending=1). Anvil applies its mempool nonce policy to the pending gas simulation and rejects it, so the replacement is never signed or broadcast.

As a control, I estimated the same addInput call without an explicit nonce, fixed that gas value on the replacement transaction, and submitted it with nonce 0 and the bumped fees. Anvil accepted the replacement, replaced the pending transaction, and it mined once blocks were resumed.

I think the remaining change should be scoped to the same-nonce replacement path:

  1. Estimate gas without the pending nonce.
  2. Set the resulting gas explicitly on the final replacement transaction.
  3. Keep the current per-nonce fee floor/bump logic.

To test this I've been performing an E2E test: Anvil with mining disabled, original poster tx, confirmation timeout, real same-nonce replacement, then resume mining and assert the receipt/nonce progression.

So, for this scenario, this PR fixes the fee-bump half of F3, but needs the gas-estimation adjustment for the poster to actually submit the replacement.

Anvil rejects eth_estimateGas with "nonce too low" when the same nonce is
already pending, so replacements never reached eth_sendRawTransaction.
Assert the original pending tx is evicted and on-wire fees clear the
≥10% floor after mining resumes.
@stephenctw

Copy link
Copy Markdown
Collaborator Author

@jplgarcia Thanks for catching this — the fee bump was correct, but the real replacement path never reached eth_sendRawTransaction because Anvil rejected eth_estimateGas with the pending nonce (nonce too low).

Addressed by scoping the change to the same-nonce replacement path: estimate gas without that nonce, pin the result on the replacement tx, and keep the existing ≥10% fee floor/bump.

Also added an Anvil E2E along the lines of your repro: --no-mining → original poster tx → confirmation timeout → same-nonce replacement → mine → assert receipt/nonce progression (plus original eviction and on-wire fee floor).

@stephenctw
stephenctw force-pushed the fix/poster-replacement-fee-bump branch from 768dde0 to 9256c40 Compare August 26, 2026 06:17
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.

2 participants