fix: include maxFeePerBlobGas in gas price even when blob base fee is 0#20260
Open
dmichael wants to merge 1 commit intoAztecProtocol:nextfrom
Open
fix: include maxFeePerBlobGas in gas price even when blob base fee is 0#20260dmichael wants to merge 1 commit intoAztecProtocol:nextfrom
dmichael wants to merge 1 commit intoAztecProtocol:nextfrom
Conversation
`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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When
getBlobBaseFee()RPC fails,maxFeePerBlobGasdefaults to0n. Because0nis 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
BlobFeeCapTooLowrejection instead of a confusing "Transaction creation failed" error. It would not have prevented the missed proposal on its own — whengetBlobBaseFee()fails, the0ndefault 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 ongetBlobBaseFee()failure (fail fast) or support per-call RPC failover.Root Cause
The failure chain:
getBlobBaseFee()RPC call fails (L1 node under memory pressure)Promise.allSettledresult hasstatus: 'rejected', soblobBaseFeestaysundefinedin the fee strategygetGasPrice()initializesmaxFeePerBlobGas = blobBaseFee ?? 0n→0n0n * 1125n / 1000n = 0n...(0n && { maxFeePerBlobGas: 0n })spreads nothingestimateGas()readsgasPrice.maxFeePerBlobGas!→ evaluates toundefinedat runtime (TypeScript!has no runtime effect)maxFeePerBlobGas: undefinedis 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).Related PRs
fix: speed-up attempt blob fee, merged Sep 2025) — Fixed a related but different issue: the speed-up code path wasn't passingmaxFeePerBlobGasto replacement transactions at all. That fix also added retry logic forgetBlobBaseFee(). However, the fix itself usedif (isBlobTx && newGasPrice.maxFeePerBlobGas)which has the same falsy-check vulnerability —0nwould cause the condition to be false. That code has since been refactored intomakeTxData()which usesgasPrice.maxFeePerBlobGas!, but the root cause ingetGasPrice()was never addressed.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, merged Sep 2023) — Fixed the same class of bug (0nis falsy in JS) in the JSON-RPC serialization layer. The pattern keeps recurring.No existing PR addresses the
getGasPrice()return statement wheremaxFeePerBlobGasis silently dropped.Fix
Three changes in
readonly_l1_tx_utils.ts:...(maxFeePerBlobGas && { maxFeePerBlobGas })...(isBlobTx ? { maxFeePerBlobGas } : {})if (maxFeePerBlobGas && effectiveMaxBlobGwei > 0n)if (maxFeePerBlobGas !== undefined && ...)if (attempt > 0 && previousGasPrice?.maxFeePerBlobGas)if (... && previousGasPrice?.maxFeePerBlobGas !== undefined)The return statement fix uses
isBlobTxrather than checking the value, becausemaxFeePerBlobGasis a required field for type-3 (EIP-4844) transactions — it must always be present when sending blobs, even if its value is zero.Evidence
v2.1.9,v2.1.11, branchesv2andnext&&pattern appears inl1_tx_utils.tsat lines 278, 450, 468, 696 but those are in logging statements only (cosmetic, not functional)Test plan
getGasPricereturnsmaxFeePerBlobGaswhengetBlobBaseFee()returns0ngetGasPricereturnsmaxFeePerBlobGaswhengetBlobBaseFee()RPC rejects entirely