Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions yarn-project/ethereum/src/l1_tx_utils/l1_tx_utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions yarn-project/ethereum/src/l1_tx_utils/readonly_l1_tx_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -226,7 +226,7 @@ export class ReadOnlyL1TxUtils {
return {
maxFeePerGas,
maxPriorityFeePerGas,
...(maxFeePerBlobGas && { maxFeePerBlobGas: maxFeePerBlobGas }),
...(isBlobTx ? { maxFeePerBlobGas } : {}),
};
}

Expand Down
Loading