Skip to content

Commit 99abdfe

Browse files
authored
fix(ethereum): deflake monitor timeout tests in l1_tx_utils (#24545)
## Problem `l1_tx_utils.test.ts` flaked in CI ([log](http://ci.aztec-labs.com/390c881d64e094b6), seen on [#24454](#24454 (comment)), which is unrelated to the failure). The failing test was `monitors all sent txs`: ``` ● L1TxUtils › L1TxUtils with blobs › monitors all sent txs TimeoutError: L1 transaction 0x82cf...aa3a timed out at TestL1TxUtils.monitorTransaction (l1_tx_utils/l1_tx_utils.ts:601:11) ``` ## Root cause The test creates the monitor promise and only attaches its rejection expectation after several intervening awaits: ```ts const monitorPromise = gasUtils.monitorTransaction(state); await sleep(100); await cheatCodes.mineEmptyBlock(); await expect(monitorPromise).rejects.toThrow('timed out'); // handler attached here ``` Anvil timestamps have 1s granularity, so when the wall clock crosses a second boundary between the send and the monitor's timeout check, the monitor (with `txTimeoutMs: 200`, `checkIntervalMs: 100`) can observe the L1 timestamp already 1s past `sentAt` and reject **before** `mineEmptyBlock()` completes. In the failing run the timeout fired at `16:41:20.355`, before the empty block was mined at `.356` and before the `.rejects` handler attached at `.357`. Node emitted `unhandledRejection` in that window and jest 30 reported it as the test failure — which is why the failure stack has no test-file frame and the test's own assertions all passed in the log. The same file already uses the safe idiom in a dozen newer tests: attach the handler synchronously with `.catch(err => err)` and assert via `resolves.toBeInstanceOf(TimeoutError)`. ## Fix Apply that established pattern to the three remaining tests that expect a monitor timeout but attach the handler only after intervening awaits: - `stops trying after timeout once block is mined` - `attempts to cancel timed out transactions` - `monitors all sent txs` (the observed flake) Test-only change; no product code touched. `TimeoutError` is exactly what `monitorTransaction` throws on timeout, so the assertions are equivalent-or-stricter than the previous message matching. Evidence: CI log http://ci.aztec-labs.com/390c881d64e094b6 (search for `monitors all sent txs`). --- *Created by [claudebox](https://claudebox.work/v2/sessions/0959caa9f03f59fa) · group: `slackbot`*
1 parent 5e0e53f commit 99abdfe

1 file changed

Lines changed: 9 additions & 6 deletions

File tree

yarn-project/ethereum/src/l1_tx_utils/l1_tx_utils.test.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,13 +1040,14 @@ describe('L1TxUtils', () => {
10401040
const txRequest: L1TxRequest = { to: '0x1234567890123456789012345678901234567890', data: '0x', value: 0n };
10411041
const { txHash, state } = await gasUtils.sendTransaction(txRequest);
10421042
const testState: L1TxState = { ...state, txConfigOverrides: { ...state.txConfigOverrides, txTimeoutAt } };
1043-
const monitorPromise = gasUtils.monitorTransaction(testState);
1043+
// Attach the rejection handler synchronously so an early timeout does not surface as an unhandled rejection
1044+
const monitorPromise = gasUtils.monitorTransaction(testState).catch(err => err);
10441045

10451046
await sleep(100);
10461047
await cheatCodes.dropTransaction(txHash);
10471048
await cheatCodes.setNextBlockTimestamp(txTimeoutAt);
10481049
await cheatCodes.mine();
1049-
await expect(monitorPromise).rejects.toThrow(/timed out/);
1050+
await expect(monitorPromise).resolves.toBeInstanceOf(TimeoutError);
10501051
expect(dateProvider.now() - now).toBeGreaterThanOrEqual(90);
10511052
}, 20_000);
10521053

@@ -1073,14 +1074,15 @@ describe('L1TxUtils', () => {
10731074
...state,
10741075
txConfigOverrides: { ...state.txConfigOverrides, txTimeoutMs: 100 },
10751076
};
1076-
const monitorPromise = gasUtils.monitorTransaction(testState);
1077+
// Attach the rejection handler synchronously so an early timeout does not surface as an unhandled rejection
1078+
const monitorPromise = gasUtils.monitorTransaction(testState).catch(err => err);
10771079
logger.warn(`Monitoring tx ${txHash}`);
10781080

10791081
// Mine a block to advance the timestamp and trigger the timeout
10801082
await cheatCodes.mineEmptyBlock();
10811083

10821084
// Wait for timeout and catch the error
1083-
await expect(monitorPromise).rejects.toThrow('timed out');
1085+
await expect(monitorPromise).resolves.toBeInstanceOf(TimeoutError);
10841086
logger.warn(`Tx monitor has timed out`);
10851087

10861088
// Wait for cancellation tx to be sent
@@ -1173,12 +1175,13 @@ describe('L1TxUtils', () => {
11731175
// Monitor the tx. We will think it has timed out and submit a cancellation.
11741176
state.txConfigOverrides.txTimeoutMs = 200;
11751177
state.txConfigOverrides.checkIntervalMs = 100;
1176-
const monitorPromise = gasUtils.monitorTransaction(state);
1178+
// Attach the rejection handler synchronously so an early timeout does not surface as an unhandled rejection
1179+
const monitorPromise = gasUtils.monitorTransaction(state).catch(err => err);
11771180

11781181
// Wait for timeout and catch the error
11791182
await sleep(100);
11801183
await cheatCodes.mineEmptyBlock();
1181-
await expect(monitorPromise).rejects.toThrow('timed out');
1184+
await expect(monitorPromise).resolves.toBeInstanceOf(TimeoutError);
11821185
logger.warn('Monitor has thrown for timeout');
11831186

11841187
// Wait for cancellation to be sent

0 commit comments

Comments
 (0)