Skip to content

fix(l1): make the batch poster re-estimate and never escalate fees - #35

Open
GCdePaula wants to merge 2 commits into
fix/poster-replacement-fee-bumpfrom
fix/poster-stateless-fee-policy
Open

fix(l1): make the batch poster re-estimate and never escalate fees#35
GCdePaula wants to merge 2 commits into
fix/poster-replacement-fee-bumpfrom
fix/poster-stateless-fee-policy

Conversation

@GCdePaula

@GCdePaula GCdePaula commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Steps the batch poster back to stateless re-estimation and documents precisely why, so #34 lands as: the real fixes the review rounds found, a quiet handling of the mempool's "already holds your tx" replies, and a written record of the gap we knowingly leave — instead of a fee-escalation policy.

Targets fix/poster-replacement-fee-bump deliberately, so the branch keeps its history: rounds 1–4 built and adversarially reviewed four escalation designs, and each closed the two-component replacement gap while opening a worse hole (an invalid tip > cap wedge; a silent batch drop via skip-and-watch; a wallet exhausted via compounding floors against geth's cumulative per-sender reservation; a recovery crash-loop plus a 5 s already-known/hold fixed point under the ceiling). Those four commits are the evidence behind the note this PR adds, which is why they stay in the history rather than being squashed away.

Stepping back: the gap needs the base fee to more than double after a broadcast while the 10-block tip median stays flat through those same full blocks — two conditions that pull against each other — so its expected cost is minutes of waiting, bounded by the danger detector (a safe, designed fallback at 900 blocks; the protocol requirement is 1200). None of the machinery earned its failure modes.

What changes

  • Poster is stateless about fees again. Every tick re-broadcasts each pending batch at that tick's fresh estimate. Removed: the in-flight floor map, fees_for_nonce, the ceiling + absolute floor + escape valve, the hold state and its confirmation-cadence tick outcome, the insufficient-funds matcher, and the duplicated cadence config (BatchSubmitterConfig is back to idle_poll_interval_ms).
  • Kept from the review rounds (correctness fixes independent of fee policy): the nonce-free, padded gas estimate at an explicit Latest on every send — Anvil rejects an estimate at an already-pending nonce with "nonce too low" before the node ever sees the re-broadcast (@jplgarcia's repro; the pin now lives in submit_batches_waits_when_mempool_already_holds_the_tx, whose doc comment says so); and pad_gas_estimate as a shared helper.
  • The flusher's bump is the flusher's again. bumped_replacement_fees moved back next to its only caller. It keeps the symmetric tip-and-cap fix from round 2 because that fixed a real pre-existing flusher bug, unrelated to poster policy: the old tip ×2 / cap ×1.1 produced tip > cap whenever the estimate's cap sat near its tip — the normal shape on a zero-base-fee chain — so every no-op was an invalid tx and recovery could not complete on devnet. No-ops are now priced with a one-shot 3× headroom over the fresh estimate (NOOP_FEE_HEADROOM): a 1.1× bump of today's market loses to a poster tx priced when the market was any higher, and both pre-fix(l1): bump EIP-1559 fees on same-nonce poster retries #34 main (tip ×2) and the round-4 ceiling (cap ×3) had headroom this branch had lost. One-shot and stateless on a 21k-gas transfer, so it cannot compound. The honest residual-gap comment is restored and corrected.
  • "replacement transaction underpriced" / "already known" are not errors. They mean the mempool already holds our tx at a price this tick's estimate does not beat by ≥10% on both components. The poster reports SubmitBatchesOutcome::Waiting { broadcast } and keeps going, so new payloads behind a refused nonce still get their fresh sends (without watching them behind a refused head); a confirmation timeout reports Waiting too. The worker sleeps idle_poll_interval and re-estimates. This fully closes the review register's "confirmation-timeout defeats pacing" finding — every non-productive path now sleeps.
  • Falsifiability instrument. The poster tracks how long each attempted nonce has been unresolved (never reset by an accepted send, so it also sees the "replacements accepted, nothing mines" stall) and warns once that exceeds PENDING_WAIT_WARN_AFTER (5 min), then once per interval. The contract: if production emits that line, bring the excerpt and the L1 block range to the register — not a floor.
  • The reasoning lives where the next reader will be: the module doc at the top of sequencer/src/l1/submitter/poster.rs — the gap's mechanics, the frequency × duration argument, the four refuted designs, what we do instead, and the revisit trigger. AGENTS.md points at it from the submitter bullet; docs/recovery/README.md records the flusher's sharpened residual. Register entries (settled decision, refuted proposal, closure of open finding 1) go to the ledger branch, where docs/review/ lives.

Net against the branch head: about −600 lines.

Return the poster to stateless re-estimation: every tick re-broadcasts
each pending batch at that tick's fresh market estimate and keeps no
per-nonce fee state. Drop the in-flight floor map, the fee ceiling and
its absolute floor, the escape valve, the hold state with its
confirmation-cadence tick, and the insufficient-funds matcher.

Keep the policy-independent fixes from the review rounds: the nonce-free
padded gas estimate at an explicit Latest (Anvil rejects an estimate at
an already-pending nonce before the node ever sees the re-broadcast) and
the shared pad helper. Move the flusher's replacement bump back beside
its only caller, keeping the symmetric tip-and-cap fix (the old tip ×2
gave tip > cap on zero-base chains, so no flush no-op was ever valid
there), and price no-ops with a one-shot 3× headroom over the fresh
estimate: a 1.1× bump of today's market loses to a poster tx priced when
the market was any higher, and both pre-#34 main and the round-4 ceiling
had headroom this branch had lost.

Classify "replacement transaction underpriced" / "already known" as what
they mean — the mempool already holds our tx at a price the market has
not beaten — and report a quiet `Waiting` outcome the worker sleeps on
instead of an error; a confirmation timeout reports the same, which
closes the review register's pacing finding in full. Track how long each
attempted nonce has been unresolved and warn past PENDING_WAIT_WARN_AFTER,
so the decision stays falsifiable against production.

The poster's module doc records the gap this leaves, why its expected
cost is minutes against a safe three-hour fallback, and the four
escalation designs that were built and refuted on #34, so the next
reader who re-finds the gap meets the reasoning rather than a rule.
Replace the poster policy essay with a concise shared document. Distinguish safe shutdown from recovery completion, document independently moving fees, and characterize the flusher limitation with a focused test.
@GCdePaula
GCdePaula force-pushed the fix/poster-stateless-fee-policy branch from 7aa16e3 to f61a7e8 Compare September 7, 2026 13:34
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.

1 participant