From 4b1e6cfeafd61273c5dced677ca556146533bf06 Mon Sep 17 00:00:00 2001 From: danielntmd Date: Tue, 10 Mar 2026 16:36:31 +0000 Subject: [PATCH] fix: add buffer to maxFeePerBlobGas for gas estimation and fix bump loop truncation The blob validateBlobs estimateGas call was intermittently failing with "max fee per blob gas less than block blob gas fee" because it computed a precise maxFeePerBlobGas that could go stale between the getBlobBaseFee RPC call and the estimateGas RPC call. Since gas estimation is a read-only, we now use a 2x buffer to pass EIP-4844 validation. Also fix integer truncation in the base fee bump loop where ceil is needed to ensure fees increase at small values (e.g. 1 wei). --- .../src/l1_tx_utils/l1_tx_utils.test.ts | 44 +++++++++++++++++++ .../src/l1_tx_utils/readonly_l1_tx_utils.ts | 12 +++-- 2 files changed, 52 insertions(+), 4 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 11684ed5ebb3..0e876f48cd1c 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 @@ -454,6 +454,50 @@ describe('L1TxUtils', () => { } }); + it('bumps gas fees correctly at very low wei values (ceiling division)', async () => { + await cheatCodes.setNextBlockBaseFeePerGas(1n); + await cheatCodes.evmMine(); + + const originalGetBlobBaseFee = l1Client.getBlobBaseFee; + l1Client.getBlobBaseFee = () => Promise.resolve(1n); + + const originalEstimate = l1Client.estimateMaxPriorityFeePerGas; + l1Client.estimateMaxPriorityFeePerGas = () => Promise.resolve(0n); + + try { + gasUtils.updateConfig({ + ...defaultL1TxUtilsConfig, + stallTimeMs: 12_000, + priorityFeeBumpPercentage: 0, + minimumPriorityFeePerGas: 0, + }); + + const gasPrice = await gasUtils['getGasPrice'](undefined, true); + + // With ceiling division: (1n * 1125n + 999n) / 1000n = 2n + expect(gasPrice.maxFeePerGas).toBe(2n); + expect(gasPrice.maxFeePerBlobGas).toBe(2n); + + // Verify compounding works across multiple iterations + gasUtils.updateConfig({ + ...defaultL1TxUtilsConfig, + stallTimeMs: 24_000, + priorityFeeBumpPercentage: 0, + minimumPriorityFeePerGas: 0, + }); + + const gasPrice2 = await gasUtils['getGasPrice'](undefined, true); + + // Iteration 1: ceil(1 * 1125 / 1000) = 2 + // Iteration 2: ceil(2 * 1125 / 1000) = ceil(2.25) = 3 + expect(gasPrice2.maxFeePerGas).toBe(3n); + expect(gasPrice2.maxFeePerBlobGas).toBe(3n); + } finally { + l1Client.getBlobBaseFee = originalGetBlobBaseFee; + l1Client.estimateMaxPriorityFeePerGas = originalEstimate; + } + }); + it('calculates correct gas prices for retry attempts', async () => { await cheatCodes.setNextBlockBaseFeePerGas(WEI_CONST); await cheatCodes.evmMine(); 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..b54b6d027b64 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 @@ -130,9 +130,10 @@ export class ReadOnlyL1TxUtils { const numBlocks = Math.ceil(gasConfig.stallTimeMs! / BLOCK_TIME_MS); for (let i = 0; i < numBlocks; i++) { // each block can go up 12.5% from previous baseFee - maxFeePerGas = (maxFeePerGas * (1_000n + 125n)) / 1_000n; + // ceil, (a+b-1)/b, to avoid truncation at small values (e.g. 1 wei blob base fee) + maxFeePerGas = (maxFeePerGas * (1_000n + 125n) + 999n) / 1_000n; // same for blob gas fee - maxFeePerBlobGas = (maxFeePerBlobGas * (1_000n + 125n)) / 1_000n; + maxFeePerBlobGas = (maxFeePerBlobGas * (1_000n + 125n) + 999n) / 1_000n; } if (attempt > 0) { @@ -242,13 +243,16 @@ export class ReadOnlyL1TxUtils { const gasConfig = { ...this.config, ..._gasConfig }; let initialEstimate = 0n; if (_blobInputs) { - // @note requests with blobs also require maxFeePerBlobGas to be set + // @note requests with blobs also require maxFeePerBlobGas to be set. + // Use 2x buffer for maxFeePerBlobGas to avoid stale fees and to pass EIP-4844 validation (even if it is a gas estimation call). + // 1. maxFeePerBlobGas >= blobBaseFee + // 2. account balance >= gas * maxFeePerGas + maxFeePerBlobGas * blobCount + value const gasPrice = await this.getGasPrice(gasConfig, true, 0); initialEstimate = await this.client.estimateGas({ account, ...request, ..._blobInputs, - maxFeePerBlobGas: gasPrice.maxFeePerBlobGas!, + maxFeePerBlobGas: gasPrice.maxFeePerBlobGas! * 2n, gas: MAX_L1_TX_LIMIT, blockTag: 'latest', });