@aztec/aztec-node 3.0.3 → 3.9.9-nightly.20260312

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,21 +1,15 @@
1
1
  import { Archiver, createArchiver } from '@aztec/archiver';
2
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';
3
+ import { type BlobClientInterface, createBlobClientWithFileStores } from '@aztec/blob-client/client';
4
+ import { Blob } from '@aztec/blob-lib';
5
+ import { ARCHIVE_HEIGHT, type L1_TO_L2_MSG_TREE_HEIGHT, type NOTE_HASH_TREE_HEIGHT } from '@aztec/constants';
12
6
  import { EpochCache, type EpochCacheInterface } from '@aztec/epoch-cache';
13
7
  import { createEthereumChain } from '@aztec/ethereum/chain';
14
8
  import { getPublicClient } from '@aztec/ethereum/client';
15
9
  import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts';
16
10
  import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses';
17
- import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types';
18
- import { compactArray, pick } from '@aztec/foundation/collection';
11
+ import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
12
+ import { chunkBy, compactArray, pick, unique } from '@aztec/foundation/collection';
19
13
  import { Fr } from '@aztec/foundation/curves/bn254';
20
14
  import { EthAddress } from '@aztec/foundation/eth-address';
21
15
  import { BadRequestError } from '@aztec/foundation/json-rpc';
@@ -23,21 +17,20 @@ import { type Logger, createLogger } from '@aztec/foundation/log';
23
17
  import { count } from '@aztec/foundation/string';
24
18
  import { DateProvider, Timer } from '@aztec/foundation/timer';
25
19
  import { MembershipWitness, SiblingPath } from '@aztec/foundation/trees';
26
- import { KeystoreManager, loadKeystores, mergeKeystores } from '@aztec/node-keystore';
20
+ import { type KeyStore, KeystoreManager, loadKeystores, mergeKeystores } from '@aztec/node-keystore';
27
21
  import { trySnapshotSync, uploadSnapshot } from '@aztec/node-lib/actions';
22
+ import { createForwarderL1TxUtilsFromSigners, createL1TxUtilsFromSigners } from '@aztec/node-lib/factories';
28
23
  import {
29
- createForwarderL1TxUtilsFromEthSigner,
30
- createL1TxUtilsWithBlobsFromEthSigner,
31
- } from '@aztec/node-lib/factories';
32
- import { type P2P, type P2PClientDeps, createP2PClient, getDefaultAllowedSetupFunctions } from '@aztec/p2p';
24
+ type P2P,
25
+ type P2PClientDeps,
26
+ createP2PClient,
27
+ createTxValidatorForAcceptingTxsOverRPC,
28
+ getDefaultAllowedSetupFunctions,
29
+ } from '@aztec/p2p';
33
30
  import { ProtocolContractAddress } from '@aztec/protocol-contracts';
34
- import {
35
- BlockBuilder,
36
- GlobalVariableBuilder,
37
- SequencerClient,
38
- type SequencerPublisher,
39
- createValidatorForAcceptingTxs,
40
- } from '@aztec/sequencer-client';
31
+ import { type ProverNode, type ProverNodeDeps, createProverNode } from '@aztec/prover-node';
32
+ import { createKeyStoreForProver } from '@aztec/prover-node/config';
33
+ import { GlobalVariableBuilder, SequencerClient, type SequencerPublisher } from '@aztec/sequencer-client';
41
34
  import { PublicProcessorFactory } from '@aztec/simulator/server';
