@aztec/ethereum 0.0.1-commit.358457c → 0.0.1-commit.381b1a9

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,10 @@
1
- import { type Anvil } from '@viem/anvil';
1
+ /** Minimal interface matching the @viem/anvil Anvil shape used by callers. */
2
+ export interface Anvil {
3
+ readonly port: number;
4
+ readonly host: string;
5
+ readonly status: 'listening' | 'idle';
6
+ stop(): Promise<void>;
7
+ }
2
8
  /**
3
9
  * Ensures there's a running Anvil instance and returns the RPC URL.
4
10
  */
@@ -9,12 +15,19 @@ export declare function startAnvil(opts?: {
9
15
  captureMethodCalls?: boolean;
10
16
  accounts?: number;
11
17
  chainId?: number;
12
- /** The hardfork to use - note: @viem/anvil types are out of date but 'cancun' and 'latest' work */
18
+ /** The hardfork to use (e.g. 'cancun', 'latest'). */
13
19
  hardfork?: string;
20
+ /**
21
+ * Number of slots per epoch used by anvil to compute the 'finalized' and 'safe' block tags.
22
+ * Anvil reports `finalized = latest - slotsInAnEpoch * 2`.
23
+ * Defaults to 1 so the finalized block advances immediately, making tests that check
24
+ * L1-finality-based logic work without needing hundreds of mined blocks.
25
+ */
26
+ slotsInAnEpoch?: number;
14
27
  }): Promise<{
15
28
  anvil: Anvil;
16
29
  methodCalls?: string[];
17
30
  rpcUrl: string;
18
31
  stop: () => Promise<void>;
19
32
  }>;
