@aztec/ethereum 0.0.1-commit.343b43af6 → 0.0.1-commit.35158ae7e

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.
@@ -1,4 +1,4 @@
1
- import type { RollupContract } from '@aztec/ethereum/contracts';
1
+ import type { ManaMinFeeComponents, RollupContract } from '@aztec/ethereum/contracts';
2
2
  import { InboxContract } from '@aztec/ethereum/contracts';
3
3
  import { CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
4
4
  import { EthAddress } from '@aztec/foundation/eth-address';
@@ -10,6 +10,20 @@ import { EventEmitter } from 'events';
10
10
 
11
11
  import type { ViemClient } from '../types.js';
12
12
 
13
+ /** L2 fee data reported by the chain monitor. */
14
+ export type L2FeeData = ManaMinFeeComponents & {
15
+ /** Total minimum fee per mana in Fee Juice (sum of sequencerCost + proverCost + congestionCost). */
16
+ minFeePerMana: bigint;
17
+ /** L1 base fee observed by the oracle. */
18
+ l1BaseFee: bigint;
19
+ /** L1 blob fee observed by the oracle. */
20
+ l1BlobFee: bigint;
21
+ /** ETH per fee asset exchange rate (1e12 precision). */
22
+ ethPerFeeAsset: bigint;
23
+ /** Mana target per checkpoint. */
24
+ manaTarget: bigint;
25
+ };
26
+
13
27
  export type ChainMonitorEventMap = {
14
28
  'l1-block': [{ l1BlockNumber: number; timestamp: bigint }];
15
29
  checkpoint: [
@@ -19,6 +33,7 @@ export type ChainMonitorEventMap = {
19
33
  'l2-messages': [{ totalL2Messages: number; l1BlockNumber: number }];
20
34
  'l2-epoch': [{ l2EpochNumber: EpochNumber; timestamp: bigint; committee: EthAddress[] | undefined }];
21
35
  'l2-slot': [{ l2SlotNumber: SlotNumber; timestamp: bigint }];
36
+ 'l2-fees': [L2FeeData];
22
37
  };
23
38
 
24
39
  /** Utility class that polls the chain on quick intervals and logs new L1 blocks, L2 blocks, and L2 proofs. */
@@ -45,6 +60,8 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
45
60
  public l2EpochNumber!: EpochNumber;
46
61
  /** Current L2 slot number */
47
62
  public l2SlotNumber!: SlotNumber;
63
+ /** Current L2 fee data (components of the minimum fee per mana). */
64
+ public l2FeeData!: L2FeeData;
48
65
 
49
66
  constructor(
50
67
  private readonly rollup: RollupContract,
@@ -77,7 +94,7 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
77
94
  }
78
95
  }
79
96
 
80
- private async getInbox() {
97
+ protected async getInbox() {
81
98
  if (!this.inbox) {
82
99
  const { inboxAddress } = await this.rollup.getRollupAddresses();
83
100
  this.inbox = new InboxContract(this.l1Client, inboxAddress);
@@ -158,7 +175,7 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
158
175
  this.l2EpochNumber = l2Epoch;
159
176
  committee = await this.rollup.getCurrentEpochCommittee();
160
177
  this.emit('l2-epoch', { l2EpochNumber: l2Epoch, timestamp, committee });
161
- msg += ` starting new epoch ${this.l2EpochNumber} `;
178
+ msg += ` starting new epoch ${this.l2EpochNumber}`;
162
179
  }
163
180
 
164
181
  if (l2SlotNumber !== this.l2SlotNumber) {
@@ -166,6 +183,13 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
166
183
  this.emit('l2-slot', { l2SlotNumber, timestamp });
167
184
  }
168
185
 
186
+ const feeData = await this.fetchFeeData(timestamp);
187
+ if (this.hasFeeDataChanged(feeData)) {
188
+ msg += ` with L2 min fee ${feeData.minFeePerMana}`;
189
+ this.l2FeeData = feeData;
190
+ this.emit('l2-fees', feeData);
191
+ }
192
+
169
193
  this.logger.info(msg, {
170
194
  currentTimestamp: this.dateProvider.nowInSeconds(),
171
195
  l1Timestamp: timestamp,
@@ -176,6 +200,7 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
176
200
  provenCheckpointNumber: this.provenCheckpointNumber,
177
201
  totalL2Messages: this.totalL2Messages,
178
202
  committee,
203
+ ...this.l2FeeData,
179
204
  });
180
205
 
181
206
  return this;
@@ -242,4 +267,36 @@ export class ChainMonitor extends EventEmitter<ChainMonitorEventMap> {
242
267
  this.on('checkpoint', listener);
243
268
  });
244
269
  }
270
+
271
+ private async fetchFeeData(timestamp: bigint): Promise<L2FeeData> {
272
+ const [components, minFeePerMana, l1Fees, ethPerFeeAsset, manaTarget] = await Promise.all([
273
+ this.rollup.getManaMinFeeComponentsAt(timestamp, true),
274
+ this.rollup.getManaMinFeeAt(timestamp, true),
275
+ this.rollup.getL1FeesAt(timestamp),
276
+ this.rollup.getEthPerFeeAsset(),
277
+ this.rollup.getManaTarget(),
278
+ ]);
279
+ return {
280
+ ...components,
281
+ minFeePerMana,
282
+ l1BaseFee: l1Fees.baseFee,
283
+ l1BlobFee: l1Fees.blobFee,
284
+ ethPerFeeAsset,
285
+ manaTarget,
286
+ };
287
+ }
288
+
289
+ private hasFeeDataChanged(newData: L2FeeData): boolean {
290
+ if (!this.l2FeeData) {
291
+ return true;
292
+ }
293
+ return (
294
+ this.l2FeeData.sequencerCost !== newData.sequencerCost ||
295
+ this.l2FeeData.proverCost !== newData.proverCost ||
296
+ this.l2FeeData.congestionCost !== newData.congestionCost ||
297
+ this.l2FeeData.l1BaseFee !== newData.l1BaseFee ||
298
+ this.l2FeeData.l1BlobFee !== newData.l1BlobFee ||
299
+ this.l2FeeData.ethPerFeeAsset !== newData.ethPerFeeAsset
300
+ );
301
+ }
245
302
  }
@@ -85,10 +85,12 @@ export class EthCheatCodes {
85
85
  }
86
86
 
87
87
  /**
88
- * Get the current timestamp
89
- * @returns The current timestamp
88
+ * Get the timestamp of the latest mined L1 block.
89
+ * Note: this is NOT the current time — it's the discrete timestamp of the last block.
90
+ * Between blocks, the actual chain time advances but no new block reflects it.
91
+ * @returns The latest block timestamp in seconds
90
92
  */
91
- public async timestamp(): Promise<number> {
93
+ public async lastBlockTimestamp(): Promise<number> {
92
94
  const res = await this.doRpcCall('eth_getBlockByNumber', ['latest', true]);
93
95
  return parseInt(res.timestamp, 16);
94
96
  }
@@ -552,7 +554,7 @@ export class EthCheatCodes {
552
554
  }
553
555
 
554
556
  public async syncDateProvider() {
555
- const timestamp = await this.timestamp();
557
+ const timestamp = await this.lastBlockTimestamp();
556
558
  if ('setTime' in this.dateProvider) {
557
559
  this.dateProvider.setTime(timestamp * 1000);
558
560
  }
@@ -1,5 +1,6 @@
1
1
  import { createLogger } from '@aztec/foundation/log';
2
2
  import { makeBackoff, retry } from '@aztec/foundation/retry';
3
+ import type { TestDateProvider } from '@aztec/foundation/timer';
3
4
  import { fileURLToPath } from '@aztec/foundation/url';
4
5
 
5
6
  import { type ChildProcess, spawn } from 'child_process';
@@ -33,6 +34,12 @@ export async function startAnvil(
33
34
  * L1-finality-based logic work without needing hundreds of mined blocks.
34
35
  */
35
36
  slotsInAnEpoch?: number;
37
+ /**
38
+ * If provided, the date provider will be synced to anvil's block time on every mined block.
39
+ * This keeps the dateProvider in lockstep with anvil's chain time, avoiding drift between
40
+ * the wall clock and the L1 chain when computing L1 slot timestamps.
41
+ */
42
+ dateProvider?: TestDateProvider;
36
43
  } = {},
37
44
  ): Promise<{ anvil: Anvil; methodCalls?: string[]; rpcUrl: string; stop: () => Promise<void> }> {
38
45
  const anvilBinary = resolve(dirname(fileURLToPath(import.meta.url)), '../../', 'scripts/anvil_kill_wrapper.sh');
@@ -108,12 +115,15 @@ export async function startAnvil(
108
115
  child.once('close', onClose);
109
116
  });
110
117
 
111
- // Continue piping for logging / method-call capture after startup.
112
- if (logger || opts.captureMethodCalls) {
118
+ // Continue piping for logging, method-call capture, and/or dateProvider sync after startup.
119
+ if (logger || opts.captureMethodCalls || opts.dateProvider) {
113
120
  child.stdout?.on('data', (data: Buffer) => {
114
121
  const text = data.toString();
115
122
  logger?.debug(text.trim());
116
123
  methodCalls?.push(...(text.match(/eth_[^\s]+/g) || []));
124
+ if (opts.dateProvider) {
125
+ syncDateProviderFromAnvilOutput(text, opts.dateProvider);
126
+ }
117
127
  });
118
128
  child.stderr?.on('data', (data: Buffer) => {
119
129
  logger?.debug(data.toString().trim());
@@ -160,6 +170,19 @@ export async function startAnvil(
160
170
  return { anvil: anvilObj, methodCalls, stop, rpcUrl: `http://127.0.0.1:${port}` };
161
171
  }
162
172
 
173
+ /** Extracts block time from anvil stdout and syncs the dateProvider. */
174
+ function syncDateProviderFromAnvilOutput(text: string, dateProvider: TestDateProvider): void {
175
+ // Anvil logs mined blocks as:
176
+ // Block Time: "Fri, 20 Mar 2026 02:10:46 +0000"
177
+ const match = text.match(/Block Time:\s*"([^"]+)"/);
178
+ if (match) {
179
+ const blockTimeMs = new Date(match[1]).getTime();
180
+ if (!isNaN(blockTimeMs)) {
181
+ dateProvider.setTime(blockTimeMs);
182
+ }
183
+ }
184
+ }
185
+
163
186
  /** Send SIGTERM, wait up to 5 s, then SIGKILL. All timers are always cleared. */
164
187
  function killChild(child: ChildProcess): Promise<void> {
165
188
  return new Promise<void>(resolve => {