42
35
  import {
43
36
  AttestationsBlockWatcher,
@@ -49,13 +42,14 @@ import {
49
42
  import { CollectionLimitsConfig, PublicSimulatorConfig } from '@aztec/stdlib/avm';
50
43
  import { AztecAddress } from '@aztec/stdlib/aztec-address';
51
44
  import {
45
+ type BlockData,
46
+ BlockHash,
52
47
  type BlockParameter,
53
48
  type DataInBlock,
54
- type L2Block,
55
- L2BlockHash,
49
+ L2Block,
56
50
  type L2BlockSource,
57
- type PublishedL2Block,
58
51
  } from '@aztec/stdlib/block';
52
+ import type { PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
59
53
  import type {
60
54
  ContractClassPublic,
61
55
  ContractDataSource,
@@ -82,9 +76,9 @@ import {
82
76
  type WorldStateSynchronizer,
83
77
  tryStop,
84
78
  } from '@aztec/stdlib/interfaces/server';
85
- import type { LogFilter, TxScopedL2Log } from '@aztec/stdlib/logs';
79
+ import type { DebugLogStore, LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs';
80
+ import { InMemoryDebugLogStore, NullDebugLogStore } from '@aztec/stdlib/logs';
86
81
  import { InboxLeaf, type L1ToL2MessageSource } from '@aztec/stdlib/messaging';
87
- import { P2PClientType } from '@aztec/stdlib/p2p';
88
82
  import type { Offense, SlashPayloadRound } from '@aztec/stdlib/slashing';
89
83
  import type { NullifierLeafPreimage, PublicDataTreeLeaf, PublicDataTreeLeafPreimage } from '@aztec/stdlib/trees';
90
84
  import { MerkleTreeId, NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees';
@@ -110,6 +104,8 @@ import {
110
104
  trackSpan,
111
105
  } from '@aztec/telemetry-client';
112
106
  import {
107
+ FullNodeCheckpointsBuilder as CheckpointsBuilder,
108
+ FullNodeCheckpointsBuilder,
113
109
  NodeKeystoreAdapter,
114
110
  ValidatorClient,
115
111
  createBlockProposalHandler,
@@ -129,6 +125,7 @@ import { NodeMetrics } from './node_metrics.js';
129
125
  */
130
126
  export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
131
127
  private metrics: NodeMetrics;
128
+ private initialHeaderHashPromise: Promise<BlockHash> | undefined = undefined;
132
129
 
133
130
  // Prevent two snapshot operations to happen simultaneously
134
131
  private isUploadingSnapshot = false;
@@ -144,6 +141,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
144
141
  protected readonly l1ToL2MessageSource: L1ToL2MessageSource,
145
142
  protected readonly worldStateSynchronizer: WorldStateSynchronizer,
146
143
  protected readonly sequencer: SequencerClient | undefined,
144
+ protected readonly proverNode: ProverNode | undefined,
147
145
  protected readonly slasherClient: SlasherClientInterface | undefined,
148
146
  protected readonly validatorsSentinel: Sentinel | undefined,
149
147
  protected readonly epochPruneWatcher: EpochPruneWatcher | undefined,
@@ -155,12 +153,23 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
155
153
  private proofVerifier: ClientProtocolCircuitVerifier,
156
154
  private telemetry: TelemetryClient = getTelemetryClient(),
157
155
  private log = createLogger('node'),
156
+ private blobClient?: BlobClientInterface,
157
+ private validatorClient?: ValidatorClient,
158
+ private keyStoreManager?: KeystoreManager,
159
+ private debugLogStore: DebugLogStore = new NullDebugLogStore(),
158
160
  ) {
159
161
  this.metrics = new NodeMetrics(telemetry, 'AztecNodeService');
160
162
  this.tracer = telemetry.getTracer('AztecNodeService');
161
163
 
162
164
  this.log.info(`Aztec Node version: ${this.packageVersion}`);
163
165
  this.log.info(`Aztec Node started on chain 0x${l1ChainId.toString(16)}`, config.l1Contracts);
166
+
167
+ // A defensive check that protects us against introducing a bug in the complex `createAndSync` function. We must
168
+ // never have debugLogStore enabled when not in test mode because then we would be accumulating debug logs in
169
+ // memory which could be a DoS vector on the sequencer (since no fees are paid for debug logs).
170
+ if (debugLogStore.isEnabled && config.realProofs) {
171
+ throw new Error('debugLogStore should never be enabled when realProofs are set');
172
+ }
164
173
  }
165
174
 
166
175
  public async getWorldStateSyncStatus(): Promise<WorldStateSyncStatus> {
@@ -184,12 +193,13 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
184
193
  logger?: Logger;
185
194
  publisher?: SequencerPublisher;
186
195
  dateProvider?: DateProvider;
187
- blobSinkClient?: BlobSinkClientInterface;
188
- p2pClientDeps?: P2PClientDeps<P2PClientType.Full>;
196
+ p2pClientDeps?: P2PClientDeps;
197
+ proverNodeDeps?: Partial<ProverNodeDeps>;
189
198
  } = {},
190
199
  options: {
191
200
  prefilledPublicData?: PublicDataTreeLeaf[];
192
201
  dontStartSequencer?: boolean;
202
+ dontStartProverNode?: boolean;
193
203
  } = {},
194
204
  ): Promise<AztecNodeService> {
195
205
  const config = { ...inputConfig }; // Copy the config so we dont mutate the input object
@@ -197,20 +207,31 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
197
207
  const packageVersion = getPackageVersion() ?? '';
198
208
  const telemetry = deps.telemetry ?? getTelemetryClient();
199
209
  const dateProvider = deps.dateProvider ?? new DateProvider();
200
- const blobSinkClient =
201
- deps.blobSinkClient ?? createBlobSinkClient(config, { logger: createLogger('node:blob-sink:client') });
202
210
  const ethereumChain = createEthereumChain(config.l1RpcUrls, config.l1ChainId);
203
211
 
204
- // Build a key store from file if given or from environment otherwise
212
+ // Build a key store from file if given or from environment otherwise.
213
+ // We keep the raw KeyStore available so we can merge with prover keys if enableProverNode is set.
205
214
  let keyStoreManager: KeystoreManager | undefined;
206
215
  const keyStoreProvided = config.keyStoreDirectory !== undefined && config.keyStoreDirectory.length > 0;
207
216
  if (keyStoreProvided) {
208
217
  const keyStores = loadKeystores(config.keyStoreDirectory!);
209
218
  keyStoreManager = new KeystoreManager(mergeKeystores(keyStores));
210
219
  } else {
211
- const keyStore = createKeyStoreForValidator(config);
212
- if (keyStore) {
213
- keyStoreManager = new KeystoreManager(keyStore);
220
+ const rawKeyStores: KeyStore[] = [];
221
+ const validatorKeyStore = createKeyStoreForValidator(config);
222
+ if (validatorKeyStore) {
223
+ rawKeyStores.push(validatorKeyStore);
224
+ }
225
+ if (config.enableProverNode) {
226
+ const proverKeyStore = createKeyStoreForProver(config);
227
+ if (proverKeyStore) {
228
+ rawKeyStores.push(proverKeyStore);
229
+ }
230
+ }
231
+ if (rawKeyStores.length > 0) {
232
+ keyStoreManager = new KeystoreManager(
233
+ rawKeyStores.length === 1 ? rawKeyStores[0] : mergeKeystores(rawKeyStores),
234
+ );
214
235
  }
215
236
  }
216
237
 
@@ -221,10 +242,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
221
242
  if (keyStoreManager === undefined) {
222
243
  throw new Error('Failed to create key store, a requirement for running a validator');
223
244
  }
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
- );
245
+ if (!keyStoreProvided && process.env.NODE_ENV !== 'test') {
246
+ log.warn("Keystore created from env: it's recommended to use a file-based key store for production");
228
247
  }
229
248
  ValidatorClient.validateKeyStoreConfiguration(keyStoreManager, log);
230
249
  }
@@ -238,7 +257,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
238
257
 
239
258
  const publicClient = createPublicClient({
240
259
  chain: ethereumChain.chainInfo,
241
- transport: fallback(config.l1RpcUrls.map((url: string) => http(url))),
260
+ transport: fallback(config.l1RpcUrls.map((url: string) => http(url, { batch: false }))),
242
261
  pollingInterval: config.viemPollingIntervalMS,
243
262
  });
244
263
 
@@ -252,10 +271,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
252
271
  config.l1Contracts = { ...config.l1Contracts, ...l1ContractsAddresses };
253
272
 
254
273
  const rollupContract = new RollupContract(publicClient, config.l1Contracts.rollupAddress.toString());
255
- const [l1GenesisTime, slotDuration, rollupVersionFromRollup] = await Promise.all([
274
+ const [l1GenesisTime, slotDuration, rollupVersionFromRollup, rollupManaLimit] = await Promise.all([
256
275
  rollupContract.getL1GenesisTime(),
257
276
  rollupContract.getSlotDuration(),
258
277
  rollupContract.getVersion(),
278
+ rollupContract.getManaLimit().then(Number),
259
279
  ] as const);
260
280
 
261
281
  config.rollupVersion ??= Number(rollupVersionFromRollup);
@@ -266,6 +286,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
266
286
  );
267
287
  }
268
288
 
289
+ const blobClient = await createBlobClientWithFileStores(config, log.createChild('blob-client'));
290
+
269
291
  // attempt snapshot sync if possible
270
292
  await trySnapshotSync(config, log);
271
293
 
@@ -273,7 +295,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
273
295
 
274
296
  const archiver = await createArchiver(
275
297
  config,
276
- { blobSinkClient, epochCache, telemetry, dateProvider },
298
+ { blobClient, epochCache, telemetry, dateProvider },
277
299
  { blockUntilSync: !config.skipArchiverInitialSync },
278
300
  );
279
301
 
@@ -288,14 +310,28 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
288
310
  config.realProofs || config.debugForceTxProofVerification
289
311
  ? await BBCircuitVerifier.new(config)
290
312
  : new TestCircuitVerifier(config.proverTestVerificationDelayMs);
313
+
314
+ let debugLogStore: DebugLogStore;
291
315
  if (!config.realProofs) {
292
316
  log.warn(`Aztec node is accepting fake proofs`);
317
+
318
+ debugLogStore = new InMemoryDebugLogStore();
319
+ log.info(
320
+ 'Aztec node started in test mode (realProofs set to false) hence debug logs from public functions will be collected and served',
321
+ );
322
+ } else {
323
+ debugLogStore = new NullDebugLogStore();
293
324
  }
325
+
294
326
  const proofVerifier = new QueuedIVCVerifier(config, circuitVerifier);
295
327
 
328
+ const proverOnly = config.enableProverNode && config.disableValidator;
329
+ if (proverOnly) {
330
+ log.info('Starting in prover-only mode: skipping validator, sequencer, sentinel, and slasher subsystems');
331
+ }
332
+
296
333
  // create the tx pool and the p2p client, which will need the l2 block source
297
334
  const p2pClient = await createP2PClient(
298
- P2PClientType.Full,
299
335
  config,
300
336
  archiver,
301
337
  proofVerifier,
@@ -307,55 +343,69 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
307
343
  deps.p2pClientDeps,
308
344
  );
309
345
 
310
- // We should really not be modifying the config object
311
- config.txPublicSetupAllowList = config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions());
346
+ // We'll accumulate sentinel watchers here
347
+ const watchers: Watcher[] = [];
312
348
 
313
- const blockBuilder = new BlockBuilder(
314
- { ...config, l1GenesisTime, slotDuration: Number(slotDuration) },
349
+ // Create FullNodeCheckpointsBuilder for block proposal handling and tx validation.
350
+ // Override maxTxsPerCheckpoint with the validator-specific limit if set.
351
+ const validatorCheckpointsBuilder = new FullNodeCheckpointsBuilder(
352
+ {
353
+ ...config,
354
+ l1GenesisTime,
355
+ slotDuration: Number(slotDuration),
356
+ rollupManaLimit,
357
+ maxTxsPerCheckpoint: config.validateMaxTxsPerCheckpoint,
358
+ },
315
359
  worldStateSynchronizer,
316
360
  archiver,
317
361
  dateProvider,
318
362
  telemetry,
319
363
  );
320
364
 
321
- // We'll accumulate sentinel watchers here
322
- const watchers: Watcher[] = [];
365
+ let validatorClient: ValidatorClient | undefined;
323
366
 
324
- // Create validator client if required
325
- const validatorClient = createValidatorClient(config, {
326
- p2pClient,
327
- telemetry,
328
- dateProvider,
329
- epochCache,
330
- blockBuilder,
331
- blockSource: archiver,
332
- l1ToL2MessageSource: archiver,
333
- keyStoreManager,
334
- });
367
+ if (!proverOnly) {
368
+ // Create validator client if required
369
+ validatorClient = await createValidatorClient(config, {
370
+ checkpointsBuilder: validatorCheckpointsBuilder,
371
+ worldState: worldStateSynchronizer,
372
+ p2pClient,
373
+ telemetry,
374
+ dateProvider,
375
+ epochCache,
376
+ blockSource: archiver,
377
+ l1ToL2MessageSource: archiver,
378
+ keyStoreManager,
379
+ blobClient,
380
+ });
335
381
 
336
- // If we have a validator client, register it as a source of offenses for the slasher,
337
- // and have it register callbacks on the p2p client *before* we start it, otherwise messages
338
- // like attestations or auths will fail.
339
- if (validatorClient) {
340
- watchers.push(validatorClient);
341
- if (!options.dontStartSequencer) {
342
- await validatorClient.registerHandlers();
382
+ // If we have a validator client, register it as a source of offenses for the slasher,
383
+ // and have it register callbacks on the p2p client *before* we start it, otherwise messages
384
+ // like attestations or auths will fail.
385
+ if (validatorClient) {
386
+ watchers.push(validatorClient);
387
+ if (!options.dontStartSequencer) {
388
+ await validatorClient.registerHandlers();
389
+ }
343
390
  }
344
391
  }
345
392
 
346
- // If there's no validator client but alwaysReexecuteBlockProposals is enabled,
347
- // create a BlockProposalHandler to reexecute block proposals for monitoring
348
- if (!validatorClient && config.alwaysReexecuteBlockProposals) {
349
- log.info('Setting up block proposal reexecution for monitoring');
393
+ // If there's no validator client, create a BlockProposalHandler to handle block proposals
394
+ // for monitoring or reexecution. Reexecution (default) allows us to follow the pending chain,
395
+ // while non-reexecution is used for validating the proposals and collecting their txs.
396
+ if (!validatorClient) {
397
+ const reexecute = !!config.alwaysReexecuteBlockProposals;
398
+ log.info(`Setting up block proposal handler` + (reexecute ? ' with reexecution of proposals' : ''));
350
399
  createBlockProposalHandler(config, {
351
- blockBuilder,
400
+ checkpointsBuilder: validatorCheckpointsBuilder,
401
+ worldState: worldStateSynchronizer,
352
402
  epochCache,
353
403
  blockSource: archiver,
354
404
  l1ToL2MessageSource: archiver,
355
405
  p2pClient,
356
406
  dateProvider,
357
407
  telemetry,
358
- }).registerForReexecution(p2pClient);
408
+ }).register(p2pClient, reexecute);
359
409
  }
360
410
 
361
411
  // Start world state and wait for it to sync to the archiver.
@@ -364,29 +414,33 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
364
414
  // Start p2p. Note that it depends on world state to be running.
365
415
  await p2pClient.start();
366
416
 
367
- const validatorsSentinel = await createSentinel(epochCache, archiver, p2pClient, config);
368
- if (validatorsSentinel && config.slashInactivityPenalty > 0n) {
369
- watchers.push(validatorsSentinel);
370
- }
371
-
417
+ let validatorsSentinel: Awaited<ReturnType<typeof createSentinel>> | undefined;
372
418
  let epochPruneWatcher: EpochPruneWatcher | undefined;
373
- if (config.slashPrunePenalty > 0n || config.slashDataWithholdingPenalty > 0n) {
374
- epochPruneWatcher = new EpochPruneWatcher(
375
- archiver,
376
- archiver,
377
- epochCache,
378
- p2pClient.getTxProvider(),
379
- blockBuilder,
380
- config,
381
- );
382
- watchers.push(epochPruneWatcher);
383
- }
384
-
385
- // We assume we want to slash for invalid attestations unless all max penalties are set to 0
386
419
  let attestationsBlockWatcher: AttestationsBlockWatcher | undefined;
387
- if (config.slashProposeInvalidAttestationsPenalty > 0n || config.slashAttestDescendantOfInvalidPenalty > 0n) {
388
- attestationsBlockWatcher = new AttestationsBlockWatcher(archiver, epochCache, config);
389
- watchers.push(attestationsBlockWatcher);
420
+
421
+ if (!proverOnly) {
422
+ validatorsSentinel = await createSentinel(epochCache, archiver, p2pClient, config);
423
+ if (validatorsSentinel && config.slashInactivityPenalty > 0n) {
424
+ watchers.push(validatorsSentinel);
425
+ }
426
+
427
+ if (config.slashPrunePenalty > 0n || config.slashDataWithholdingPenalty > 0n) {
428
+ epochPruneWatcher = new EpochPruneWatcher(
429
+ archiver,
430
+ archiver,
431
+ epochCache,
432
+ p2pClient.getTxProvider(),
433
+ validatorCheckpointsBuilder,
434
+ config,
435
+ );
436
+ watchers.push(epochPruneWatcher);
437
+ }
438
+
439
+ // We assume we want to slash for invalid attestations unless all max penalties are set to 0
440
+ if (config.slashProposeInvalidAttestationsPenalty > 0n || config.slashAttestDescendantOfInvalidPenalty > 0n) {
441
+ attestationsBlockWatcher = new AttestationsBlockWatcher(archiver, epochCache, config);
442
+ watchers.push(attestationsBlockWatcher);
443
+ }
390
444
  }
391
445
 
392
446
  // Start p2p-related services once the archiver has completed sync
@@ -404,7 +458,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
404
458
  // Validator enabled, create/start relevant service
405
459
  let sequencer: SequencerClient | undefined;
406
460
  let slasherClient: SlasherClientInterface | undefined;
407
- if (!config.disableValidator) {
461
+ if (!config.disableValidator && validatorClient) {
408
462
  // We create a slasher only if we have a sequencer, since all slashing actions go through the sequencer publisher
409
463
  // as they are executed when the node is selected as proposer.
410
464
  const validatorAddresses = keyStoreManager
@@ -423,22 +477,31 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
423
477
  );
424
478
  await slasherClient.start();
425
479
 
426
- const l1TxUtils = config.publisherForwarderAddress
427
- ? await createForwarderL1TxUtilsFromEthSigner(
480
+ const l1TxUtils = config.sequencerPublisherForwarderAddress
481
+ ? await createForwarderL1TxUtilsFromSigners(
428
482
  publicClient,
429
483
  keyStoreManager!.createAllValidatorPublisherSigners(),
430
- config.publisherForwarderAddress,
484
+ config.sequencerPublisherForwarderAddress,
431
485
  { ...config, scope: 'sequencer' },
432
- { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider },
486
+ { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider, kzg: Blob.getViemKzgInstance() },
433
487
  )
434
- : await createL1TxUtilsWithBlobsFromEthSigner(
488
+ : await createL1TxUtilsFromSigners(
435
489
  publicClient,
436
490
  keyStoreManager!.createAllValidatorPublisherSigners(),
437
491
  { ...config, scope: 'sequencer' },
438
- { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider },
492
+ { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider, kzg: Blob.getViemKzgInstance() },
439
493
  );
440
494
 
441
495
  // Create and start the sequencer client
496
+ const checkpointsBuilder = new CheckpointsBuilder(
497
+ { ...config, l1GenesisTime, slotDuration: Number(slotDuration), rollupManaLimit },
498
+ worldStateSynchronizer,
499
+ archiver,
500
+ dateProvider,
501
+ telemetry,
502
+ debugLogStore,
503
+ );
504
+
442
505
  sequencer = await SequencerClient.new(config, {
443
506
  ...deps,
444
507
  epochCache,
@@ -447,12 +510,12 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
447
510
  p2pClient,
448
511
  worldStateSynchronizer,
449
512
  slasherClient,
450
- blockBuilder,
513
+ checkpointsBuilder,
451
514
  l2BlockSource: archiver,
452
515
  l1ToL2MessageSource: archiver,
453
516
  telemetry,
454
517
  dateProvider,
455
- blobSinkClient,
518
+ blobClient,
456
519
  nodeKeyStore: keyStoreManager!,
457
520
  });
458
521
  }
@@ -464,7 +527,37 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
464
527
  log.warn(`Sequencer created but not started`);
465
528
  }
466
529
 
467
- return new AztecNodeService(
530
+ // Create prover node subsystem if enabled
531
+ let proverNode: ProverNode | undefined;
532
+ if (config.enableProverNode) {
533
+ proverNode = await createProverNode(config, {
534
+ ...deps.proverNodeDeps,
535
+ telemetry,
536
+ dateProvider,
537
+ archiver,
538
+ worldStateSynchronizer,
539
+ p2pClient,
540
+ epochCache,
541
+ blobClient,
542
+ keyStoreManager,
543
+ });
544
+
545
+ if (!options.dontStartProverNode) {
546
+ await proverNode.start();
547
+ log.info(`Prover node subsystem started`);
548
+ } else {
549
+ log.info(`Prover node subsystem created but not started`);
550
+ }
551
+ }
552
+
553
+ const globalVariableBuilder = new GlobalVariableBuilder({
554
+ ...config,
555
+ rollupVersion: BigInt(config.rollupVersion),
556
+ l1GenesisTime,
557
+ slotDuration: Number(slotDuration),
558
+ });
559
+
560
+ const node = new AztecNodeService(
468
561
  config,
469
562
  p2pClient,
470
563
  archiver,
@@ -473,18 +566,25 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
473
566
  archiver,
474
567
  worldStateSynchronizer,
475
568
  sequencer,
569
+ proverNode,
476
570
  slasherClient,
477
571
  validatorsSentinel,
478
572
  epochPruneWatcher,
479
573
  ethereumChain.chainInfo.id,
480
574
  config.rollupVersion,
481
- new GlobalVariableBuilder(config),
575
+ globalVariableBuilder,
482
576
  epochCache,
483
577
  packageVersion,
484
578
  proofVerifier,
485
579
  telemetry,
486
580
  log,
581
+ blobClient,
582
+ validatorClient,
583
+ keyStoreManager,
584
+ debugLogStore,
487
585
  );
586
+
587
+ return node;
488
588
  }
489
589
 
490
590
  /**
@@ -495,6 +595,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
495
595
  return this.sequencer;
496
596
  }
497
597
 
598
+ /** Returns the prover node subsystem, if enabled. */
599
+ public getProverNode(): ProverNode | undefined {
600
+ return this.proverNode;
601
+ }
602
+
498
603
  public getBlockSource(): L2BlockSource {
499
604
  return this.blockSource;
500
605
  }
@@ -520,7 +625,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
520
625
  }
521
626
 
522
627
  public async getAllowedPublicSetup(): Promise<AllowedElement[]> {
523
- return this.config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions());
628
+ return [...(await getDefaultAllowedSetupFunctions()), ...(this.config.txPublicSetupAllowListExtend ?? [])];
524
629
  }
525
630
 
526
631
  /**
@@ -548,19 +653,26 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
548
653
  enr,
549
654
  l1ContractAddresses: contractAddresses,
550
655
  protocolContractAddresses: protocolContractAddresses,
656
+ realProofs: !!this.config.realProofs,
551
657
  };
552
658
 
553
659
  return nodeInfo;
554
660
  }
555
661
 
556
662
  /**
557
- * Get a block specified by its number.
558
- * @param number - The block number being requested.
663
+ * Get a block specified by its block number, block hash, or 'latest'.
664
+ * @param block - The block parameter (block number, block hash, or 'latest').
559
665
  * @returns The requested block.
560
666
  */
561
- public async getBlock(number: BlockParameter): Promise<L2Block | undefined> {
562
- const blockNumber = number === 'latest' ? await this.getBlockNumber() : (number as BlockNumber);
563
- return await this.blockSource.getBlock(blockNumber);
667
+ public async getBlock(block: BlockParameter): Promise<L2Block | undefined> {
668
+ if (BlockHash.isBlockHash(block)) {
669
+ return this.getBlockByHash(block);
670
+ }
671
+ const blockNumber = block === 'latest' ? await this.getBlockNumber() : (block as BlockNumber);
672
+ if (blockNumber === BlockNumber.ZERO) {
673
+ return this.buildInitialBlock();
674
+ }
675
+ return await this.blockSource.getL2Block(blockNumber);
564
676
  }
565
677
 
566
678
  /**
@@ -568,9 +680,17 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
568
680
  * @param blockHash - The block hash being requested.
569
681
  * @returns The requested block.
570
682
  */
571
- public async getBlockByHash(blockHash: Fr): Promise<L2Block | undefined> {
572
- const publishedBlock = await this.blockSource.getPublishedBlockByHash(blockHash);
573
- return publishedBlock?.block;
683
+ public async getBlockByHash(blockHash: BlockHash): Promise<L2Block | undefined> {
684
+ const initialBlockHash = await this.#getInitialHeaderHash();
685
+ if (blockHash.equals(initialBlockHash)) {
686
+ return this.buildInitialBlock();
687
+ }
688
+ return await this.blockSource.getL2BlockByHash(blockHash);
689
+ }
690
+
691
+ private buildInitialBlock(): L2Block {
692
+ const initialHeader = this.worldStateSynchronizer.getCommitted().getInitialHeader();
693
+ return L2Block.empty(initialHeader);
574
694
  }
575
695
 
576
696
  /**
@@ -579,8 +699,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
579
699
  * @returns The requested block.
580
700
  */
581
701
  public async getBlockByArchive(archive: Fr): Promise<L2Block | undefined> {
582
- const publishedBlock = await this.blockSource.getPublishedBlockByArchive(archive);
583
- return publishedBlock?.block;
702
+ return await this.blockSource.getL2BlockByArchive(archive);
584
703
  }
585
704
 
586
705
  /**
@@ -590,19 +709,23 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
590
709
  * @returns The blocks requested.
591
710
  */
592
711
  public async getBlocks(from: BlockNumber, limit: number): Promise<L2Block[]> {
593
- return (await this.blockSource.getBlocks(from, limit)) ?? [];
712
+ return (await this.blockSource.getBlocks(from, BlockNumber(limit))) ?? [];
594
713
  }
595
714
 
596
- public async getPublishedBlocks(from: BlockNumber, limit: number): Promise<PublishedL2Block[]> {
597
- return (await this.blockSource.getPublishedBlocks(from, limit)) ?? [];
715
+ public async getCheckpoints(from: CheckpointNumber, limit: number): Promise<PublishedCheckpoint[]> {
716
+ return (await this.blockSource.getCheckpoints(from, limit)) ?? [];
717
+ }
718
+
719
+ public async getCheckpointedBlocks(from: BlockNumber, limit: number) {
720
+ return (await this.blockSource.getCheckpointedBlocks(from, limit)) ?? [];
598
721
  }
599
722
 
600
723
  /**
601
- * Method to fetch the current base fees.
602
- * @returns The current base fees.
724
+ * Method to fetch the current min L2 fees.
725
+ * @returns The current min L2 fees.
603
726
  */
604
- public async getCurrentBaseFees(): Promise<GasFees> {
605
- return await this.globalVariableBuilder.getCurrentBaseFees();
727
+ public async getCurrentMinFees(): Promise<GasFees> {
728
+ return await this.globalVariableBuilder.getCurrentMinFees();
606
729
  }
607
730
 
608
731
  public async getMaxPriorityFees(): Promise<GasFees> {
@@ -625,6 +748,14 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
625
748
  return await this.blockSource.getProvenBlockNumber();
626
749
  }
627
750
 
751
+ public async getCheckpointedBlockNumber(): Promise<BlockNumber> {
752
+ return await this.blockSource.getCheckpointedL2BlockNumber();
753
+ }
754
+
755
+ public getCheckpointNumber(): Promise<CheckpointNumber> {
756
+ return this.blockSource.getCheckpointNumber();
757
+ }
758
+
628
759
  /**
629
760
  * Method to fetch the version of the package.
630
761
  * @returns The node package version
@@ -657,15 +788,43 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
657
788
  return this.contractDataSource.getContract(address);
658
789
  }
659
790
 
660
- /**
661
- * Gets all logs that match any of the received tags (i.e. logs with their first field equal to a tag).
662
- * @param tags - The tags to filter the logs by.
663
- * @param logsPerTag - The maximum number of logs to return for each tag. By default no limit is set
664
- * @returns For each received tag, an array of matching logs is returned. An empty array implies no logs match
665
- * that tag.
666
- */
667
- public getLogsByTags(tags: Fr[], logsPerTag?: number): Promise<TxScopedL2Log[][]> {
668
- return this.logsSource.getLogsByTags(tags, logsPerTag);
791
+ public async getPrivateLogsByTags(
792
+ tags: SiloedTag[],
793
+ page?: number,
794
+ referenceBlock?: BlockHash,
795
+ ): Promise<TxScopedL2Log[][]> {
796
+ if (referenceBlock) {
797
+ const initialBlockHash = await this.#getInitialHeaderHash();
798
+ if (!referenceBlock.equals(initialBlockHash)) {
799
+ const header = await this.blockSource.getBlockHeaderByHash(referenceBlock);
800
+ if (!header) {
801
+ throw new Error(
802
+ `Block ${referenceBlock.toString()} not found in the node. This might indicate a reorg has occurred.`,
803
+ );
804
+ }
805
+ }
806
+ }
807
+ return this.logsSource.getPrivateLogsByTags(tags, page);
808
+ }
809
+
810
+ public async getPublicLogsByTagsFromContract(
811
+ contractAddress: AztecAddress,
812
+ tags: Tag[],
813
+ page?: number,
814
+ referenceBlock?: BlockHash,
815
+ ): Promise<TxScopedL2Log[][]> {
816
+ if (referenceBlock) {
817
+ const initialBlockHash = await this.#getInitialHeaderHash();
818
+ if (!referenceBlock.equals(initialBlockHash)) {
819
+ const header = await this.blockSource.getBlockHeaderByHash(referenceBlock);
820
+ if (!header) {
821
+ throw new Error(
822
+ `Block ${referenceBlock.toString()} not found in the node. This might indicate a reorg has occurred.`,
823
+ );
824
+ }
825
+ }
826
+ }
827
+ return this.logsSource.getPublicLogsByTagsFromContract(contractAddress, tags, page);
669
828
  }
670
829
 
671
830
  /**
@@ -707,26 +866,36 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
707
866
  }
708
867
 
709
868
  await this.p2pClient!.sendTx(tx);
710
- this.metrics.receivedTx(timer.ms(), true);
711
- this.log.info(`Received tx ${txHash}`, { txHash });
869
+ const duration = timer.ms();
870
+ this.metrics.receivedTx(duration, true);
871
+ this.log.info(`Received tx ${txHash} in ${duration}ms`, { txHash });
712
872
  }
713
873
 
714
874
  public async getTxReceipt(txHash: TxHash): Promise<TxReceipt> {
715
- let txReceipt = new TxReceipt(txHash, TxStatus.DROPPED, 'Tx dropped by P2P node.');
716
-
717
- // We first check if the tx is in pending (instead of first checking if it is mined) because if we first check
718
- // for mined and then for pending there could be a race condition where the tx is mined between the two checks
719
- // and we would incorrectly return a TxReceipt with status DROPPED
720
- if ((await this.p2pClient.getTxStatus(txHash)) === 'pending') {
721
- txReceipt = new TxReceipt(txHash, TxStatus.PENDING, '');
722
- }
875
+ // Check the tx pool status first. If the tx is known to the pool (pending or mined), we'll use that
876
+ // as a fallback if we don't find a settled receipt in the archiver.
877
+ const txPoolStatus = await this.p2pClient.getTxStatus(txHash);
878
+ const isKnownToPool = txPoolStatus === 'pending' || txPoolStatus === 'mined';
723
879
 
880
+ // Then get the actual tx from the archiver, which tracks every tx in a mined block.
724
881
  const settledTxReceipt = await this.blockSource.getSettledTxReceipt(txHash);
882
+
883
+ let receipt: TxReceipt;
725
884
  if (settledTxReceipt) {
726
- txReceipt = settledTxReceipt;
885
+ receipt = settledTxReceipt;
886
+ } else if (isKnownToPool) {
887
+ // If the tx is in the pool but not in the archiver, it's pending.
888
+ // This handles race conditions between archiver and p2p, where the archiver
889
+ // has pruned the block in which a tx was mined, but p2p has not caught up yet.
890
+ receipt = new TxReceipt(txHash, TxStatus.PENDING, undefined, undefined);
891
+ } else {
892
+ // Otherwise, if we don't know the tx, we consider it dropped.
893
+ receipt = new TxReceipt(txHash, TxStatus.DROPPED, undefined, 'Tx dropped by P2P node');
727
894
  }
728
895
 
729
- return txReceipt;
896
+ this.debugLogStore.decorateReceiptWithLogs(txHash.toString(), receipt);
897
+
898
+ return receipt;
730
899
  }
731
900
 
732
901
  public getTxEffect(txHash: TxHash): Promise<IndexedTxEffect | undefined> {
@@ -743,13 +912,23 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
743
912
  await tryStop(this.slasherClient);
744
913
  await tryStop(this.proofVerifier);
745
914
  await tryStop(this.sequencer);
915
+ await tryStop(this.proverNode);
746
916
  await tryStop(this.p2pClient);
747
917
  await tryStop(this.worldStateSynchronizer);
748
918
  await tryStop(this.blockSource);
919
+ await tryStop(this.blobClient);
749
920
  await tryStop(this.telemetry);
750
921
  this.log.info(`Stopped Aztec Node`);
751
922
  }
752
923
 
924
+ /**
925
+ * Returns the blob client used by this node.
926
+ * @internal - Exposed for testing purposes only.
927
+ */
928
+ public getBlobClient(): BlobClientInterface | undefined {
929
+ return this.blobClient;
930
+ }
931
+
753
932
  /**
754
933
  * Method to retrieve pending txs.
755
934
  * @param limit - The number of items to returns
@@ -782,121 +961,91 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
782
961
  return compactArray(await Promise.all(txHashes.map(txHash => this.getTxByHash(txHash))));
783
962
  }
784
963
 
785
- /**
786
- * Find the indexes of the given leaves in the given tree along with a block metadata pointing to the block in which
787
- * the leaves were inserted.
788
- * @param blockNumber - The block number at which to get the data or 'latest' for latest data.
789
- * @param treeId - The tree to search in.
790
- * @param leafValues - The values to search for.
791
- * @returns The indices of leaves and the block metadata of a block in which the leaves were inserted.
792
- */
793
964
  public async findLeavesIndexes(
794
- blockNumber: BlockParameter,
965
+ referenceBlock: BlockParameter,
795
966
  treeId: MerkleTreeId,
796
967
  leafValues: Fr[],
797
968
  ): Promise<(DataInBlock<bigint> | undefined)[]> {
798
- const committedDb = await this.#getWorldState(blockNumber);
969
+ const committedDb = await this.getWorldState(referenceBlock);
799
970
  const maybeIndices = await committedDb.findLeafIndices(
800
971
  treeId,
801
972
  leafValues.map(x => x.toBuffer()),
802
973
  );
803
- // We filter out undefined values
804
- const indices = maybeIndices.filter(x => x !== undefined) as bigint[];
974
+ // Filter out undefined values to query block numbers only for found leaves
975
+ const definedIndices = maybeIndices.filter(x => x !== undefined);
805
976
 
806
- // Now we find the block numbers for the indices
807
- const blockNumbers = await committedDb.getBlockNumbersForLeafIndices(treeId, indices);
977
+ // Now we find the block numbers for the defined indices
978
+ const blockNumbers = await committedDb.getBlockNumbersForLeafIndices(treeId, definedIndices);
808
979
 
809
- // If any of the block numbers are undefined, we throw an error.
810
- for (let i = 0; i < indices.length; i++) {
811
- if (blockNumbers[i] === undefined) {
812
- throw new Error(`Block number is undefined for leaf index ${indices[i]} in tree ${MerkleTreeId[treeId]}`);
980
+ // Build a map from leaf index to block number
981
+ const indexToBlockNumber = new Map<bigint, BlockNumber>();
982
+ for (let i = 0; i < definedIndices.length; i++) {
983
+ const blockNumber = blockNumbers[i];
984
+ if (blockNumber === undefined) {
985
+ throw new Error(
986
+ `Block number is undefined for leaf index ${definedIndices[i]} in tree ${MerkleTreeId[treeId]}`,
987
+ );
813
988
  }
989
+ indexToBlockNumber.set(definedIndices[i], blockNumber);
814
990
  }
815
991
 
816
992
  // Get unique block numbers in order to optimize num calls to getLeafValue function.
817
- const uniqueBlockNumbers = [...new Set(blockNumbers.filter(x => x !== undefined))];
993
+ const uniqueBlockNumbers = [...new Set(indexToBlockNumber.values())];
818
994
 
819
- // Now we obtain the block hashes from the archive tree by calling await `committedDb.getLeafValue(treeId, index)`
820
- // (note that block number corresponds to the leaf index in the archive tree).
995
+ // Now we obtain the block hashes from the archive tree (block number = leaf index in archive tree).
821
996
  const blockHashes = await Promise.all(
822
997
  uniqueBlockNumbers.map(blockNumber => {
823
998
  return committedDb.getLeafValue(MerkleTreeId.ARCHIVE, BigInt(blockNumber));
824
999
  }),
825
1000
  );
826
1001
 
827
- // If any of the block hashes are undefined, we throw an error.
1002
+ // Build a map from block number to block hash
1003
+ const blockNumberToHash = new Map<BlockNumber, Fr>();
828
1004
  for (let i = 0; i < uniqueBlockNumbers.length; i++) {
829
- if (blockHashes[i] === undefined) {
1005
+ const blockHash = blockHashes[i];
1006
+ if (blockHash === undefined) {
830
1007
  throw new Error(`Block hash is undefined for block number ${uniqueBlockNumbers[i]}`);
831
1008
  }
1009
+ blockNumberToHash.set(uniqueBlockNumbers[i], blockHash);
832
1010
  }
833
1011
 
834
1012
  // Create DataInBlock objects by combining indices, blockNumbers and blockHashes and return them.
835
- return maybeIndices.map((index, i) => {
1013
+ return maybeIndices.map(index => {
836
1014
  if (index === undefined) {
837
1015
  return undefined;
838
1016
  }
839
- const blockNumber = blockNumbers[i];
1017
+ const blockNumber = indexToBlockNumber.get(index);
840
1018
  if (blockNumber === undefined) {
841
- return undefined;
1019
+ throw new Error(`Block number not found for leaf index ${index} in tree ${MerkleTreeId[treeId]}`);
842
1020
  }
843
- const blockHashIndex = uniqueBlockNumbers.indexOf(blockNumber);
844
- const blockHash = blockHashes[blockHashIndex];
845
- if (!blockHash) {
846
- return undefined;
1021
+ const blockHash = blockNumberToHash.get(blockNumber);
1022
+ if (blockHash === undefined) {
1023
+ throw new Error(`Block hash not found for block number ${blockNumber}`);
847
1024
  }
848
1025
  return {
849
- l2BlockNumber: BlockNumber(Number(blockNumber)),
850
- l2BlockHash: L2BlockHash.fromField(blockHash),
1026
+ l2BlockNumber: blockNumber,
1027
+ l2BlockHash: new BlockHash(blockHash),
851
1028
  data: index,
852
1029
  };
853
1030
  });
854
1031
  }
855
1032
 
856
- /**
857
- * Returns a sibling path for the given index in the nullifier tree.
858
- * @param blockNumber - The block number at which to get the data.
859
- * @param leafIndex - The index of the leaf for which the sibling path is required.
860
- * @returns The sibling path for the leaf index.
861
- */
862
- public async getNullifierSiblingPath(
863
- blockNumber: BlockParameter,
864
- leafIndex: bigint,
865
- ): Promise<SiblingPath<typeof NULLIFIER_TREE_HEIGHT>> {
866
- const committedDb = await this.#getWorldState(blockNumber);
867
- return committedDb.getSiblingPath(MerkleTreeId.NULLIFIER_TREE, leafIndex);
868
- }
869
-
870
- /**
871
- * Returns a sibling path for the given index in the data tree.
872
- * @param blockNumber - The block number at which to get the data.
873
- * @param leafIndex - The index of the leaf for which the sibling path is required.
874
- * @returns The sibling path for the leaf index.
875
- */
876
- public async getNoteHashSiblingPath(
877
- blockNumber: BlockParameter,
878
- leafIndex: bigint,
879
- ): Promise<SiblingPath<typeof NOTE_HASH_TREE_HEIGHT>> {
880
- const committedDb = await this.#getWorldState(blockNumber);
881
- return committedDb.getSiblingPath(MerkleTreeId.NOTE_HASH_TREE, leafIndex);
882
- }
883
-
884
- public async getArchiveMembershipWitness(
885
- blockNumber: BlockParameter,
886
- archive: Fr,
1033
+ public async getBlockHashMembershipWitness(
1034
+ referenceBlock: BlockParameter,
1035
+ blockHash: BlockHash,
887
1036
  ): Promise<MembershipWitness<typeof ARCHIVE_HEIGHT> | undefined> {
888
- const committedDb = await this.#getWorldState(blockNumber);
889
- const [pathAndIndex] = await committedDb.findSiblingPaths<MerkleTreeId.ARCHIVE>(MerkleTreeId.ARCHIVE, [archive]);
1037
+ const committedDb = await this.getWorldState(referenceBlock);
1038
+ const [pathAndIndex] = await committedDb.findSiblingPaths<MerkleTreeId.ARCHIVE>(MerkleTreeId.ARCHIVE, [blockHash]);
890
1039
  return pathAndIndex === undefined
891
1040
  ? undefined
892
1041
  : MembershipWitness.fromSiblingPath(pathAndIndex.index, pathAndIndex.path);
893
1042
  }
894
1043
 
895
1044
  public async getNoteHashMembershipWitness(
896
- blockNumber: BlockParameter,
1045
+ referenceBlock: BlockParameter,
897
1046
  noteHash: Fr,
898
1047
  ): Promise<MembershipWitness<typeof NOTE_HASH_TREE_HEIGHT> | undefined> {
899
- const committedDb = await this.#getWorldState(blockNumber);
1048
+ const committedDb = await this.getWorldState(referenceBlock);
900
1049
  const [pathAndIndex] = await committedDb.findSiblingPaths<MerkleTreeId.NOTE_HASH_TREE>(
901
1050
  MerkleTreeId.NOTE_HASH_TREE,
902
1051
  [noteHash],
@@ -906,17 +1055,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
906
1055
  : MembershipWitness.fromSiblingPath(pathAndIndex.index, pathAndIndex.path);
907
1056
  }
908
1057
 
909
- /**
910
- * Returns the index and a sibling path for a leaf in the committed l1 to l2 data tree.
911
- * @param blockNumber - The block number at which to get the data.
912
- * @param l1ToL2Message - The l1ToL2Message to get the index / sibling path for.
913
- * @returns A tuple of the index and the sibling path of the L1ToL2Message (undefined if not found).
914
- */
915
1058
  public async getL1ToL2MessageMembershipWitness(
916
- blockNumber: BlockParameter,
1059
+ referenceBlock: BlockParameter,
917
1060
  l1ToL2Message: Fr,
918
1061
  ): Promise<[bigint, SiblingPath<typeof L1_TO_L2_MSG_TREE_HEIGHT>] | undefined> {
919
- const db = await this.#getWorldState(blockNumber);
1062
+ const db = await this.getWorldState(referenceBlock);
920
1063
  const [witness] = await db.findSiblingPaths(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, [l1ToL2Message]);
921
1064
  if (!witness) {
922
1065
  return undefined;
@@ -926,11 +1069,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
926
1069
  return [witness.index, witness.path];
927
1070
  }
928
1071
 
929
- public async getL1ToL2MessageBlock(l1ToL2Message: Fr): Promise<BlockNumber | undefined> {
1072
+ public async getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise<CheckpointNumber | undefined> {
930
1073
  const messageIndex = await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message);
931
- return messageIndex
932
- ? BlockNumber.fromCheckpointNumber(InboxLeaf.checkpointNumberFromIndex(messageIndex))
933
- : undefined;
1074
+ return messageIndex ? InboxLeaf.checkpointNumberFromIndex(messageIndex) : undefined;
934
1075
  }
935
1076
 
936
1077
  /**
@@ -944,56 +1085,26 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
944
1085
  }
945
1086
 
946
1087
  /**
947
- * Returns all the L2 to L1 messages in a block.
948
- * @param blockNumber - The block number at which to get the data.
949
- * @returns The L2 to L1 messages (undefined if the block number is not found).
1088
+ * Returns all the L2 to L1 messages in an epoch.
1089
+ * @param epoch - The epoch at which to get the data.
1090
+ * @returns The L2 to L1 messages (empty array if the epoch is not found).
950
1091
  */
951
- public async getL2ToL1Messages(blockNumber: BlockParameter): Promise<Fr[][] | undefined> {
952
- const block = await this.blockSource.getBlock(
953
- blockNumber === 'latest' ? await this.getBlockNumber() : (blockNumber as BlockNumber),
1092
+ public async getL2ToL1Messages(epoch: EpochNumber): Promise<Fr[][][][]> {
1093
+ // Assumes `getCheckpointedBlocksForEpoch` returns blocks in ascending order of block number.
1094
+ const checkpointedBlocks = await this.blockSource.getCheckpointedBlocksForEpoch(epoch);
1095
+ const blocksInCheckpoints = chunkBy(checkpointedBlocks, cb => cb.block.header.globalVariables.slotNumber).map(
1096
+ group => group.map(cb => cb.block),
1097
+ );
1098
+ return blocksInCheckpoints.map(blocks =>
1099
+ blocks.map(block => block.body.txEffects.map(txEffect => txEffect.l2ToL1Msgs)),
954
1100
  );
955
- return block?.body.txEffects.map(txEffect => txEffect.l2ToL1Msgs);
956
- }
957
-
958
- /**
959
- * Returns a sibling path for a leaf in the committed blocks tree.
960
- * @param blockNumber - The block number at which to get the data.
961
- * @param leafIndex - Index of the leaf in the tree.
962
- * @returns The sibling path.
963
- */
964
- public async getArchiveSiblingPath(
965
- blockNumber: BlockParameter,
966
- leafIndex: bigint,
967
- ): Promise<SiblingPath<typeof ARCHIVE_HEIGHT>> {
968
- const committedDb = await this.#getWorldState(blockNumber);
969
- return committedDb.getSiblingPath(MerkleTreeId.ARCHIVE, leafIndex);
970
- }
971
-
972
- /**
973
- * Returns a sibling path for a leaf in the committed public data tree.
974
- * @param blockNumber - The block number at which to get the data.
975
- * @param leafIndex - Index of the leaf in the tree.
976
- * @returns The sibling path.
977
- */
978
- public async getPublicDataSiblingPath(
979
- blockNumber: BlockParameter,
980
- leafIndex: bigint,
981
- ): Promise<SiblingPath<typeof PUBLIC_DATA_TREE_HEIGHT>> {
982
- const committedDb = await this.#getWorldState(blockNumber);
983
- return committedDb.getSiblingPath(MerkleTreeId.PUBLIC_DATA_TREE, leafIndex);
984
1101
  }
985
1102
 
986
- /**
987
- * Returns a nullifier membership witness for a given nullifier at a given block.
988
- * @param blockNumber - The block number at which to get the index.
989
- * @param nullifier - Nullifier we try to find witness for.
990
- * @returns The nullifier membership witness (if found).
991
- */
992
1103
  public async getNullifierMembershipWitness(
993
- blockNumber: BlockParameter,
1104
+ referenceBlock: BlockParameter,
994
1105
  nullifier: Fr,
995
1106
  ): Promise<NullifierMembershipWitness | undefined> {
996
- const db = await this.#getWorldState(blockNumber);
1107
+ const db = await this.getWorldState(referenceBlock);
997
1108
  const [witness] = await db.findSiblingPaths(MerkleTreeId.NULLIFIER_TREE, [nullifier.toBuffer()]);
998
1109
  if (!witness) {
999
1110
  return undefined;
@@ -1010,7 +1121,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1010
1121
 
1011
1122
  /**
1012
1123
  * Returns a low nullifier membership witness for a given nullifier at a given block.
1013
- * @param blockNumber - The block number at which to get the index.
1124
+ * @param referenceBlock - The block parameter (block number, block hash, or 'latest') at which to get the data
1125
+ * (which contains the root of the nullifier tree in which we are searching for the nullifier).
1014
1126
  * @param nullifier - Nullifier we try to find the low nullifier witness for.
1015
1127
  * @returns The low nullifier membership witness (if found).
1016
1128
  * @remarks Low nullifier witness can be used to perform a nullifier non-inclusion proof by leveraging the "linked
@@ -1023,10 +1135,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1023
1135
  * TODO: This is a confusing behavior and we should eventually address that.
1024
1136
  */
1025
1137
  public async getLowNullifierMembershipWitness(
1026
- blockNumber: BlockParameter,
1138
+ referenceBlock: BlockParameter,
1027
1139
  nullifier: Fr,
1028
1140
  ): Promise<NullifierMembershipWitness | undefined> {
1029
- const committedDb = await this.#getWorldState(blockNumber);
1141
+ const committedDb = await this.getWorldState(referenceBlock);
1030
1142
  const findResult = await committedDb.getPreviousValueIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBigInt());
1031
1143
  if (!findResult) {
1032
1144
  return undefined;
@@ -1041,8 +1153,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1041
1153
  return new NullifierMembershipWitness(BigInt(index), preimageData as NullifierLeafPreimage, siblingPath);
1042
1154
  }
1043
1155
 
1044
- async getPublicDataWitness(blockNumber: BlockParameter, leafSlot: Fr): Promise<PublicDataWitness | undefined> {
1045
- const committedDb = await this.#getWorldState(blockNumber);
1156
+ async getPublicDataWitness(referenceBlock: BlockParameter, leafSlot: Fr): Promise<PublicDataWitness | undefined> {
1157
+ const committedDb = await this.getWorldState(referenceBlock);
1046
1158
  const lowLeafResult = await committedDb.getPreviousValueIndex(MerkleTreeId.PUBLIC_DATA_TREE, leafSlot.toBigInt());
1047
1159
  if (!lowLeafResult) {
1048
1160
  return undefined;
@@ -1056,19 +1168,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1056
1168
  }
1057
1169
  }
1058
1170
 
1059
- /**
1060
- * Gets the storage value at the given contract storage slot.
1061
- *
1062
- * @remarks The storage slot here refers to the slot as it is defined in Noir not the index in the merkle tree.
1063
- * Aztec's version of `eth_getStorageAt`.
1064
- *
1065
- * @param contract - Address of the contract to query.
1066
- * @param slot - Slot to query.
1067
- * @param blockNumber - The block number at which to get the data or 'latest'.
1068
- * @returns Storage value at the given contract slot.
1069
- */
1070
- public async getPublicStorageAt(blockNumber: BlockParameter, contract: AztecAddress, slot: Fr): Promise<Fr> {
1071
- const committedDb = await this.#getWorldState(blockNumber);
1171
+ public async getPublicStorageAt(referenceBlock: BlockParameter, contract: AztecAddress, slot: Fr): Promise<Fr> {
1172
+ const committedDb = await this.getWorldState(referenceBlock);
1072
1173
  const leafSlot = await computePublicDataTreeLeafSlot(contract, slot);
1073
1174
 
1074
1175
  const lowLeafResult = await committedDb.getPreviousValueIndex(MerkleTreeId.PUBLIC_DATA_TREE, leafSlot.toBigInt());
@@ -1082,24 +1183,22 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1082
1183
  return preimage.leaf.value;
1083
1184
  }
1084
1185
 
1085
- /**
1086
- * Returns the currently committed block header, or the initial header if no blocks have been produced.
1087
- * @returns The current committed block header.
1088
- */
1089
- public async getBlockHeader(blockNumber: BlockParameter = 'latest'): Promise<BlockHeader | undefined> {
1090
- return blockNumber === BlockNumber.ZERO ||
1091
- (blockNumber === 'latest' && (await this.blockSource.getBlockNumber()) === BlockNumber.ZERO)
1092
- ? this.worldStateSynchronizer.getCommitted().getInitialHeader()
1093
- : this.blockSource.getBlockHeader(blockNumber === 'latest' ? blockNumber : (blockNumber as BlockNumber));
1094
- }
1095
-
1096
- /**
1097
- * Get a block header specified by its hash.
1098
- * @param blockHash - The block hash being requested.
1099
- * @returns The requested block header.
1100
- */
1101
- public async getBlockHeaderByHash(blockHash: Fr): Promise<BlockHeader | undefined> {
1102
- return await this.blockSource.getBlockHeaderByHash(blockHash);
1186
+ public async getBlockHeader(block: BlockParameter = 'latest'): Promise<BlockHeader | undefined> {
1187
+ if (BlockHash.isBlockHash(block)) {
1188
+ const initialBlockHash = await this.#getInitialHeaderHash();
1189
+ if (block.equals(initialBlockHash)) {
1190
+ // Block source doesn't handle initial header so we need to handle the case separately.
1191
+ return this.worldStateSynchronizer.getCommitted().getInitialHeader();
1192
+ }
1193
+ return this.blockSource.getBlockHeaderByHash(block);
1194
+ } else {
1195
+ // Block source doesn't handle initial header so we need to handle the case separately.
1196
+ const blockNumber = block === 'latest' ? await this.getBlockNumber() : (block as BlockNumber);
1197
+ if (blockNumber === BlockNumber.ZERO) {
1198
+ return this.worldStateSynchronizer.getCommitted().getInitialHeader();
1199
+ }
1200
+ return this.blockSource.getBlockHeader(block);
1201
+ }
1103
1202
  }
1104
1203
 
1105
1204
  /**
@@ -1111,6 +1210,14 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1111
1210
  return await this.blockSource.getBlockHeaderByArchive(archive);
1112
1211
  }
1113
1212
 
1213
+ public getBlockData(number: BlockNumber): Promise<BlockData | undefined> {
1214
+ return this.blockSource.getBlockData(number);
1215
+ }
1216
+
1217
+ public getBlockDataByArchive(archive: Fr): Promise<BlockData | undefined> {
1218
+ return this.blockSource.getBlockDataByArchive(archive);
1219
+ }
1220
+
1114
1221
  /**
1115
1222
  * Simulates the public part of a transaction with the current state.
1116
1223
  * @param tx - The transaction to simulate.
@@ -1134,7 +1241,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1134
1241
  }
1135
1242
 
1136
1243
  const txHash = tx.getTxHash();
1137
- const blockNumber = BlockNumber((await this.blockSource.getBlockNumber()) + 1);
1244
+ const latestBlockNumber = await this.blockSource.getBlockNumber();
1245
+ const blockNumber = BlockNumber.add(latestBlockNumber, 1);
1138
1246
 
1139
1247
  // If sequencer is not initialized, we just set these values to zero for simulation.
1140
1248
  const coinbase = EthAddress.ZERO;
@@ -1149,6 +1257,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1149
1257
  this.contractDataSource,
1150
1258
  new DateProvider(),
1151
1259
  this.telemetry,
1260
+ this.log.getBindings(),
1152
1261
  );
1153
1262
 
1154
1263
  this.log.verbose(`Simulating public calls for tx ${txHash}`, {
@@ -1157,6 +1266,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1157
1266
  blockNumber,
1158
1267
  });
1159
1268
 
1269
+ // Ensure world-state has caught up with the latest block we loaded from the archiver
1270
+ await this.worldStateSynchronizer.syncImmediate(latestBlockNumber);
1160
1271
  const merkleTreeFork = await this.worldStateSynchronizer.fork();
1161
1272
  try {
1162
1273
  const config = PublicSimulatorConfig.from({
@@ -1172,7 +1283,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1172
1283
  const processor = publicProcessorFactory.create(merkleTreeFork, newGlobalVariables, config);
1173
1284
 
1174
1285
  // REFACTOR: Consider merging ProcessReturnValues into ProcessedTx
1175
- const [processedTxs, failedTxs, _usedTxs, returns] = await processor.process([tx]);
1286
+ const [processedTxs, failedTxs, _usedTxs, returns, debugLogs] = await processor.process([tx]);
1176
1287
  // REFACTOR: Consider returning the error rather than throwing
1177
1288
  if (failedTxs.length) {
1178
1289
  this.log.warn(`Simulated tx ${txHash} fails: ${failedTxs[0].error}`, { txHash });
@@ -1186,6 +1297,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1186
1297
  processedTx.txEffect,
1187
1298
  returns,
1188
1299
  processedTx.gasUsed,
1300
+ debugLogs,
1189
1301
  );
1190
1302
  } finally {
1191
1303
  await merkleTreeFork.close();
@@ -1199,19 +1311,32 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1199
1311
  const db = this.worldStateSynchronizer.getCommitted();
1200
1312
  const verifier = isSimulation ? undefined : this.proofVerifier;
1201
1313
 
1202
- // We accept transactions if they are not expired by the next slot (checked based on the IncludeByTimestamp field)
1314
+ // We accept transactions if they are not expired by the next slot (checked based on the ExpirationTimestamp field)
1203
1315
  const { ts: nextSlotTimestamp } = this.epochCache.getEpochAndSlotInNextL1Slot();
1204
1316
  const blockNumber = BlockNumber((await this.blockSource.getBlockNumber()) + 1);
1205
- const validator = createValidatorForAcceptingTxs(db, this.contractDataSource, verifier, {
1206
- timestamp: nextSlotTimestamp,
1207
- blockNumber,
1208
- l1ChainId: this.l1ChainId,
1209
- rollupVersion: this.version,
1210
- setupAllowList: this.config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions()),
1211
- gasFees: await this.getCurrentBaseFees(),
1212
- skipFeeEnforcement,
1213
- txsPermitted: !this.config.disableTransactions,
1214
- });
1317
+ const l1Constants = await this.blockSource.getL1Constants();
1318
+ const validator = createTxValidatorForAcceptingTxsOverRPC(
1319
+ db,
1320
+ this.contractDataSource,
1321
+ verifier,
1322
+ {
1323
+ timestamp: nextSlotTimestamp,
1324
+ blockNumber,
1325
+ l1ChainId: this.l1ChainId,
1326
+ rollupVersion: this.version,
1327
+ setupAllowList: [
1328
+ ...(await getDefaultAllowedSetupFunctions()),
1329
+ ...(this.config.txPublicSetupAllowListExtend ?? []),
1330
+ ],
1331
+ gasFees: await this.getCurrentMinFees(),
1332
+ skipFeeEnforcement,
1333
+ txsPermitted: !this.config.disableTransactions,
1334
+ rollupManaLimit: l1Constants.rollupManaLimit,
1335
+ maxBlockL2Gas: this.config.validateMaxL2BlockGas,
1336
+ maxBlockDAGas: this.config.validateMaxDABlockGas,
1337
+ },
1338
+ this.log.getBindings(),
1339
+ );
1215
1340
 
1216
1341
  return await validator.validateTx(tx);
1217
1342
  }
@@ -1280,7 +1405,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1280
1405
  }
1281
1406
 
1282
1407
  // And it has an L2 block hash
1283
- const l2BlockHash = await archiver.getL2Tips().then(tips => tips.latest.hash);
1408
+ const l2BlockHash = await archiver.getL2Tips().then(tips => tips.proposed.hash);
1284
1409
  if (!l2BlockHash) {
1285
1410
  this.metrics.recordSnapshotError();
1286
1411
  throw new Error(`Archiver has no latest L2 block hash downloaded. Cannot start snapshot.`);
@@ -1314,7 +1439,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1314
1439
  throw new Error('Archiver implementation does not support rollbacks.');
1315
1440
  }
1316
1441
 
1317
- const finalizedBlock = await archiver.getL2Tips().then(tips => tips.finalized.number);
1442
+ const finalizedBlock = await archiver.getL2Tips().then(tips => tips.finalized.block.number);
1318
1443
  if (targetBlock < finalizedBlock) {
1319
1444
  if (force) {
1320
1445
  this.log.warn(`Clearing world state database to allow rolling back behind finalized block ${finalizedBlock}`);
@@ -1375,16 +1500,107 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1375
1500
  }
1376
1501
  }
1377
1502
 
1503
+ public async reloadKeystore(): Promise<void> {
1504
+ if (!this.config.keyStoreDirectory?.length) {
1505
+ throw new BadRequestError(
1506
+ 'Cannot reload keystore: node is not using a file-based keystore. ' +
1507
+ 'Set KEY_STORE_DIRECTORY to use file-based keystores.',
1508
+ );
1509
+ }
1510
+ if (!this.validatorClient) {
1511
+ throw new BadRequestError('Cannot reload keystore: validator is not enabled.');
1512
+ }
1513
+
1514
+ this.log.info('Reloading keystore from disk');
1515
+
1516
+ // Re-read and validate keystore files
1517
+ const keyStores = loadKeystores(this.config.keyStoreDirectory);
1518
+ const newManager = new KeystoreManager(mergeKeystores(keyStores));
1519
+ await newManager.validateSigners();
1520
+ ValidatorClient.validateKeyStoreConfiguration(newManager, this.log);
1521
+
1522
+ // Validate that every validator's publisher keys overlap with the L1 signers
1523
+ // that were initialized at startup. Publishers cannot be hot-reloaded, so a
1524
+ // validator with a publisher key that doesn't match any existing L1 signer
1525
+ // would silently fail on every proposer slot.
1526
+ if (this.keyStoreManager && this.sequencer) {
1527
+ const oldAdapter = NodeKeystoreAdapter.fromKeyStoreManager(this.keyStoreManager);
1528
+ const availablePublishers = new Set(
1529
+ oldAdapter
1530
+ .getAttesterAddresses()
1531
+ .flatMap(a => oldAdapter.getPublisherAddresses(a).map(p => p.toString().toLowerCase())),
1532
+ );
1533
+
1534
+ const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
1535
+ for (const attester of newAdapter.getAttesterAddresses()) {
1536
+ const pubs = newAdapter.getPublisherAddresses(attester);
1537
+ if (pubs.length > 0 && !pubs.some(p => availablePublishers.has(p.toString().toLowerCase()))) {
1538
+ throw new BadRequestError(
1539
+ `Cannot reload keystore: validator ${attester} has publisher keys ` +
1540
+ `[${pubs.map(p => p.toString()).join(', ')}] but none match the L1 signers initialized at startup ` +
1541
+ `[${[...availablePublishers].join(', ')}]. Publishers cannot be hot-reloaded — ` +
1542
+ `use an existing publisher key or restart the node.`,
1543
+ );
1544
+ }
1545
+ }
1546
+ }
1547
+
1548
+ // Build adapters for old and new keystores to compute diff
1549
+ const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
1550
+ const newAddresses = newAdapter.getAttesterAddresses();
1551
+ const oldAddresses = this.keyStoreManager
1552
+ ? NodeKeystoreAdapter.fromKeyStoreManager(this.keyStoreManager).getAttesterAddresses()
1553
+ : [];
1554
+
1555
+ const oldSet = new Set(oldAddresses.map(a => a.toString()));
1556
+ const newSet = new Set(newAddresses.map(a => a.toString()));
1557
+ const added = newAddresses.filter(a => !oldSet.has(a.toString()));
1558
+ const removed = oldAddresses.filter(a => !newSet.has(a.toString()));
1559
+
1560
+ if (added.length > 0) {
1561
+ this.log.info(`Keystore reload: adding attester keys: ${added.map(a => a.toString()).join(', ')}`);
1562
+ }
1563
+ if (removed.length > 0) {
1564
+ this.log.info(`Keystore reload: removing attester keys: ${removed.map(a => a.toString()).join(', ')}`);
1565
+ }
1566
+ if (added.length === 0 && removed.length === 0) {
1567
+ this.log.info('Keystore reload: attester keys unchanged');
1568
+ }
1569
+
1570
+ // Update the validator client (coinbase, feeRecipient, attester keys)
1571
+ this.validatorClient.reloadKeystore(newManager);
1572
+
1573
+ // Update the publisher factory's keystore so newly-added validators
1574
+ // can be matched to existing publisher keys when proposing blocks.
1575
+ if (this.sequencer) {
1576
+ this.sequencer.updatePublisherNodeKeyStore(newAdapter);
1577
+ }
1578
+
1579
+ // Update slasher's "don't-slash-self" list with new validator addresses
1580
+ if (this.slasherClient && !this.config.slashSelfAllowed) {
1581
+ const slashValidatorsNever = unique(
1582
+ [...(this.config.slashValidatorsNever ?? []), ...newAddresses].map(a => a.toString()),
1583
+ ).map(EthAddress.fromString);
1584
+ this.slasherClient.updateConfig({ slashValidatorsNever });
1585
+ }
1586
+
1587
+ this.keyStoreManager = newManager;
1588
+ this.log.info('Keystore reloaded: coinbase, feeRecipient, and attester keys updated');
1589
+ }
1590
+
1591
+ #getInitialHeaderHash(): Promise<BlockHash> {
1592
+ if (!this.initialHeaderHashPromise) {
1593
+ this.initialHeaderHashPromise = this.worldStateSynchronizer.getCommitted().getInitialHeader().hash();
1594
+ }
1595
+ return this.initialHeaderHashPromise;
1596
+ }
1597
+
1378
1598
  /**
1379
1599
  * Returns an instance of MerkleTreeOperations having first ensured the world state is fully synched
1380
- * @param blockNumber - The block number at which to get the data.
1600
+ * @param block - The block parameter (block number, block hash, or 'latest') at which to get the data.
1381
1601
  * @returns An instance of a committed MerkleTreeOperations
1382
1602
  */
1383
- async #getWorldState(blockNumber: BlockParameter) {
1384
- if (typeof blockNumber === 'number' && blockNumber < INITIAL_L2_BLOCK_NUM - 1) {
1385
- throw new Error('Invalid block number to get world state for: ' + blockNumber);
1386
- }
1387
-
1603
+ protected async getWorldState(block: BlockParameter) {
1388
1604
  let blockSyncedTo: BlockNumber = BlockNumber.ZERO;
1389
1605
  try {
1390
1606
  // Attempt to sync the world state if necessary
@@ -1393,16 +1609,51 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1393
1609
  this.log.error(`Error getting world state: ${err}`);
1394
1610
  }
1395
1611
 
1396
- // using a snapshot could be less efficient than using the committed db
1397
- if (blockNumber === 'latest' /*|| blockNumber === blockSyncedTo*/) {
1398
- this.log.debug(`Using committed db for block ${blockNumber}, world state synced upto ${blockSyncedTo}`);
1612
+ if (block === 'latest') {
1613
+ this.log.debug(`Using committed db for block 'latest', world state synced upto ${blockSyncedTo}`);
1399
1614
  return this.worldStateSynchronizer.getCommitted();
1400
- } else if (blockNumber <= blockSyncedTo) {
1401
- this.log.debug(`Using snapshot for block ${blockNumber}, world state synced upto ${blockSyncedTo}`);
1402
- return this.worldStateSynchronizer.getSnapshot(blockNumber as BlockNumber);
1615
+ }
1616
+
1617
+ // Get the block number, either directly from the parameter or by quering the archiver with the block hash
1618
+ let blockNumber: BlockNumber;
1619
+ if (BlockHash.isBlockHash(block)) {
1620
+ const initialBlockHash = await this.#getInitialHeaderHash();
1621
+ if (block.equals(initialBlockHash)) {
1622
+ // Block source doesn't handle initial header so we need to handle the case separately.
1623
+ return this.worldStateSynchronizer.getSnapshot(BlockNumber.ZERO);
1624
+ }
1625
+
1626
+ const header = await this.blockSource.getBlockHeaderByHash(block);
1627
+ if (!header) {
1628
+ throw new Error(
1629
+ `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.`,
1630
+ );
1631
+ }
1632
+
1633
+ blockNumber = header.getBlockNumber();
1403
1634
  } else {
1404
- throw new Error(`Block ${blockNumber} not yet synced`);
1635
+ blockNumber = block as BlockNumber;
1636
+ }
1637
+
1638
+ // Check it's within world state sync range
1639
+ if (blockNumber > blockSyncedTo) {
1640
+ throw new Error(`Queried block ${block} not yet synced by the node (node is synced upto ${blockSyncedTo}).`);
1405
1641
  }
1642
+ this.log.debug(`Using snapshot for block ${blockNumber}, world state synced upto ${blockSyncedTo}`);
1643
+
1644
+ const snapshot = this.worldStateSynchronizer.getSnapshot(blockNumber);
1645
+
1646
+ // Double-check world-state synced to the same block hash as was requested
1647
+ if (BlockHash.isBlockHash(block)) {
1648
+ const blockHash = await snapshot.getLeafValue(MerkleTreeId.ARCHIVE, BigInt(blockNumber));
1649
+ if (!blockHash || !new BlockHash(blockHash).equals(block)) {
1650
+ throw new Error(
1651
+ `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.`,
1652
+ );
1653
+ }
1654
+ }
1655
+
1656
+ return snapshot;
1406
1657
  }
1407
1658
 
1408
1659
  /**