@aztec-labs/ethereum 6.0.0-rc.3 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dest/blob_tx.d.ts +24 -0
  2. package/dest/blob_tx.d.ts.map +1 -0
  3. package/dest/blob_tx.js +94 -0
  4. package/dest/bn254/hash_to_point.d.ts +32 -0
  5. package/dest/bn254/hash_to_point.d.ts.map +1 -0
  6. package/dest/bn254/hash_to_point.js +80 -0
  7. package/dest/bn254/index.d.ts +3 -0
  8. package/dest/bn254/index.d.ts.map +1 -0
  9. package/dest/bn254/index.js +2 -0
  10. package/dest/bn254/proof_of_possession_gas.d.ts +29 -0
  11. package/dest/bn254/proof_of_possession_gas.d.ts.map +1 -0
  12. package/dest/bn254/proof_of_possession_gas.js +24 -0
  13. package/dest/client.d.ts +1 -1
  14. package/dest/client.d.ts.map +1 -1
  15. package/dest/client.js +5 -3
  16. package/dest/contracts/errors.d.ts +10 -3
  17. package/dest/contracts/errors.d.ts.map +1 -1
  18. package/dest/contracts/errors.js +10 -4
  19. package/dest/contracts/inbox.d.ts +1 -1
  20. package/dest/contracts/inbox.d.ts.map +1 -1
  21. package/dest/contracts/inbox.js +2 -0
  22. package/dest/contracts/log.d.ts +7 -1
  23. package/dest/contracts/log.d.ts.map +1 -1
  24. package/dest/contracts/log.js +6 -0
  25. package/dest/contracts/rollup.d.ts +32 -4
  26. package/dest/contracts/rollup.d.ts.map +1 -1
  27. package/dest/contracts/rollup.js +63 -7
  28. package/dest/deploy_aztec_l1_contracts.d.ts +16 -1
  29. package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
  30. package/dest/deploy_aztec_l1_contracts.js +201 -65
  31. package/dest/foundry_binary.d.ts +1 -1
  32. package/dest/foundry_binary.d.ts.map +1 -1
  33. package/dest/foundry_binary.js +2 -1
  34. package/dest/l1_tx_utils/amsterdam.d.ts +1 -1
  35. package/dest/l1_tx_utils/amsterdam.d.ts.map +1 -1
  36. package/dest/l1_tx_utils/amsterdam.js +4 -1
  37. package/dest/l1_tx_utils/constants.d.ts +2 -1
  38. package/dest/l1_tx_utils/constants.d.ts.map +1 -1
  39. package/dest/l1_tx_utils/constants.js +3 -0
  40. package/dest/l1_tx_utils/fee-strategies/p75_competitive.d.ts +1 -1
  41. package/dest/l1_tx_utils/fee-strategies/p75_competitive.d.ts.map +1 -1
  42. package/dest/l1_tx_utils/fee-strategies/p75_competitive.js +6 -1
  43. package/dest/l1_tx_utils/index.d.ts +2 -1
  44. package/dest/l1_tx_utils/index.d.ts.map +1 -1
  45. package/dest/l1_tx_utils/index.js +1 -0
  46. package/dest/l1_tx_utils/l1_tx_utils.d.ts +21 -1
  47. package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
  48. package/dest/l1_tx_utils/l1_tx_utils.js +71 -5
  49. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +8 -1
  50. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  51. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +1 -1
  52. package/dest/l1_tx_utils/send_cost.d.ts +15 -0
  53. package/dest/l1_tx_utils/send_cost.d.ts.map +1 -0
  54. package/dest/l1_tx_utils/send_cost.js +10 -0
  55. package/dest/l1_tx_utils/signer.d.ts +2 -2
  56. package/dest/l1_tx_utils/signer.d.ts.map +1 -1
  57. package/dest/l1_tx_utils/signer.js +2 -2
  58. package/dest/l1_tx_utils/tx_delayer.d.ts +1 -1
  59. package/dest/l1_tx_utils/tx_delayer.d.ts.map +1 -1
  60. package/dest/l1_tx_utils/tx_delayer.js +8 -26
  61. package/dest/publisher_manager.d.ts +127 -13
  62. package/dest/publisher_manager.d.ts.map +1 -1
  63. package/dest/publisher_manager.js +320 -79
  64. package/dest/test/anvil_hardfork.d.ts +42 -0
  65. package/dest/test/anvil_hardfork.d.ts.map +1 -0
  66. package/dest/test/anvil_hardfork.js +107 -0
  67. package/dest/test/eth_cheat_codes.d.ts +30 -5
  68. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  69. package/dest/test/eth_cheat_codes.js +283 -101
  70. package/dest/test/index.d.ts +3 -1
  71. package/dest/test/index.d.ts.map +1 -1
  72. package/dest/test/index.js +2 -0
  73. package/dest/test/l1_hardfork.d.ts +43 -0
  74. package/dest/test/l1_hardfork.d.ts.map +1 -0
  75. package/dest/test/l1_hardfork.js +51 -0
  76. package/dest/test/recording_rpc_server.d.ts +1 -1
  77. package/dest/test/recording_rpc_server.d.ts.map +1 -1
  78. package/dest/test/recording_rpc_server.js +1 -0
  79. package/dest/test/rollup_cheat_codes.d.ts +1 -1
  80. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  81. package/dest/test/rollup_cheat_codes.js +7 -7
  82. package/dest/test/start_anvil.d.ts +14 -2
  83. package/dest/test/start_anvil.d.ts.map +1 -1
  84. package/dest/test/start_anvil.js +28 -11
  85. package/package.json +13 -11
  86. package/src/blob_tx.ts +118 -0
  87. package/src/bn254/hash_to_point.ts +95 -0
  88. package/src/bn254/index.ts +2 -0
  89. package/src/bn254/proof_of_possession_gas.ts +54 -0
  90. package/src/client.ts +6 -3
  91. package/src/contracts/errors.ts +12 -4
  92. package/src/contracts/inbox.ts +4 -0
  93. package/src/contracts/log.ts +12 -0
  94. package/src/contracts/rollup.ts +63 -6
  95. package/src/deploy_aztec_l1_contracts.ts +238 -63
  96. package/src/foundry_binary.ts +2 -1
  97. package/src/l1_tx_utils/amsterdam.ts +4 -1
  98. package/src/l1_tx_utils/constants.ts +4 -0
  99. package/src/l1_tx_utils/fee-strategies/p75_competitive.ts +11 -1
  100. package/src/l1_tx_utils/index.ts +1 -0
  101. package/src/l1_tx_utils/l1_tx_utils.ts +72 -5
  102. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +1 -1
  103. package/src/l1_tx_utils/send_cost.ts +21 -0
  104. package/src/l1_tx_utils/signer.ts +3 -2
  105. package/src/l1_tx_utils/tx_delayer.ts +8 -37
  106. package/src/publisher_manager.ts +458 -83
  107. package/src/test/anvil_hardfork.ts +95 -0
  108. package/src/test/eth_cheat_codes.ts +306 -81
  109. package/src/test/fixtures/sepolia_blob_tx.json +10 -0
  110. package/src/test/index.ts +2 -0
  111. package/src/test/l1_hardfork.ts +71 -0
  112. package/src/test/recording_rpc_server.ts +1 -0
  113. package/src/test/rollup_cheat_codes.ts +9 -9
  114. package/src/test/start_anvil.ts +41 -13