20
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RhcnRfYW52aWwuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90ZXN0L3N0YXJ0X2FudmlsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUlBLE9BQU8sRUFBRSxLQUFLLEtBQUssRUFBZSxNQUFNLGFBQWEsQ0FBQztBQUd0RDs7R0FFRztBQUNILHdCQUFzQixVQUFVLENBQzlCLElBQUksR0FBRTtJQUNKLElBQUksQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUNkLFdBQVcsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUNyQixHQUFHLENBQUMsRUFBRSxPQUFPLENBQUM7SUFDZCxrQkFBa0IsQ0FBQyxFQUFFLE9BQU8sQ0FBQztJQUM3QixRQUFRLENBQUMsRUFBRSxNQUFNLENBQUM7SUFDbEIsT0FBTyxDQUFDLEVBQUUsTUFBTSxDQUFDO0lBQ2pCLG1HQUFtRztJQUNuRyxRQUFRLENBQUMsRUFBRSxNQUFNLENBQUM7Q0FDZCxHQUNMLE9BQU8sQ0FBQztJQUFFLEtBQUssRUFBRSxLQUFLLENBQUM7SUFBQyxXQUFXLENBQUMsRUFBRSxNQUFNLEVBQUUsQ0FBQztJQUFDLE1BQU0sRUFBRSxNQUFNLENBQUM7SUFBQyxJQUFJLEVBQUUsTUFBTSxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUE7Q0FBRSxDQUFDLENBaUQ5RiJ9
33
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3RhcnRfYW52aWwuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90ZXN0L3N0YXJ0X2FudmlsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQU9BLDhFQUE4RTtBQUM5RSxNQUFNLFdBQVcsS0FBSztJQUNwQixRQUFRLENBQUMsSUFBSSxFQUFFLE1BQU0sQ0FBQztJQUN0QixRQUFRLENBQUMsSUFBSSxFQUFFLE1BQU0sQ0FBQztJQUN0QixRQUFRLENBQUMsTUFBTSxFQUFFLFdBQVcsR0FBRyxNQUFNLENBQUM7SUFDdEMsSUFBSSxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FBQztDQUN2QjtBQUVEOztHQUVHO0FBQ0gsd0JBQXNCLFVBQVUsQ0FDOUIsSUFBSSxHQUFFO0lBQ0osSUFBSSxDQUFDLEVBQUUsTUFBTSxDQUFDO0lBQ2QsV0FBVyxDQUFDLEVBQUUsTUFBTSxDQUFDO0lBQ3JCLEdBQUcsQ0FBQyxFQUFFLE9BQU8sQ0FBQztJQUNkLGtCQUFrQixDQUFDLEVBQUUsT0FBTyxDQUFDO0lBQzdCLFFBQVEsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUNsQixPQUFPLENBQUMsRUFBRSxNQUFNLENBQUM7SUFDakIscURBQXFEO0lBQ3JELFFBQVEsQ0FBQyxFQUFFLE1BQU0sQ0FBQztJQUNsQjs7Ozs7T0FLRztJQUNILGNBQWMsQ0FBQyxFQUFFLE1BQU0sQ0FBQztDQUNwQixHQUNMLE9BQU8sQ0FBQztJQUFFLEtBQUssRUFBRSxLQUFLLENBQUM7SUFBQyxXQUFXLENBQUMsRUFBRSxNQUFNLEVBQUUsQ0FBQztJQUFDLE1BQU0sRUFBRSxNQUFNLENBQUM7SUFBQyxJQUFJLEVBQUUsTUFBTSxPQUFPLENBQUMsSUFBSSxDQUFDLENBQUE7Q0FBRSxDQUFDLENBNEg5RiJ9
@@ -1 +1 @@
1
- {"version":3,"file":"start_anvil.d.ts","sourceRoot":"","sources":["../../src/test/start_anvil.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,KAAK,EAAe,MAAM,aAAa,CAAC;AAGtD;;GAEG;AACH,wBAAsB,UAAU,CAC9B,IAAI,GAAE;IACJ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,mGAAmG;IACnG,QAAQ,CAAC,EAAE,MAAM,CAAC;CACd,GACL,OAAO,CAAC;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAiD9F"}
1
+ {"version":3,"file":"start_anvil.d.ts","sourceRoot":"","sources":["../../src/test/start_anvil.ts"],"names":[],"mappings":"AAOA,8EAA8E;AAC9E,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAAC;IACtC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,IAAI,GAAE;IACJ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACpB,GACL,OAAO,CAAC;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CA4H9F"}
@@ -1,7 +1,7 @@
1
1
  import { createLogger } from '@aztec/foundation/log';
2
2
  import { makeBackoff, retry } from '@aztec/foundation/retry';
3
3
  import { fileURLToPath } from '@aztec/foundation/url';
4
- import { createAnvil } from '@viem/anvil';
4
+ import { spawn } from 'child_process';
5
5
  import { dirname, resolve } from 'path';
6
6
  /**
7
7
  * Ensures there's a running Anvil instance and returns the RPC URL.
@@ -9,47 +9,147 @@ import { dirname, resolve } from 'path';
9
9
  const anvilBinary = resolve(dirname(fileURLToPath(import.meta.url)), '../../', 'scripts/anvil_kill_wrapper.sh');
10
10
  const logger = opts.log ? createLogger('ethereum:anvil') : undefined;
11
11
  const methodCalls = opts.captureMethodCalls ? [] : undefined;
12
- let port;
13
- // Start anvil.
14
- // We go via a wrapper script to ensure if the parent dies, anvil dies.
12
+ let detectedPort;
15
13
  const anvil = await retry(async ()=>{
16
- const anvil = createAnvil({
17
- anvilBinary,
18
- host: '127.0.0.1',
19
- port: opts.port ?? (process.env.ANVIL_PORT ? parseInt(process.env.ANVIL_PORT) : 8545),
20
- blockTime: opts.l1BlockTime,
21
- stopTimeout: 1000,
22
- accounts: opts.accounts ?? 20,
23
- gasLimit: 45_000_000n,
24
- chainId: opts.chainId ?? 31337
25
- });
26
- // Listen to the anvil output to get the port.
27
- const removeHandler = anvil.on('message', (message)=>{
28
- logger?.debug(message.trim());
29
- methodCalls?.push(...message.match(/eth_[^\s]+/g) || []);
30
- if (port === undefined && message.includes('Listening on')) {
31
- port = parseInt(message.match(/Listening on ([^:]+):(\d+)/)[2]);
14
+ const port = opts.port ?? (process.env.ANVIL_PORT ? parseInt(process.env.ANVIL_PORT) : 8545);
15
+ const args = [
16
+ '--host',
17
+ '127.0.0.1',
18
+ '--port',
19
+ String(port),
20
+ '--accounts',
21
+ String(opts.accounts ?? 20),
22
+ '--gas-limit',
23
+ String(45_000_000),
24
+ '--chain-id',
25
+ String(opts.chainId ?? 31337)
26
+ ];
27
+ if (opts.l1BlockTime !== undefined) {
28
+ args.push('--block-time', String(opts.l1BlockTime));
29
+ }
30
+ if (opts.hardfork !== undefined) {
31
+ args.push('--hardfork', opts.hardfork);
32
+ }
33
+ args.push('--slots-in-an-epoch', String(opts.slotsInAnEpoch ?? 1));
34
+ const child = spawn(anvilBinary, args, {
35
+ stdio: [
36
+ 'ignore',
37
+ 'pipe',
38
+ 'pipe'
39
+ ],
40
+ env: {
41
+ ...process.env,
42
+ RAYON_NUM_THREADS: '1'
32
43
  }
33
44
  });
34
- await anvil.start();
35
- if (!logger && !opts.captureMethodCalls) {
36
- removeHandler();
45
+ // Wait for "Listening on" or an early exit.
46
+ await new Promise((resolve, reject)=>{
47
+ let stderr = '';
48
+ const onStdout = (data)=>{
49
+ const text = data.toString();
50
+ logger?.debug(text.trim());
51
+ methodCalls?.push(...text.match(/eth_[^\s]+/g) || []);
52
+ if (detectedPort === undefined && text.includes('Listening on')) {
53
+ const match = text.match(/Listening on ([^:]+):(\d+)/);
54
+ if (match) {
55
+ detectedPort = parseInt(match[2]);
56
+ }
57
+ }
58
+ if (detectedPort !== undefined) {
59
+ child.stdout?.removeListener('data', onStdout);
60
+ child.stderr?.removeListener('data', onStderr);
61
+ child.removeListener('close', onClose);
62
+ resolve();
63
+ }
64
+ };
65
+ const onStderr = (data)=>{
66
+ stderr += data.toString();
67
+ logger?.debug(data.toString().trim());
68
+ };
69
+ const onClose = (code)=>{
70
+ child.stdout?.removeListener('data', onStdout);
71
+ child.stderr?.removeListener('data', onStderr);
72
+ reject(new Error(`Anvil exited with code ${code} before listening. stderr: ${stderr}`));
73
+ };
74
+ child.stdout?.on('data', onStdout);
75
+ child.stderr?.on('data', onStderr);
76
+ child.once('close', onClose);
77
+ });
78
+ // Continue piping for logging / method-call capture after startup.
79
+ if (logger || opts.captureMethodCalls) {
80
+ child.stdout?.on('data', (data)=>{
81
+ const text = data.toString();
82
+ logger?.debug(text.trim());
83
+ methodCalls?.push(...text.match(/eth_[^\s]+/g) || []);
84
+ });
85
+ child.stderr?.on('data', (data)=>{
86
+ logger?.debug(data.toString().trim());
87
+ });
88
+ } else {
89
+ // Consume streams so the child process doesn't block on full pipe buffers.
90
+ child.stdout?.resume();
91
+ child.stderr?.resume();
37
92
  }
38
- return anvil;
93
+ return child;
39
94
  }, 'Start anvil', makeBackoff([
40
95
  5,
41
96
  5,
42
97
  5
43
98
  ]));
44
- if (!port) {
99
+ if (!detectedPort) {
45
100
  throw new Error('Failed to start anvil');
46
101
  }
47
- // Monkeypatch the anvil instance to include the actually assigned port
48
- // Object.defineProperty(anvil, 'port', { value: port, writable: false });
102
+ const port = detectedPort;
103
+ let status = 'listening';
104
+ anvil.once('close', ()=>{
105
+ status = 'idle';
106
+ });
107
+ const stop = async ()=>{
108
+ if (status === 'idle') {
109
+ return;
110
+ }
111
+ await killChild(anvil);
112
+ };
113
+ const anvilObj = {
114
+ port,
115
+ host: '127.0.0.1',
116
+ get status () {
117
+ return status;
118
+ },
119
+ stop
120
+ };
49
121
  return {
50
- anvil,
122
+ anvil: anvilObj,
51
123
  methodCalls,
52
- stop: ()=>anvil.stop(),
124
+ stop,
53
125
  rpcUrl: `http://127.0.0.1:${port}`
54
126
  };
55
127
  }
128
+ /** Send SIGTERM, wait up to 5 s, then SIGKILL. All timers are always cleared. */ function killChild(child) {
129
+ return new Promise((resolve)=>{
130
+ if (child.exitCode !== null || child.killed) {
131
+ child.stdout?.destroy();
132
+ child.stderr?.destroy();
133
+ resolve();
134
+ return;
135
+ }
136
+ let killTimer;
137
+ const onClose = ()=>{
138
+ if (killTimer !== undefined) {
139
+ clearTimeout(killTimer);
140
+ }
141
+ // Destroy stdio streams so their PipeWrap handles don't keep the event loop alive.
142
+ child.stdout?.destroy();
143
+ child.stderr?.destroy();
144
+ resolve();
145
+ };
146
+ child.once('close', onClose);
147
+ child.kill('SIGTERM');
148
+ killTimer = setTimeout(()=>{
149
+ killTimer = undefined;
150
+ child.kill('SIGKILL');
151
+ }, 5000);
152
+ // Ensure the timer does not prevent Node from exiting.
153
+ killTimer.unref();
154
+ });
155
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/ethereum",
3
- "version": "0.0.1-commit.358457c",
3
+ "version": "0.0.1-commit.381b1a9",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./account": "./dest/account.js",
@@ -50,10 +50,10 @@
50
50
  "../package.common.json"
51
51
  ],
52
52
  "dependencies": {
53
- "@aztec/blob-lib": "0.0.1-commit.358457c",
54
- "@aztec/constants": "0.0.1-commit.358457c",
55
- "@aztec/foundation": "0.0.1-commit.358457c",
56
- "@aztec/l1-artifacts": "0.0.1-commit.358457c",
53
+ "@aztec/blob-lib": "0.0.1-commit.381b1a9",
54
+ "@aztec/constants": "0.0.1-commit.381b1a9",
55
+ "@aztec/foundation": "0.0.1-commit.381b1a9",
56
+ "@aztec/l1-artifacts": "0.0.1-commit.381b1a9",
57
57
  "@viem/anvil": "^0.0.10",
58
58
  "dotenv": "^16.0.3",
59
59
  "lodash.chunk": "^4.2.0",
package/src/config.ts CHANGED
@@ -19,6 +19,8 @@ export type GenesisStateConfig = {
19
19
  testAccounts: boolean;
20
20
  /** Whether to populate the genesis state with initial fee juice for the sponsored FPC */
21
21
  sponsoredFPC: boolean;
22
+ /** Additional addresses to prefund with fee juice at genesis */
23
+ prefundAddresses: string[];
22
24
  };
23
25
 
24
26
  export type L1ContractsConfig = {
@@ -259,6 +261,16 @@ export const genesisStateConfigMappings: ConfigMappingsType<GenesisStateConfig>
259
261
  description: 'Whether to populate the genesis state with initial fee juice for the sponsored FPC.',
260
262
  ...booleanConfigHelper(false),
261
263
  },
264
+ prefundAddresses: {
265
+ env: 'PREFUND_ADDRESSES',
266
+ description: 'Comma-separated list of Aztec addresses to prefund with fee juice at genesis.',
267
+ parseEnv: (val: string) =>
268
+ val
269
+ .split(',')
270
+ .map(a => a.trim())
271
+ .filter(a => a.length > 0),
272
+ defaultValue: [],
273
+ },
262
274
  };
263
275
 
264
276
  export function getL1ContractsConfigEnvVars(): L1ContractsConfig {
@@ -3,7 +3,7 @@ import { createLogger } from '@aztec/foundation/log';
3
3
  import { RegistryAbi } from '@aztec/l1-artifacts/RegistryAbi';
4
4
  import { TestERC20Abi } from '@aztec/l1-artifacts/TestERC20Abi';
5
5
 
6
- import { type GetContractReturnType, type Hex, getContract } from 'viem';
6
+ import { type GetContractReturnType, type Hex, getAbiItem, getContract } from 'viem';
7
7
 
8
8
  import type { L1ContractAddresses } from '../l1_contract_addresses.js';
9
9
  import type { ViemClient } from '../types.js';
@@ -128,4 +128,34 @@ export class RegistryContract {
128
128
  public async getRewardDistributor(): Promise<EthAddress> {
129
129
  return EthAddress.fromString(await this.registry.read.getRewardDistributor());
130
130
  }
131
+
132
+ /** Returns the L1 timestamp at which the given rollup was registered via addRollup(). */
133
+ public async getCanonicalRollupRegistrationTimestamp(
134
+ rollupAddress: EthAddress,
135
+ fromBlock?: bigint,
136
+ ): Promise<bigint | undefined> {
137
+ const event = getAbiItem({ abi: RegistryAbi, name: 'CanonicalRollupUpdated' });
138
+ const start = fromBlock ?? 0n;
139
+ const latestBlock = await this.client.getBlockNumber();
140
+ const chunkSize = 1_000n;
141
+
142
+ for (let from = start; from <= latestBlock; from += chunkSize) {
143
+ const to = from + chunkSize - 1n > latestBlock ? latestBlock : from + chunkSize - 1n;
144
+ const logs = await this.client.getLogs({
145
+ address: this.address.toString(),
146
+ fromBlock: from,
147
+ toBlock: to,
148
+ strict: true,
149
+ event,
150
+ args: { instance: rollupAddress.toString() },
151
+ });
152
+
153
+ if (logs.length > 0) {
154
+ const block = await this.client.getBlock({ blockNumber: logs[0].blockNumber });
155
+ return block.timestamp;
156
+ }
157
+ }
158
+
159
+ return undefined;
160
+ }
131
161
  }
@@ -379,6 +379,20 @@ export class RollupContract {
379
379
  return Fr.fromString(await this.rollup.read.archiveAt([0n]));
380
380
  }
381
381
 
382
+ @memoize
383
+ async getVkTreeRoot(): Promise<Fr> {
384
+ const slot = BigInt(RollupContract.stfStorageSlot) + 3n;
385
+ const value = await this.client.getStorageAt({ address: this.address, slot: `0x${slot.toString(16)}` });
386
+ return Fr.fromString(value ?? '0x0');
387
+ }
388
+
389
+ @memoize
390
+ async getProtocolContractsHash(): Promise<Fr> {
391
+ const slot = BigInt(RollupContract.stfStorageSlot) + 4n;
392
+ const value = await this.client.getStorageAt({ address: this.address, slot: `0x${slot.toString(16)}` });
393
+ return Fr.fromString(value ?? '0x0');
394
+ }
395
+
382
396
  /**
383
397
  * Returns rollup constants used for epoch queries.
384
398
  * Return type is `L1RollupConstants` which is defined in stdlib,
@@ -392,16 +406,25 @@ export class RollupContract {
392
406
  epochDuration: number;
393
407
  proofSubmissionEpochs: number;
394
408
  targetCommitteeSize: number;
409
+ rollupManaLimit: number;
395
410
  }> {
396
- const [l1StartBlock, l1GenesisTime, slotDuration, epochDuration, proofSubmissionEpochs, targetCommitteeSize] =
397
- await Promise.all([
398
- this.getL1StartBlock(),
399
- this.getL1GenesisTime(),
400
- this.getSlotDuration(),
401
- this.getEpochDuration(),
402
- this.getProofSubmissionEpochs(),
403
- this.getTargetCommitteeSize(),
404
- ]);
411
+ const [
412
+ l1StartBlock,
413
+ l1GenesisTime,
414
+ slotDuration,
415
+ epochDuration,
416
+ proofSubmissionEpochs,
417
+ targetCommitteeSize,
418
+ rollupManaLimit,
419
+ ] = await Promise.all([
420
+ this.getL1StartBlock(),
421
+ this.getL1GenesisTime(),
422
+ this.getSlotDuration(),
423
+ this.getEpochDuration(),
424
+ this.getProofSubmissionEpochs(),
425
+ this.getTargetCommitteeSize(),
426
+ this.getManaLimit(),
427
+ ]);
405
428
  return {
406
429
  l1StartBlock,
407
430
  l1GenesisTime,
@@ -409,6 +432,7 @@ export class RollupContract {
409
432
  epochDuration: Number(epochDuration),
410
433
  proofSubmissionEpochs: Number(proofSubmissionEpochs),
411
434
  targetCommitteeSize,
435
+ rollupManaLimit: Number(rollupManaLimit),
412
436
  };
413
437
  }
414
438
 
@@ -503,8 +527,9 @@ export class RollupContract {
503
527
  return CheckpointNumber.fromBigInt(await this.rollup.read.getPendingCheckpointNumber());
504
528
  }
505
529
 
506
- async getProvenCheckpointNumber(): Promise<CheckpointNumber> {
507
- return CheckpointNumber.fromBigInt(await this.rollup.read.getProvenCheckpointNumber());
530
+ async getProvenCheckpointNumber(options?: { blockNumber?: bigint }): Promise<CheckpointNumber> {
531
+ await checkBlockTag(options?.blockNumber, this.client);
532
+ return CheckpointNumber.fromBigInt(await this.rollup.read.getProvenCheckpointNumber(options));
508
533
  }
509
534
 
510
535
  async getSlotNumber(): Promise<SlotNumber> {
@@ -4,11 +4,11 @@
4
4
  /** Default L1 contracts configuration values from network-defaults.yml */
5
5
  export const l1ContractsDefaultEnv = {
6
6
  ETHEREUM_SLOT_DURATION: 12,
7
- AZTEC_SLOT_DURATION: 36,
7
+ AZTEC_SLOT_DURATION: 72,
8
8
  AZTEC_EPOCH_DURATION: 32,
9
9
  AZTEC_TARGET_COMMITTEE_SIZE: 48,
10
10
  AZTEC_LAG_IN_EPOCHS_FOR_VALIDATOR_SET: 2,
11
- AZTEC_LAG_IN_EPOCHS_FOR_RANDAO: 2,
11
+ AZTEC_LAG_IN_EPOCHS_FOR_RANDAO: 1,
12
12
  AZTEC_ACTIVATION_THRESHOLD: 100000000000000000000,
13
13
  AZTEC_EJECTION_THRESHOLD: 50000000000000000000,
14
14
  AZTEC_LOCAL_EJECTION_THRESHOLD: 98000000000000000000,
@@ -14,16 +14,13 @@ import {
14
14
  type Abi,
15
15
  type BlockOverrides,
16
16
  type Hex,
17
- type NonceManager,
18
17
  type PrepareTransactionRequestRequest,
19
18
  type StateOverride,
20
19
  type TransactionReceipt,
21
20
  type TransactionSerializable,
22
- createNonceManager,
23
21
  formatGwei,
24
22
  serializeTransaction,
25
23
  } from 'viem';
26
- import { jsonRpc } from 'viem/nonce';
27
24
 
28
25
  import type { ViemClient } from '../types.js';
29
26
  import { formatViemError } from '../utils.js';
@@ -47,8 +44,9 @@ import {
47
44
  const MAX_L1_TX_STATES = 32;
48
45
 
49
46
  export class L1TxUtils extends ReadOnlyL1TxUtils {
50
- protected nonceManager: NonceManager;
51
47
  protected txs: L1TxState[] = [];
48
+ /** Last nonce successfully sent to the chain. Used as a lower bound when a fallback RPC node returns a stale count. */
49
+ private lastSentNonce: number | undefined;
52
50
  /** Tx delayer for testing. Only set when enableDelayer config is true. */
53
51
  public delayer?: Delayer;
54
52
  /** KZG instance for blob operations. */
@@ -68,7 +66,6 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
68
66
  delayer?: Delayer,
69
67
  ) {
70
68
  super(client, logger, dateProvider, config, debugMaxGasLimit);
71
- this.nonceManager = createNonceManager({ source: jsonRpc() });
72
69
  this.kzg = kzg;
73
70
 
74
71
  // Set up delayer: use provided one or create new
@@ -110,6 +107,11 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
110
107
  this.metrics?.recordMinedTx(l1TxState, new Date(l1Timestamp));
111
108
  } else if (newState === TxUtilsState.NOT_MINED) {
112
109
  this.metrics?.recordDroppedTx(l1TxState);
110
+ // The tx was dropped: the chain nonce reverted to l1TxState.nonce, so our lower bound is
111
+ // no longer valid. Clear it so the next send fetches the real nonce from the chain.
112
+ if (this.lastSentNonce === l1TxState.nonce) {
113
+ this.lastSentNonce = undefined;
114
+ }
113
115
  }
114
116
 
115
117
  // Update state in the store
@@ -244,9 +246,6 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
244
246
  throw new InterruptError(`Transaction sending is interrupted`);
245
247
  }
246
248
 
247
- // Check timeout before consuming nonce to avoid leaking a nonce that was never sent.
248
- // A leaked nonce creates a gap (e.g. nonce 107 consumed but unsent), so all subsequent
249
- // transactions (108, 109, ...) can never be mined since the chain expects 107 first.
250
249
  const now = new Date(await this.getL1Timestamp());
251
250
  if (gasConfig.txTimeoutAt && now > gasConfig.txTimeoutAt) {
252
251
  throw new TimeoutError(
@@ -254,11 +253,11 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
254
253
  );
255
254
  }
256
255
 
257
- const nonce = await this.nonceManager.consume({
258
- client: this.client,
259
- address: account,
260
- chainId: this.client.chain.id,
261
- });
256
+ const chainNonce = await this.client.getTransactionCount({ address: account, blockTag: 'pending' });
257
+ // If a fallback RPC node returns a stale count (lower than what we last sent), use our
258
+ // local lower bound to avoid sending a duplicate of an already-pending transaction.
259
+ const nonce =
260
+ this.lastSentNonce !== undefined && chainNonce <= this.lastSentNonce ? this.lastSentNonce + 1 : chainNonce;
262
261
 
263
262
  const baseState = { request, gasLimit, blobInputs, gasPrice, nonce };
264
263
  const txData = this.makeTxData(baseState, { isCancelTx: false });
@@ -266,6 +265,8 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
266
265
  // Send the new tx
267
266
  const signedRequest = await this.prepareSignedTransaction(txData);
268
267
  const txHash = await this.client.sendRawTransaction({ serializedTransaction: signedRequest });
268
+ // Update after tx is sent successfully
269
+ this.lastSentNonce = nonce;
269
270
 
270
271
  // Create the new state for monitoring
271
272
  const l1TxState: L1TxState = {
@@ -449,7 +450,6 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
449
450
  { nonce, account, pendingNonce, timePassed },
450
451
  );
451
452
  await this.updateState(state, TxUtilsState.NOT_MINED);
452
- this.nonceManager.reset({ address: account, chainId: this.client.chain.id });
453
453
  throw new DroppedTransactionError(nonce, account);
454
454
  }
455
455
 
@@ -541,12 +541,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
541
541
 
542
542
  // Oh no, the transaction has timed out!
543
543
  if (isCancelTx || !gasConfig.cancelTxOnTimeout) {
544
- // If this was already a cancellation tx, or we are configured to not cancel txs, we just mark it as NOT_MINED
545
- // and reset the nonce manager, so the next tx that comes along can reuse the nonce if/when this tx gets dropped.
546
- // This is the nastiest scenario for us, since the new tx could acquire the next nonce, but then this tx is dropped,
547
- // and the new tx would never get mined. Eventually, the new tx would also drop.
548
544
  await this.updateState(state, TxUtilsState.NOT_MINED);
549
- this.nonceManager.reset({ address: account, chainId: this.client.chain.id });
550
545
  } else {
551
546
  // Otherwise we fire the cancellation without awaiting to avoid blocking the caller,
552
547
  // and monitor it in the background so we can speed it up as needed.
@@ -685,7 +680,6 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
685
680
  { nonce, account },
686
681
  );
687
682
  await this.updateState(state, TxUtilsState.NOT_MINED);
688
- this.nonceManager.reset({ address: account, chainId: this.client.chain.id });
689
683
  return;
690
684
  }
691
685
 
@@ -697,7 +691,6 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
697
691
  { nonce, account, currentNonce },
698
692
  );
699
693
  await this.updateState(state, TxUtilsState.NOT_MINED);
700
- this.nonceManager.reset({ address: account, chainId: this.client.chain.id });
701
694
  return;
702
695
  }
703
696
 
@@ -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
  });