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', });