Skip to content

fix: include maxFeePerBlobGas in gas price even when blob base fee is 0#20260

Open
dmichael wants to merge 1 commit intoAztecProtocol:nextfrom
dmichael:fix/blob-fee-falsy-check
Open

fix: include maxFeePerBlobGas in gas price even when blob base fee is 0#20260
dmichael wants to merge 1 commit intoAztecProtocol:nextfrom
dmichael:fix/blob-fee-falsy-check

Conversation

@dmichael
Copy link

@dmichael dmichael commented Feb 7, 2026

Summary

When getBlobBaseFee() RPC fails, maxFeePerBlobGas defaults to 0n. Because 0n is falsy in JS, ...(maxFeePerBlobGas && { maxFeePerBlobGas }) silently drops the field from the gas price result. This produces a malformed blob transaction that the execution client rejects, and the sequencer retries until the slot expires.

The getBlobBaseFee() call can fail when the L1 node is overloaded — in our case, Reth 1.9.3 under heavy memory pressure (8 GiB swap, <1 GiB free RAM) caused the RPC to time out, triggering this bug and resulting in a missed block proposal. The sequencer retried 71 times over ~48 seconds, each hitting the same failing RPC, until the slot expired.

Note: This is a correctness fix that eliminates silent field-dropping and produces a clear BlobFeeCapTooLow rejection instead of a confusing "Transaction creation failed" error. It would not have prevented the missed proposal on its own — when getBlobBaseFee() fails, the 0n default still produces an invalid blob fee. It also does not enable fallback to another RPC; all fee strategy calls go through the same single viem client. To prevent missed proposals from RPC failures, the fee strategy would need to either throw on getBlobBaseFee() failure (fail fast) or support per-call RPC failover.

Root Cause

The failure chain:

  1. getBlobBaseFee() RPC call fails (L1 node under memory pressure)
  2. Promise.allSettled result has status: 'rejected', so blobBaseFee stays undefined in the fee strategy
  3. getGasPrice() initializes maxFeePerBlobGas = blobBaseFee ?? 0n0n
  4. Stall-time bump loop has no effect: 0n * 1125n / 1000n = 0n
  5. Falsy check drops the field: ...(0n && { maxFeePerBlobGas: 0n }) spreads nothing
  6. estimateGas() reads gasPrice.maxFeePerBlobGas! → evaluates to undefined at runtime (TypeScript ! has no runtime effect)
  7. maxFeePerBlobGas: undefined is passed to viem (the L1 client library), which serializes it as zero. The execution client rejects the blob transaction because a zero blob fee is below the minimum base fee (always >= 1 wei per EIP-4844).
  8. Sequencer retries 71 times against the same failing RPC → slot expires

Related PRs

  • fix: speed-up attempt blob fee  #16931 (fix: speed-up attempt blob fee, merged Sep 2025) — Fixed a related but different issue: the speed-up code path wasn't passing maxFeePerBlobGas to replacement transactions at all. That fix also added retry logic for getBlobBaseFee(). However, the fix itself used if (isBlobTx && newGasPrice.maxFeePerBlobGas) which has the same falsy-check vulnerability0n would cause the condition to be false. That code has since been refactored into makeTxData() which uses gasPrice.maxFeePerBlobGas!, but the root cause in getGasPrice() was never addressed.
  • fix: blob fees & l1-publisher logging #11029 (fix: blob fees & l1-publisher logging, merged Jan 2025) — Earlier blob fee calculation fixes; predates the current fee strategy architecture.
  • fix: Handle falsy bigints in json-rpc #2403 (fix: Handle falsy bigints in json-rpc, merged Sep 2023) — Fixed the same class of bug (0n is falsy in JS) in the JSON-RPC serialization layer. The pattern keeps recurring.

No existing PR addresses the getGasPrice() return statement where maxFeePerBlobGas is silently dropped.

Fix

Three changes in readonly_l1_tx_utils.ts:

Location Before (buggy) After (fixed)
Return statement (line 229) ...(maxFeePerBlobGas && { maxFeePerBlobGas }) ...(isBlobTx ? { maxFeePerBlobGas } : {})
maxBlobGwei cap (line 194) if (maxFeePerBlobGas && effectiveMaxBlobGwei > 0n) if (maxFeePerBlobGas !== undefined && ...)
Retry blob bump (line 201) if (attempt > 0 && previousGasPrice?.maxFeePerBlobGas) if (... && previousGasPrice?.maxFeePerBlobGas !== undefined)

The return statement fix uses isBlobTx rather than checking the value, because maxFeePerBlobGas is a required field for type-3 (EIP-4844) transactions — it must always be present when sending blobs, even if its value is zero.

Evidence

  • Bug confirmed present on tags v2.1.9, v2.1.11, branches v2 and next
  • No existing PR addresses this — searched for "maxFeePerBlobGas falsy" and "maxFeePerBlobGas 0n" with zero results
  • Same && pattern appears in l1_tx_utils.ts at lines 278, 450, 468, 696 but those are in logging statements only (cosmetic, not functional)
  • Incident details: Sequencer rebuilt block 85779 for slot 103630 seventy-one times over ~48 seconds, each attempt failing with "Failed to validate blobs: Transaction creation failed" before the slot expired

Test plan

  • Added regression test: getGasPrice returns maxFeePerBlobGas when getBlobBaseFee() returns 0n
  • Added regression test: getGasPrice returns maxFeePerBlobGas when getBlobBaseFee() RPC rejects entirely
  • Existing tests pass (gas price calculations, blob tx handling, speed-up blob fee)

`getGasPrice()` uses the pattern `...(maxFeePerBlobGas && { maxFeePerBlobGas })`
to conditionally include the blob fee field. Since `0n` is falsy in JavaScript,
this silently drops `maxFeePerBlobGas` from the returned `GasPrice` object when
the blob base fee is zero.

Downstream, `estimateGas()` reads `gasPrice.maxFeePerBlobGas!` which evaluates
to `undefined`, producing a malformed type-3 transaction that the execution
client rejects with "Transaction creation failed". The sequencer then retries
in a loop until the slot expires, causing a missed block proposal.

The trigger is `getBlobBaseFee()` RPC failure under L1 node memory pressure:
the `Promise.allSettled` result has status 'rejected', so `blobBaseFee` stays
at its default of `0n`. The EIP-4844 formula cannot return 0 (minimum 1 wei),
so this only manifests when the RPC call itself fails.

Fix:
- Replace falsy check with `isBlobTx` flag in return statement
- Use `!== undefined` checks for maxBlobGwei cap and retry bump guards
- Add regression tests for both zero blob fee and RPC failure scenarios

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dmichael dmichael marked this pull request as ready for review February 7, 2026 13:59
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