@aztec/ethereum 0.0.1-commit.ffe5b04ea → 0.0.1-commit.fff30aa

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 (42) hide show
  1. package/dest/client.d.ts +10 -2
  2. package/dest/client.d.ts.map +1 -1
  3. package/dest/client.js +13 -7
  4. package/dest/config.js +1 -1
  5. package/dest/contracts/rollup.d.ts +17 -4
  6. package/dest/contracts/rollup.d.ts.map +1 -1
  7. package/dest/contracts/rollup.js +50 -10
  8. package/dest/generated/l1-contracts-defaults.d.ts +1 -1
  9. package/dest/generated/l1-contracts-defaults.js +1 -1
  10. package/dest/l1_reader.d.ts +3 -1
  11. package/dest/l1_reader.d.ts.map +1 -1
  12. package/dest/l1_reader.js +6 -1
  13. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts +1 -1
  14. package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
  15. package/dest/l1_tx_utils/readonly_l1_tx_utils.js +8 -4
  16. package/dest/test/chain_monitor.d.ts +22 -3
  17. package/dest/test/chain_monitor.d.ts.map +1 -1
  18. package/dest/test/chain_monitor.js +33 -2
  19. package/dest/test/eth_cheat_codes.d.ts +6 -4
  20. package/dest/test/eth_cheat_codes.d.ts.map +1 -1
  21. package/dest/test/eth_cheat_codes.js +6 -4
  22. package/dest/test/rollup_cheat_codes.d.ts +5 -1
  23. package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
  24. package/dest/test/rollup_cheat_codes.js +9 -1
  25. package/dest/test/start_anvil.d.ts +15 -1
  26. package/dest/test/start_anvil.d.ts.map +1 -1
  27. package/dest/test/start_anvil.js +17 -2
  28. package/dest/utils.d.ts +1 -1
  29. package/dest/utils.d.ts.map +1 -1
  30. package/dest/utils.js +12 -16
  31. package/package.json +7 -5
  32. package/src/client.ts +10 -2
  33. package/src/config.ts +1 -1
  34. package/src/contracts/rollup.ts +59 -18
  35. package/src/generated/l1-contracts-defaults.ts +1 -1
  36. package/src/l1_reader.ts +13 -1
  37. package/src/l1_tx_utils/readonly_l1_tx_utils.ts +8 -4
  38. package/src/test/chain_monitor.ts +60 -3
  39. package/src/test/eth_cheat_codes.ts +6 -4
  40. package/src/test/rollup_cheat_codes.ts +11 -1
  41. package/src/test/start_anvil.ts +33 -2
  42. package/src/utils.ts +14 -17
package/src/l1_reader.ts CHANGED
@@ -1,4 +1,9 @@
1
- import { type ConfigMappingsType, getConfigFromMappings, numberConfigHelper } from '@aztec/foundation/config';
1
+ import {
2
+ type ConfigMappingsType,
3
+ getConfigFromMappings,
4
+ numberConfigHelper,
5
+ optionalNumberConfigHelper,
6
+ } from '@aztec/foundation/config';
2
7
 
3
8
  import { type L1ContractAddresses, l1ContractAddressesMapping } from './l1_contract_addresses.js';
4
9
 
@@ -14,6 +19,8 @@ export interface L1ReaderConfig {
14
19
  l1Contracts: L1ContractAddresses;
15
20
  /** The polling interval viem uses in ms */
16
21
  viemPollingIntervalMS: number;
22
+ /** Timeout for HTTP requests to the L1 RPC node in ms. */
23
+ l1HttpTimeoutMS?: number;
17
24
  }
18
25
 
19
26
  export const l1ReaderConfigMappings: ConfigMappingsType<L1ReaderConfig> = {
@@ -43,6 +50,11 @@ export const l1ReaderConfigMappings: ConfigMappingsType<L1ReaderConfig> = {
43
50
  description: 'The polling interval viem uses in ms',
44
51
  ...numberConfigHelper(1_000),
45
52
  },
53
+ l1HttpTimeoutMS: {
54
+ env: 'ETHEREUM_HTTP_TIMEOUT_MS',
55
+ description: 'Timeout for HTTP requests to the L1 RPC node in ms.',
56
+ ...optionalNumberConfigHelper(),
57
+ },
46
58
  };
