@aztec/aztec-node 0.0.1-commit.9b94fc1 → 0.0.1-commit.9ee6fcc6

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,43 +1,38 @@
1
1
  import { Archiver, createArchiver } from '@aztec/archiver';
2
- import { BBCircuitVerifier, QueuedIVCVerifier, TestCircuitVerifier } from '@aztec/bb-prover';
3
- import { type BlobSinkClientInterface, createBlobSinkClient } from '@aztec/blob-sink/client';
4
- import {
5
- ARCHIVE_HEIGHT,
6
- INITIAL_L2_BLOCK_NUM,
7
- type L1_TO_L2_MSG_TREE_HEIGHT,
8
- type NOTE_HASH_TREE_HEIGHT,
9
- type NULLIFIER_TREE_HEIGHT,
10
- type PUBLIC_DATA_TREE_HEIGHT,
11
- } from '@aztec/constants';
2
+ import { BBCircuitVerifier, BatchChonkVerifier, QueuedIVCVerifier } from '@aztec/bb-prover';
3
+ import { TestCircuitVerifier } from '@aztec/bb-prover/test';
4
+ import { type BlobClientInterface, createBlobClientWithFileStores } from '@aztec/blob-client/client';
5
+ import { Blob } from '@aztec/blob-lib';
6
+ import { ARCHIVE_HEIGHT, type L1_TO_L2_MSG_TREE_HEIGHT, type NOTE_HASH_TREE_HEIGHT } from '@aztec/constants';
12
7
  import { EpochCache, type EpochCacheInterface } from '@aztec/epoch-cache';
13
- import {
14
- type L1ContractAddresses,
15
- RegistryContract,
16
- RollupContract,
17
- createEthereumChain,
18
- getPublicClient,
19
- } from '@aztec/ethereum';
20
- import { SlotNumber } from '@aztec/foundation/branded-types';
21
- import { compactArray, pick } from '@aztec/foundation/collection';
8
+ import { createEthereumChain } from '@aztec/ethereum/chain';
9
+ import { getPublicClient, makeL1HttpTransport } from '@aztec/ethereum/client';
10
+ import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts';
11
+ import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses';
12
+ import type { L1TxUtils } from '@aztec/ethereum/l1-tx-utils';
13
+ import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
14
+ import { chunkBy, compactArray, pick, unique } from '@aztec/foundation/collection';
15
+ import { Fr } from '@aztec/foundation/curves/bn254';
22
16
  import { EthAddress } from '@aztec/foundation/eth-address';
23
- import { Fr } from '@aztec/foundation/fields';
24
17
  import { BadRequestError } from '@aztec/foundation/json-rpc';
25
18
  import { type Logger, createLogger } from '@aztec/foundation/log';
26
19
  import { count } from '@aztec/foundation/string';
27
20
  import { DateProvider, Timer } from '@aztec/foundation/timer';
28
21
  import { MembershipWitness, SiblingPath } from '@aztec/foundation/trees';
29
- import { KeystoreManager, loadKeystores, mergeKeystores } from '@aztec/node-keystore';
22
+ import { type KeyStore, KeystoreManager, loadKeystores, mergeKeystores } from '@aztec/node-keystore';
30
23
  import { trySnapshotSync, uploadSnapshot } from '@aztec/node-lib/actions';
31
- import { createL1TxUtilsWithBlobsFromEthSigner } from '@aztec/node-lib/factories';
32
- import { type P2P, type P2PClientDeps, createP2PClient, getDefaultAllowedSetupFunctions } from '@aztec/p2p';
33
- import { ProtocolContractAddress } from '@aztec/protocol-contracts';
24
+ import { createForwarderL1TxUtilsFromSigners, createL1TxUtilsFromSigners } from '@aztec/node-lib/factories';
34
25
  import {
35
- BlockBuilder,
36
- GlobalVariableBuilder,
37
- SequencerClient,
38
- type SequencerPublisher,
39
- createValidatorForAcceptingTxs,
40
- } from '@aztec/sequencer-client';
26
+ type P2P,
27
+ type P2PClientDeps,
28
+ createP2PClient,
29
+ createTxValidatorForAcceptingTxsOverRPC,
30
+ getDefaultAllowedSetupFunctions,
31
+ } from '@aztec/p2p';
32
+ import { ProtocolContractAddress } from '@aztec/protocol-contracts';
33
+ import { type ProverNode, type ProverNodeDeps, createProverNode } from '@aztec/prover-node';
34
+ import { createKeyStoreForProver } from '@aztec/prover-node/config';
35
+ import { GlobalVariableBuilder, SequencerClient, type SequencerPublisher } from '@aztec/sequencer-client';
41
36
  import { PublicProcessorFactory } from '@aztec/simulator/server';
