From 06118040326c63fa74247868bddea57f579c9dd2 Mon Sep 17 00:00:00 2001 From: David Michael Date: Sat, 7 Feb 2026 08:37:50 -0500 Subject: [PATCH] fix: include maxFeePerBlobGas in gas price even when blob base fee is 0 `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 --- .../src/l1_tx_utils/l1_tx_utils.test.ts | 52 +++++++++++++++++++ .../src/l1_tx_utils/readonly_l1_tx_utils.ts | 6 +-- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/yarn-project/ethereum/src/l1_tx_utils/l1_tx_utils.test.ts b/yarn-project/ethereum/src/l1_tx_utils/l1_tx_utils.test.ts index 5d75639dcc20..339885c00c8f 100644 --- a/yarn-project/ethereum/src/l1_tx_utils/l1_tx_utils.test.ts +++ b/yarn-project/ethereum/src/l1_tx_utils/l1_tx_utils.test.ts @@ -608,6 +608,58 @@ describe('L1TxUtils', () => { } }); + // Regression: getGasPrice must include maxFeePerBlobGas even when blob base fee is 0n. + // 0n is falsy in JS, so `...(maxFeePerBlobGas && { maxFeePerBlobGas })` silently drops it. + // This caused missed block proposals when getBlobBaseFee() RPC failed and defaulted to 0n. + it('includes maxFeePerBlobGas in result even when blob base fee is zero', async () => { + await cheatCodes.setNextBlockBaseFeePerGas(WEI_CONST); + await cheatCodes.evmMine(); + + // Mock getBlobBaseFee to return 0n (simulates RPC failure defaulting to 0n) + const originalGetBlobBaseFee = l1Client.getBlobBaseFee; + l1Client.getBlobBaseFee = () => Promise.resolve(0n); + + try { + gasUtils.updateConfig({ + ...defaultL1TxUtilsConfig, + stallTimeMs: 0, + }); + + const gasPrice = await gasUtils['getGasPrice'](undefined, true); + + // maxFeePerBlobGas MUST be present for blob transactions, even when 0n + expect(gasPrice.maxFeePerBlobGas).toBeDefined(); + expect(typeof gasPrice.maxFeePerBlobGas).toBe('bigint'); + } finally { + l1Client.getBlobBaseFee = originalGetBlobBaseFee; + } + }); + + // Regression: same bug when getBlobBaseFee() RPC call fails entirely + it('includes maxFeePerBlobGas in result even when getBlobBaseFee RPC fails', async () => { + await cheatCodes.setNextBlockBaseFeePerGas(WEI_CONST); + await cheatCodes.evmMine(); + + // Mock getBlobBaseFee to reject (simulates RPC timeout under memory pressure) + const originalGetBlobBaseFee = l1Client.getBlobBaseFee; + l1Client.getBlobBaseFee = () => Promise.reject(new Error('RPC timeout')); + + try { + gasUtils.updateConfig({ + ...defaultL1TxUtilsConfig, + stallTimeMs: 0, + }); + + const gasPrice = await gasUtils['getGasPrice'](undefined, true); + + // maxFeePerBlobGas MUST be present for blob transactions, even on RPC failure + expect(gasPrice.maxFeePerBlobGas).toBeDefined(); + expect(typeof gasPrice.maxFeePerBlobGas).toBe('bigint'); + } finally { + l1Client.getBlobBaseFee = originalGetBlobBaseFee; + } + }); + it('respects minimum gas price bump for replacements', async () => { gasUtils.updateConfig({ ...defaultL1TxUtilsConfig, diff --git a/yarn-project/ethereum/src/l1_tx_utils/readonly_l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils/readonly_l1_tx_utils.ts index 526a64ea4c18..9b7be66607a9 100644 --- a/yarn-project/ethereum/src/l1_tx_utils/readonly_l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils/readonly_l1_tx_utils.ts @@ -191,14 +191,14 @@ export class ReadOnlyL1TxUtils { } // Ensure we don't exceed maxBlobGwei - if (maxFeePerBlobGas && effectiveMaxBlobGwei > 0n) { + if (maxFeePerBlobGas !== undefined && effectiveMaxBlobGwei > 0n) { maxFeePerBlobGas = maxFeePerBlobGas > effectiveMaxBlobGwei ? effectiveMaxBlobGwei : maxFeePerBlobGas; } // Ensure priority fee doesn't exceed max fee const maxPriorityFeePerGas = priorityFee > maxFeePerGas ? maxFeePerGas : priorityFee; - if (attempt > 0 && previousGasPrice?.maxFeePerBlobGas) { + if (attempt > 0 && previousGasPrice?.maxFeePerBlobGas !== undefined) { const bumpPercentage = gasConfig.priorityFeeRetryBumpPercentage! > MIN_BLOB_REPLACEMENT_BUMP_PERCENTAGE ? gasConfig.priorityFeeRetryBumpPercentage! @@ -226,7 +226,7 @@ export class ReadOnlyL1TxUtils { return { maxFeePerGas, maxPriorityFeePerGas, - ...(maxFeePerBlobGas && { maxFeePerBlobGas: maxFeePerBlobGas }), + ...(isBlobTx ? { maxFeePerBlobGas } : {}), }; }