47
59
 
48
60
  export function getL1ReaderConfigFromEnv(): L1ReaderConfig {
@@ -130,9 +130,10 @@ export class ReadOnlyL1TxUtils {
130
130
  const numBlocks = Math.ceil(gasConfig.stallTimeMs! / BLOCK_TIME_MS);
131
131
  for (let i = 0; i < numBlocks; i++) {
132
132
  // each block can go up 12.5% from previous baseFee
133
- maxFeePerGas = (maxFeePerGas * (1_000n + 125n)) / 1_000n;
133
+ // ceil, (a+b-1)/b, to avoid truncation at small values (e.g. 1 wei blob base fee)
134
+ maxFeePerGas = (maxFeePerGas * (1_000n + 125n) + 999n) / 1_000n;
134
135
  // same for blob gas fee
135
- maxFeePerBlobGas = (maxFeePerBlobGas * (1_000n + 125n)) / 1_000n;
136
+ maxFeePerBlobGas = (maxFeePerBlobGas * (1_000n + 125n) + 999n) / 1_000n;
136
137
  }
137
138
 
138
139
  if (attempt > 0) {
@@ -242,13 +243,16 @@ export class ReadOnlyL1TxUtils {
242
243
  const gasConfig = { ...this.config, ..._gasConfig };
243
244
  let initialEstimate = 0n;
244
245
  if (_blobInputs) {
245
- // @note requests with blobs also require maxFeePerBlobGas to be set
246
+ // @note requests with blobs also require maxFeePerBlobGas to be set.
247
+ // Use 2x buffer for maxFeePerBlobGas to avoid stale fees and to pass EIP-4844 validation (even if it is a gas estimation call).
248
+ // 1. maxFeePerBlobGas >= blobBaseFee
249
+ // 2. account balance >= gas * maxFeePerGas + maxFeePerBlobGas * blobCount + value
246
250
  const gasPrice = await this.getGasPrice(gasConfig, true, 0);
247
251
  initialEstimate = await this.client.estimateGas({
248
252
  account,
249
253
  ...request,
250
254
  ..._blobInputs,
251
- maxFeePerBlobGas: gasPrice.maxFeePerBlobGas!,
255
+ maxFeePerBlobGas: gasPrice.maxFeePerBlobGas! * 2n,
252
256
  gas: MAX_L1_TX_LIMIT,
253
257
  blockTag: 'latest',
254
258
  });
@@ -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
  }
@@ -53,7 +53,17 @@ export class RollupCheatCodes {
53
53
  /** Returns the current slot */
54
54
  public async getSlot(): Promise<SlotNumber> {
55
55
  const ts = BigInt((await this.client.getBlock()).timestamp);
56
- return SlotNumber.fromBigInt(await this.rollup.read.getSlotAt([ts]));
56
+ return this.getSlotAt(ts);
57
+ }
58
+
59
+ /** Returns the slot number at a given timestamp. */
60
+ public async getSlotAt(timestamp: bigint): Promise<SlotNumber> {
61
+ return SlotNumber.fromBigInt(await this.rollup.read.getSlotAt([timestamp]));
62
+ }
63
+
64
+ /** Returns the timestamp for the start of a given slot. */
65
+ public async getTimestampForSlot(slot: SlotNumber): Promise<bigint> {
66
+ return await this.rollup.read.getTimestampForSlot([BigInt(slot)]);
57
67
  }
58
68
 
59
69
  /** Returns the number of seconds until the start of the given slot based on L1 block timestamp. */
@@ -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';
@@ -26,6 +27,19 @@ export async function startAnvil(
26
27
  chainId?: number;
27
28
  /** The hardfork to use (e.g. 'cancun', 'latest'). */
28
29
  hardfork?: string;
30
+ /**
31
+ * Number of slots per epoch used by anvil to compute the 'finalized' and 'safe' block tags.
32
+ * Anvil reports `finalized = latest - slotsInAnEpoch * 2`.
33
+ * Defaults to 1 so the finalized block advances immediately, making tests that check
34
+ * L1-finality-based logic work without needing hundreds of mined blocks.
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;
29
43
  } = {},
30
44
  ): Promise<{ anvil: Anvil; methodCalls?: string[]; rpcUrl: string; stop: () => Promise<void> }> {
31
45
  const anvilBinary = resolve(dirname(fileURLToPath(import.meta.url)), '../../', 'scripts/anvil_kill_wrapper.sh');
@@ -55,6 +69,7 @@ export async function startAnvil(
55
69
  if (opts.hardfork !== undefined) {
56
70
  args.push('--hardfork', opts.hardfork);
57
71
  }
72
+ args.push('--slots-in-an-epoch', String(opts.slotsInAnEpoch ?? 1));
58
73
 
59
74
  const child = spawn(anvilBinary, args, {
60
75
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -100,12 +115,15 @@ export async function startAnvil(
100
115
  child.once('close', onClose);
101
116
  });
102
117
 
103
- // Continue piping for logging / method-call capture after startup.
104
- 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) {
105
120
  child.stdout?.on('data', (data: Buffer) => {
106
121
  const text = data.toString();
107
122
  logger?.debug(text.trim());
108
123
  methodCalls?.push(...(text.match(/eth_[^\s]+/g) || []));
124
+ if (opts.dateProvider) {
125
+ syncDateProviderFromAnvilOutput(text, opts.dateProvider);
126
+ }
109
127
  });
110
128
  child.stderr?.on('data', (data: Buffer) => {
111
129
  logger?.debug(data.toString().trim());
@@ -152,6 +170,19 @@ export async function startAnvil(
152
170
  return { anvil: anvilObj, methodCalls, stop, rpcUrl: `http://127.0.0.1:${port}` };
153
171
  }
154
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
+
155
186
  /** Send SIGTERM, wait up to 5 s, then SIGKILL. All timers are always cleared. */
156
187
  function killChild(child: ChildProcess): Promise<void> {
157
188
  return new Promise<void>(resolve => {
package/src/utils.ts CHANGED
@@ -170,21 +170,6 @@ function getNestedErrorData(error: unknown): string | undefined {
170
170
  return undefined;
171
171
  }
172
172
 
173
- /**
174
- * Truncates an error message to a safe length for log renderers.
175
- * LogExplorer can only render up to 2500 characters in its summary view.
176
- * We cap at 2000 to leave room for decorating context added by callers.
177
- */
178
- function truncateErrorMessage(message: string): string {
179
- const MAX = 2000;
180
- const CHUNK = 950;
181
- if (message.length <= MAX) {
182
- return message;
183
- }
184
- const truncated = message.length - 2 * CHUNK;
185
- return message.slice(0, CHUNK) + `...${truncated} characters truncated...` + message.slice(-CHUNK);
186
- }
187
-
188
173
  /**
189
174
  * Formats a Viem error into a FormattedViemError instance.
190
175
  * @param error - The error to format.
@@ -247,10 +232,22 @@ export function formatViemError(error: any, abi: Abi = ErrorsAbi): FormattedViem
247
232
 
248
233
  // If it's a regular Error instance, return it with its message
249
234
  if (error instanceof Error) {
250
- return new FormattedViemError(truncateErrorMessage(error.message), (error as any)?.metaMessages);
235
+ return new FormattedViemError(error.message, (error as any)?.metaMessages);
236
+ }
237
+
238
+ const body = String(error);
239
+ const length = body.length;
240
+ // LogExplorer can only render up to 2500 characters in it's summary view. Try to keep the whole message below this number
241
+ // Limit the error to 2000 chacaters in order to allow code higher up to decorate this error with extra details (up to 500 characters)
242
+ if (length > 2000) {
243
+ const chunk = 950;
244
+ const truncated = length - 2 * chunk;
245
+ return new FormattedViemError(
246
+ body.slice(0, chunk) + `...${truncated} characters truncated...` + body.slice(-1 * chunk),
247
+ );
251
248
  }
252
249
 
253
- return new FormattedViemError(truncateErrorMessage(String(error)));
250
+ return new FormattedViemError(body);
254
251
  }
255
252
 
256
253
  function stripAbis(obj: any) {