42
37
  import {
43
38
  AttestationsBlockWatcher,
@@ -46,16 +41,17 @@ import {
46
41
  type Watcher,
47
42
  createSlasher,
48
43
  } from '@aztec/slasher';
49
- import { PublicSimulatorConfig } from '@aztec/stdlib/avm';
44
+ import { CollectionLimitsConfig, PublicSimulatorConfig } from '@aztec/stdlib/avm';
50
45
  import { AztecAddress } from '@aztec/stdlib/aztec-address';
51
46
  import {
52
- type InBlock,
53
- type L2Block,
54
- L2BlockHash,
55
- type L2BlockNumber,
47
+ type BlockData,
48
+ BlockHash,
49
+ type BlockParameter,
50
+ type DataInBlock,
51
+ L2Block,
56
52
  type L2BlockSource,
57
- type PublishedL2Block,
58
53
  } from '@aztec/stdlib/block';
54
+ import type { PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
59
55
  import type {
60
56
  ContractClassPublic,
61
57
  ContractDataSource,
@@ -63,7 +59,7 @@ import type {
63
59
  NodeInfo,
64
60
  ProtocolContractAddresses,
65
61
  } from '@aztec/stdlib/contract';
66
- import type { GasFees } from '@aztec/stdlib/gas';
62
+ import { GasFees } from '@aztec/stdlib/gas';
67
63
  import { computePublicDataTreeLeafSlot } from '@aztec/stdlib/hash';
68
64
  import {
69
65
  type AztecNode,
@@ -82,9 +78,9 @@ import {
82
78
  type WorldStateSynchronizer,
83
79
  tryStop,
84
80
  } from '@aztec/stdlib/interfaces/server';
85
- import type { LogFilter, PrivateLog, TxScopedL2Log } from '@aztec/stdlib/logs';
81
+ import type { DebugLogStore, LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs';
82
+ import { InMemoryDebugLogStore, NullDebugLogStore } from '@aztec/stdlib/logs';
86
83
  import { InboxLeaf, type L1ToL2MessageSource } from '@aztec/stdlib/messaging';
87
- import { P2PClientType } from '@aztec/stdlib/p2p';
88
84
  import type { Offense, SlashPayloadRound } from '@aztec/stdlib/slashing';
89
85
  import type { NullifierLeafPreimage, PublicDataTreeLeaf, PublicDataTreeLeafPreimage } from '@aztec/stdlib/trees';
90
86
  import { MerkleTreeId, NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees';
@@ -110,14 +106,17 @@ import {
110
106
  trackSpan,
111
107
  } from '@aztec/telemetry-client';
112
108
  import {
109
+ FullNodeCheckpointsBuilder as CheckpointsBuilder,
110
+ FullNodeCheckpointsBuilder,
113
111
  NodeKeystoreAdapter,
114
112
  ValidatorClient,
115
113
  createBlockProposalHandler,
116
114
  createValidatorClient,
117
115
  } from '@aztec/validator-client';
116
+ import type { SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
118
117
  import { createWorldStateSynchronizer } from '@aztec/world-state';
119
118
 
120
- import { createPublicClient, fallback, http } from 'viem';
119
+ import { createPublicClient } from 'viem';
121
120
 
122
121
  import { createSentinel } from '../sentinel/factory.js';
123
122
  import { Sentinel } from '../sentinel/sentinel.js';
@@ -129,6 +128,7 @@ import { NodeMetrics } from './node_metrics.js';
129
128
  */
130
129
  export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
131
130
  private metrics: NodeMetrics;
131
+ private initialHeaderHashPromise: Promise<BlockHash> | undefined = undefined;
132
132
 
133
133
  // Prevent two snapshot operations to happen simultaneously
134
134
  private isUploadingSnapshot = false;
@@ -144,6 +144,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
144
144
  protected readonly l1ToL2MessageSource: L1ToL2MessageSource,
145
145
  protected readonly worldStateSynchronizer: WorldStateSynchronizer,
146
146
  protected readonly sequencer: SequencerClient | undefined,
147
+ protected readonly proverNode: ProverNode | undefined,
147
148
  protected readonly slasherClient: SlasherClientInterface | undefined,
148
149
  protected readonly validatorsSentinel: Sentinel | undefined,
149
150
  protected readonly epochPruneWatcher: EpochPruneWatcher | undefined,
@@ -152,15 +153,32 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
152
153
  protected readonly globalVariableBuilder: GlobalVariableBuilderInterface,
153
154
  protected readonly epochCache: EpochCacheInterface,
154
155
  protected readonly packageVersion: string,
155
- private proofVerifier: ClientProtocolCircuitVerifier,
156
+ private peerProofVerifier: ClientProtocolCircuitVerifier,
157
+ private rpcProofVerifier: ClientProtocolCircuitVerifier,
156
158
  private telemetry: TelemetryClient = getTelemetryClient(),
157
159
  private log = createLogger('node'),
160
+ private blobClient?: BlobClientInterface,
161
+ private validatorClient?: ValidatorClient,
162
+ private keyStoreManager?: KeystoreManager,
163
+ private debugLogStore: DebugLogStore = new NullDebugLogStore(),
158
164
  ) {
159
165
  this.metrics = new NodeMetrics(telemetry, 'AztecNodeService');
160
166
  this.tracer = telemetry.getTracer('AztecNodeService');
161
167
 
162
168
  this.log.info(`Aztec Node version: ${this.packageVersion}`);
163
169
  this.log.info(`Aztec Node started on chain 0x${l1ChainId.toString(16)}`, config.l1Contracts);
170
+
171
+ // A defensive check that protects us against introducing a bug in the complex `createAndSync` function. We must
172
+ // never have debugLogStore enabled when not in test mode because then we would be accumulating debug logs in
173
+ // memory which could be a DoS vector on the sequencer (since no fees are paid for debug logs).
174
+ if (debugLogStore.isEnabled && config.realProofs) {
175
+ throw new Error('debugLogStore should never be enabled when realProofs are set');
176
+ }
177
+ }
178
+
179
+ /** @internal Exposed for testing — returns the RPC proof verifier. */
180
+ public getProofVerifier(): ClientProtocolCircuitVerifier {
181
+ return this.rpcProofVerifier;
164
182
  }
165
183
 
166
184
  public async getWorldStateSyncStatus(): Promise<WorldStateSyncStatus> {
@@ -184,12 +202,14 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
184
202
  logger?: Logger;
185
203
  publisher?: SequencerPublisher;
186
204
  dateProvider?: DateProvider;
187
- blobSinkClient?: BlobSinkClientInterface;
188
- p2pClientDeps?: P2PClientDeps<P2PClientType.Full>;
205
+ p2pClientDeps?: P2PClientDeps;
206
+ proverNodeDeps?: Partial<ProverNodeDeps>;
207
+ slashingProtectionDb?: SlashingProtectionDatabase;
189
208
  } = {},
190
209
  options: {
191
210
  prefilledPublicData?: PublicDataTreeLeaf[];
192
211
  dontStartSequencer?: boolean;
212
+ dontStartProverNode?: boolean;
193
213
  } = {},
194
214
  ): Promise<AztecNodeService> {
195
215
  const config = { ...inputConfig }; // Copy the config so we dont mutate the input object
@@ -197,20 +217,31 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
197
217
  const packageVersion = getPackageVersion() ?? '';
198
218
  const telemetry = deps.telemetry ?? getTelemetryClient();
199
219
  const dateProvider = deps.dateProvider ?? new DateProvider();
200
- const blobSinkClient =
201
- deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('node:blob-sink:client') });
202
220
  const ethereumChain = createEthereumChain(config.l1RpcUrls, config.l1ChainId);
203
221
 
204
- // Build a key store from file if given or from environment otherwise
222
+ // Build a key store from file if given or from environment otherwise.
223
+ // We keep the raw KeyStore available so we can merge with prover keys if enableProverNode is set.
205
224
  let keyStoreManager: KeystoreManager | undefined;
206
225
  const keyStoreProvided = config.keyStoreDirectory !== undefined && config.keyStoreDirectory.length > 0;
207
226
  if (keyStoreProvided) {
208
227
  const keyStores = loadKeystores(config.keyStoreDirectory!);
209
228
  keyStoreManager = new KeystoreManager(mergeKeystores(keyStores));
210
229
  } else {
211
- const keyStore = createKeyStoreForValidator(config);
212
- if (keyStore) {
213
- keyStoreManager = new KeystoreManager(keyStore);
230
+ const rawKeyStores: KeyStore[] = [];
231
+ const validatorKeyStore = createKeyStoreForValidator(config);
232
+ if (validatorKeyStore) {
233
+ rawKeyStores.push(validatorKeyStore);
234
+ }
235
+ if (config.enableProverNode) {
236
+ const proverKeyStore = createKeyStoreForProver(config);
237
+ if (proverKeyStore) {
238
+ rawKeyStores.push(proverKeyStore);
239
+ }
240
+ }
241
+ if (rawKeyStores.length > 0) {
242
+ keyStoreManager = new KeystoreManager(
243
+ rawKeyStores.length === 1 ? rawKeyStores[0] : mergeKeystores(rawKeyStores),
244
+ );
214
245
  }
215
246
  }
216
247
 
@@ -221,10 +252,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
221
252
  if (keyStoreManager === undefined) {
222
253
  throw new Error('Failed to create key store, a requirement for running a validator');
223
254
  }
224
- if (!keyStoreProvided) {
225
- log.warn(
226
- 'KEY STORE CREATED FROM ENVIRONMENT, IT IS RECOMMENDED TO USE A FILE-BASED KEY STORE IN PRODUCTION ENVIRONMENTS',
227
- );
255
+ if (!keyStoreProvided && process.env.NODE_ENV !== 'test') {
256
+ log.warn("Keystore created from env: it's recommended to use a file-based key store for production");
228
257
  }
229
258
  ValidatorClient.validateKeyStoreConfiguration(keyStoreManager, log);
230
259
  }
@@ -238,7 +267,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
238
267
 
239
268
  const publicClient = createPublicClient({
240
269
  chain: ethereumChain.chainInfo,
241
- transport: fallback(config.l1RpcUrls.map((url: string) => http(url))),
270
+ transport: makeL1HttpTransport(config.l1RpcUrls, { timeout: config.l1HttpTimeoutMS }),
242
271
  pollingInterval: config.viemPollingIntervalMS,
243
272
  });
244
273
 
@@ -252,10 +281,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
252
281
  config.l1Contracts = { ...config.l1Contracts, ...l1ContractsAddresses };
253
282
 
254
283
  const rollupContract = new RollupContract(publicClient, config.l1Contracts.rollupAddress.toString());
255
- const [l1GenesisTime, slotDuration, rollupVersionFromRollup] = await Promise.all([
284
+ const [l1GenesisTime, slotDuration, rollupVersionFromRollup, rollupManaLimit] = await Promise.all([
256
285
  rollupContract.getL1GenesisTime(),
257
286
  rollupContract.getSlotDuration(),
258
287
  rollupContract.getVersion(),
288
+ rollupContract.getManaLimit().then(Number),
259
289
  ] as const);
260
290
 
261
291
  config.rollupVersion ??= Number(rollupVersionFromRollup);
@@ -266,6 +296,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
266
296
  );
267
297
  }
268
298
 
299
+ const blobClient = await createBlobClientWithFileStores(config, log.createChild('blob-client'));
300
+
269
301
  // attempt snapshot sync if possible
270
302
  await trySnapshotSync(config, log);
271
303
 
@@ -273,7 +305,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
273
305
 
274
306
  const archiver = await createArchiver(
275
307
  config,
276
- { blobSinkClient, epochCache, telemetry, dateProvider },
308
+ { blobClient, epochCache, telemetry, dateProvider },
277
309
  { blockUntilSync: !config.skipArchiverInitialSync },
278
310
  );
279
311
 
@@ -284,20 +316,40 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
284
316
  options.prefilledPublicData,
285
317
  telemetry,
286
318
  );
287
- const circuitVerifier = config.realProofs
288
- ? await BBCircuitVerifier.new(config)
289
- : new TestCircuitVerifier(config.proverTestVerificationDelayMs);
319
+ const useRealVerifiers = config.realProofs || config.debugForceTxProofVerification;
320
+ let peerProofVerifier: ClientProtocolCircuitVerifier;
321
+ let rpcProofVerifier: ClientProtocolCircuitVerifier;
322
+ if (useRealVerifiers) {
323
+ peerProofVerifier = await BatchChonkVerifier.new(config, config.bbChonkVerifyMaxBatch, 'peer');
324
+ const rpcVerifier = await BBCircuitVerifier.new(config);
325
+ rpcProofVerifier = new QueuedIVCVerifier(rpcVerifier, config.numConcurrentIVCVerifiers);
326
+ } else {
327
+ peerProofVerifier = new TestCircuitVerifier(config.proverTestVerificationDelayMs);
328
+ rpcProofVerifier = new TestCircuitVerifier(config.proverTestVerificationDelayMs);
329
+ }
330
+
331
+ let debugLogStore: DebugLogStore;
290
332
  if (!config.realProofs) {
291
333
  log.warn(`Aztec node is accepting fake proofs`);
334
+
335
+ debugLogStore = new InMemoryDebugLogStore();
336
+ log.info(
337
+ 'Aztec node started in test mode (realProofs set to false) hence debug logs from public functions will be collected and served',
338
+ );
339
+ } else {
340
+ debugLogStore = new NullDebugLogStore();
341
+ }
342
+
343
+ const proverOnly = config.enableProverNode && config.disableValidator;
344
+ if (proverOnly) {
345
+ log.info('Starting in prover-only mode: skipping validator, sequencer, sentinel, and slasher subsystems');
292
346
  }
293
- const proofVerifier = new QueuedIVCVerifier(config, circuitVerifier);
294
347
 
295
348
  // create the tx pool and the p2p client, which will need the l2 block source
296
349
  const p2pClient = await createP2PClient(
297
- P2PClientType.Full,
298
350
  config,
299
351
  archiver,
300
- proofVerifier,
352
+ peerProofVerifier,
301
353
  worldStateSynchronizer,
302
354
  epochCache,
303
355
  packageVersion,
@@ -306,55 +358,70 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
306
358
  deps.p2pClientDeps,
307
359
  );
308
360
 
309
- // We should really not be modifying the config object
310
- config.txPublicSetupAllowList = config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions());
361
+ // We'll accumulate sentinel watchers here
362
+ const watchers: Watcher[] = [];
311
363
 
312
- const blockBuilder = new BlockBuilder(
313
- { ...config, l1GenesisTime, slotDuration: Number(slotDuration) },
364
+ // Create FullNodeCheckpointsBuilder for block proposal handling and tx validation.
365
+ // Override maxTxsPerCheckpoint with the validator-specific limit if set.
366
+ const validatorCheckpointsBuilder = new FullNodeCheckpointsBuilder(
367
+ {
368
+ ...config,
369
+ l1GenesisTime,
370
+ slotDuration: Number(slotDuration),
371
+ rollupManaLimit,
372
+ maxTxsPerCheckpoint: config.validateMaxTxsPerCheckpoint,
373
+ },
314
374
  worldStateSynchronizer,
315
375
  archiver,
316
376
  dateProvider,
317
377
  telemetry,
318
378
  );
319
379
 
320
- // We'll accumulate sentinel watchers here
321
- const watchers: Watcher[] = [];
380
+ let validatorClient: ValidatorClient | undefined;
322
381
 
323
- // Create validator client if required
324
- const validatorClient = createValidatorClient(config, {
325
- p2pClient,
326
- telemetry,
327
- dateProvider,
328
- epochCache,
329
- blockBuilder,
330
- blockSource: archiver,
331
- l1ToL2MessageSource: archiver,
332
- keyStoreManager,
333
- });
382
+ if (!proverOnly) {
383
+ // Create validator client if required
384
+ validatorClient = await createValidatorClient(config, {
385
+ checkpointsBuilder: validatorCheckpointsBuilder,
386
+ worldState: worldStateSynchronizer,
387
+ p2pClient,
388
+ telemetry,
389
+ dateProvider,
390
+ epochCache,
391
+ blockSource: archiver,
392
+ l1ToL2MessageSource: archiver,
393
+ keyStoreManager,
394
+ blobClient,
395
+ slashingProtectionDb: deps.slashingProtectionDb,
396
+ });
334
397
 
335
- // If we have a validator client, register it as a source of offenses for the slasher,
336
- // and have it register callbacks on the p2p client *before* we start it, otherwise messages
337
- // like attestations or auths will fail.
338
- if (validatorClient) {
339
- watchers.push(validatorClient);
340
- if (!options.dontStartSequencer) {
341
- await validatorClient.registerHandlers();
398
+ // If we have a validator client, register it as a source of offenses for the slasher,
399
+ // and have it register callbacks on the p2p client *before* we start it, otherwise messages
400
+ // like attestations or auths will fail.
401
+ if (validatorClient) {
402
+ watchers.push(validatorClient);
403
+ if (!options.dontStartSequencer) {
404
+ await validatorClient.registerHandlers();
405
+ }
342
406
  }
343
407
  }
344
408
 
345
- // If there's no validator client but alwaysReexecuteBlockProposals is enabled,
346
- // create a BlockProposalHandler to reexecute block proposals for monitoring
347
- if (!validatorClient && config.alwaysReexecuteBlockProposals) {
348
- log.info('Setting up block proposal reexecution for monitoring');
409
+ // If there's no validator client, create a BlockProposalHandler to handle block proposals
410
+ // for monitoring or reexecution. Reexecution (default) allows us to follow the pending chain,
411
+ // while non-reexecution is used for validating the proposals and collecting their txs.
412
+ if (!validatorClient) {
413
+ const reexecute = !!config.alwaysReexecuteBlockProposals;
414
+ log.info(`Setting up block proposal handler` + (reexecute ? ' with reexecution of proposals' : ''));
349
415
  createBlockProposalHandler(config, {
350
- blockBuilder,
416
+ checkpointsBuilder: validatorCheckpointsBuilder,
417
+ worldState: worldStateSynchronizer,
351
418
  epochCache,
352
419
  blockSource: archiver,
353
420
  l1ToL2MessageSource: archiver,
354
421
  p2pClient,
355
422
  dateProvider,
356
423
  telemetry,
357
- }).registerForReexecution(p2pClient);
424
+ }).register(p2pClient, reexecute);
358
425
  }
359
426
 
360
427
  // Start world state and wait for it to sync to the archiver.
@@ -363,29 +430,33 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
363
430
  // Start p2p. Note that it depends on world state to be running.
364
431
  await p2pClient.start();
365
432
 
366
- const validatorsSentinel = await createSentinel(epochCache, archiver, p2pClient, config);
367
- if (validatorsSentinel && config.slashInactivityPenalty > 0n) {
368
- watchers.push(validatorsSentinel);
369
- }
370
-
433
+ let validatorsSentinel: Awaited<ReturnType<typeof createSentinel>> | undefined;
371
434
  let epochPruneWatcher: EpochPruneWatcher | undefined;
372
- if (config.slashPrunePenalty > 0n || config.slashDataWithholdingPenalty > 0n) {
373
- epochPruneWatcher = new EpochPruneWatcher(
374
- archiver,
375
- archiver,
376
- epochCache,
377
- p2pClient.getTxProvider(),
378
- blockBuilder,
379
- config,
380
- );
381
- watchers.push(epochPruneWatcher);
382
- }
383
-
384
- // We assume we want to slash for invalid attestations unless all max penalties are set to 0
385
435
  let attestationsBlockWatcher: AttestationsBlockWatcher | undefined;
386
- if (config.slashProposeInvalidAttestationsPenalty > 0n || config.slashAttestDescendantOfInvalidPenalty > 0n) {
387
- attestationsBlockWatcher = new AttestationsBlockWatcher(archiver, epochCache, config);
388
- watchers.push(attestationsBlockWatcher);
436
+
437
+ if (!proverOnly) {
438
+ validatorsSentinel = await createSentinel(epochCache, archiver, p2pClient, config);
439
+ if (validatorsSentinel && config.slashInactivityPenalty > 0n) {
440
+ watchers.push(validatorsSentinel);
441
+ }
442
+
443
+ if (config.slashPrunePenalty > 0n || config.slashDataWithholdingPenalty > 0n) {
444
+ epochPruneWatcher = new EpochPruneWatcher(
445
+ archiver,
446
+ archiver,
447
+ epochCache,
448
+ p2pClient.getTxProvider(),
449
+ validatorCheckpointsBuilder,
450
+ config,
451
+ );
452
+ watchers.push(epochPruneWatcher);
453
+ }
454
+
455
+ // We assume we want to slash for invalid attestations unless all max penalties are set to 0
456
+ if (config.slashProposeInvalidAttestationsPenalty > 0n || config.slashAttestDescendantOfInvalidPenalty > 0n) {
457
+ attestationsBlockWatcher = new AttestationsBlockWatcher(archiver, epochCache, config);
458
+ watchers.push(attestationsBlockWatcher);
459
+ }
389
460
  }
390
461
 
391
462
  // Start p2p-related services once the archiver has completed sync
@@ -400,10 +471,18 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
400
471
  })
401
472
  .catch(err => log.error('Failed to start p2p services after archiver sync', err));
402
473
 
474
+ const globalVariableBuilder = new GlobalVariableBuilder(dateProvider, publicClient, {
475
+ l1Contracts: config.l1Contracts,
476
+ ethereumSlotDuration: config.ethereumSlotDuration,
477
+ rollupVersion: BigInt(config.rollupVersion),
478
+ l1GenesisTime,
479
+ slotDuration: Number(slotDuration),
480
+ });
481
+
403
482
  // Validator enabled, create/start relevant service
404
483
  let sequencer: SequencerClient | undefined;
405
484
  let slasherClient: SlasherClientInterface | undefined;
406
- if (!config.disableValidator) {
485
+ if (!config.disableValidator && validatorClient) {
407
486
  // We create a slasher only if we have a sequencer, since all slashing actions go through the sequencer publisher
408
487
  // as they are executed when the node is selected as proposer.
409
488
  const validatorAddresses = keyStoreManager
@@ -422,29 +501,61 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
422
501
  );
423
502
  await slasherClient.start();
424
503
 
425
- const l1TxUtils = await createL1TxUtilsWithBlobsFromEthSigner(
426
- publicClient,
427
- keyStoreManager!.createAllValidatorPublisherSigners(),
428
- { ...config, scope: 'sequencer' },
429
- { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider },
430
- );
504
+ const l1TxUtils = config.sequencerPublisherForwarderAddress
505
+ ? await createForwarderL1TxUtilsFromSigners(
506
+ publicClient,
507
+ keyStoreManager!.createAllValidatorPublisherSigners(),
508
+ config.sequencerPublisherForwarderAddress,
509
+ { ...config, scope: 'sequencer' },
510
+ { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider, kzg: Blob.getViemKzgInstance() },
511
+ )
512
+ : await createL1TxUtilsFromSigners(
513
+ publicClient,
514
+ keyStoreManager!.createAllValidatorPublisherSigners(),
515
+ { ...config, scope: 'sequencer' },
516
+ { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider, kzg: Blob.getViemKzgInstance() },
517
+ );
518
+
519
+ // Create a funder L1TxUtils from the keystore funding account (if configured)
520
+ const fundingSigner = keyStoreManager?.createFundingSigner();
521
+ let funderL1TxUtils: L1TxUtils | undefined;
522
+ if (fundingSigner) {
523
+ const [funder] = await createL1TxUtilsFromSigners(
524
+ publicClient,
525
+ [fundingSigner],
526
+ { ...config, scope: 'sequencer' },
527
+ { telemetry, logger: log.createChild('l1-tx-utils:funder'), dateProvider },
528
+ );
529
+ funderL1TxUtils = funder;
530
+ }
431
531
 
432
532
  // Create and start the sequencer client
533
+ const checkpointsBuilder = new CheckpointsBuilder(
534
+ { ...config, l1GenesisTime, slotDuration: Number(slotDuration), rollupManaLimit },
535
+ worldStateSynchronizer,
536
+ archiver,
537
+ dateProvider,
538
+ telemetry,
539
+ debugLogStore,
540
+ );
541
+
433
542
  sequencer = await SequencerClient.new(config, {
434
543
  ...deps,
435
544
  epochCache,
436
545
  l1TxUtils,
546
+ funderL1TxUtils,
437
547
  validatorClient,
438
548
  p2pClient,
439
549
  worldStateSynchronizer,
440
550
  slasherClient,
441
- blockBuilder,
551
+ checkpointsBuilder,
442
552
  l2BlockSource: archiver,
443
553
  l1ToL2MessageSource: archiver,
444
554
  telemetry,
445
555
  dateProvider,
446
- blobSinkClient,
556
+ blobClient,
447
557
  nodeKeyStore: keyStoreManager!,
558
+ globalVariableBuilder,
448
559
  });
449
560
  }
450
561
 
@@ -455,7 +566,30 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
455
566
  log.warn(`Sequencer created but not started`);
456
567
  }
457
568
 
458
- return new AztecNodeService(
569
+ // Create prover node subsystem if enabled
570
+ let proverNode: ProverNode | undefined;
571
+ if (config.enableProverNode) {
572
+ proverNode = await createProverNode(config, {
573
+ ...deps.proverNodeDeps,
574
+ telemetry,
575
+ dateProvider,
576
+ archiver,
577
+ worldStateSynchronizer,
578
+ p2pClient,
579
+ epochCache,
580
+ blobClient,
581
+ keyStoreManager,
582
+ });
583
+
584
+ if (!options.dontStartProverNode) {
585
+ await proverNode.start();
586
+ log.info(`Prover node subsystem started`);
587
+ } else {
588
+ log.info(`Prover node subsystem created but not started`);
589
+ }
590
+ }
591
+
592
+ const node = new AztecNodeService(
459
593
  config,
460
594
  p2pClient,
461
595
  archiver,
@@ -464,18 +598,26 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
464
598
  archiver,
465
599
  worldStateSynchronizer,
466
600
  sequencer,
601
+ proverNode,
467
602
  slasherClient,
468
603
  validatorsSentinel,
469
604
  epochPruneWatcher,
470
605
  ethereumChain.chainInfo.id,
471
606
  config.rollupVersion,
472
- new GlobalVariableBuilder(config),
607
+ globalVariableBuilder,
473
608
  epochCache,
474
609
  packageVersion,
475
- proofVerifier,
610
+ peerProofVerifier,
611
+ rpcProofVerifier,
476
612
  telemetry,
477
613
  log,
614
+ blobClient,
615
+ validatorClient,
616
+ keyStoreManager,
617
+ debugLogStore,
478
618
  );
619
+
620
+ return node;
479
621
  }
480
622
 
481
623
  /**
@@ -486,6 +628,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
486
628
  return this.sequencer;
487
629
  }
488
630
 
631
+ /** Returns the prover node subsystem, if enabled. */
632
+ public getProverNode(): ProverNode | undefined {
633
+ return this.proverNode;
634
+ }
635
+
489
636
  public getBlockSource(): L2BlockSource {
490
637
  return this.blockSource;
491
638
  }
@@ -511,7 +658,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
511
658
  }
512
659
 
513
660
  public async getAllowedPublicSetup(): Promise<AllowedElement[]> {
514
- return this.config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions());
661
+ return [...(await getDefaultAllowedSetupFunctions()), ...(this.config.txPublicSetupAllowListExtend ?? [])];
515
662
  }
516
663
 
517
664
  /**
@@ -539,19 +686,26 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
539
686
  enr,
540
687
  l1ContractAddresses: contractAddresses,
541
688
  protocolContractAddresses: protocolContractAddresses,
689
+ realProofs: !!this.config.realProofs,
542
690
  };
543
691
 
544
692
  return nodeInfo;
545
693
  }
546
694
 
547
695
  /**
548
- * Get a block specified by its number.
549
- * @param number - The block number being requested.
696
+ * Get a block specified by its block number, block hash, or 'latest'.
697
+ * @param block - The block parameter (block number, block hash, or 'latest').
550
698
  * @returns The requested block.
551
699
  */
552
- public async getBlock(number: L2BlockNumber): Promise<L2Block | undefined> {
553
- const blockNumber = number === 'latest' ? await this.getBlockNumber() : number;
554
- return await this.blockSource.getBlock(blockNumber);
700
+ public async getBlock(block: BlockParameter): Promise<L2Block | undefined> {
701
+ if (BlockHash.isBlockHash(block)) {
702
+ return this.getBlockByHash(block);
703
+ }
704
+ const blockNumber = block === 'latest' ? await this.getBlockNumber() : (block as BlockNumber);
705
+ if (blockNumber === BlockNumber.ZERO) {
706
+ return this.buildInitialBlock();
707
+ }
708
+ return await this.blockSource.getL2Block(blockNumber);
555
709
  }
556
710
 
557
711
  /**
@@ -559,9 +713,17 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
559
713
  * @param blockHash - The block hash being requested.
560
714
  * @returns The requested block.
561
715
  */
562
- public async getBlockByHash(blockHash: Fr): Promise<L2Block | undefined> {
563
- const publishedBlock = await this.blockSource.getPublishedBlockByHash(blockHash);
564
- return publishedBlock?.block;
716
+ public async getBlockByHash(blockHash: BlockHash): Promise<L2Block | undefined> {
717
+ const initialBlockHash = await this.#getInitialHeaderHash();
718
+ if (blockHash.equals(initialBlockHash)) {
719
+ return this.buildInitialBlock();
720
+ }
721
+ return await this.blockSource.getL2BlockByHash(blockHash);
722
+ }
723
+
724
+ private buildInitialBlock(): L2Block {
725
+ const initialHeader = this.worldStateSynchronizer.getCommitted().getInitialHeader();
726
+ return L2Block.empty(initialHeader);
565
727
  }
566
728
 
567
729
  /**
@@ -570,8 +732,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
570
732
  * @returns The requested block.
571
733
  */
572
734
  public async getBlockByArchive(archive: Fr): Promise<L2Block | undefined> {
573
- const publishedBlock = await this.blockSource.getPublishedBlockByArchive(archive);
574
- return publishedBlock?.block;
735
+ return await this.blockSource.getL2BlockByArchive(archive);
575
736
  }
576
737
 
577
738
  /**
@@ -580,34 +741,58 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
580
741
  * @param limit - The maximum number of blocks to obtain.
581
742
  * @returns The blocks requested.
582
743
  */
583
- public async getBlocks(from: number, limit: number): Promise<L2Block[]> {
584
- return (await this.blockSource.getBlocks(from, limit)) ?? [];
744
+ public async getBlocks(from: BlockNumber, limit: number): Promise<L2Block[]> {
745
+ return (await this.blockSource.getBlocks(from, BlockNumber(limit))) ?? [];
746
+ }
747
+
748
+ public async getCheckpoints(from: CheckpointNumber, limit: number): Promise<PublishedCheckpoint[]> {
749
+ return (await this.blockSource.getCheckpoints(from, limit)) ?? [];
585
750
  }
586
751
 
587
- public async getPublishedBlocks(from: number, limit: number): Promise<PublishedL2Block[]> {
588
- return (await this.blockSource.getPublishedBlocks(from, limit)) ?? [];
752
+ public async getCheckpointedBlocks(from: BlockNumber, limit: number) {
753
+ return (await this.blockSource.getCheckpointedBlocks(from, limit)) ?? [];
754
+ }
755
+
756
+ public getCheckpointsDataForEpoch(epochNumber: EpochNumber) {
757
+ return this.blockSource.getCheckpointsDataForEpoch(epochNumber);
589
758
  }
590
759
 
591
760
  /**
592
- * Method to fetch the current base fees.
593
- * @returns The current base fees.
761
+ * Method to fetch the current min L2 fees.
762
+ * @returns The current min L2 fees.
594
763
  */
595
- public async getCurrentBaseFees(): Promise<GasFees> {
596
- return await this.globalVariableBuilder.getCurrentBaseFees();
764
+ public async getCurrentMinFees(): Promise<GasFees> {
765
+ return await this.globalVariableBuilder.getCurrentMinFees();
766
+ }
767
+
768
+ public async getMaxPriorityFees(): Promise<GasFees> {
769
+ for await (const tx of this.p2pClient.iteratePendingTxs()) {
770
+ return tx.getGasSettings().maxPriorityFeesPerGas;
771
+ }
772
+
773
+ return GasFees.from({ feePerDaGas: 0n, feePerL2Gas: 0n });
597
774
  }
598
775
 
599
776
  /**
600
777
  * Method to fetch the latest block number synchronized by the node.
601
778
  * @returns The block number.
602
779
  */
603
- public async getBlockNumber(): Promise<number> {
780
+ public async getBlockNumber(): Promise<BlockNumber> {
604
781
  return await this.blockSource.getBlockNumber();
605
782
  }
606
783
 
607
- public async getProvenBlockNumber(): Promise<number> {
784
+ public async getProvenBlockNumber(): Promise<BlockNumber> {
608
785
  return await this.blockSource.getProvenBlockNumber();
609
786
  }
610
787
 
788
+ public async getCheckpointedBlockNumber(): Promise<BlockNumber> {
789
+ return await this.blockSource.getCheckpointedL2BlockNumber();
790
+ }
791
+
792
+ public getCheckpointNumber(): Promise<CheckpointNumber> {
793
+ return this.blockSource.getCheckpointNumber();
794
+ }
795
+
611
796
  /**
612
797
  * Method to fetch the version of the package.
613
798
  * @returns The node package version
@@ -640,25 +825,51 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
640
825
  return this.contractDataSource.getContract(address);
641
826
  }
642
827
 
643
- /**
644
- * Retrieves all private logs from up to `limit` blocks, starting from the block number `from`.
645
- * @param from - The block number from which to begin retrieving logs.
646
- * @param limit - The maximum number of blocks to retrieve logs from.
647
- * @returns An array of private logs from the specified range of blocks.
648
- */
649
- public getPrivateLogs(from: number, limit: number): Promise<PrivateLog[]> {
650
- return this.logsSource.getPrivateLogs(from, limit);
651
- }
652
-
653
- /**
654
- * Gets all logs that match any of the received tags (i.e. logs with their first field equal to a tag).
655
- * @param tags - The tags to filter the logs by.
656
- * @param logsPerTag - The maximum number of logs to return for each tag. By default no limit is set
657
- * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match
658
- * that tag.
659
- */
660
- public getLogsByTags(tags: Fr[], logsPerTag?: number): Promise<TxScopedL2Log[][]> {
661
- return this.logsSource.getLogsByTags(tags, logsPerTag);
828
+ public async getPrivateLogsByTags(
829
+ tags: SiloedTag[],
830
+ page?: number,
831
+ referenceBlock?: BlockHash,
832
+ ): Promise<TxScopedL2Log[][]> {
833
+ let upToBlockNumber: BlockNumber | undefined;
834
+ if (referenceBlock) {
835
+ const initialBlockHash = await this.#getInitialHeaderHash();
836
+ if (referenceBlock.equals(initialBlockHash)) {
837
+ upToBlockNumber = BlockNumber(0);
838
+ } else {
839
+ const header = await this.blockSource.getBlockHeaderByHash(referenceBlock);
840
+ if (!header) {
841
+ throw new Error(
842
+ `Block ${referenceBlock.toString()} not found in the node. This might indicate a reorg has occurred.`,
843
+ );
844
+ }
845
+ upToBlockNumber = header.globalVariables.blockNumber;
846
+ }
847
+ }
848
+ return this.logsSource.getPrivateLogsByTags(tags, page, upToBlockNumber);
849
+ }
850
+
851
+ public async getPublicLogsByTagsFromContract(
852
+ contractAddress: AztecAddress,
853
+ tags: Tag[],
854
+ page?: number,
855
+ referenceBlock?: BlockHash,
856
+ ): Promise<TxScopedL2Log[][]> {
857
+ let upToBlockNumber: BlockNumber | undefined;
858
+ if (referenceBlock) {
859
+ const initialBlockHash = await this.#getInitialHeaderHash();
860
+ if (referenceBlock.equals(initialBlockHash)) {
861
+ upToBlockNumber = BlockNumber(0);
862
+ } else {
863
+ const header = await this.blockSource.getBlockHeaderByHash(referenceBlock);
864
+ if (!header) {
865
+ throw new Error(
866
+ `Block ${referenceBlock.toString()} not found in the node. This might indicate a reorg has occurred.`,
867
+ );
868
+ }
869
+ upToBlockNumber = header.globalVariables.blockNumber;
870
+ }
871
+ }
872
+ return this.logsSource.getPublicLogsByTagsFromContract(contractAddress, tags, page, upToBlockNumber);
662
873
  }
663
874
 
664
875
  /**
@@ -700,26 +911,36 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
700
911
  }
701
912
 
702
913
  await this.p2pClient!.sendTx(tx);
703
- this.metrics.receivedTx(timer.ms(), true);
704
- this.log.info(`Received tx ${txHash}`, { txHash });
914
+ const duration = timer.ms();
915
+ this.metrics.receivedTx(duration, true);
916
+ this.log.info(`Received tx ${txHash} in ${duration}ms`, { txHash });
705
917
  }
706
918
 
707
919
  public async getTxReceipt(txHash: TxHash): Promise<TxReceipt> {
708
- let txReceipt = new TxReceipt(txHash, TxStatus.DROPPED, 'Tx dropped by P2P node.');
709
-
710
- // We first check if the tx is in pending (instead of first checking if it is mined) because if we first check
711
- // for mined and then for pending there could be a race condition where the tx is mined between the two checks
712
- // and we would incorrectly return a TxReceipt with status DROPPED
713
- if ((await this.p2pClient.getTxStatus(txHash)) === 'pending') {
714
- txReceipt = new TxReceipt(txHash, TxStatus.PENDING, '');
715
- }
920
+ // Check the tx pool status first. If the tx is known to the pool (pending or mined), we'll use that
921
+ // as a fallback if we don't find a settled receipt in the archiver.
922
+ const txPoolStatus = await this.p2pClient.getTxStatus(txHash);
923
+ const isKnownToPool = txPoolStatus === 'pending' || txPoolStatus === 'mined';
716
924
 
925
+ // Then get the actual tx from the archiver, which tracks every tx in a mined block.
717
926
  const settledTxReceipt = await this.blockSource.getSettledTxReceipt(txHash);
927
+
928
+ let receipt: TxReceipt;
718
929
  if (settledTxReceipt) {
719
- txReceipt = settledTxReceipt;
930
+ receipt = settledTxReceipt;
931
+ } else if (isKnownToPool) {
932
+ // If the tx is in the pool but not in the archiver, it's pending.
933
+ // This handles race conditions between archiver and p2p, where the archiver
934
+ // has pruned the block in which a tx was mined, but p2p has not caught up yet.
935
+ receipt = new TxReceipt(txHash, TxStatus.PENDING, undefined, undefined);
936
+ } else {
937
+ // Otherwise, if we don't know the tx, we consider it dropped.
938
+ receipt = new TxReceipt(txHash, TxStatus.DROPPED, undefined, 'Tx dropped by P2P node');
720
939
  }
721
940
 
722
- return txReceipt;
941
+ this.debugLogStore.decorateReceiptWithLogs(txHash.toString(), receipt);
942
+
943
+ return receipt;
723
944
  }
724
945
 
725
946
  public getTxEffect(txHash: TxHash): Promise<IndexedTxEffect | undefined> {
@@ -734,15 +955,25 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
734
955
  await tryStop(this.validatorsSentinel);
735
956
  await tryStop(this.epochPruneWatcher);
736
957
  await tryStop(this.slasherClient);
737
- await tryStop(this.proofVerifier);
958
+ await Promise.all([tryStop(this.peerProofVerifier), tryStop(this.rpcProofVerifier)]);
738
959
  await tryStop(this.sequencer);
960
+ await tryStop(this.proverNode);
739
961
  await tryStop(this.p2pClient);
740
962
  await tryStop(this.worldStateSynchronizer);
741
963
  await tryStop(this.blockSource);
964
+ await tryStop(this.blobClient);
742
965
  await tryStop(this.telemetry);
743
966
  this.log.info(`Stopped Aztec Node`);
744
967
  }
745
968
 
969
+ /**
970
+ * Returns the blob client used by this node.
971
+ * @internal - Exposed for testing purposes only.
972
+ */
973
+ public getBlobClient(): BlobClientInterface | undefined {
974
+ return this.blobClient;
975
+ }
976
+
746
977
  /**
747
978
  * Method to retrieve pending txs.
748
979
  * @param limit - The number of items to returns
@@ -775,121 +1006,95 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
775
1006
  return compactArray(await Promise.all(txHashes.map(txHash => this.getTxByHash(txHash))));
776
1007
  }
777
1008
 
778
- /**
779
- * Find the indexes of the given leaves in the given tree along with a block metadata pointing to the block in which
780
- * the leaves were inserted.
781
- * @param blockNumber - The block number at which to get the data or 'latest' for latest data.
782
- * @param treeId - The tree to search in.
783
- * @param leafValues - The values to search for.
784
- * @returns The indices of leaves and the block metadata of a block in which the leaves were inserted.
785
- */
786
1009
  public async findLeavesIndexes(
787
- blockNumber: L2BlockNumber,
1010
+ referenceBlock: BlockParameter,
788
1011
  treeId: MerkleTreeId,
789
1012
  leafValues: Fr[],
790
- ): Promise<(InBlock<bigint> | undefined)[]> {
791
- const committedDb = await this.#getWorldState(blockNumber);
1013
+ ): Promise<(DataInBlock<bigint> | undefined)[]> {
1014
+ const committedDb = await this.getWorldState(referenceBlock);
792
1015
  const maybeIndices = await committedDb.findLeafIndices(
793
1016
  treeId,
794
1017
  leafValues.map(x => x.toBuffer()),
795
1018
  );
796
- // We filter out undefined values
797
- const indices = maybeIndices.filter(x => x !== undefined) as bigint[];
1019
+ // Filter out undefined values to query block numbers only for found leaves
1020
+ const definedIndices = maybeIndices.filter(x => x !== undefined);
798
1021
 
799
- // Now we find the block numbers for the indices
800
- const blockNumbers = await committedDb.getBlockNumbersForLeafIndices(treeId, indices);
1022
+ // Now we find the block numbers for the defined indices
1023
+ const blockNumbers = await committedDb.getBlockNumbersForLeafIndices(treeId, definedIndices);
801
1024
 
802
- // If any of the block numbers are undefined, we throw an error.
803
- for (let i = 0; i < indices.length; i++) {
804
- if (blockNumbers[i] === undefined) {
805
- throw new Error(`Block number is undefined for leaf index ${indices[i]} in tree ${MerkleTreeId[treeId]}`);
1025
+ // Build a map from leaf index to block number
1026
+ const indexToBlockNumber = new Map<bigint, BlockNumber>();
1027
+ for (let i = 0; i < definedIndices.length; i++) {
1028
+ const blockNumber = blockNumbers[i];
1029
+ if (blockNumber === undefined) {
1030
+ throw new Error(
1031
+ `Block number is undefined for leaf index ${definedIndices[i]} in tree ${MerkleTreeId[treeId]}`,
1032
+ );
806
1033
  }
1034
+ indexToBlockNumber.set(definedIndices[i], blockNumber);
807
1035
  }
808
1036
 
809
1037
  // Get unique block numbers in order to optimize num calls to getLeafValue function.
810
- const uniqueBlockNumbers = [...new Set(blockNumbers.filter(x => x !== undefined))];
1038
+ const uniqueBlockNumbers = [...new Set(indexToBlockNumber.values())];
811
1039
 
812
- // Now we obtain the block hashes from the archive tree by calling await `committedDb.getLeafValue(treeId, index)`
813
- // (note that block number corresponds to the leaf index in the archive tree).
1040
+ // Now we obtain the block hashes from the archive tree (block number = leaf index in archive tree).
814
1041
  const blockHashes = await Promise.all(
815
1042
  uniqueBlockNumbers.map(blockNumber => {
816
- return committedDb.getLeafValue(MerkleTreeId.ARCHIVE, blockNumber!);
1043
+ return committedDb.getLeafValue(MerkleTreeId.ARCHIVE, BigInt(blockNumber));
817
1044
  }),
818
1045
  );
819
1046
 
820
- // If any of the block hashes are undefined, we throw an error.
1047
+ // Build a map from block number to block hash
1048
+ const blockNumberToHash = new Map<BlockNumber, Fr>();
821
1049
  for (let i = 0; i < uniqueBlockNumbers.length; i++) {
822
- if (blockHashes[i] === undefined) {
1050
+ const blockHash = blockHashes[i];
1051
+ if (blockHash === undefined) {
823
1052
  throw new Error(`Block hash is undefined for block number ${uniqueBlockNumbers[i]}`);
824
1053
  }
1054
+ blockNumberToHash.set(uniqueBlockNumbers[i], blockHash);
825
1055
  }
826
1056
 
827
- // Create InBlock objects by combining indices, blockNumbers and blockHashes and return them.
828
- return maybeIndices.map((index, i) => {
1057
+ // Create DataInBlock objects by combining indices, blockNumbers and blockHashes and return them.
1058
+ return maybeIndices.map(index => {
829
1059
  if (index === undefined) {
830
1060
  return undefined;
831
1061
  }
832
- const blockNumber = blockNumbers[i];
1062
+ const blockNumber = indexToBlockNumber.get(index);
833
1063
  if (blockNumber === undefined) {
834
- return undefined;
1064
+ throw new Error(`Block number not found for leaf index ${index} in tree ${MerkleTreeId[treeId]}`);
835
1065
  }
836
- const blockHashIndex = uniqueBlockNumbers.indexOf(blockNumber);
837
- const blockHash = blockHashes[blockHashIndex];
838
- if (!blockHash) {
839
- return undefined;
1066
+ const blockHash = blockNumberToHash.get(blockNumber);
1067
+ if (blockHash === undefined) {
1068
+ throw new Error(`Block hash not found for block number ${blockNumber}`);
840
1069
  }
841
1070
  return {
842
- l2BlockNumber: Number(blockNumber),
843
- l2BlockHash: L2BlockHash.fromField(blockHash),
1071
+ l2BlockNumber: blockNumber,
1072
+ l2BlockHash: new BlockHash(blockHash),
844
1073
  data: index,
845
1074
  };
846
1075
  });
847
1076
  }
848
1077
 
849
- /**
850
- * Returns a sibling path for the given index in the nullifier tree.
851
- * @param blockNumber - The block number at which to get the data.
852
- * @param leafIndex - The index of the leaf for which the sibling path is required.
853
- * @returns The sibling path for the leaf index.
854
- */
855
- public async getNullifierSiblingPath(
856
- blockNumber: L2BlockNumber,
857
- leafIndex: bigint,
858
- ): Promise<SiblingPath<typeof NULLIFIER_TREE_HEIGHT>> {
859
- const committedDb = await this.#getWorldState(blockNumber);
860
- return committedDb.getSiblingPath(MerkleTreeId.NULLIFIER_TREE, leafIndex);
861
- }
862
-
863
- /**
864
- * Returns a sibling path for the given index in the data tree.
865
- * @param blockNumber - The block number at which to get the data.
866
- * @param leafIndex - The index of the leaf for which the sibling path is required.
867
- * @returns The sibling path for the leaf index.
868
- */
869
- public async getNoteHashSiblingPath(
870
- blockNumber: L2BlockNumber,
871
- leafIndex: bigint,
872
- ): Promise<SiblingPath<typeof NOTE_HASH_TREE_HEIGHT>> {
873
- const committedDb = await this.#getWorldState(blockNumber);
874
- return committedDb.getSiblingPath(MerkleTreeId.NOTE_HASH_TREE, leafIndex);
875
- }
876
-
877
- public async getArchiveMembershipWitness(
878
- blockNumber: L2BlockNumber,
879
- archive: Fr,
1078
+ public async getBlockHashMembershipWitness(
1079
+ referenceBlock: BlockParameter,
1080
+ blockHash: BlockHash,
880
1081
  ): Promise<MembershipWitness<typeof ARCHIVE_HEIGHT> | undefined> {
881
- const committedDb = await this.#getWorldState(blockNumber);
882
- const [pathAndIndex] = await committedDb.findSiblingPaths<MerkleTreeId.ARCHIVE>(MerkleTreeId.ARCHIVE, [archive]);
1082
+ // The Noir circuit checks the archive membership proof against `anchor_block_header.last_archive.root`,
1083
+ // which is the archive tree root BEFORE the anchor block was added (i.e. the state after block N-1).
1084
+ // So we need the world state at block N-1, not block N, to produce a sibling path matching that root.
1085
+ const referenceBlockNumber = await this.resolveBlockNumber(referenceBlock);
1086
+ const committedDb = await this.getWorldState(BlockNumber(referenceBlockNumber - 1));
1087
+ const [pathAndIndex] = await committedDb.findSiblingPaths<MerkleTreeId.ARCHIVE>(MerkleTreeId.ARCHIVE, [blockHash]);
883
1088
  return pathAndIndex === undefined
884
1089
  ? undefined
885
1090
  : MembershipWitness.fromSiblingPath(pathAndIndex.index, pathAndIndex.path);
886
1091
  }
887
1092
 
888
1093
  public async getNoteHashMembershipWitness(
889
- blockNumber: L2BlockNumber,
1094
+ referenceBlock: BlockParameter,
890
1095
  noteHash: Fr,
891
1096
  ): Promise<MembershipWitness<typeof NOTE_HASH_TREE_HEIGHT> | undefined> {
892
- const committedDb = await this.#getWorldState(blockNumber);
1097
+ const committedDb = await this.getWorldState(referenceBlock);
893
1098
  const [pathAndIndex] = await committedDb.findSiblingPaths<MerkleTreeId.NOTE_HASH_TREE>(
894
1099
  MerkleTreeId.NOTE_HASH_TREE,
895
1100
  [noteHash],
@@ -899,17 +1104,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
899
1104
  : MembershipWitness.fromSiblingPath(pathAndIndex.index, pathAndIndex.path);
900
1105
  }
901
1106
 
902
- /**
903
- * Returns the index and a sibling path for a leaf in the committed l1 to l2 data tree.
904
- * @param blockNumber - The block number at which to get the data.
905
- * @param l1ToL2Message - The l1ToL2Message to get the index / sibling path for.
906
- * @returns A tuple of the index and the sibling path of the L1ToL2Message (undefined if not found).
907
- */
908
1107
  public async getL1ToL2MessageMembershipWitness(
909
- blockNumber: L2BlockNumber,
1108
+ referenceBlock: BlockParameter,
910
1109
  l1ToL2Message: Fr,
911
1110
  ): Promise<[bigint, SiblingPath<typeof L1_TO_L2_MSG_TREE_HEIGHT>] | undefined> {
912
- const db = await this.#getWorldState(blockNumber);
1111
+ const db = await this.getWorldState(referenceBlock);
913
1112
  const [witness] = await db.findSiblingPaths(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, [l1ToL2Message]);
914
1113
  if (!witness) {
915
1114
  return undefined;
@@ -919,9 +1118,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
919
1118
  return [witness.index, witness.path];
920
1119
  }
921
1120
 
922
- public async getL1ToL2MessageBlock(l1ToL2Message: Fr): Promise<number | undefined> {
1121
+ public async getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise<CheckpointNumber | undefined> {
923
1122
  const messageIndex = await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message);
924
- return messageIndex ? InboxLeaf.l2BlockFromIndex(messageIndex) : undefined;
1123
+ return messageIndex ? InboxLeaf.checkpointNumberFromIndex(messageIndex) : undefined;
925
1124
  }
926
1125
 
927
1126
  /**
@@ -935,54 +1134,26 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
935
1134
  }
936
1135
 
937
1136
  /**
938
- * Returns all the L2 to L1 messages in a block.
939
- * @param blockNumber - The block number at which to get the data.
940
- * @returns The L2 to L1 messages (undefined if the block number is not found).
1137
+ * Returns all the L2 to L1 messages in an epoch.
1138
+ * @param epoch - The epoch at which to get the data.
1139
+ * @returns The L2 to L1 messages (empty array if the epoch is not found).
941
1140
  */
942
- public async getL2ToL1Messages(blockNumber: L2BlockNumber): Promise<Fr[][] | undefined> {
943
- const block = await this.blockSource.getBlock(blockNumber === 'latest' ? await this.getBlockNumber() : blockNumber);
944
- return block?.body.txEffects.map(txEffect => txEffect.l2ToL1Msgs);
945
- }
946
-
947
- /**
948
- * Returns a sibling path for a leaf in the committed blocks tree.
949
- * @param blockNumber - The block number at which to get the data.
950
- * @param leafIndex - Index of the leaf in the tree.
951
- * @returns The sibling path.
952
- */
953
- public async getArchiveSiblingPath(
954
- blockNumber: L2BlockNumber,
955
- leafIndex: bigint,
956
- ): Promise<SiblingPath<typeof ARCHIVE_HEIGHT>> {
957
- const committedDb = await this.#getWorldState(blockNumber);
958
- return committedDb.getSiblingPath(MerkleTreeId.ARCHIVE, leafIndex);
959
- }
960
-
961
- /**
962
- * Returns a sibling path for a leaf in the committed public data tree.
963
- * @param blockNumber - The block number at which to get the data.
964
- * @param leafIndex - Index of the leaf in the tree.
965
- * @returns The sibling path.
966
- */
967
- public async getPublicDataSiblingPath(
968
- blockNumber: L2BlockNumber,
969
- leafIndex: bigint,
970
- ): Promise<SiblingPath<typeof PUBLIC_DATA_TREE_HEIGHT>> {
971
- const committedDb = await this.#getWorldState(blockNumber);
972
- return committedDb.getSiblingPath(MerkleTreeId.PUBLIC_DATA_TREE, leafIndex);
1141
+ public async getL2ToL1Messages(epoch: EpochNumber): Promise<Fr[][][][]> {
1142
+ // Assumes `getCheckpointedBlocksForEpoch` returns blocks in ascending order of block number.
1143
+ const checkpointedBlocks = await this.blockSource.getCheckpointedBlocksForEpoch(epoch);
1144
+ const blocksInCheckpoints = chunkBy(checkpointedBlocks, cb => cb.block.header.globalVariables.slotNumber).map(
1145
+ group => group.map(cb => cb.block),
1146
+ );
1147
+ return blocksInCheckpoints.map(blocks =>
1148
+ blocks.map(block => block.body.txEffects.map(txEffect => txEffect.l2ToL1Msgs)),
1149
+ );
973
1150
  }
974
1151
 
975
- /**
976
- * Returns a nullifier membership witness for a given nullifier at a given block.
977
- * @param blockNumber - The block number at which to get the index.
978
- * @param nullifier - Nullifier we try to find witness for.
979
- * @returns The nullifier membership witness (if found).
980
- */
981
1152
  public async getNullifierMembershipWitness(
982
- blockNumber: L2BlockNumber,
1153
+ referenceBlock: BlockParameter,
983
1154
  nullifier: Fr,
984
1155
  ): Promise<NullifierMembershipWitness | undefined> {
985
- const db = await this.#getWorldState(blockNumber);
1156
+ const db = await this.getWorldState(referenceBlock);
986
1157
  const [witness] = await db.findSiblingPaths(MerkleTreeId.NULLIFIER_TREE, [nullifier.toBuffer()]);
987
1158
  if (!witness) {
988
1159
  return undefined;
@@ -997,32 +1168,20 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
997
1168
  return new NullifierMembershipWitness(index, leafPreimage as NullifierLeafPreimage, path);
998
1169
  }
999
1170
 
1000
- /**
1001
- * Returns a low nullifier membership witness for a given nullifier at a given block.
1002
- * @param blockNumber - The block number at which to get the index.
1003
- * @param nullifier - Nullifier we try to find the low nullifier witness for.
1004
- * @returns The low nullifier membership witness (if found).
1005
- * @remarks Low nullifier witness can be used to perform a nullifier non-inclusion proof by leveraging the "linked
1006
- * list structure" of leaves and proving that a lower nullifier is pointing to a bigger next value than the nullifier
1007
- * we are trying to prove non-inclusion for.
1008
- *
1009
- * Note: This function returns the membership witness of the nullifier itself and not the low nullifier when
1010
- * the nullifier already exists in the tree. This is because the `getPreviousValueIndex` function returns the
1011
- * index of the nullifier itself when it already exists in the tree.
1012
- * TODO: This is a confusing behavior and we should eventually address that.
1013
- */
1014
1171
  public async getLowNullifierMembershipWitness(
1015
- blockNumber: L2BlockNumber,
1172
+ referenceBlock: BlockParameter,
1016
1173
  nullifier: Fr,
1017
1174
  ): Promise<NullifierMembershipWitness | undefined> {
1018
- const committedDb = await this.#getWorldState(blockNumber);
1175
+ const committedDb = await this.getWorldState(referenceBlock);
1019
1176
  const findResult = await committedDb.getPreviousValueIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBigInt());
1020
1177
  if (!findResult) {
1021
1178
  return undefined;
1022
1179
  }
1023
1180
  const { index, alreadyPresent } = findResult;
1024
1181
  if (alreadyPresent) {
1025
- this.log.warn(`Nullifier ${nullifier.toBigInt()} already exists in the tree`);
1182
+ throw new Error(
1183
+ `Cannot prove nullifier non-inclusion: nullifier ${nullifier.toBigInt()} already exists in the tree`,
1184
+ );
1026
1185
  }
1027
1186
  const preimageData = (await committedDb.getLeafPreimage(MerkleTreeId.NULLIFIER_TREE, index))!;
1028
1187
 
@@ -1030,8 +1189,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1030
1189
  return new NullifierMembershipWitness(BigInt(index), preimageData as NullifierLeafPreimage, siblingPath);
1031
1190
  }
1032
1191
 
1033
- async getPublicDataWitness(blockNumber: L2BlockNumber, leafSlot: Fr): Promise<PublicDataWitness | undefined> {
1034
- const committedDb = await this.#getWorldState(blockNumber);
1192
+ async getPublicDataWitness(referenceBlock: BlockParameter, leafSlot: Fr): Promise<PublicDataWitness | undefined> {
1193
+ const committedDb = await this.getWorldState(referenceBlock);
1035
1194
  const lowLeafResult = await committedDb.getPreviousValueIndex(MerkleTreeId.PUBLIC_DATA_TREE, leafSlot.toBigInt());
1036
1195
  if (!lowLeafResult) {
1037
1196
  return undefined;
@@ -1045,19 +1204,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1045
1204
  }
1046
1205
  }
1047
1206
 
1048
- /**
1049
- * Gets the storage value at the given contract storage slot.
1050
- *
1051
- * @remarks The storage slot here refers to the slot as it is defined in Noir not the index in the merkle tree.
1052
- * Aztec's version of `eth_getStorageAt`.
1053
- *
1054
- * @param contract - Address of the contract to query.
1055
- * @param slot - Slot to query.
1056
- * @param blockNumber - The block number at which to get the data or 'latest'.
1057
- * @returns Storage value at the given contract slot.
1058
- */
1059
- public async getPublicStorageAt(blockNumber: L2BlockNumber, contract: AztecAddress, slot: Fr): Promise<Fr> {
1060
- const committedDb = await this.#getWorldState(blockNumber);
1207
+ public async getPublicStorageAt(referenceBlock: BlockParameter, contract: AztecAddress, slot: Fr): Promise<Fr> {
1208
+ const committedDb = await this.getWorldState(referenceBlock);
1061
1209
  const leafSlot = await computePublicDataTreeLeafSlot(contract, slot);
1062
1210
 
1063
1211
  const lowLeafResult = await committedDb.getPreviousValueIndex(MerkleTreeId.PUBLIC_DATA_TREE, leafSlot.toBigInt());
@@ -1071,23 +1219,22 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1071
1219
  return preimage.leaf.value;
1072
1220
  }
1073
1221
 
1074
- /**
1075
- * Returns the currently committed block header, or the initial header if no blocks have been produced.
1076
- * @returns The current committed block header.
1077
- */
1078
- public async getBlockHeader(blockNumber: L2BlockNumber = 'latest'): Promise<BlockHeader | undefined> {
1079
- return blockNumber === 0 || (blockNumber === 'latest' && (await this.blockSource.getBlockNumber()) === 0)
1080
- ? this.worldStateSynchronizer.getCommitted().getInitialHeader()
1081
- : this.blockSource.getBlockHeader(blockNumber);
1082
- }
1083
-
1084
- /**
1085
- * Get a block header specified by its hash.
1086
- * @param blockHash - The block hash being requested.
1087
- * @returns The requested block header.
1088
- */
1089
- public async getBlockHeaderByHash(blockHash: Fr): Promise<BlockHeader | undefined> {
1090
- return await this.blockSource.getBlockHeaderByHash(blockHash);
1222
+ public async getBlockHeader(block: BlockParameter = 'latest'): Promise<BlockHeader | undefined> {
1223
+ if (BlockHash.isBlockHash(block)) {
1224
+ const initialBlockHash = await this.#getInitialHeaderHash();
1225
+ if (block.equals(initialBlockHash)) {
1226
+ // Block source doesn't handle initial header so we need to handle the case separately.
1227
+ return this.worldStateSynchronizer.getCommitted().getInitialHeader();
1228
+ }
1229
+ return this.blockSource.getBlockHeaderByHash(block);
1230
+ } else {
1231
+ // Block source doesn't handle initial header so we need to handle the case separately.
1232
+ const blockNumber = block === 'latest' ? await this.getBlockNumber() : (block as BlockNumber);
1233
+ if (blockNumber === BlockNumber.ZERO) {
1234
+ return this.worldStateSynchronizer.getCommitted().getInitialHeader();
1235
+ }
1236
+ return this.blockSource.getBlockHeader(block);
1237
+ }
1091
1238
  }
1092
1239
 
1093
1240
  /**
@@ -1099,6 +1246,14 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1099
1246
  return await this.blockSource.getBlockHeaderByArchive(archive);
1100
1247
  }
1101
1248
 
1249
+ public getBlockData(number: BlockNumber): Promise<BlockData | undefined> {
1250
+ return this.blockSource.getBlockData(number);
1251
+ }
1252
+
1253
+ public getBlockDataByArchive(archive: Fr): Promise<BlockData | undefined> {
1254
+ return this.blockSource.getBlockDataByArchive(archive);
1255
+ }
1256
+
1102
1257
  /**
1103
1258
  * Simulates the public part of a transaction with the current state.
1104
1259
  * @param tx - The transaction to simulate.
@@ -1122,7 +1277,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1122
1277
  }
1123
1278
 
1124
1279
  const txHash = tx.getTxHash();
1125
- const blockNumber = (await this.blockSource.getBlockNumber()) + 1;
1280
+ const latestBlockNumber = await this.blockSource.getBlockNumber();
1281
+ const blockNumber = BlockNumber.add(latestBlockNumber, 1);
1126
1282
 
1127
1283
  // If sequencer is not initialized, we just set these values to zero for simulation.
1128
1284
  const coinbase = EthAddress.ZERO;
@@ -1137,6 +1293,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1137
1293
  this.contractDataSource,
1138
1294
  new DateProvider(),
1139
1295
  this.telemetry,
1296
+ this.log.getBindings(),
1140
1297
  );
1141
1298
 
1142
1299
  this.log.verbose(`Simulating public calls for tx ${txHash}`, {
@@ -1145,6 +1302,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1145
1302
  blockNumber,
1146
1303
  });
1147
1304
 
1305
+ // Ensure world-state has caught up with the latest block we loaded from the archiver
1306
+ await this.worldStateSynchronizer.syncImmediate(latestBlockNumber);
1148
1307
  const merkleTreeFork = await this.worldStateSynchronizer.fork();
1149
1308
  try {
1150
1309
  const config = PublicSimulatorConfig.from({
@@ -1152,13 +1311,15 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1152
1311
  collectDebugLogs: true,
1153
1312
  collectHints: false,
1154
1313
  collectCallMetadata: true,
1155
- maxDebugLogMemoryReads: this.config.rpcSimulatePublicMaxDebugLogMemoryReads,
1156
1314
  collectStatistics: false,
1315
+ collectionLimits: CollectionLimitsConfig.from({
1316
+ maxDebugLogMemoryReads: this.config.rpcSimulatePublicMaxDebugLogMemoryReads,
1317
+ }),
1157
1318
  });
1158
1319
  const processor = publicProcessorFactory.create(merkleTreeFork, newGlobalVariables, config);
1159
1320
 
1160
1321
  // REFACTOR: Consider merging ProcessReturnValues into ProcessedTx
1161
- const [processedTxs, failedTxs, _usedTxs, returns] = await processor.process([tx]);
1322
+ const [processedTxs, failedTxs, _usedTxs, returns, debugLogs] = await processor.process([tx]);
1162
1323
  // REFACTOR: Consider returning the error rather than throwing
1163
1324
  if (failedTxs.length) {
1164
1325
  this.log.warn(`Simulated tx ${txHash} fails: ${failedTxs[0].error}`, { txHash });
@@ -1172,6 +1333,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1172
1333
  processedTx.txEffect,
1173
1334
  returns,
1174
1335
  processedTx.gasUsed,
1336
+ debugLogs,
1175
1337
  );
1176
1338
  } finally {
1177
1339
  await merkleTreeFork.close();
@@ -1183,21 +1345,34 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1183
1345
  { isSimulation, skipFeeEnforcement }: { isSimulation?: boolean; skipFeeEnforcement?: boolean } = {},
1184
1346
  ): Promise<TxValidationResult> {
1185
1347
  const db = this.worldStateSynchronizer.getCommitted();
1186
- const verifier = isSimulation ? undefined : this.proofVerifier;
1348
+ const verifier = isSimulation ? undefined : this.rpcProofVerifier;
1187
1349
 
1188
- // We accept transactions if they are not expired by the next slot (checked based on the IncludeByTimestamp field)
1350
+ // We accept transactions if they are not expired by the next slot (checked based on the ExpirationTimestamp field)
1189
1351
  const { ts: nextSlotTimestamp } = this.epochCache.getEpochAndSlotInNextL1Slot();
1190
- const blockNumber = (await this.blockSource.getBlockNumber()) + 1;
1191
- const validator = createValidatorForAcceptingTxs(db, this.contractDataSource, verifier, {
1192
- timestamp: nextSlotTimestamp,
1193
- blockNumber,
1194
- l1ChainId: this.l1ChainId,
1195
- rollupVersion: this.version,
1196
- setupAllowList: this.config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions()),
1197
- gasFees: await this.getCurrentBaseFees(),
1198
- skipFeeEnforcement,
1199
- txsPermitted: !this.config.disableTransactions,
1200
- });
1352
+ const blockNumber = BlockNumber((await this.blockSource.getBlockNumber()) + 1);
1353
+ const l1Constants = await this.blockSource.getL1Constants();
1354
+ const validator = createTxValidatorForAcceptingTxsOverRPC(
1355
+ db,
1356
+ this.contractDataSource,
1357
+ verifier,
1358
+ {
1359
+ timestamp: nextSlotTimestamp,
1360
+ blockNumber,
1361
+ l1ChainId: this.l1ChainId,
1362
+ rollupVersion: this.version,
1363
+ setupAllowList: [
1364
+ ...(await getDefaultAllowedSetupFunctions()),
1365
+ ...(this.config.txPublicSetupAllowListExtend ?? []),
1366
+ ],
1367
+ gasFees: await this.getCurrentMinFees(),
1368
+ skipFeeEnforcement,
1369
+ txsPermitted: !this.config.disableTransactions,
1370
+ rollupManaLimit: l1Constants.rollupManaLimit,
1371
+ maxBlockL2Gas: this.config.validateMaxL2BlockGas,
1372
+ maxBlockDAGas: this.config.validateMaxDABlockGas,
1373
+ },
1374
+ this.log.getBindings(),
1375
+ );
1201
1376
 
1202
1377
  return await validator.validateTx(tx);
1203
1378
  }
@@ -1219,7 +1394,15 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1219
1394
  archiver.updateConfig(config);
1220
1395
  }
1221
1396
  if (newConfig.realProofs !== this.config.realProofs) {
1222
- this.proofVerifier = config.realProofs ? await BBCircuitVerifier.new(newConfig) : new TestCircuitVerifier();
1397
+ await Promise.all([tryStop(this.peerProofVerifier), tryStop(this.rpcProofVerifier)]);
1398
+ if (newConfig.realProofs) {
1399
+ this.peerProofVerifier = await BatchChonkVerifier.new(newConfig, newConfig.bbChonkVerifyMaxBatch, 'peer');
1400
+ const rpcVerifier = await BBCircuitVerifier.new(newConfig);
1401
+ this.rpcProofVerifier = new QueuedIVCVerifier(rpcVerifier, newConfig.numConcurrentIVCVerifiers);
1402
+ } else {
1403
+ this.peerProofVerifier = new TestCircuitVerifier();
1404
+ this.rpcProofVerifier = new TestCircuitVerifier();
1405
+ }
1223
1406
  }
1224
1407
 
1225
1408
  this.config = newConfig;
@@ -1266,7 +1449,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1266
1449
  }
1267
1450
 
1268
1451
  // And it has an L2 block hash
1269
- const l2BlockHash = await archiver.getL2Tips().then(tips => tips.latest.hash);
1452
+ const l2BlockHash = await archiver.getL2Tips().then(tips => tips.proposed.hash);
1270
1453
  if (!l2BlockHash) {
1271
1454
  this.metrics.recordSnapshotError();
1272
1455
  throw new Error(`Archiver has no latest L2 block hash downloaded. Cannot start snapshot.`);
@@ -1294,13 +1477,13 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1294
1477
  return Promise.resolve();
1295
1478
  }
1296
1479
 
1297
- public async rollbackTo(targetBlock: number, force?: boolean): Promise<void> {
1480
+ public async rollbackTo(targetBlock: BlockNumber, force?: boolean): Promise<void> {
1298
1481
  const archiver = this.blockSource as Archiver;
1299
1482
  if (!('rollbackTo' in archiver)) {
1300
1483
  throw new Error('Archiver implementation does not support rollbacks.');
1301
1484
  }
1302
1485
 
1303
- const finalizedBlock = await archiver.getL2Tips().then(tips => tips.finalized.number);
1486
+ const finalizedBlock = await archiver.getL2Tips().then(tips => tips.finalized.block.number);
1304
1487
  if (targetBlock < finalizedBlock) {
1305
1488
  if (force) {
1306
1489
  this.log.warn(`Clearing world state database to allow rolling back behind finalized block ${finalizedBlock}`);
@@ -1361,17 +1544,108 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1361
1544
  }
1362
1545
  }
1363
1546
 
1547
+ public async reloadKeystore(): Promise<void> {
1548
+ if (!this.config.keyStoreDirectory?.length) {
1549
+ throw new BadRequestError(
1550
+ 'Cannot reload keystore: node is not using a file-based keystore. ' +
1551
+ 'Set KEY_STORE_DIRECTORY to use file-based keystores.',
1552
+ );
1553
+ }
1554
+ if (!this.validatorClient) {
1555
+ throw new BadRequestError('Cannot reload keystore: validator is not enabled.');
1556
+ }
1557
+
1558
+ this.log.info('Reloading keystore from disk');
1559
+
1560
+ // Re-read and validate keystore files
1561
+ const keyStores = loadKeystores(this.config.keyStoreDirectory);
1562
+ const newManager = new KeystoreManager(mergeKeystores(keyStores));
1563
+ await newManager.validateSigners();
1564
+ ValidatorClient.validateKeyStoreConfiguration(newManager, this.log);
1565
+
1566
+ // Validate that every validator's publisher keys overlap with the L1 signers
1567
+ // that were initialized at startup. Publishers cannot be hot-reloaded, so a
1568
+ // validator with a publisher key that doesn't match any existing L1 signer
1569
+ // would silently fail on every proposer slot.
1570
+ if (this.keyStoreManager && this.sequencer) {
1571
+ const oldAdapter = NodeKeystoreAdapter.fromKeyStoreManager(this.keyStoreManager);
1572
+ const availablePublishers = new Set(
1573
+ oldAdapter
1574
+ .getAttesterAddresses()
1575
+ .flatMap(a => oldAdapter.getPublisherAddresses(a).map(p => p.toString().toLowerCase())),
1576
+ );
1577
+
1578
+ const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
1579
+ for (const attester of newAdapter.getAttesterAddresses()) {
1580
+ const pubs = newAdapter.getPublisherAddresses(attester);
1581
+ if (pubs.length > 0 && !pubs.some(p => availablePublishers.has(p.toString().toLowerCase()))) {
1582
+ throw new BadRequestError(
1583
+ `Cannot reload keystore: validator ${attester} has publisher keys ` +
1584
+ `[${pubs.map(p => p.toString()).join(', ')}] but none match the L1 signers initialized at startup ` +
1585
+ `[${[...availablePublishers].join(', ')}]. Publishers cannot be hot-reloaded — ` +
1586
+ `use an existing publisher key or restart the node.`,
1587
+ );
1588
+ }
1589
+ }
1590
+ }
1591
+
1592
+ // Build adapters for old and new keystores to compute diff
1593
+ const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
1594
+ const newAddresses = newAdapter.getAttesterAddresses();
1595
+ const oldAddresses = this.keyStoreManager
1596
+ ? NodeKeystoreAdapter.fromKeyStoreManager(this.keyStoreManager).getAttesterAddresses()
1597
+ : [];
1598
+
1599
+ const oldSet = new Set(oldAddresses.map(a => a.toString()));
1600
+ const newSet = new Set(newAddresses.map(a => a.toString()));
1601
+ const added = newAddresses.filter(a => !oldSet.has(a.toString()));
1602
+ const removed = oldAddresses.filter(a => !newSet.has(a.toString()));
1603
+
1604
+ if (added.length > 0) {
1605
+ this.log.info(`Keystore reload: adding attester keys: ${added.map(a => a.toString()).join(', ')}`);
1606
+ }
1607
+ if (removed.length > 0) {
1608
+ this.log.info(`Keystore reload: removing attester keys: ${removed.map(a => a.toString()).join(', ')}`);
1609
+ }
1610
+ if (added.length === 0 && removed.length === 0) {
1611
+ this.log.info('Keystore reload: attester keys unchanged');
1612
+ }
1613
+
1614
+ // Update the validator client (coinbase, feeRecipient, attester keys)
1615
+ this.validatorClient.reloadKeystore(newManager);
1616
+
1617
+ // Update the publisher factory's keystore so newly-added validators
1618
+ // can be matched to existing publisher keys when proposing blocks.
1619
+ if (this.sequencer) {
1620
+ this.sequencer.updatePublisherNodeKeyStore(newAdapter);
1621
+ }
1622
+
1623
+ // Update slasher's "don't-slash-self" list with new validator addresses
1624
+ if (this.slasherClient && !this.config.slashSelfAllowed) {
1625
+ const slashValidatorsNever = unique(
1626
+ [...(this.config.slashValidatorsNever ?? []), ...newAddresses].map(a => a.toString()),
1627
+ ).map(EthAddress.fromString);
1628
+ this.slasherClient.updateConfig({ slashValidatorsNever });
1629
+ }
1630
+
1631
+ this.keyStoreManager = newManager;
1632
+ this.log.info('Keystore reloaded: coinbase, feeRecipient, and attester keys updated');
1633
+ }
1634
+
1635
+ #getInitialHeaderHash(): Promise<BlockHash> {
1636
+ if (!this.initialHeaderHashPromise) {
1637
+ this.initialHeaderHashPromise = this.worldStateSynchronizer.getCommitted().getInitialHeader().hash();
1638
+ }
1639
+ return this.initialHeaderHashPromise;
1640
+ }
1641
+
1364
1642
  /**
1365
1643
  * Returns an instance of MerkleTreeOperations having first ensured the world state is fully synched
1366
- * @param blockNumber - The block number at which to get the data.
1644
+ * @param block - The block parameter (block number, block hash, or 'latest') at which to get the data.
1367
1645
  * @returns An instance of a committed MerkleTreeOperations
1368
1646
  */
1369
- async #getWorldState(blockNumber: L2BlockNumber) {
1370
- if (typeof blockNumber === 'number' && blockNumber < INITIAL_L2_BLOCK_NUM - 1) {
1371
- throw new Error('Invalid block number to get world state for: ' + blockNumber);
1372
- }
1373
-
1374
- let blockSyncedTo: number = 0;
1647
+ protected async getWorldState(block: BlockParameter) {
1648
+ let blockSyncedTo: BlockNumber = BlockNumber.ZERO;
1375
1649
  try {
1376
1650
  // Attempt to sync the world state if necessary
1377
1651
  blockSyncedTo = await this.#syncWorldState();
@@ -1379,24 +1653,78 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1379
1653
  this.log.error(`Error getting world state: ${err}`);
1380
1654
  }
1381
1655
 
1382
- // using a snapshot could be less efficient than using the committed db
1383
- if (blockNumber === 'latest' /*|| blockNumber === blockSyncedTo*/) {
1384
- this.log.debug(`Using committed db for block ${blockNumber}, world state synced upto ${blockSyncedTo}`);
1656
+ if (block === 'latest') {
1657
+ this.log.debug(`Using committed db for block 'latest', world state synced upto ${blockSyncedTo}`);
1385
1658
  return this.worldStateSynchronizer.getCommitted();
1386
- } else if (blockNumber <= blockSyncedTo) {
1387
- this.log.debug(`Using snapshot for block ${blockNumber}, world state synced upto ${blockSyncedTo}`);
1388
- return this.worldStateSynchronizer.getSnapshot(blockNumber);
1659
+ }
1660
+
1661
+ // Get the block number, either directly from the parameter or by quering the archiver with the block hash
1662
+ let blockNumber: BlockNumber;
1663
+ if (BlockHash.isBlockHash(block)) {
1664
+ const initialBlockHash = await this.#getInitialHeaderHash();
1665
+ if (block.equals(initialBlockHash)) {
1666
+ // Block source doesn't handle initial header so we need to handle the case separately.
1667
+ return this.worldStateSynchronizer.getSnapshot(BlockNumber.ZERO);
1668
+ }
1669
+
1670
+ const header = await this.blockSource.getBlockHeaderByHash(block);
1671
+ if (!header) {
1672
+ throw new Error(
1673
+ `Block hash ${block.toString()} not found when querying world state. If the node API has been queried with anchor block hash possibly a reorg has occurred.`,
1674
+ );
1675
+ }
1676
+
1677
+ blockNumber = header.getBlockNumber();
1389
1678
  } else {
1390
- throw new Error(`Block ${blockNumber} not yet synced`);
1679
+ blockNumber = block as BlockNumber;
1680
+ }
1681
+
1682
+ // Check it's within world state sync range
1683
+ if (blockNumber > blockSyncedTo) {
1684
+ throw new Error(`Queried block ${block} not yet synced by the node (node is synced upto ${blockSyncedTo}).`);
1685
+ }
1686
+ this.log.debug(`Using snapshot for block ${blockNumber}, world state synced upto ${blockSyncedTo}`);
1687
+
1688
+ const snapshot = this.worldStateSynchronizer.getSnapshot(blockNumber);
1689
+
1690
+ // Double-check world-state synced to the same block hash as was requested
1691
+ if (BlockHash.isBlockHash(block)) {
1692
+ const blockHash = await snapshot.getLeafValue(MerkleTreeId.ARCHIVE, BigInt(blockNumber));
1693
+ if (!blockHash || !new BlockHash(blockHash).equals(block)) {
1694
+ throw new Error(
1695
+ `Block hash ${block.toString()} not found in world state at block number ${blockNumber}. If the node API has been queried with anchor block hash possibly a reorg has occurred.`,
1696
+ );
1697
+ }
1698
+ }
1699
+
1700
+ return snapshot;
1701
+ }
1702
+
1703
+ /** Resolves a block parameter to a block number. */
1704
+ protected async resolveBlockNumber(block: BlockParameter): Promise<BlockNumber> {
1705
+ if (block === 'latest') {
1706
+ return BlockNumber(await this.blockSource.getBlockNumber());
1707
+ }
1708
+ if (BlockHash.isBlockHash(block)) {
1709
+ const initialBlockHash = await this.#getInitialHeaderHash();
1710
+ if (block.equals(initialBlockHash)) {
1711
+ return BlockNumber.ZERO;
1712
+ }
1713
+ const header = await this.blockSource.getBlockHeaderByHash(block);
1714
+ if (!header) {
1715
+ throw new Error(`Block hash ${block.toString()} not found.`);
1716
+ }
1717
+ return header.getBlockNumber();
1391
1718
  }
1719
+ return block as BlockNumber;
1392
1720
  }
1393
1721
 
1394
1722
  /**
1395
1723
  * Ensure we fully sync the world state
1396
1724
  * @returns A promise that fulfils once the world state is synced
1397
1725
  */
1398
- async #syncWorldState(): Promise<number> {
1726
+ async #syncWorldState(): Promise<BlockNumber> {
1399
1727
  const blockSourceHeight = await this.blockSource.getBlockNumber();
1400
- return this.worldStateSynchronizer.syncImmediate(blockSourceHeight);
1728
+ return await this.worldStateSynchronizer.syncImmediate(blockSourceHeight);
1401
1729
  }
1402
1730
  }