@aztec/aztec-node 0.0.1-commit.2b2662070 → 0.0.1-commit.2c0ee1788

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.
@@ -50,14 +50,13 @@ import {
50
50
  import { CollectionLimitsConfig, PublicSimulatorConfig } from '@aztec/stdlib/avm';
51
51
  import { AztecAddress } from '@aztec/stdlib/aztec-address';
52
52
  import {
53
- type BlockData,
54
53
  BlockHash,
55
54
  type BlockParameter,
56
55
  type DataInBlock,
57
56
  L2Block,
58
57
  type L2BlockSource,
58
+ inspectBlockParameter,
59
59
  } from '@aztec/stdlib/block';
60
- import type { PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
61
60
  import type {
62
61
  ContractClassPublic,
63
62
  ContractDataSource,
@@ -67,15 +66,22 @@ import type {
67
66
  } from '@aztec/stdlib/contract';
68
67
  import { GasFees, type ManaUsageEstimate } from '@aztec/stdlib/gas';
69
68
  import { computePublicDataTreeLeafSlot } from '@aztec/stdlib/hash';
70
- import {
71
- type AztecNode,
72
- type AztecNodeAdmin,
73
- type AztecNodeAdminConfig,
74
- AztecNodeAdminConfigSchema,
75
- type AztecNodeDebug,
76
- type GetContractClassLogsResponse,
77
- type GetPublicLogsResponse,
69
+ import type {
70
+ AztecNode,
71
+ AztecNodeAdmin,
72
+ AztecNodeAdminConfig,
73
+ AztecNodeDebug,
74
+ BlockIncludeOptions,
75
+ BlockResponse,
76
+ ChainTip,
77
+ ChainTips,
78
+ CheckpointIncludeOptions,
79
+ CheckpointParameter,
80
+ CheckpointResponse,
81
+ GetContractClassLogsResponse,
82
+ GetPublicLogsResponse,
78
83
  } from '@aztec/stdlib/interfaces/client';
84
+ import { AztecNodeAdminConfigSchema } from '@aztec/stdlib/interfaces/client';
79
85
  import {
80
86
  type AllowedElement,
81
87
  type ClientProtocolCircuitVerifier,
@@ -129,6 +135,12 @@ import { createPublicClient } from 'viem';
129
135
 
130
136
  import { createSentinel } from '../sentinel/factory.js';
131
137
  import { Sentinel } from '../sentinel/sentinel.js';
138
+ import {
139
+ blockResponseFromBlockData,
140
+ blockResponseFromL2Block,
141
+ checkpointResponseFromCheckpointData,
142
+ checkpointResponseFromPublishedCheckpoint,
143
+ } from './block_response_helpers.js';
132
144
  import { type AztecNodeConfig, createKeyStoreForValidator } from './config.js';
133
145
  import { NodeMetrics } from './node_metrics.js';
134
146
 
@@ -157,6 +169,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
157
169
  protected readonly slasherClient: SlasherClientInterface | undefined,
158
170
  protected readonly validatorsSentinel: Sentinel | undefined,
159
171
  protected readonly epochPruneWatcher: EpochPruneWatcher | undefined,
172
+ protected readonly attestationsBlockWatcher: AttestationsBlockWatcher | undefined,
160
173
  protected readonly l1ChainId: number,
161
174
  protected readonly version: number,
162
175
  protected readonly globalVariableBuilder: GlobalVariableBuilderInterface,
@@ -196,10 +209,281 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
196
209
  return status.syncSummary;
197
210
  }
198
211
 
212
+ public async getChainTips(): Promise<ChainTips> {
213
+ const { proposed, checkpointed, proven, finalized } = await this.blockSource.getL2Tips();
214
+ return { proposed, checkpointed, proven, finalized };
215
+ }
216
+
199
217
  public getL2Tips() {
200
218
  return this.blockSource.getL2Tips();
201
219
  }
202
220
 
221
+ public async getBlockHeader(number: BlockNumber | 'latest'): Promise<BlockHeader | undefined> {
222
+ const resolvedNumber = number === 'latest' ? await this.blockSource.getBlockNumber() : number;
223
+ if (resolvedNumber === BlockNumber.ZERO) {
224
+ return this.worldStateSynchronizer.getCommitted().getInitialHeader();
225
+ }
226
+ return this.blockSource.getBlockHeader(resolvedNumber);
227
+ }
228
+
229
+ public async getCheckpointedBlocks(from: BlockNumber, limit: number) {
230
+ return (await this.blockSource.getCheckpointedBlocks(from, limit)) ?? [];
231
+ }
232
+
233
+ public getCheckpointsDataForEpoch(epoch: EpochNumber) {
234
+ return this.blockSource.getCheckpointsDataForEpoch(epoch);
235
+ }
236
+
237
+ public getBlockNumber(tip?: ChainTip): Promise<BlockNumber> {
238
+ switch (tip) {
239
+ case undefined:
240
+ case 'proposed':
241
+ return this.blockSource.getBlockNumber();
242
+ case 'checkpointed':
243
+ return this.blockSource.getCheckpointedL2BlockNumber();
244
+ case 'proven':
245
+ return this.blockSource.getProvenBlockNumber();
246
+ case 'finalized':
247
+ return this.blockSource.getFinalizedL2BlockNumber();
248
+ }
249
+ }
250
+
251
+ public async getCheckpointNumber(tip?: ChainTip): Promise<CheckpointNumber> {
252
+ switch (tip) {
253
+ case undefined:
254
+ case 'proposed':
255
+ case 'checkpointed':
256
+ return await this.blockSource.getCheckpointNumber();
257
+ case 'proven':
258
+ case 'finalized': {
259
+ const tips = await this.blockSource.getL2Tips();
260
+ return tip === 'proven' ? tips.proven.checkpoint.number : tips.finalized.checkpoint.number;
261
+ }
262
+ }
263
+ }
264
+
265
+ private isChainTip(value: unknown): value is ChainTip {
266
+ return value === 'proposed' || value === 'checkpointed' || value === 'proven' || value === 'finalized';
267
+ }
268
+
269
+ private async resolveBlockParameter(
270
+ param: BlockParameter,
271
+ ): Promise<{ number?: BlockNumber; hash?: BlockHash; archive?: Fr }> {
272
+ if (BlockHash.isBlockHash(param)) {
273
+ return { hash: param };
274
+ }
275
+ if (typeof param === 'number') {
276
+ return { number: param as BlockNumber };
277
+ }
278
+ if (param === 'latest') {
279
+ return { number: await this.blockSource.getBlockNumber() };
280
+ }
281
+ if (this.isChainTip(param)) {
282
+ return { number: await this.getBlockNumber(param) };
283
+ }
284
+ if (typeof param === 'object' && param !== null) {
285
+ if ('number' in param) {
286
+ return { number: param.number };
287
+ }
288
+ if ('hash' in param) {
289
+ return { hash: param.hash };
290
+ }
291
+ if ('archive' in param) {
292
+ return { archive: param.archive };
293
+ }
294
+ }
295
+ throw new BadRequestError(`Invalid BlockParameter: ${JSON.stringify(param)}`);
296
+ }
297
+
298
+ private async resolveCheckpointParameter(
299
+ param: CheckpointParameter,
300
+ ): Promise<{ number?: CheckpointNumber; slot?: SlotNumber }> {
301
+ if (typeof param === 'number') {
302
+ return { number: param as CheckpointNumber };
303
+ }
304
+ if (param === 'latest') {
305
+ return { number: await this.blockSource.getCheckpointNumber() };
306
+ }
307
+ if (this.isChainTip(param)) {
308
+ return { number: await this.getCheckpointNumber(param) };
309
+ }
310
+ if (typeof param === 'object' && param !== null) {
311
+ if ('number' in param) {
312
+ return { number: param.number };
313
+ }
314
+ if ('slot' in param) {
315
+ return { slot: param.slot };
316
+ }
317
+ }
318
+ throw new BadRequestError(`Invalid CheckpointParameter: ${JSON.stringify(param)}`);
319
+ }
320
+
321
+ public async getBlock<Opts extends BlockIncludeOptions = {}>(
322
+ param: BlockParameter,
323
+ options: Opts = {} as Opts,
324
+ ): Promise<BlockResponse<Opts> | undefined> {
325
+ const resolved = await this.resolveBlockParameter(param);
326
+ const wantTxs = !!options.includeTransactions;
327
+ const wantContext = !!options.includeL1PublishInfo || !!options.includeAttestations;
328
+
329
+ if (resolved.hash !== undefined) {
330
+ const initial = await this.#getInitialHeaderHash();
331
+ if (resolved.hash.equals(initial)) {
332
+ return (await this.buildGenesisBlockResponse(options)) as BlockResponse<Opts>;
333
+ }
334
+ if (wantTxs) {
335
+ const block = await this.blockSource.getL2BlockByHash(resolved.hash);
336
+ if (!block) {
337
+ return undefined;
338
+ }
339
+ const ctx = wantContext ? await this.blockSource.getBlockDataWithCheckpointContext(block.number) : undefined;
340
+ return (await blockResponseFromL2Block(block, options, ctx)) as BlockResponse<Opts>;
341
+ }
342
+ const data = await this.blockSource.getBlockHeaderByHash(resolved.hash);
343
+ if (!data) {
344
+ return undefined;
345
+ }
346
+ const blockNumber = data.globalVariables.blockNumber;
347
+ const ctx = wantContext ? await this.blockSource.getBlockDataWithCheckpointContext(blockNumber) : undefined;
348
+ if (ctx) {
349
+ return blockResponseFromBlockData(ctx.data, blockNumber, options, ctx) as BlockResponse<Opts>;
350
+ }
351
+ const blockData = await this.blockSource.getBlockData(blockNumber);
352
+ if (!blockData) {
353
+ return undefined;
354
+ }
355
+ return blockResponseFromBlockData(blockData, blockNumber, options) as BlockResponse<Opts>;
356
+ }
357
+
358
+ if (resolved.archive !== undefined) {
359
+ if (wantTxs) {
360
+ const block = await this.blockSource.getL2BlockByArchive(resolved.archive);
361
+ if (!block) {
362
+ return undefined;
363
+ }
364
+ const ctx = wantContext ? await this.blockSource.getBlockDataWithCheckpointContext(block.number) : undefined;
365
+ return (await blockResponseFromL2Block(block, options, ctx)) as BlockResponse<Opts>;
366
+ }
367
+ const data = await this.blockSource.getBlockDataByArchive(resolved.archive);
368
+ if (!data) {
369
+ return undefined;
370
+ }
371
+ const blockNumber = data.header.globalVariables.blockNumber;
372
+ const ctx = wantContext ? await this.blockSource.getBlockDataWithCheckpointContext(blockNumber) : undefined;
373
+ return blockResponseFromBlockData(data, blockNumber, options, ctx) as BlockResponse<Opts>;
374
+ }
375
+
376
+ const blockNumber = resolved.number!;
377
+ if (blockNumber === BlockNumber.ZERO) {
378
+ return (await this.buildGenesisBlockResponse(options)) as BlockResponse<Opts>;
379
+ }
380
+ if (wantTxs) {
381
+ const block = await this.blockSource.getL2Block(blockNumber);
382
+ if (!block) {
383
+ return undefined;
384
+ }
385
+ const ctx = wantContext ? await this.blockSource.getBlockDataWithCheckpointContext(blockNumber) : undefined;
386
+ return (await blockResponseFromL2Block(block, options, ctx)) as BlockResponse<Opts>;
387
+ }
388
+ const ctx = await this.blockSource.getBlockDataWithCheckpointContext(blockNumber);
389
+ if (!ctx) {
390
+ return undefined;
391
+ }
392
+ return blockResponseFromBlockData(ctx.data, blockNumber, options, ctx) as BlockResponse<Opts>;
393
+ }
394
+
395
+ public async getBlocks<Opts extends BlockIncludeOptions = {}>(
396
+ from: BlockNumber,
397
+ limit: number,
398
+ options: Opts = {} as Opts,
399
+ ): Promise<BlockResponse<Opts>[]> {
400
+ const wantTxs = !!options.includeTransactions;
401
+ const wantContext = !!options.includeL1PublishInfo || !!options.includeAttestations;
402
+ if (wantTxs) {
403
+ const blocks = await this.blockSource.getBlocks(from, limit);
404
+ return (await Promise.all(
405
+ blocks.map(async block => {
406
+ const ctx = wantContext ? await this.blockSource.getBlockDataWithCheckpointContext(block.number) : undefined;
407
+ return blockResponseFromL2Block(block, options, ctx);
408
+ }),
409
+ )) as BlockResponse<Opts>[];
410
+ }
411
+ const results: BlockResponse<Opts>[] = [];
412
+ for (let i = 0; i < limit; i++) {
413
+ const blockNumber = BlockNumber(from + i);
414
+ const ctx = await this.blockSource.getBlockDataWithCheckpointContext(blockNumber);
415
+ if (!ctx) {
416
+ break;
417
+ }
418
+ results.push(blockResponseFromBlockData(ctx.data, blockNumber, options, ctx) as BlockResponse<Opts>);
419
+ }
420
+ return results;
421
+ }
422
+
423
+ public async getCheckpoint<Opts extends CheckpointIncludeOptions = {}>(
424
+ param: CheckpointParameter,
425
+ options: Opts = {} as Opts,
426
+ ): Promise<CheckpointResponse<Opts> | undefined> {
427
+ const resolved = await this.resolveCheckpointParameter(param);
428
+ let checkpointNumber = resolved.number;
429
+ if (checkpointNumber === undefined && resolved.slot !== undefined) {
430
+ checkpointNumber = await this.blockSource.getCheckpointNumberBySlot(resolved.slot);
431
+ }
432
+ if (checkpointNumber === undefined) {
433
+ return undefined;
434
+ }
435
+ if (options.includeBlocks) {
436
+ const [checkpoint] = await this.blockSource.getCheckpoints(checkpointNumber, 1);
437
+ if (!checkpoint) {
438
+ return undefined;
439
+ }
440
+ return (await checkpointResponseFromPublishedCheckpoint(checkpoint, options)) as CheckpointResponse<Opts>;
441
+ }
442
+ const data = await this.blockSource.getCheckpointData(checkpointNumber);
443
+ if (!data) {
444
+ return undefined;
445
+ }
446
+ return checkpointResponseFromCheckpointData(data, options) as CheckpointResponse<Opts>;
447
+ }
448
+
449
+ public async getCheckpoints<Opts extends CheckpointIncludeOptions = {}>(
450
+ from: CheckpointNumber,
451
+ limit: number,
452
+ options: Opts = {} as Opts,
453
+ ): Promise<CheckpointResponse<Opts>[]> {
454
+ if (options.includeBlocks) {
455
+ const checkpoints = await this.blockSource.getCheckpoints(from, limit);
456
+ return (await Promise.all(
457
+ checkpoints.map(cp => checkpointResponseFromPublishedCheckpoint(cp, options)),
458
+ )) as CheckpointResponse<Opts>[];
459
+ }
460
+ const datas = await this.blockSource.getCheckpointDataRange(from, limit);
461
+ return datas.map(d => checkpointResponseFromCheckpointData(d, options)) as CheckpointResponse<Opts>[];
462
+ }
463
+
464
+ private async buildGenesisBlockResponse(options: BlockIncludeOptions): Promise<BlockResponse> {
465
+ const initial = this.worldStateSynchronizer.getCommitted().getInitialHeader();
466
+ const empty = L2Block.empty(initial);
467
+ const response: BlockResponse = {
468
+ header: empty.header,
469
+ archive: empty.archive,
470
+ hash: await this.#getInitialHeaderHash(),
471
+ checkpointNumber: empty.checkpointNumber,
472
+ indexWithinCheckpoint: empty.indexWithinCheckpoint,
473
+ number: empty.number,
474
+ };
475
+ if (options.includeTransactions) {
476
+ (response as BlockResponse).body = empty.body;
477
+ }
478
+ if (options.includeL1PublishInfo) {
479
+ (response as BlockResponse).l1 = { published: false };
480
+ }
481
+ if (options.includeAttestations) {
482
+ (response as BlockResponse).attestations = [];
483
+ }
484
+ return response;
485
+ }
486
+
203
487
  /**
204
488
  * initializes the Aztec Node, wait for component to sync.
205
489
  * @param config - The configuration to be used by the aztec node.
@@ -313,326 +597,354 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
313
597
 
314
598
  const epochCache = await EpochCache.create(config.l1Contracts.rollupAddress, config, { dateProvider });
315
599
 
316
- const archiver = await createArchiver(
317
- config,
318
- { blobClient, epochCache, telemetry, dateProvider },
319
- { blockUntilSync: !config.skipArchiverInitialSync },
320
- );
321
-
322
- // now create the merkle trees and the world state synchronizer
323
- const worldStateSynchronizer = await createWorldStateSynchronizer(config, archiver, options.genesis, telemetry);
324
- const useRealVerifiers = config.realProofs || config.debugForceTxProofVerification;
325
- let peerProofVerifier: ClientProtocolCircuitVerifier;
326
- let rpcProofVerifier: ClientProtocolCircuitVerifier;
327
- if (useRealVerifiers) {
328
- peerProofVerifier = await BatchChonkVerifier.new(config, config.bbChonkVerifyMaxBatch, 'peer');
329
- const rpcVerifier = await BBCircuitVerifier.new(config);
330
- rpcProofVerifier = new QueuedIVCVerifier(rpcVerifier, config.numConcurrentIVCVerifiers);
331
- } else {
332
- peerProofVerifier = new TestCircuitVerifier(config.proverTestVerificationDelayMs);
333
- rpcProofVerifier = new TestCircuitVerifier(config.proverTestVerificationDelayMs);
334
- }
335
-
336
- let debugLogStore: DebugLogStore;
337
- if (!config.realProofs) {
338
- log.warn(`Aztec node is accepting fake proofs`);
339
-
340
- debugLogStore = new InMemoryDebugLogStore();
341
- log.info(
342
- 'Aztec node started in test mode (realProofs set to false) hence debug logs from public functions will be collected and served',
600
+ // Track started resources so we can clean up on partial failure during node creation.
601
+ const started: { stop?(): Promise<void> | void }[] = [];
602
+ try {
603
+ const archiver = await createArchiver(
604
+ config,
605
+ { blobClient, epochCache, telemetry, dateProvider },
606
+ { blockUntilSync: !config.skipArchiverInitialSync },
343
607
  );
344
- } else {
345
- debugLogStore = new NullDebugLogStore();
346
- }
347
-
348
- const proverOnly = config.enableProverNode && config.disableValidator;
349
- if (proverOnly) {
350
- log.info('Starting in prover-only mode: skipping validator, sequencer, sentinel, and slasher subsystems');
351
- }
608
+ started.push(archiver);
609
+
610
+ // now create the merkle trees and the world state synchronizer
611
+ const worldStateSynchronizer = await createWorldStateSynchronizer(config, archiver, options.genesis, telemetry);
612
+ started.push(worldStateSynchronizer);
613
+ const useRealVerifiers = config.realProofs || config.debugForceTxProofVerification;
614
+ let peerProofVerifier: ClientProtocolCircuitVerifier;
615
+ let rpcProofVerifier: ClientProtocolCircuitVerifier;
616
+ if (useRealVerifiers) {
617
+ peerProofVerifier = await BatchChonkVerifier.new(config, config.bbChonkVerifyMaxBatch, 'peer');
618
+ const rpcVerifier = await BBCircuitVerifier.new(config);
619
+ rpcProofVerifier = new QueuedIVCVerifier(rpcVerifier, config.numConcurrentIVCVerifiers);
620
+ } else {
621
+ peerProofVerifier = new TestCircuitVerifier(config.proverTestVerificationDelayMs);
622
+ rpcProofVerifier = new TestCircuitVerifier(config.proverTestVerificationDelayMs);
623
+ }
624
+ started.push(peerProofVerifier, rpcProofVerifier);
352
625
 
353
- // create the tx pool and the p2p client, which will need the l2 block source
354
- const p2pClient = await createP2PClient(
355
- config,
356
- archiver,
357
- peerProofVerifier,
358
- worldStateSynchronizer,
359
- epochCache,
360
- packageVersion,
361
- dateProvider,
362
- telemetry,
363
- deps.p2pClientDeps,
364
- );
626
+ let debugLogStore: DebugLogStore;
627
+ if (!config.realProofs) {
628
+ log.warn(`Aztec node is accepting fake proofs`);
365
629
 
366
- // We'll accumulate sentinel watchers here
367
- const watchers: Watcher[] = [];
630
+ debugLogStore = new InMemoryDebugLogStore();
631
+ log.info(
632
+ 'Aztec node started in test mode (realProofs set to false) hence debug logs from public functions will be collected and served',
633
+ );
634
+ } else {
635
+ debugLogStore = new NullDebugLogStore();
636
+ }
368
637
 
369
- // Create FullNodeCheckpointsBuilder for block proposal handling and tx validation.
370
- // Override maxTxsPerCheckpoint with the validator-specific limit if set.
371
- const validatorCheckpointsBuilder = new FullNodeCheckpointsBuilder(
372
- {
373
- ...config,
638
+ const globalVariableBuilderConfig = {
639
+ l1Contracts: config.l1Contracts,
640
+ ethereumSlotDuration: config.ethereumSlotDuration,
641
+ rollupVersion: BigInt(config.rollupVersion),
374
642
  l1GenesisTime,
375
643
  slotDuration: Number(slotDuration),
376
- rollupManaLimit,
377
- maxTxsPerCheckpoint: config.validateMaxTxsPerCheckpoint,
378
- },
379
- worldStateSynchronizer,
380
- archiver,
381
- dateProvider,
382
- telemetry,
383
- );
384
-
385
- let validatorClient: ValidatorClient | undefined;
386
-
387
- if (!proverOnly) {
388
- // Create validator client if required
389
- validatorClient = await createValidatorClient(config, {
390
- checkpointsBuilder: validatorCheckpointsBuilder,
391
- worldState: worldStateSynchronizer,
392
- p2pClient,
393
- telemetry,
394
- dateProvider,
395
- epochCache,
396
- blockSource: archiver,
397
- l1ToL2MessageSource: archiver,
398
- keyStoreManager,
399
- blobClient,
400
- slashingProtectionDb: deps.slashingProtectionDb,
401
- });
402
-
403
- // If we have a validator client, register it as a source of offenses for the slasher,
404
- // and have it register callbacks on the p2p client *before* we start it, otherwise messages
405
- // like attestations or auths will fail.
406
- if (validatorClient) {
407
- watchers.push(validatorClient);
644
+ };
408
645
 
409
- const vc = validatorClient;
410
- const getValidatorAddresses = () => vc.getValidatorAddresses().map(a => a.toString());
411
- validatorClient.getProposalHandler().register(p2pClient, true, archiver, getValidatorAddresses);
646
+ const globalVariableBuilder = new GlobalVariableBuilder(dateProvider, publicClient, globalVariableBuilderConfig);
647
+ const feeProvider = new FeeProviderImpl(dateProvider, publicClient, globalVariableBuilderConfig);
412
648
 
413
- if (!options.dontStartSequencer) {
414
- await validatorClient.registerHandlers();
415
- }
649
+ const proverOnly = config.enableProverNode && config.disableValidator;
650
+ if (proverOnly) {
651
+ log.info('Starting in prover-only mode: skipping validator, sequencer, sentinel, and slasher subsystems');
416
652
  }
417
- }
418
653
 
419
- // If there's no validator client, create a ProposalHandler to handle block and checkpoint proposals
420
- // for monitoring or reexecution. Reexecution (default) allows us to follow the pending chain,
421
- // while non-reexecution is used for validating the proposals and collecting their txs.
422
- // Checkpoint proposals rebuild blobs if the blob client can upload blobs.
423
- if (!validatorClient) {
424
- const reexecute = !!config.alwaysReexecuteBlockProposals;
425
- log.info(`Setting up proposal handler` + (reexecute ? ' with reexecution of proposals' : ''));
426
- createProposalHandler(config, {
427
- checkpointsBuilder: validatorCheckpointsBuilder,
428
- worldState: worldStateSynchronizer,
654
+ // create the tx pool and the p2p client, which will need the l2 block source
655
+ const p2pClient = await createP2PClient(
656
+ config,
657
+ archiver,
658
+ peerProofVerifier,
659
+ worldStateSynchronizer,
429
660
  epochCache,
430
- blockSource: archiver,
431
- l1ToL2MessageSource: archiver,
432
- p2pClient,
433
- blobClient,
661
+ feeProvider,
662
+ packageVersion,
434
663
  dateProvider,
435
664
  telemetry,
436
- }).register(p2pClient, reexecute, archiver);
437
- }
438
-
439
- // Start world state and wait for it to sync to the archiver.
440
- await worldStateSynchronizer.start();
441
-
442
- // Start p2p. Note that it depends on world state to be running.
443
- await p2pClient.start();
665
+ deps.p2pClientDeps,
666
+ );
667
+ started.push(p2pClient);
668
+
669
+ // We'll accumulate sentinel watchers here
670
+ const watchers: Watcher[] = [];
671
+
672
+ // Create FullNodeCheckpointsBuilder for block proposal handling and tx validation.
673
+ // Override maxTxsPerCheckpoint with the validator-specific limit if set.
674
+ const validatorCheckpointsBuilder = new FullNodeCheckpointsBuilder(
675
+ {
676
+ ...config,
677
+ l1GenesisTime,
678
+ slotDuration: Number(slotDuration),
679
+ rollupManaLimit,
680
+ maxTxsPerCheckpoint: config.validateMaxTxsPerCheckpoint,
681
+ },
682
+ worldStateSynchronizer,
683
+ archiver,
684
+ dateProvider,
685
+ telemetry,
686
+ );
444
687
 
445
- let validatorsSentinel: Awaited<ReturnType<typeof createSentinel>> | undefined;
446
- let epochPruneWatcher: EpochPruneWatcher | undefined;
447
- let attestationsBlockWatcher: AttestationsBlockWatcher | undefined;
688
+ let validatorClient: ValidatorClient | undefined;
448
689
 
449
- if (!proverOnly) {
450
- validatorsSentinel = await createSentinel(epochCache, archiver, p2pClient, config);
451
- if (validatorsSentinel && config.slashInactivityPenalty > 0n) {
452
- watchers.push(validatorsSentinel);
690
+ if (!config.disableValidator) {
691
+ // Create validator client if required
692
+ validatorClient = await createValidatorClient(config, {
693
+ checkpointsBuilder: validatorCheckpointsBuilder,
694
+ worldState: worldStateSynchronizer,
695
+ p2pClient,
696
+ telemetry,
697
+ dateProvider,
698
+ epochCache,
699
+ blockSource: archiver,
700
+ l1ToL2MessageSource: archiver,
701
+ keyStoreManager,
702
+ blobClient,
703
+ slashingProtectionDb: deps.slashingProtectionDb,
704
+ });
705
+
706
+ // If we have a validator client, register it as a source of offenses for the slasher,
707
+ // and have it register callbacks on the p2p client *before* we start it, otherwise messages
708
+ // like attestations or auths will fail.
709
+ if (validatorClient) {
710
+ watchers.push(validatorClient);
711
+
712
+ const vc = validatorClient;
713
+ const getValidatorAddresses = () => vc.getValidatorAddresses().map(a => a.toString());
714
+ validatorClient.getProposalHandler().register(p2pClient, true, archiver, getValidatorAddresses);
715
+
716
+ if (!options.dontStartSequencer) {
717
+ await validatorClient.registerHandlers();
718
+ }
719
+ }
453
720
  }
454
721
 
455
- if (config.slashPrunePenalty > 0n || config.slashDataWithholdingPenalty > 0n) {
456
- epochPruneWatcher = new EpochPruneWatcher(
457
- archiver,
458
- archiver,
722
+ // If there's no validator client, create a ProposalHandler to handle block and checkpoint proposals
723
+ // for monitoring or reexecution. Reexecution (default) allows us to follow the pending chain,
724
+ // while non-reexecution is used for validating the proposals and collecting their txs.
725
+ // Checkpoint proposals rebuild blobs if the blob client can upload blobs.
726
+ if (!validatorClient) {
727
+ const reexecute = !!config.alwaysReexecuteBlockProposals;
728
+ log.info(`Setting up proposal handler` + (reexecute ? ' with reexecution of proposals' : ''));
729
+ createProposalHandler(config, {
730
+ checkpointsBuilder: validatorCheckpointsBuilder,
731
+ worldState: worldStateSynchronizer,
459
732
  epochCache,
460
- p2pClient.getTxProvider(),
461
- validatorCheckpointsBuilder,
462
- config,
463
- );
464
- watchers.push(epochPruneWatcher);
733
+ blockSource: archiver,
734
+ l1ToL2MessageSource: archiver,
735
+ p2pClient,
736
+ blobClient,
737
+ dateProvider,
738
+ telemetry,
739
+ }).register(p2pClient, reexecute, archiver);
465
740
  }
466
741
 
467
- // We assume we want to slash for invalid attestations unless all max penalties are set to 0
468
- if (config.slashProposeInvalidAttestationsPenalty > 0n || config.slashAttestDescendantOfInvalidPenalty > 0n) {
469
- attestationsBlockWatcher = new AttestationsBlockWatcher(archiver, epochCache, config);
470
- watchers.push(attestationsBlockWatcher);
471
- }
472
- }
742
+ // Start world state and wait for it to sync to the archiver.
743
+ await worldStateSynchronizer.start();
473
744
 
474
- // Start p2p-related services once the archiver has completed sync
475
- void archiver
476
- .waitForInitialSync()
477
- .then(async () => {
478
- await validatorsSentinel?.start();
479
- await epochPruneWatcher?.start();
480
- await attestationsBlockWatcher?.start();
481
- log.info(`All p2p services started`);
482
- })
483
- .catch(err => log.error('Failed to start p2p services after archiver sync', err));
484
-
485
- const globalVariableBuilderConfig = {
486
- l1Contracts: config.l1Contracts,
487
- ethereumSlotDuration: config.ethereumSlotDuration,
488
- rollupVersion: BigInt(config.rollupVersion),
489
- l1GenesisTime,
490
- slotDuration: Number(slotDuration),
491
- };
745
+ // Start p2p. Note that it depends on world state to be running.
746
+ await p2pClient.start();
492
747
 
493
- const globalVariableBuilder = new GlobalVariableBuilder(dateProvider, publicClient, globalVariableBuilderConfig);
494
- const feeProvider = new FeeProviderImpl(dateProvider, publicClient, globalVariableBuilderConfig);
748
+ let validatorsSentinel: Awaited<ReturnType<typeof createSentinel>> | undefined;
749
+ let epochPruneWatcher: EpochPruneWatcher | undefined;
750
+ let attestationsBlockWatcher: AttestationsBlockWatcher | undefined;
495
751
 
496
- // Validator enabled, create/start relevant service
497
- let sequencer: SequencerClient | undefined;
498
- let slasherClient: SlasherClientInterface | undefined;
499
- if (!config.disableValidator && validatorClient) {
500
- // We create a slasher only if we have a sequencer, since all slashing actions go through the sequencer publisher
501
- // as they are executed when the node is selected as proposer.
502
- const validatorAddresses = keyStoreManager
503
- ? NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager).getAddresses()
504
- : [];
752
+ if (!proverOnly) {
753
+ validatorsSentinel = await createSentinel(epochCache, archiver, p2pClient, config);
754
+ if (validatorsSentinel && config.slashInactivityPenalty > 0n) {
755
+ watchers.push(validatorsSentinel);
756
+ }
505
757
 
506
- slasherClient = await createSlasher(
507
- config,
508
- config.l1Contracts,
509
- getPublicClient(config),
510
- watchers,
511
- dateProvider,
512
- epochCache,
513
- validatorAddresses,
514
- undefined, // logger
515
- );
516
- await slasherClient.start();
758
+ if (config.slashPrunePenalty > 0n || config.slashDataWithholdingPenalty > 0n) {
759
+ epochPruneWatcher = new EpochPruneWatcher(
760
+ archiver,
761
+ archiver,
762
+ epochCache,
763
+ p2pClient.getTxProvider(),
764
+ validatorCheckpointsBuilder,
765
+ config,
766
+ );
767
+ watchers.push(epochPruneWatcher);
768
+ }
517
769
 
518
- const l1TxUtils = config.sequencerPublisherForwarderAddress
519
- ? await createForwarderL1TxUtilsFromSigners(
520
- publicClient,
521
- keyStoreManager!.createAllValidatorPublisherSigners(),
522
- config.sequencerPublisherForwarderAddress,
523
- { ...config, scope: 'sequencer' },
524
- { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider, kzg: Blob.getViemKzgInstance() },
525
- )
526
- : await createL1TxUtilsFromSigners(
770
+ // We assume we want to slash for invalid attestations unless all max penalties are set to 0
771
+ if (config.slashProposeInvalidAttestationsPenalty > 0n || config.slashAttestDescendantOfInvalidPenalty > 0n) {
772
+ attestationsBlockWatcher = new AttestationsBlockWatcher(archiver, epochCache, config);
773
+ watchers.push(attestationsBlockWatcher);
774
+ }
775
+ }
776
+
777
+ // Start p2p-related services once the archiver has completed sync
778
+ void archiver
779
+ .waitForInitialSync()
780
+ .then(async () => {
781
+ if (validatorsSentinel) {
782
+ await validatorsSentinel.start();
783
+ started.push(validatorsSentinel);
784
+ }
785
+ if (epochPruneWatcher) {
786
+ await epochPruneWatcher.start();
787
+ started.push(epochPruneWatcher);
788
+ }
789
+ if (attestationsBlockWatcher) {
790
+ await attestationsBlockWatcher.start();
791
+ started.push(attestationsBlockWatcher);
792
+ }
793
+ log.info(`All p2p services started`);
794
+ })
795
+ .catch(err => log.error('Failed to start p2p services after archiver sync', err));
796
+
797
+ // Validator enabled, create/start relevant service
798
+ let sequencer: SequencerClient | undefined;
799
+ let slasherClient: SlasherClientInterface | undefined;
800
+ if (!config.disableValidator && validatorClient) {
801
+ // We create a slasher only if we have a sequencer, since all slashing actions go through the sequencer publisher
802
+ // as they are executed when the node is selected as proposer.
803
+ const validatorAddresses = keyStoreManager
804
+ ? NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager).getAddresses()
805
+ : [];
806
+
807
+ slasherClient = await createSlasher(
808
+ config,
809
+ config.l1Contracts,
810
+ getPublicClient(config),
811
+ watchers,
812
+ dateProvider,
813
+ epochCache,
814
+ validatorAddresses,
815
+ undefined, // logger
816
+ );
817
+ await slasherClient.start();
818
+ started.push(slasherClient);
819
+
820
+ const l1TxUtils = config.sequencerPublisherForwarderAddress
821
+ ? await createForwarderL1TxUtilsFromSigners(
822
+ publicClient,
823
+ keyStoreManager!.createAllValidatorPublisherSigners(),
824
+ config.sequencerPublisherForwarderAddress,
825
+ { ...config, scope: 'sequencer' },
826
+ { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider, kzg: Blob.getViemKzgInstance() },
827
+ )
828
+ : await createL1TxUtilsFromSigners(
829
+ publicClient,
830
+ keyStoreManager!.createAllValidatorPublisherSigners(),
831
+ { ...config, scope: 'sequencer' },
832
+ { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider, kzg: Blob.getViemKzgInstance() },
833
+ );
834
+
835
+ // Create a funder L1TxUtils from the keystore funding account (if configured)
836
+ const fundingSigner = keyStoreManager?.createFundingSigner();
837
+ let funderL1TxUtils: L1TxUtils | undefined;
838
+ if (fundingSigner) {
839
+ const [funder] = await createL1TxUtilsFromSigners(
527
840
  publicClient,
528
- keyStoreManager!.createAllValidatorPublisherSigners(),
841
+ [fundingSigner],
529
842
  { ...config, scope: 'sequencer' },
530
- { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider, kzg: Blob.getViemKzgInstance() },
843
+ { telemetry, logger: log.createChild('l1-tx-utils:funder'), dateProvider },
531
844
  );
845
+ funderL1TxUtils = funder;
846
+ }
532
847
 
533
- // Create a funder L1TxUtils from the keystore funding account (if configured)
534
- const fundingSigner = keyStoreManager?.createFundingSigner();
535
- let funderL1TxUtils: L1TxUtils | undefined;
536
- if (fundingSigner) {
537
- const [funder] = await createL1TxUtilsFromSigners(
538
- publicClient,
539
- [fundingSigner],
540
- { ...config, scope: 'sequencer' },
541
- { telemetry, logger: log.createChild('l1-tx-utils:funder'), dateProvider },
848
+ // Create and start the sequencer client
849
+ const checkpointsBuilder = new CheckpointsBuilder(
850
+ { ...config, l1GenesisTime, slotDuration: Number(slotDuration), rollupManaLimit },
851
+ worldStateSynchronizer,
852
+ archiver,
853
+ dateProvider,
854
+ telemetry,
855
+ debugLogStore,
542
856
  );
543
- funderL1TxUtils = funder;
857
+
858
+ sequencer = await SequencerClient.new(config, {
859
+ ...deps,
860
+ epochCache,
861
+ l1TxUtils,
862
+ funderL1TxUtils,
863
+ validatorClient,
864
+ p2pClient,
865
+ worldStateSynchronizer,
866
+ slasherClient,
867
+ checkpointsBuilder,
868
+ l2BlockSource: archiver,
869
+ l1ToL2MessageSource: archiver,
870
+ telemetry,
871
+ dateProvider,
872
+ blobClient,
873
+ nodeKeyStore: keyStoreManager!,
874
+ globalVariableBuilder,
875
+ });
544
876
  }
545
877
 
546
- // Create and start the sequencer client
547
- const checkpointsBuilder = new CheckpointsBuilder(
548
- { ...config, l1GenesisTime, slotDuration: Number(slotDuration), rollupManaLimit },
549
- worldStateSynchronizer,
550
- archiver,
551
- dateProvider,
552
- telemetry,
553
- debugLogStore,
554
- );
878
+ if (!options.dontStartSequencer && sequencer) {
879
+ await sequencer.start();
880
+ started.push(sequencer);
881
+ log.verbose(`Sequencer started`);
882
+ } else if (sequencer) {
883
+ log.warn(`Sequencer created but not started`);
884
+ }
555
885
 
556
- sequencer = await SequencerClient.new(config, {
557
- ...deps,
558
- epochCache,
559
- l1TxUtils,
560
- funderL1TxUtils,
561
- validatorClient,
886
+ // Create prover node subsystem if enabled
887
+ let proverNode: ProverNode | undefined;
888
+ if (config.enableProverNode) {
889
+ proverNode = await createProverNode(config, {
890
+ ...deps.proverNodeDeps,
891
+ telemetry,
892
+ dateProvider,
893
+ archiver,
894
+ worldStateSynchronizer,
895
+ p2pClient,
896
+ epochCache,
897
+ blobClient,
898
+ keyStoreManager,
899
+ });
900
+
901
+ if (!options.dontStartProverNode) {
902
+ await proverNode.start();
903
+ started.push(proverNode);
904
+ log.info(`Prover node subsystem started`);
905
+ } else {
906
+ log.info(`Prover node subsystem created but not started`);
907
+ }
908
+ }
909
+
910
+ const node = new AztecNodeService(
911
+ config,
562
912
  p2pClient,
913
+ archiver,
914
+ archiver,
915
+ archiver,
916
+ archiver,
563
917
  worldStateSynchronizer,
918
+ sequencer,
919
+ proverNode,
564
920
  slasherClient,
565
- checkpointsBuilder,
566
- l2BlockSource: archiver,
567
- l1ToL2MessageSource: archiver,
568
- telemetry,
569
- dateProvider,
570
- blobClient,
571
- nodeKeyStore: keyStoreManager!,
921
+ validatorsSentinel,
922
+ epochPruneWatcher,
923
+ attestationsBlockWatcher,
924
+ ethereumChain.chainInfo.id,
925
+ config.rollupVersion,
572
926
  globalVariableBuilder,
573
- });
574
- }
575
-
576
- if (!options.dontStartSequencer && sequencer) {
577
- await sequencer.start();
578
- log.verbose(`Sequencer started`);
579
- } else if (sequencer) {
580
- log.warn(`Sequencer created but not started`);
581
- }
582
-
583
- // Create prover node subsystem if enabled
584
- let proverNode: ProverNode | undefined;
585
- if (config.enableProverNode) {
586
- proverNode = await createProverNode(config, {
587
- ...deps.proverNodeDeps,
588
- telemetry,
589
- dateProvider,
590
- archiver,
591
- worldStateSynchronizer,
592
- p2pClient,
927
+ feeProvider,
593
928
  epochCache,
929
+ packageVersion,
930
+ peerProofVerifier,
931
+ rpcProofVerifier,
932
+ telemetry,
933
+ log,
594
934
  blobClient,
935
+ validatorClient,
595
936
  keyStoreManager,
596
- });
937
+ debugLogStore,
938
+ );
597
939
 
598
- if (!options.dontStartProverNode) {
599
- await proverNode.start();
600
- log.info(`Prover node subsystem started`);
601
- } else {
602
- log.info(`Prover node subsystem created but not started`);
940
+ return node;
941
+ } catch (err) {
942
+ log.error('Failed during node creation, stopping started resources', err);
943
+ for (const resource of started.reverse()) {
944
+ await tryStop(resource);
603
945
  }
946
+ throw err;
604
947
  }
605
-
606
- const node = new AztecNodeService(
607
- config,
608
- p2pClient,
609
- archiver,
610
- archiver,
611
- archiver,
612
- archiver,
613
- worldStateSynchronizer,
614
- sequencer,
615
- proverNode,
616
- slasherClient,
617
- validatorsSentinel,
618
- epochPruneWatcher,
619
- ethereumChain.chainInfo.id,
620
- config.rollupVersion,
621
- globalVariableBuilder,
622
- feeProvider,
623
- epochCache,
624
- packageVersion,
625
- peerProofVerifier,
626
- rpcProofVerifier,
627
- telemetry,
628
- log,
629
- blobClient,
630
- validatorClient,
631
- keyStoreManager,
632
- debugLogStore,
633
- );
634
-
635
- return node;
636
948
  }
637
949
 
638
950
  /**
@@ -707,71 +1019,6 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
707
1019
  return nodeInfo;
708
1020
  }
709
1021
 
710
- /**
711
- * Get a block specified by its block number, block hash, or 'latest'.
712
- * @param block - The block parameter (block number, block hash, or 'latest').
713
- * @returns The requested block.
714
- */
715
- public async getBlock(block: BlockParameter): Promise<L2Block | undefined> {
716
- if (BlockHash.isBlockHash(block)) {
717
- return this.getBlockByHash(block);
718
- }
719
- const blockNumber = block === 'latest' ? await this.getBlockNumber() : (block as BlockNumber);
720
- if (blockNumber === BlockNumber.ZERO) {
721
- return this.buildInitialBlock();
722
- }
723
- return await this.blockSource.getL2Block(blockNumber);
724
- }
725
-
726
- /**
727
- * Get a block specified by its hash.
728
- * @param blockHash - The block hash being requested.
729
- * @returns The requested block.
730
- */
731
- public async getBlockByHash(blockHash: BlockHash): Promise<L2Block | undefined> {
732
- const initialBlockHash = await this.#getInitialHeaderHash();
733
- if (blockHash.equals(initialBlockHash)) {
734
- return this.buildInitialBlock();
735
- }
736
- return await this.blockSource.getL2BlockByHash(blockHash);
737
- }
738
-
739
- private buildInitialBlock(): L2Block {
740
- const initialHeader = this.worldStateSynchronizer.getCommitted().getInitialHeader();
741
- return L2Block.empty(initialHeader);
742
- }
743
-
744
- /**
745
- * Get a block specified by its archive root.
746
- * @param archive - The archive root being requested.
747
- * @returns The requested block.
748
- */
749
- public async getBlockByArchive(archive: Fr): Promise<L2Block | undefined> {
750
- return await this.blockSource.getL2BlockByArchive(archive);
751
- }
752
-
753
- /**
754
- * Method to request blocks. Will attempt to return all requested blocks but will return only those available.
755
- * @param from - The start of the range of blocks to return.
756
- * @param limit - The maximum number of blocks to obtain.
757
- * @returns The blocks requested.
758
- */
759
- public async getBlocks(from: BlockNumber, limit: number): Promise<L2Block[]> {
760
- return (await this.blockSource.getBlocks(from, BlockNumber(limit))) ?? [];
761
- }
762
-
763
- public async getCheckpoints(from: CheckpointNumber, limit: number): Promise<PublishedCheckpoint[]> {
764
- return (await this.blockSource.getCheckpoints(from, limit)) ?? [];
765
- }
766
-
767
- public async getCheckpointedBlocks(from: BlockNumber, limit: number) {
768
- return (await this.blockSource.getCheckpointedBlocks(from, limit)) ?? [];
769
- }
770
-
771
- public getCheckpointsDataForEpoch(epochNumber: EpochNumber) {
772
- return this.blockSource.getCheckpointsDataForEpoch(epochNumber);
773
- }
774
-
775
1022
  public async getCurrentMinFees(): Promise<GasFees> {
776
1023
  return await this.feeProvider.getCurrentMinFees();
777
1024
  }
@@ -789,26 +1036,6 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
789
1036
  return GasFees.from({ feePerDaGas: 0n, feePerL2Gas: 0n });
790
1037
  }
791
1038
 
792
- /**
793
- * Method to fetch the latest block number synchronized by the node.
794
- * @returns The block number.
795
- */
796
- public async getBlockNumber(): Promise<BlockNumber> {
797
- return await this.blockSource.getBlockNumber();
798
- }
799
-
800
- public async getProvenBlockNumber(): Promise<BlockNumber> {
801
- return await this.blockSource.getProvenBlockNumber();
802
- }
803
-
804
- public async getCheckpointedBlockNumber(): Promise<BlockNumber> {
805
- return await this.blockSource.getCheckpointedL2BlockNumber();
806
- }
807
-
808
- public getCheckpointNumber(): Promise<CheckpointNumber> {
809
- return this.blockSource.getCheckpointNumber();
810
- }
811
-
812
1039
  /**
813
1040
  * Method to fetch the version of the package.
814
1041
  * @returns The node package version
@@ -926,7 +1153,13 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
926
1153
  throw new Error(`Invalid tx: ${reason}`);
927
1154
  }
928
1155
 
929
- await this.p2pClient!.sendTx(tx);
1156
+ try {
1157
+ await this.p2pClient!.sendTx(tx);
1158
+ } catch (err) {
1159
+ this.metrics.receivedTx(timer.ms(), false);
1160
+ this.log.warn(`Mempool rejected tx ${txHash}: ${(err as Error).message}`, { txHash });
1161
+ throw err;
1162
+ }
930
1163
  const duration = timer.ms();
931
1164
  this.metrics.receivedTx(duration, true);
932
1165
  this.log.info(`Received tx ${txHash} in ${duration}ms`, { txHash });
@@ -948,10 +1181,15 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
948
1181
  // If the tx is in the pool but not in the archiver, it's pending.
949
1182
  // This handles race conditions between archiver and p2p, where the archiver
950
1183
  // has pruned the block in which a tx was mined, but p2p has not caught up yet.
951
- receipt = new TxReceipt(txHash, TxStatus.PENDING, undefined, undefined);
1184
+ receipt = new TxReceipt(txHash, TxStatus.PENDING, /*executionResult=*/ undefined, /*error=*/ undefined);
952
1185
  } else {
953
1186
  // Otherwise, if we don't know the tx, we consider it dropped.
954
- receipt = new TxReceipt(txHash, TxStatus.DROPPED, undefined, 'Tx dropped by P2P node');
1187
+ receipt = new TxReceipt(
1188
+ txHash,
1189
+ TxStatus.DROPPED,
1190
+ /*executionResult=*/ undefined,
1191
+ /*error=*/ 'Tx dropped by P2P node',
1192
+ );
955
1193
  }
956
1194
 
957
1195
  this.debugLogStore.decorateReceiptWithLogs(txHash.toString(), receipt);
@@ -968,6 +1206,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
968
1206
  */
969
1207
  public async stop() {
970
1208
  this.log.info(`Stopping Aztec Node`);
1209
+ await tryStop(this.attestationsBlockWatcher);
971
1210
  await tryStop(this.validatorsSentinel);
972
1211
  await tryStop(this.epochPruneWatcher);
973
1212
  await tryStop(this.slasherClient);
@@ -1239,41 +1478,6 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
1239
1478
  return preimage.leaf.value;
1240
1479
  }
1241
1480
 
1242
- public async getBlockHeader(block: BlockParameter = 'latest'): Promise<BlockHeader | undefined> {
1243
- if (BlockHash.isBlockHash(block)) {
1244
- const initialBlockHash = await this.#getInitialHeaderHash();
1245
- if (block.equals(initialBlockHash)) {
1246
- // Block source doesn't handle initial header so we need to handle the case separately.
1247
- return this.worldStateSynchronizer.getCommitted().getInitialHeader();
1248
- }
1249
- return this.blockSource.getBlockHeaderByHash(block);
1250
- } else {
1251
- // Block source doesn't handle initial header so we need to handle the case separately.
1252
- const blockNumber = block === 'latest' ? await this.getBlockNumber() : (block as BlockNumber);
1253
- if (blockNumber === BlockNumber.ZERO) {
1254
- return this.worldStateSynchronizer.getCommitted().getInitialHeader();
1255
- }
1256
- return this.blockSource.getBlockHeader(block);
1257
- }
1258
- }
1259
-
1260
- /**
1261
- * Get a block header specified by its archive root.
1262
- * @param archive - The archive root being requested.
1263
- * @returns The requested block header.
1264
- */
1265
- public async getBlockHeaderByArchive(archive: Fr): Promise<BlockHeader | undefined> {
1266
- return await this.blockSource.getBlockHeaderByArchive(archive);
1267
- }
1268
-
1269
- public getBlockData(number: BlockNumber): Promise<BlockData | undefined> {
1270
- return this.blockSource.getBlockData(number);
1271
- }
1272
-
1273
- public getBlockDataByArchive(archive: Fr): Promise<BlockData | undefined> {
1274
- return this.blockSource.getBlockDataByArchive(archive);
1275
- }
1276
-
1277
1481
  /**
1278
1482
  * Simulates the public part of a transaction with the current state.
1279
1483
  * @param tx - The transaction to simulate.
@@ -1714,25 +1918,28 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
1714
1918
  if (BlockHash.isBlockHash(block)) {
1715
1919
  const initialBlockHash = await this.#getInitialHeaderHash();
1716
1920
  if (block.equals(initialBlockHash)) {
1717
- // Block source doesn't handle initial header so we need to handle the case separately.
1718
- return this.worldStateSynchronizer.getSnapshot(BlockNumber.ZERO);
1719
- }
1720
-
1721
- const header = await this.blockSource.getBlockHeaderByHash(block);
1722
- if (!header) {
1723
- throw new Error(
1724
- `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.`,
1725
- );
1921
+ // Block 0 is a first-class historical block: its state lives in the trees' persisted
1922
+ // block-0 payload. Resolving the genesis hash to block number 0 lets the snapshot path
1923
+ // pin reads to genesis state even after the node has advanced past it.
1924
+ blockNumber = BlockNumber.ZERO;
1925
+ } else {
1926
+ const header = await this.blockSource.getBlockHeaderByHash(block);
1927
+ if (!header) {
1928
+ throw new Error(
1929
+ `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.`,
1930
+ );
1931
+ }
1932
+ blockNumber = header.getBlockNumber();
1726
1933
  }
1727
-
1728
- blockNumber = header.getBlockNumber();
1729
1934
  } else {
1730
1935
  blockNumber = block as BlockNumber;
1731
1936
  }
1732
1937
 
1733
1938
  // Check it's within world state sync range
1734
1939
  if (blockNumber > blockSyncedTo) {
1735
- throw new Error(`Queried block ${block} not yet synced by the node (node is synced upto ${blockSyncedTo}).`);
1940
+ throw new Error(
1941
+ `Queried block ${inspectBlockParameter(block)} not yet synced by the node (node is synced upto ${blockSyncedTo}).`,
1942
+ );
1736
1943
  }
1737
1944
  this.log.debug(`Using snapshot for block ${blockNumber}, world state synced upto ${blockSyncedTo}`);
1738
1945
 
@@ -1742,8 +1949,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
1742
1949
  if (BlockHash.isBlockHash(block)) {
1743
1950
  const blockHash = await snapshot.getLeafValue(MerkleTreeId.ARCHIVE, BigInt(blockNumber));
1744
1951
  if (!blockHash || !block.equals(blockHash)) {
1952
+ const initialBlockHash = await this.#getInitialHeaderHash();
1745
1953
  throw new Error(
1746
- `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.`,
1954
+ `Block hash ${block.toString()} not found in world state at block number ${blockNumber} (world state has ${blockHash?.toString() ?? 'no hash'} at that index, genesis header hash is ${initialBlockHash.toString()}). If the node API has been queried with anchor block hash possibly a reorg has occurred.`,
1747
1955
  );
1748
1956
  }
1749
1957
  }
@@ -1751,23 +1959,31 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDeb
1751
1959
  return snapshot;
1752
1960
  }
1753
1961
 
1754
- /** Resolves a block parameter to a block number. */
1962
+ /** Resolves any {@link BlockParameter} variant to a concrete block number. */
1755
1963
  protected async resolveBlockNumber(block: BlockParameter): Promise<BlockNumber> {
1756
- if (block === 'latest') {
1757
- return BlockNumber(await this.blockSource.getBlockNumber());
1964
+ const resolved = await this.resolveBlockParameter(block);
1965
+ if (resolved.number !== undefined) {
1966
+ return resolved.number;
1758
1967
  }
1759
- if (BlockHash.isBlockHash(block)) {
1968
+ if (resolved.hash !== undefined) {
1760
1969
  const initialBlockHash = await this.#getInitialHeaderHash();
1761
- if (block.equals(initialBlockHash)) {
1970
+ if (resolved.hash.equals(initialBlockHash)) {
1762
1971
  return BlockNumber.ZERO;
1763
1972
  }
1764
- const header = await this.blockSource.getBlockHeaderByHash(block);
1973
+ const header = await this.blockSource.getBlockHeaderByHash(resolved.hash);
1974
+ if (!header) {
1975
+ throw new Error(`Block hash ${resolved.hash.toString()} not found.`);
1976
+ }
1977
+ return header.getBlockNumber();
1978
+ }
1979
+ if (resolved.archive !== undefined) {
1980
+ const header = await this.blockSource.getBlockHeaderByArchive(resolved.archive);
1765
1981
  if (!header) {
1766
- throw new Error(`Block hash ${block.toString()} not found.`);
1982
+ throw new Error(`Block with archive ${resolved.archive.toString()} not found.`);
1767
1983
  }
1768
1984
  return header.getBlockNumber();
1769
1985
  }
1770
- return block as BlockNumber;
1986
+ throw new BadRequestError(`Invalid BlockParameter: ${JSON.stringify(block)}`);
1771
1987
  }
1772
1988
 
1773
1989
  /**