@@ -0,0 +1,95 @@
1
+ import { Buffer } from 'buffer';
2
+ import { writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { gunzipSync } from 'node:zlib';
5
+ import { type EIP1193RequestFn, type Hex, createPublicClient, http } from 'viem';
6
+
7
+ import { resolveFoundryBinary } from '../foundry_binary.js';
8
+ import { isAmsterdamBlock } from '../l1_tx_utils/amsterdam.js';
9
+ import type { EthCheatCodes } from './eth_cheat_codes.js';
10
+ import { type Anvil, startAnvil } from './start_anvil.js';
11
+
12
+ /**
13
+ * Starts a throwaway anvil on `hardfork` and checks that it mines blocks of that fork, so a test that needs a fork
14
+ * the installed foundry cannot run fails within seconds instead of after its whole setup. Never skips.
15
+ */
16
+ export async function assertAnvilSupportsHardfork(hardfork: string): Promise<void> {
17
+ let res: Awaited<ReturnType<typeof startAnvil>> | undefined;
18
+ try {
19
+ res = await startAnvil({ port: 0, hardfork });
20
+ const client = createPublicClient({ transport: http(res.rpcUrl, { batch: false }) });
21
+ await client.request({ method: 'evm_mine' as any, params: [] as any });
22
+ const block = await client.getBlock({ blockTag: 'latest' });
23
+ if (isAmsterdamBlock(block) !== (hardfork === 'amsterdam')) {
24
+ throw new Error(`anvil --hardfork ${hardfork} mined block ${block.number} with the wrong header shape`);
25
+ }
26
+ } catch (err) {
27
+ throw new Error(
28
+ `Anvil at ${resolveFoundryBinary('anvil')} cannot run --hardfork ${hardfork}; foundry >= 1.8.5 is required`,
29
+ { cause: err },
30
+ );
31
+ } finally {
32
+ await res?.stop();
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Writes anvil's state, including the historical states it still keeps, as the plain JSON file `--load-state` reads.
38
+ * Without history, reads at blocks before a restart fail on the reloaded anvil. A dump with history easily exceeds
39
+ * viem's default response size limit, so `client` must be one built without it, such as `createExtendedL1Client`'s.
40
+ */
41
+ export async function dumpAnvilStateWithHistory(client: { request: EIP1193RequestFn }, path: string): Promise<void> {
42
+ const hex = await client.request<{ Method: 'anvil_dumpState'; Parameters: [boolean]; ReturnType: Hex }>({
43
+ method: 'anvil_dumpState',
44
+ params: [true],
45
+ });
46
+ const dump = Buffer.from(hex.slice(2), 'hex');
47
+ const isGzip = dump[0] === 0x1f && dump[1] === 0x8b;
48
+ // Kept as a Buffer: a dump with history can exceed the maximum length of a V8 string.
49
+ await writeFile(path, isGzip ? gunzipSync(dump) : dump);
50
+ }
51
+
52
+ /**
53
+ * Moves an interval-mining anvil to another hardfork, continuing its chain. Anvil cannot change hardfork at runtime,
54
+ * so this stops mining, dumps the state with history to `stateDir` using `dumpClient`, stops `anvil`, and starts a new
55
+ * anvil on the same port from the dump with `startOpts`. Mining stays stopped until the old anvil is gone, so no block
56
+ * mined after the dump is lost. Pending txs are lost, since the mempool is not part of the dump.
57
+ *
58
+ * The reloaded anvil would stamp its next block as if no time had passed since the dump, so the first block on the new
59
+ * hardfork is mined at the first L1 slot of the old chain's grid that is not in the past, and interval mining resumes
60
+ * from there.
61
+ *
62
+ * @returns The new anvil, which replaces `anvil`, and the first block mined on `hardfork`.
63
+ */
64
+ export async function restartAnvilOnHardfork(
65
+ anvil: Anvil,
66
+ cheat: EthCheatCodes,
67
+ dumpClient: { request: EIP1193RequestFn },
68
+ opts: { hardfork: string; stateDir: string; startOpts?: Parameters<typeof startAnvil>[0] },
69
+ ): Promise<{ anvil: Anvil; forkBlock: { number: bigint; timestamp: bigint } }> {
70
+ const blockTimeS = await cheat.getIntervalMining();
71
+ if (!blockTimeS) {
72
+ throw new Error('restartAnvilOnHardfork needs an anvil in interval mining');
73
+ }
74
+ await cheat.setIntervalMining(0, { silent: true });
75
+ const tip = await cheat.publicClient.getBlock({ blockTag: 'latest' });
76
+ const statePath = join(opts.stateDir, `anvil-state-${tip.number}.json`);
77
+ await dumpAnvilStateWithHistory(dumpClient, statePath);
78
+ await anvil.stop();
79
+
80
+ const restarted = await startAnvil({
81
+ ...opts.startOpts,
82
+ port: anvil.port,
83
+ hardfork: opts.hardfork,
84
+ loadState: statePath,
85
+ l1BlockTime: blockTimeS,
86
+ });
87
+
88
+ const blockTime = BigInt(blockTimeS);
89
+ const nowS = BigInt(Math.ceil(cheat.dateProvider.now() / 1000));
90
+ const slotsSinceTip = nowS > tip.timestamp ? (nowS - tip.timestamp + blockTime - 1n) / blockTime : 1n;
91
+ await cheat.warp(tip.timestamp + blockTime * slotsSinceTip, { silent: true, resetBlockInterval: true });
92
+
93
+ const forkBlock = await cheat.publicClient.getBlock({ blockNumber: tip.number + 1n });
94
+ return { anvil: restarted.anvil, forkBlock: { number: forkBlock.number, timestamp: forkBlock.timestamp } };
95
+ }
@@ -3,8 +3,10 @@ import { keccak256 } from '@aztec-labs/foundation/crypto/keccak';
3
3
  import { EthAddress } from '@aztec-labs/foundation/eth-address';
4
4
  import { jsonStringify } from '@aztec-labs/foundation/json-rpc';
5
5
  import { createLogger } from '@aztec-labs/foundation/log';
6
+ import { Semaphore } from '@aztec-labs/foundation/queue';
6
7
  import { pluralize } from '@aztec-labs/foundation/string';
7
8
  import type { DateProvider, TestDateProvider } from '@aztec-labs/foundation/timer';
9
+ import { Buffer } from 'buffer';
8
10
  import {
9
11
  type Chain,
10
12
  type Hex,
@@ -19,6 +21,66 @@ import { foundry } from 'viem/chains';
19
21
 
20
22
  import type { ViemPublicClient } from '../types.js';
21
23
 
24
+ /** Interval mining that a finished pause hands back to anvil on the next tick of the block grid. */
25
+ type PendingResume = { interval: number; timer: ReturnType<typeof setTimeout> };
26
+
27
+ /**
28
+ * Mining state of the test anvil, shared by every {@link EthCheatCodes} instance in the process: e2e builds several over
29
+ * one anvil (the fixture's, `CheatCodes.eth`, each `RollupCheatCodes`), and a pause or pending resume started by one
30
+ * must be visible to the others. Assumes every mining-mode cheat code in the process targets one anvil: other anvils may
31
+ * be started and stopped alongside it, but not paused or switched between mining modes.
32
+ */
33
+ const anvilMining: {
34
+ /** Serializes changes to anvil's mining mode against the pending resume. Never held while a caller's code runs. */
35
+ lock: Semaphore;
36
+ /** Ports of the anvil the latest pause targeted, which the pause state and any pending resume belong to. */
37
+ ownerPorts?: string[];
38
+ /**
39
+ * Mining mode captured by the pause in progress, restored when it ends. `nextTimestamp` is the next block's timestamp
40
+ * a caller chose during the pause, which a resume on the grid would overwrite if no block has used it yet.
41
+ */
42
+ paused?: { interval: number | null; automine: boolean; nextTimestamp?: number };
43
+ pendingResume?: PendingResume;
44
+ /** Bumped on reset, so a resume being scheduled across a reset does not arm a timer for the next anvil. */
45
+ generation: number;
46
+ } = { lock: new Semaphore(1), generation: 0 };
47
+
48
+ /** Stops a pending resume and returns the interval it would have restored. */
49
+ function takePendingResume(): number | undefined {
50
+ const pending = anvilMining.pendingResume;
51
+ if (!pending) {
52
+ return undefined;
53
+ }
54
+ clearTimeout(pending.timer);
55
+ anvilMining.pendingResume = undefined;
56
+ return pending.interval;
57
+ }
58
+
59
+ /**
60
+ * Cancels any pending interval-mining resume and forgets the pause state, if they belong to the anvil listening on
61
+ * `port`. Called when that anvil is stopped, so no timer outlives it and the next anvil starts clean. Stopping any other
62
+ * anvil, such as a throwaway one a test starts next to its main one, leaves the state alone.
63
+ */
64
+ export function resetAnvilMiningState(port: number): void {
65
+ // The owner is kept, so the reset on the anvil's close still clears a timer armed while it was shutting down.
66
+ if (!anvilMining.ownerPorts?.includes(String(port))) {
67
+ return;
68
+ }
69
+ takePendingResume();
70
+ anvilMining.paused = undefined;
71
+ anvilMining.generation++;
72
+ }
73
+
74
+ /**
75
+ * A tick missed by at most this long, because a pause ended or the resume timer fired that much after it, is still
76
+ * mined straight away with its grid timestamp: the block lands up to this late rather than leaving a gap in the chain.
77
+ * A tick missed by more is skipped, like a missed L1 slot, and mining resumes on the next one.
78
+ */
79
+ const OVERDUE_TICK_GRACE_MS = 1000;
80
+
81
+ /** A grid block mined at most this long after its tick restarts anvil's countdown from it; a later one does not. */
82
+ const ON_TIME_TOLERANCE_MS = 100;
83
+
22
84
  /**
23
85
  * A class that provides utility functions for interacting with ethereum (L1).
24
86
  */
@@ -118,7 +180,7 @@ export class EthCheatCodes {
118
180
  * @param numberOfBlocks - The number of blocks to mine
119
181
  */
120
182
  public async mine(numberOfBlocks: number | bigint = 1): Promise<void> {
121
- await this.doMine(Number(numberOfBlocks));
183
+ await this.withMiningLock(() => this.doMine(Number(numberOfBlocks)));
122
184
  this.logger.warn(`Mined ${numberOfBlocks} L1 blocks`);
123
185
  }
124
186
 
@@ -134,9 +196,13 @@ export class EthCheatCodes {
134
196
  * Mines a single block with evm_mine
135
197
  */
136
198
  public async evmMine(): Promise<void> {
199
+ await this.withMiningLock(() => this.doEvmMine());
200
+ this.logger.warn(`Mined 1 L1 block with evm_mine`);
201
+ }
202
+
203
+ private async doEvmMine(): Promise<void> {
137
204
  try {
138
205
  await this.doRpcCall('evm_mine', []);
139
- this.logger.warn(`Mined 1 L1 block with evm_mine`);
140
206
  } catch (err) {
141
207
  throw new Error(`Error mining: ${err}`);
142
208
  }
@@ -188,30 +254,44 @@ export class EthCheatCodes {
188
254
  }
189
255
 
190
256
  /**
191
- * Get interval mining if set.
192
- * @param seconds - The interval to use between blocks
257
+ * Get interval mining if set. A pending resume left by {@link execWithPausedAnvil} counts as set, although anvil
258
+ * itself reports interval mining as off until the resume fires.
193
259
  */
194
- public getIntervalMining(): Promise<number | null> {
260
+ public async getIntervalMining(): Promise<number | null> {
261
+ return await this.withMiningLock(
262
+ async () => anvilMining.pendingResume?.interval ?? (await this.doGetIntervalMining()),
263
+ );
264
+ }
265
+
266
+ private async doGetIntervalMining(): Promise<number | null> {
195
267
  try {
196
- return this.doRpcCall('anvil_getIntervalMining', []);
268
+ return await this.doRpcCall('anvil_getIntervalMining', []);
197
269
  } catch (err) {
198
270
  throw new Error(`Error getting interval mining: ${err}`);
199
271
  }
200
272
  }
201
273
 
202
274
  /**
203
- * Enable interval mining at the given interval (block time)
275
+ * Enable interval mining at the given interval (block time). Replaces any pending resume left by
276
+ * {@link execWithPausedAnvil}.
204
277
  * @param seconds - The interval to use between blocks
205
278
  */
206
279
  public async setIntervalMining(seconds: number, opts: { silent?: boolean } = {}): Promise<void> {
280
+ await this.withMiningLock(async () => {
281
+ takePendingResume();
282
+ await this.doSetIntervalMining(seconds);
283
+ });
284
+ if (!opts.silent) {
285
+ this.logger.warn(`Set L1 interval mining to ${seconds} seconds`);
286
+ }
287
+ }
288
+
289
+ private async doSetIntervalMining(seconds: number): Promise<void> {
207
290
  try {
208
291
  await this.rpcCall('anvil_setIntervalMining', [seconds]);
209
292
  } catch (err) {
210
293
  throw new Error(`Error setting interval mining: ${err}`);
211
294
  }
212
- if (!opts.silent) {
213
- this.logger.warn(`Set L1 interval mining to ${seconds} seconds`);
214
- }
215
295
  }
216
296
 
217
297
  /**
@@ -222,11 +302,14 @@ export class EthCheatCodes {
222
302
  throw new Error(`Interval mining requires a positive interval, got ${seconds}`);
223
303
  }
224
304
 
225
- await this.setAutomine(false);
226
- await this.setIntervalMining(0, { silent: true });
227
- await this.evmMine();
228
- await this.syncDateProvider();
229
- await this.setIntervalMining(seconds);
305
+ await this.withMiningLock(async () => {
306
+ takePendingResume();
307
+ await this.doSetAutomine(false);
308
+ await this.doSetIntervalMining(0);
309
+ await this.doEvmMine();
310
+ await this.syncDateProvider();
311
+ await this.doSetIntervalMining(seconds);
312
+ });
230
313
  this.logger.warn(`Started L1 interval mining at ${seconds} seconds from a fresh block`);
231
314
  }
232
315
 
@@ -235,14 +318,18 @@ export class EthCheatCodes {
235
318
  * @param automine - The automine status to set
236
319
  */
237
320
  public async setAutomine(automine: boolean, opts: { silent?: boolean } = {}): Promise<void> {
321
+ await this.withMiningLock(() => this.doSetAutomine(automine));
322
+ if (!opts.silent) {
323
+ this.logger.warn(`Set L1 automine to ${automine}`);
324
+ }
325
+ }
326
+
327
+ private async doSetAutomine(automine: boolean): Promise<void> {
238
328
  try {
239
329
  await this.rpcCall('anvil_setAutomine', [automine]);
240
330
  } catch (err) {
241
331
  throw new Error(`Error setting automine: ${err}`);
242
332
  }
243
- if (!opts.silent) {
244
- this.logger.warn(`Set L1 automine to ${automine}`);
245
- }
246
333
  }
247
334
 
248
335
  /**
@@ -259,17 +346,27 @@ export class EthCheatCodes {
259
346
  }
260
347
 
261
348
  /**
262
- * Set the next block timestamp
349
+ * Set the next block timestamp. A resume on the block grid after {@link execWithPausedAnvil} would overwrite it, so
350
+ * interval mining is instead restarted straight away: now if a resume is pending, or when the current pause ends if
351
+ * no block mined during the pause has used the timestamp.
263
352
  * @param timestamp - The timestamp to set the next block to
264
353
  */
265
354
  public async setNextBlockTimestamp(timestamp: number | Date): Promise<void> {
266
- try {
267
- await this.rpcCall('evm_setNextBlockTimestamp', [
268
- timestamp instanceof Date ? Math.floor(timestamp.getTime() / 1000) : timestamp,
269
- ]);
270
- } catch (err: any) {
271
- throw new Error(`Error setting next block timestamp: ${err.message}`);
272
- }
355
+ const seconds = timestamp instanceof Date ? Math.floor(timestamp.getTime() / 1000) : timestamp;
356
+ await this.withMiningLock(async () => {
357
+ const pendingInterval = takePendingResume();
358
+ if (pendingInterval !== undefined) {
359
+ await this.doSetIntervalMining(pendingInterval);
360
+ }
361
+ if (anvilMining.paused) {
362
+ anvilMining.paused.nextTimestamp = seconds;
363
+ }
364
+ try {
365
+ await this.rpcCall('evm_setNextBlockTimestamp', [seconds]);
366
+ } catch (err: any) {
367
+ throw new Error(`Error setting next block timestamp: ${err.message}`);
368
+ }
369
+ });
273
370
  this.logger.warn(`Set L1 next block timestamp to ${timestamp}`);
274
371
  }
275
372
 
@@ -283,31 +380,34 @@ export class EthCheatCodes {
283
380
  timestamp: number | bigint,
284
381
  opts: { silent?: boolean; resetBlockInterval?: boolean } = {},
285
382
  ): Promise<void> {
286
- let blockInterval: number | null = null;
287
- try {
288
- // Load current block interval and disable it
289
- if (opts.resetBlockInterval) {
290
- blockInterval = await this.getIntervalMining();
291
- if (blockInterval !== null) {
292
- await this.setIntervalMining(0, { silent: true });
383
+ await this.withMiningLock(async () => {
384
+ let blockInterval: number | null = null;
385
+ try {
386
+ // Load current block interval and disable it. A pending resume would put mining back on the grid from
387
+ // before the warp, so take it over: the warp re-phases the grid onto its own timestamp.
388
+ if (opts.resetBlockInterval) {
389
+ blockInterval = takePendingResume() ?? (await this.doGetIntervalMining());
390
+ if (blockInterval !== null) {
391
+ await this.doSetIntervalMining(0);
392
+ }
393
+ }
394
+ // Set the timestamp of the next block to be mined
395
+ await this.rpcCall('evm_setNextBlockTimestamp', [Number(timestamp)]);
396
+ // And mine a block so the timestamp goes into effect now
397
+ await this.doMine();
398
+ // Update the injected date provider so it follows L1 time
399
+ if ('setTime' in this.dateProvider) {
400
+ this.dateProvider.setTime(Number(timestamp) * 1000);
401
+ }
402
+ } catch (err) {
403
+ throw new Error(`Error warping: ${err}`);
404
+ } finally {
405
+ // Restore interval mining so the next block is mined in `blockInterval` seconds from this one
406
+ if (opts.resetBlockInterval && blockInterval !== null && blockInterval > 0) {
407
+ await this.doSetIntervalMining(blockInterval);
293
408
  }
294
409
  }
295
- // Set the timestamp of the next block to be mined
296
- await this.rpcCall('evm_setNextBlockTimestamp', [Number(timestamp)]);
297
- // And mine a block so the timestamp goes into effect now
298
- await this.doMine();
299
- // Update the injected date provider so it follows L1 time
300
- if ('setTime' in this.dateProvider) {
301
- this.dateProvider.setTime(Number(timestamp) * 1000);
302
- }
303
- } catch (err) {
304
- throw new Error(`Error warping: ${err}`);
305
- } finally {
306
- // Restore interval mining so the next block is mined in `blockInterval` seconds from this one
307
- if (opts.resetBlockInterval && blockInterval !== null && blockInterval > 0) {
308
- await this.setIntervalMining(blockInterval, { silent: true });
309
- }
310
- }
410
+ });
311
411
  if (!opts.silent) {
312
412
  this.logger.warn(`Warped L1 timestamp to ${timestamp}`);
313
413
  }
@@ -483,17 +583,19 @@ export class EthCheatCodes {
483
583
  ): Promise<void> {
484
584
  this.logger.verbose(`Preparing L1 reorg with depth ${depth}`);
485
585
  try {
486
- await this.rpcCall('anvil_reorg', [
487
- depth,
488
- newBlocks.flatMap((txs, index) =>
489
- txs.map(tx => [
490
- typeof tx === 'string'
491
- ? tx
492
- : { value: 0, ...tx, ...(tx.gas === undefined ? {} : { gas: numberToHex(tx.gas) }) },
493
- index,
494
- ]),
495
- ),
496
- ]);
586
+ await this.withMiningLock(() =>
587
+ this.rpcCall('anvil_reorg', [
588
+ depth,
589
+ newBlocks.flatMap((txs, index) =>
590
+ txs.map(tx => [
591
+ typeof tx === 'string'
592
+ ? tx
593
+ : { value: 0, ...tx, ...(tx.gas === undefined ? {} : { gas: numberToHex(tx.gas) }) },
594
+ index,
595
+ ]),
596
+ ),
597
+ ]),
598
+ );
497
599
  } catch (err) {
498
600
  throw new Error(`Error reorging: ${err}`);
499
601
  }
@@ -576,43 +678,166 @@ export class EthCheatCodes {
576
678
  await this.doRpcCall('anvil_setBlockGasLimit', [toHex(gasLimit)]);
577
679
  }
578
680
 
681
+ /**
682
+ * Runs `fn` with automine and interval mining off, so no block is mined while it runs. Called while another pause is
683
+ * in progress, it just runs `fn` and leaves the mining mode to that pause.
684
+ *
685
+ * Automine comes back straight away. Interval mining comes back in the background, on the grid the chain was
686
+ * already on (see {@link scheduleResume}), so the caller never waits for an L1 tick.
687
+ */
579
688
  public async execWithPausedAnvil<T>(fn: () => Promise<T>): Promise<T> {
580
- const [blockInterval, wasAutoMining] = await Promise.all([this.getIntervalMining(), this.isAutoMining()]);
581
- try {
582
- if (blockInterval !== null) {
583
- await this.setIntervalMining(0, { silent: true });
689
+ const ownsPause = await this.withMiningLock(async () => {
690
+ if (anvilMining.paused) {
691
+ return false;
584
692
  }
693
+ await this.pauseMining();
694
+ return true;
695
+ });
696
+ if (!ownsPause) {
697
+ return await fn();
698
+ }
585
699
 
586
- if (wasAutoMining) {
587
- await this.setAutomine(false, { silent: true });
588
- }
700
+ try {
701
+ return await fn();
702
+ } finally {
703
+ await this.withMiningLock(() => this.restoreMining());
704
+ }
705
+ }
706
+
707
+ public async syncDateProvider() {
708
+ const timestamp = await this.lastBlockTimestamp();
709
+ if ('setTime' in this.dateProvider) {
710
+ this.dateProvider.setTime(timestamp * 1000);
711
+ }
712
+ }
589
713
 
714
+ private async withMiningLock<T>(fn: () => Promise<T>): Promise<T> {
715
+ await anvilMining.lock.acquire();
716
+ try {
590
717
  return await fn();
591
718
  } finally {
719
+ anvilMining.lock.release();
720
+ }
721
+ }
722
+
723
+ private async pauseMining(): Promise<void> {
724
+ // A resume still pending from an earlier pause is taken over: anvil reports interval mining as off meanwhile.
725
+ const interval = takePendingResume() ?? (await this.doGetIntervalMining());
726
+ const paused = { interval, automine: await this.isAutoMining() };
727
+ anvilMining.paused = paused;
728
+ anvilMining.ownerPorts = this.rpcUrls.map(url => new URL(url).port);
729
+ try {
730
+ if (interval !== null) {
731
+ await this.doSetIntervalMining(0);
732
+ }
733
+ if (paused.automine) {
734
+ await this.doSetAutomine(false);
735
+ }
736
+ } catch (err) {
737
+ await this.restoreMining();
738
+ throw err;
739
+ }
740
+ }
741
+
742
+ private async restoreMining(): Promise<void> {
743
+ const saved = anvilMining.paused;
744
+ anvilMining.paused = undefined;
745
+ if (saved?.automine) {
592
746
  try {
593
- // restore automine if necessary
594
- if (wasAutoMining) {
595
- await this.setAutomine(true, { silent: true });
596
- }
747
+ await this.doSetAutomine(true);
597
748
  } catch (err) {
598
749
  this.logger.warn(`Failed to reenable automining: ${err}`);
599
750
  }
600
-
751
+ }
752
+ if (saved?.interval) {
601
753
  try {
602
- // restore automine if necessary
603
- if (blockInterval !== null) {
604
- await this.setIntervalMining(blockInterval, { silent: true });
605
- }
754
+ await this.scheduleResume(saved.interval, saved.nextTimestamp);
606
755
  } catch (err) {
607
- this.logger.warn(`Failed to reenable interval mining: ${err}`);
756
+ this.logger.warn(`Failed to schedule interval mining on the block grid, restarting it now: ${err}`);
757
+ try {
758
+ await this.doSetIntervalMining(saved.interval);
759
+ } catch (err) {
760
+ this.logger.warn(`Failed to reenable interval mining: ${err}`);
761
+ }
608
762
  }
609
763
  }
610
764
  }
611
765
 
612
- public async syncDateProvider() {
613
- const timestamp = await this.lastBlockTimestamp();
614
- if ('setTime' in this.dateProvider) {
615
- this.dateProvider.setTime(timestamp * 1000);
766
+ /**
767
+ * Re-enables interval mining so the next block lands on the grid the chain is already on: the first
768
+ * `latest + k * interval` tick the node clock has not yet passed. Anvil restarts its countdown from zero on
769
+ * `anvil_setIntervalMining`, so re-enabling it right after a pause would mine the next block late while still
770
+ * stamping it `latest + interval`, leaving L1 time behind the node clock for good. Instead mining stays off until
771
+ * that tick, when a background timer mines the tick's block and restarts the countdown from it.
772
+ *
773
+ * A tick the pause ran past by up to {@link OVERDUE_TICK_GRACE_MS} is mined straight away; one missed by more is
774
+ * skipped, so the lag stays bounded, at the cost of a gap in the chain like a missed L1 slot. Must be called holding
775
+ * the mining lock.
776
+ *
777
+ * @param nextTimestamp - A next-block timestamp a caller set during the pause. Unless a block has already used it, it
778
+ * is kept: interval mining restarts straight away instead, so the resume does not overwrite it.
779
+ */
780
+ private async scheduleResume(interval: number, nextTimestamp?: number): Promise<void> {
781
+ const generation = anvilMining.generation;
782
+ const latest = await this.lastBlockTimestamp();
783
+ if (anvilMining.generation !== generation) {
784
+ return;
785
+ }
786
+ if (nextTimestamp !== undefined && latest < nextTimestamp) {
787
+ await this.doSetIntervalMining(interval);
788
+ return;
789
+ }
790
+ const nowMs = this.dateProvider.now();
791
+ const ticks = Math.max(1, Math.ceil((nowMs - OVERDUE_TICK_GRACE_MS - latest * 1000) / (interval * 1000)));
792
+ const tick = latest + ticks * interval;
793
+ const delayMs = tick * 1000 - nowMs;
794
+ if (delayMs > interval * 1000) {
795
+ // The chain is ahead of the node clock (say, a warp the date provider could not follow), so no tick is due
796
+ // within an interval: restart the countdown now, as anvil would.
797
+ await this.doSetIntervalMining(interval);
798
+ return;
799
+ }
800
+ const resume: PendingResume = {
801
+ interval,
802
+ timer: setTimeout(() => void this.resumeOnGrid(resume, latest, tick), Math.max(0, delayMs)),
803
+ };
804
+ anvilMining.pendingResume = resume;
805
+ }
806
+
807
+ private async resumeOnGrid(resume: PendingResume, latest: number, tick: number) {
808
+ try {
809
+ await this.withMiningLock(async () => {
810
+ if (anvilMining.pendingResume !== resume) {
811
+ return;
812
+ }
813
+ anvilMining.pendingResume = undefined;
814
+ try {
815
+ const chainMoved = (await this.lastBlockTimestamp()) !== latest;
816
+ const firedMs = this.dateProvider.now();
817
+ if (chainMoved || firedMs - tick * 1000 > OVERDUE_TICK_GRACE_MS) {
818
+ // Blocks were mined or rolled back while mining was off, so the grid may have moved; or the timer ran
819
+ // late (a busy event loop, a wait on the lock), and mining the old tick now would put L1 behind again.
820
+ await this.scheduleResume(resume.interval);
821
+ return;
822
+ }
823
+ await this.doRpcCall('evm_setNextBlockTimestamp', [tick]);
824
+ await this.doEvmMine();
825
+ if (this.dateProvider.now() - tick * 1000 > ON_TIME_TOLERANCE_MS) {
826
+ // The tick was mined late, so a countdown restarted now would keep every later block that late too: wait
827
+ // for the next tick and restart it there instead.
828
+ await this.scheduleResume(resume.interval);
829
+ return;
830
+ }
831
+ } catch (err) {
832
+ this.logger.warn(`Failed to mine the L1 block due at ${tick}, restarting interval mining off the grid`, {
833
+ err: String(err),
834
+ tick,
835
+ });
836
+ }
837
+ await this.doSetIntervalMining(resume.interval);
838
+ });
839
+ } catch (err) {
840
+ this.logger.error(`Failed to resume L1 interval mining`, err, { interval: resume.interval, tick });
616
841
  }
617
842
  }
618
843
  }