@aztec/aztec-node 0.0.1-commit.b1c78909e → 0.0.1-commit.b2a5d0dd1

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,19 +1,22 @@
1
1
  import { Archiver, createArchiver } from '@aztec/archiver';
2
- import { BBCircuitVerifier, QueuedIVCVerifier, TestCircuitVerifier } from '@aztec/bb-prover';
2
+ import { BBCircuitVerifier, BatchChonkVerifier, QueuedIVCVerifier } from '@aztec/bb-prover';
3
+ import { TestCircuitVerifier } from '@aztec/bb-prover/test';
3
4
  import { type BlobClientInterface, createBlobClientWithFileStores } from '@aztec/blob-client/client';
4
5
  import { Blob } from '@aztec/blob-lib';
5
6
  import { ARCHIVE_HEIGHT, type L1_TO_L2_MSG_TREE_HEIGHT, type NOTE_HASH_TREE_HEIGHT } from '@aztec/constants';
6
7
  import { EpochCache, type EpochCacheInterface } from '@aztec/epoch-cache';
7
8
  import { createEthereumChain } from '@aztec/ethereum/chain';
8
- import { getPublicClient } from '@aztec/ethereum/client';
9
+ import { getPublicClient, makeL1HttpTransport } from '@aztec/ethereum/client';
9
10
  import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts';
10
11
  import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses';
12
+ import type { L1TxUtils } from '@aztec/ethereum/l1-tx-utils';
11
13
  import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
12
- import { compactArray, pick, unique } from '@aztec/foundation/collection';
14
+ import { chunkBy, compactArray, pick, unique } from '@aztec/foundation/collection';
13
15
  import { Fr } from '@aztec/foundation/curves/bn254';
14
16
  import { EthAddress } from '@aztec/foundation/eth-address';
15
17
  import { BadRequestError } from '@aztec/foundation/json-rpc';
16
18
  import { type Logger, createLogger } from '@aztec/foundation/log';
19
+ import { retryUntil } from '@aztec/foundation/retry';
17
20
  import { count } from '@aztec/foundation/string';
18
21
  import { DateProvider, Timer } from '@aztec/foundation/timer';
19
22
  import { MembershipWitness, SiblingPath } from '@aztec/foundation/trees';
@@ -30,7 +33,12 @@ import {
30
33
  import { ProtocolContractAddress } from '@aztec/protocol-contracts';
31
34
  import { type ProverNode, type ProverNodeDeps, createProverNode } from '@aztec/prover-node';
32
35
  import { createKeyStoreForProver } from '@aztec/prover-node/config';
33
- import { GlobalVariableBuilder, SequencerClient, type SequencerPublisher } from '@aztec/sequencer-client';
36
+ import {
37
+ FeeProviderImpl,
38
+ GlobalVariableBuilder,
39
+ SequencerClient,
40
+ type SequencerPublisher,
41
+ } from '@aztec/sequencer-client';
34
42
  import { PublicProcessorFactory } from '@aztec/simulator/server';
35
43
  import {
36
44
  AttestationsBlockWatcher,
@@ -57,13 +65,14 @@ import type {
57
65
  NodeInfo,
58
66
  ProtocolContractAddresses,
59
67
  } from '@aztec/stdlib/contract';
60
- import { GasFees } from '@aztec/stdlib/gas';
68
+ import { GasFees, type ManaUsageEstimate } from '@aztec/stdlib/gas';
61
69
  import { computePublicDataTreeLeafSlot } from '@aztec/stdlib/hash';
62
70
  import {
63
71
  type AztecNode,
64
72
  type AztecNodeAdmin,
65
73
  type AztecNodeAdminConfig,
66
74
  AztecNodeAdminConfigSchema,
75
+ type AztecNodeDebug,
67
76
  type GetContractClassLogsResponse,
68
77
  type GetPublicLogsResponse,
69
78
  } from '@aztec/stdlib/interfaces/client';
@@ -79,11 +88,12 @@ import {
79
88
  import type { DebugLogStore, LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs';
80
89
  import { InMemoryDebugLogStore, NullDebugLogStore } from '@aztec/stdlib/logs';
81
90
  import { InboxLeaf, type L1ToL2MessageSource } from '@aztec/stdlib/messaging';
82
- import type { Offense, SlashPayloadRound } from '@aztec/stdlib/slashing';
83
- import type { NullifierLeafPreimage, PublicDataTreeLeaf, PublicDataTreeLeafPreimage } from '@aztec/stdlib/trees';
91
+ import type { Offense } from '@aztec/stdlib/slashing';
92
+ import type { NullifierLeafPreimage, PublicDataTreeLeafPreimage } from '@aztec/stdlib/trees';
84
93
  import { MerkleTreeId, NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees';
85
94
  import {
86
95
  type BlockHeader,
96
+ type FeeProvider,
87
97
  type GlobalVariableBuilder as GlobalVariableBuilderInterface,
88
98
  type IndexedTxEffect,
89
99
  PublicSimulationOutput,
@@ -95,6 +105,7 @@ import {
95
105
  } from '@aztec/stdlib/tx';
96
106
  import { getPackageVersion } from '@aztec/stdlib/update-checker';
97
107
  import type { SingleValidatorStats, ValidatorsStats } from '@aztec/stdlib/validators';
108
+ import type { GenesisData } from '@aztec/stdlib/world-state';
98
109
  import {
99
110
  Attributes,
100
111
  type TelemetryClient,
@@ -108,12 +119,13 @@ import {
108
119
  FullNodeCheckpointsBuilder,
109
120
  NodeKeystoreAdapter,
110
121
  ValidatorClient,
111
- createBlockProposalHandler,
122
+ createProposalHandler,
112
123
  createValidatorClient,
113
124
  } from '@aztec/validator-client';
125
+ import type { SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
114
126
  import { createWorldStateSynchronizer } from '@aztec/world-state';
115
127
 
116
- import { createPublicClient, fallback, http } from 'viem';
128
+ import { createPublicClient } from 'viem';
117
129
 
118
130
  import { createSentinel } from '../sentinel/factory.js';
119
131
  import { Sentinel } from '../sentinel/sentinel.js';
@@ -123,7 +135,7 @@ import { NodeMetrics } from './node_metrics.js';
123
135
  /**
124
136
  * The aztec node.
125
137
  */
126
- export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
138
+ export class AztecNodeService implements AztecNode, AztecNodeAdmin, AztecNodeDebug, Traceable {
127
139
  private metrics: NodeMetrics;
128
140
  private initialHeaderHashPromise: Promise<BlockHash> | undefined = undefined;
129
141
 
@@ -148,9 +160,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
148
160
  protected readonly l1ChainId: number,
149
161
  protected readonly version: number,
150
162
  protected readonly globalVariableBuilder: GlobalVariableBuilderInterface,
163
+ protected readonly feeProvider: FeeProvider,
151
164
  protected readonly epochCache: EpochCacheInterface,
152
165
  protected readonly packageVersion: string,
153
- private proofVerifier: ClientProtocolCircuitVerifier,
166
+ private peerProofVerifier: ClientProtocolCircuitVerifier,
167
+ private rpcProofVerifier: ClientProtocolCircuitVerifier,
154
168
  private telemetry: TelemetryClient = getTelemetryClient(),
155
169
  private log = createLogger('node'),
156
170
  private blobClient?: BlobClientInterface,
@@ -172,6 +186,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
172
186
  }
173
187
  }
174
188
 
189
+ /** @internal Exposed for testing — returns the RPC proof verifier. */
190
+ public getProofVerifier(): ClientProtocolCircuitVerifier {
191
+ return this.rpcProofVerifier;
192
+ }
193
+
175
194
  public async getWorldStateSyncStatus(): Promise<WorldStateSyncStatus> {
176
195
  const status = await this.worldStateSynchronizer.status();
177
196
  return status.syncSummary;
@@ -195,9 +214,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
195
214
  dateProvider?: DateProvider;
196
215
  p2pClientDeps?: P2PClientDeps;
197
216
  proverNodeDeps?: Partial<ProverNodeDeps>;
217
+ slashingProtectionDb?: SlashingProtectionDatabase;
198
218
  } = {},
199
219
  options: {
200
- prefilledPublicData?: PublicDataTreeLeaf[];
220
+ genesis?: GenesisData;
201
221
  dontStartSequencer?: boolean;
202
222
  dontStartProverNode?: boolean;
203
223
  } = {},
@@ -257,7 +277,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
257
277
 
258
278
  const publicClient = createPublicClient({
259
279
  chain: ethereumChain.chainInfo,
260
- transport: fallback(config.l1RpcUrls.map((url: string) => http(url, { batch: false }))),
280
+ transport: makeL1HttpTransport(config.l1RpcUrls, { timeout: config.l1HttpTimeoutMS }),
261
281
  pollingInterval: config.viemPollingIntervalMS,
262
282
  });
263
283
 
@@ -293,298 +313,344 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
293
313
 
294
314
  const epochCache = await EpochCache.create(config.l1Contracts.rollupAddress, config, { dateProvider });
295
315
 
296
- const archiver = await createArchiver(
297
- config,
298
- { blobClient, epochCache, telemetry, dateProvider },
299
- { blockUntilSync: !config.skipArchiverInitialSync },
300
- );
301
-
302
- // now create the merkle trees and the world state synchronizer
303
- const worldStateSynchronizer = await createWorldStateSynchronizer(
304
- config,
305
- archiver,
306
- options.prefilledPublicData,
307
- telemetry,
308
- );
309
- const circuitVerifier =
310
- config.realProofs || config.debugForceTxProofVerification
311
- ? await BBCircuitVerifier.new(config)
312
- : new TestCircuitVerifier(config.proverTestVerificationDelayMs);
313
-
314
- let debugLogStore: DebugLogStore;
315
- if (!config.realProofs) {
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',
316
+ // Track started resources so we can clean up on partial failure during node creation.
317
+ const started: { stop?(): Promise<void> | void }[] = [];
318
+ try {
319
+ const archiver = await createArchiver(
320
+ config,
321
+ { blobClient, epochCache, telemetry, dateProvider },
322
+ { blockUntilSync: !config.skipArchiverInitialSync },
321
323
  );
322
- } else {
323
- debugLogStore = new NullDebugLogStore();
324
- }
325
-
326
- const proofVerifier = new QueuedIVCVerifier(config, circuitVerifier);
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
- }
324
+ started.push(archiver);
325
+
326
+ // now create the merkle trees and the world state synchronizer
327
+ const worldStateSynchronizer = await createWorldStateSynchronizer(config, archiver, options.genesis, telemetry);
328
+ started.push(worldStateSynchronizer);
329
+ const useRealVerifiers = config.realProofs || config.debugForceTxProofVerification;
330
+ let peerProofVerifier: ClientProtocolCircuitVerifier;
331
+ let rpcProofVerifier: ClientProtocolCircuitVerifier;
332
+ if (useRealVerifiers) {
333
+ peerProofVerifier = await BatchChonkVerifier.new(config, config.bbChonkVerifyMaxBatch, 'peer');
334
+ const rpcVerifier = await BBCircuitVerifier.new(config);
335
+ rpcProofVerifier = new QueuedIVCVerifier(rpcVerifier, config.numConcurrentIVCVerifiers);
336
+ } else {
337
+ peerProofVerifier = new TestCircuitVerifier(config.proverTestVerificationDelayMs);
338
+ rpcProofVerifier = new TestCircuitVerifier(config.proverTestVerificationDelayMs);
339
+ }
340
+ started.push(peerProofVerifier, rpcProofVerifier);
332
341
 
333
- // create the tx pool and the p2p client, which will need the l2 block source
334
- const p2pClient = await createP2PClient(
335
- config,
336
- archiver,
337
- proofVerifier,
338
- worldStateSynchronizer,
339
- epochCache,
340
- packageVersion,
341
- dateProvider,
342
- telemetry,
343
- deps.p2pClientDeps,
344
- );
342
+ let debugLogStore: DebugLogStore;
343
+ if (!config.realProofs) {
344
+ log.warn(`Aztec node is accepting fake proofs`);
345
345
 
346
- // We'll accumulate sentinel watchers here
347
- const watchers: Watcher[] = [];
346
+ debugLogStore = new InMemoryDebugLogStore();
347
+ log.info(
348
+ 'Aztec node started in test mode (realProofs set to false) hence debug logs from public functions will be collected and served',
349
+ );
350
+ } else {
351
+ debugLogStore = new NullDebugLogStore();
352
+ }
348
353
 
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
+ const globalVariableBuilderConfig = {
355
+ l1Contracts: config.l1Contracts,
356
+ ethereumSlotDuration: config.ethereumSlotDuration,
357
+ rollupVersion: BigInt(config.rollupVersion),
354
358
  l1GenesisTime,
355
359
  slotDuration: Number(slotDuration),
356
- rollupManaLimit,
357
- maxTxsPerCheckpoint: config.validateMaxTxsPerCheckpoint,
358
- },
359
- worldStateSynchronizer,
360
- archiver,
361
- dateProvider,
362
- telemetry,
363
- );
360
+ };
364
361
 
365
- let validatorClient: ValidatorClient | undefined;
362
+ const globalVariableBuilder = new GlobalVariableBuilder(dateProvider, publicClient, globalVariableBuilderConfig);
363
+ const feeProvider = new FeeProviderImpl(dateProvider, publicClient, globalVariableBuilderConfig);
366
364
 
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
- });
381
-
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
- }
365
+ const proverOnly = config.enableProverNode && config.disableValidator;
366
+ if (proverOnly) {
367
+ log.info('Starting in prover-only mode: skipping validator, sequencer, sentinel, and slasher subsystems');
390
368
  }
391
- }
392
369
 
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' : ''));
399
- createBlockProposalHandler(config, {
400
- checkpointsBuilder: validatorCheckpointsBuilder,
401
- worldState: worldStateSynchronizer,
370
+ // create the tx pool and the p2p client, which will need the l2 block source
371
+ const p2pClient = await createP2PClient(
372
+ config,
373
+ archiver,
374
+ peerProofVerifier,
375
+ worldStateSynchronizer,
402
376
  epochCache,
403
- blockSource: archiver,
404
- l1ToL2MessageSource: archiver,
405
- p2pClient,
377
+ feeProvider,
378
+ packageVersion,
406
379
  dateProvider,
407
380
  telemetry,
408
- }).register(p2pClient, reexecute);
409
- }
410
-
411
- // Start world state and wait for it to sync to the archiver.
412
- await worldStateSynchronizer.start();
413
-
414
- // Start p2p. Note that it depends on world state to be running.
415
- await p2pClient.start();
381
+ deps.p2pClientDeps,
382
+ );
383
+ started.push(p2pClient);
384
+
385
+ // We'll accumulate sentinel watchers here
386
+ const watchers: Watcher[] = [];
387
+
388
+ // Create FullNodeCheckpointsBuilder for block proposal handling and tx validation.
389
+ // Override maxTxsPerCheckpoint with the validator-specific limit if set.
390
+ const validatorCheckpointsBuilder = new FullNodeCheckpointsBuilder(
391
+ {
392
+ ...config,
393
+ l1GenesisTime,
394
+ slotDuration: Number(slotDuration),
395
+ rollupManaLimit,
396
+ maxTxsPerCheckpoint: config.validateMaxTxsPerCheckpoint,
397
+ },
398
+ worldStateSynchronizer,
399
+ archiver,
400
+ dateProvider,
401
+ telemetry,
402
+ );
416
403
 
417
- let validatorsSentinel: Awaited<ReturnType<typeof createSentinel>> | undefined;
418
- let epochPruneWatcher: EpochPruneWatcher | undefined;
419
- let attestationsBlockWatcher: AttestationsBlockWatcher | undefined;
404
+ let validatorClient: ValidatorClient | undefined;
420
405
 
421
- if (!proverOnly) {
422
- validatorsSentinel = await createSentinel(epochCache, archiver, p2pClient, config);
423
- if (validatorsSentinel && config.slashInactivityPenalty > 0n) {
424
- watchers.push(validatorsSentinel);
406
+ if (!proverOnly) {
407
+ // Create validator client if required
408
+ validatorClient = await createValidatorClient(config, {
409
+ checkpointsBuilder: validatorCheckpointsBuilder,
410
+ worldState: worldStateSynchronizer,
411
+ p2pClient,
412
+ telemetry,
413
+ dateProvider,
414
+ epochCache,
415
+ blockSource: archiver,
416
+ l1ToL2MessageSource: archiver,
417
+ keyStoreManager,
418
+ blobClient,
419
+ slashingProtectionDb: deps.slashingProtectionDb,
420
+ });
421
+
422
+ // If we have a validator client, register it as a source of offenses for the slasher,
423
+ // and have it register callbacks on the p2p client *before* we start it, otherwise messages
424
+ // like attestations or auths will fail.
425
+ if (validatorClient) {
426
+ watchers.push(validatorClient);
427
+
428
+ const vc = validatorClient;
429
+ const getValidatorAddresses = () => vc.getValidatorAddresses().map(a => a.toString());
430
+ validatorClient.getProposalHandler().register(p2pClient, true, archiver, getValidatorAddresses);
431
+
432
+ if (!options.dontStartSequencer) {
433
+ await validatorClient.registerHandlers();
434
+ }
435
+ }
425
436
  }
426
437
 
427
- if (config.slashPrunePenalty > 0n || config.slashDataWithholdingPenalty > 0n) {
428
- epochPruneWatcher = new EpochPruneWatcher(
429
- archiver,
430
- archiver,
438
+ // If there's no validator client, create a ProposalHandler to handle block and checkpoint proposals
439
+ // for monitoring or reexecution. Reexecution (default) allows us to follow the pending chain,
440
+ // while non-reexecution is used for validating the proposals and collecting their txs.
441
+ // Checkpoint proposals rebuild blobs if the blob client can upload blobs.
442
+ if (!validatorClient) {
443
+ const reexecute = !!config.alwaysReexecuteBlockProposals;
444
+ log.info(`Setting up proposal handler` + (reexecute ? ' with reexecution of proposals' : ''));
445
+ createProposalHandler(config, {
446
+ checkpointsBuilder: validatorCheckpointsBuilder,
447
+ worldState: worldStateSynchronizer,
431
448
  epochCache,
432
- p2pClient.getTxProvider(),
433
- validatorCheckpointsBuilder,
434
- config,
435
- );
436
- watchers.push(epochPruneWatcher);
449
+ blockSource: archiver,
450
+ l1ToL2MessageSource: archiver,
451
+ p2pClient,
452
+ blobClient,
453
+ dateProvider,
454
+ telemetry,
455
+ }).register(p2pClient, reexecute, archiver);
437
456
  }
438
457
 
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
- }
444
- }
458
+ // Start world state and wait for it to sync to the archiver.
459
+ await worldStateSynchronizer.start();
445
460
 
446
- // Start p2p-related services once the archiver has completed sync
447
- void archiver
448
- .waitForInitialSync()
449
- .then(async () => {
450
- await p2pClient.start();
451
- await validatorsSentinel?.start();
452
- await epochPruneWatcher?.start();
453
- await attestationsBlockWatcher?.start();
454
- log.info(`All p2p services started`);
455
- })
456
- .catch(err => log.error('Failed to start p2p services after archiver sync', err));
457
-
458
- // Validator enabled, create/start relevant service
459
- let sequencer: SequencerClient | undefined;
460
- let slasherClient: SlasherClientInterface | undefined;
461
- if (!config.disableValidator && validatorClient) {
462
- // We create a slasher only if we have a sequencer, since all slashing actions go through the sequencer publisher
463
- // as they are executed when the node is selected as proposer.
464
- const validatorAddresses = keyStoreManager
465
- ? NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager).getAddresses()
466
- : [];
467
-
468
- slasherClient = await createSlasher(
469
- config,
470
- config.l1Contracts,
471
- getPublicClient(config),
472
- watchers,
473
- dateProvider,
474
- epochCache,
475
- validatorAddresses,
476
- undefined, // logger
477
- );
478
- await slasherClient.start();
461
+ // Start p2p. Note that it depends on world state to be running.
462
+ await p2pClient.start();
479
463
 
480
- const l1TxUtils = config.sequencerPublisherForwarderAddress
481
- ? await createForwarderL1TxUtilsFromSigners(
482
- publicClient,
483
- keyStoreManager!.createAllValidatorPublisherSigners(),
484
- config.sequencerPublisherForwarderAddress,
485
- { ...config, scope: 'sequencer' },
486
- { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider, kzg: Blob.getViemKzgInstance() },
487
- )
488
- : await createL1TxUtilsFromSigners(
464
+ let validatorsSentinel: Awaited<ReturnType<typeof createSentinel>> | undefined;
465
+ let epochPruneWatcher: EpochPruneWatcher | undefined;
466
+ let attestationsBlockWatcher: AttestationsBlockWatcher | undefined;
467
+
468
+ if (!proverOnly) {
469
+ validatorsSentinel = await createSentinel(epochCache, archiver, p2pClient, config);
470
+ if (validatorsSentinel && config.slashInactivityPenalty > 0n) {
471
+ watchers.push(validatorsSentinel);
472
+ }
473
+
474
+ if (config.slashPrunePenalty > 0n || config.slashDataWithholdingPenalty > 0n) {
475
+ epochPruneWatcher = new EpochPruneWatcher(
476
+ archiver,
477
+ archiver,
478
+ epochCache,
479
+ p2pClient.getTxProvider(),
480
+ validatorCheckpointsBuilder,
481
+ config,
482
+ );
483
+ watchers.push(epochPruneWatcher);
484
+ }
485
+
486
+ // We assume we want to slash for invalid attestations unless all max penalties are set to 0
487
+ if (config.slashProposeInvalidAttestationsPenalty > 0n || config.slashAttestDescendantOfInvalidPenalty > 0n) {
488
+ attestationsBlockWatcher = new AttestationsBlockWatcher(archiver, epochCache, config);
489
+ watchers.push(attestationsBlockWatcher);
490
+ }
491
+ }
492
+
493
+ // Start p2p-related services once the archiver has completed sync
494
+ void archiver
495
+ .waitForInitialSync()
496
+ .then(async () => {
497
+ await validatorsSentinel?.start();
498
+ await epochPruneWatcher?.start();
499
+ await attestationsBlockWatcher?.start();
500
+ log.info(`All p2p services started`);
501
+ })
502
+ .catch(err => log.error('Failed to start p2p services after archiver sync', err));
503
+
504
+ // Validator enabled, create/start relevant service
505
+ let sequencer: SequencerClient | undefined;
506
+ let slasherClient: SlasherClientInterface | undefined;
507
+ if (!config.disableValidator && validatorClient) {
508
+ // We create a slasher only if we have a sequencer, since all slashing actions go through the sequencer publisher
509
+ // as they are executed when the node is selected as proposer.
510
+ const validatorAddresses = keyStoreManager
511
+ ? NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager).getAddresses()
512
+ : [];
513
+
514
+ slasherClient = await createSlasher(
515
+ config,
516
+ config.l1Contracts,
517
+ getPublicClient(config),
518
+ watchers,
519
+ dateProvider,
520
+ epochCache,
521
+ validatorAddresses,
522
+ undefined, // logger
523
+ );
524
+ await slasherClient.start();
525
+ started.push(slasherClient);
526
+
527
+ const l1TxUtils = config.sequencerPublisherForwarderAddress
528
+ ? await createForwarderL1TxUtilsFromSigners(
529
+ publicClient,
530
+ keyStoreManager!.createAllValidatorPublisherSigners(),
531
+ config.sequencerPublisherForwarderAddress,
532
+ { ...config, scope: 'sequencer' },
533
+ { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider, kzg: Blob.getViemKzgInstance() },
534
+ )
535
+ : await createL1TxUtilsFromSigners(
536
+ publicClient,
537
+ keyStoreManager!.createAllValidatorPublisherSigners(),
538
+ { ...config, scope: 'sequencer' },
539
+ { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider, kzg: Blob.getViemKzgInstance() },
540
+ );
541
+
542
+ // Create a funder L1TxUtils from the keystore funding account (if configured)
543
+ const fundingSigner = keyStoreManager?.createFundingSigner();
544
+ let funderL1TxUtils: L1TxUtils | undefined;
545
+ if (fundingSigner) {
546
+ const [funder] = await createL1TxUtilsFromSigners(
489
547
  publicClient,
490
- keyStoreManager!.createAllValidatorPublisherSigners(),
548
+ [fundingSigner],
491
549
  { ...config, scope: 'sequencer' },
492
- { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider, kzg: Blob.getViemKzgInstance() },
550
+ { telemetry, logger: log.createChild('l1-tx-utils:funder'), dateProvider },
493
551
  );
552
+ funderL1TxUtils = funder;
553
+ }
494
554
 
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
- );
555
+ // Create and start the sequencer client
556
+ const checkpointsBuilder = new CheckpointsBuilder(
557
+ { ...config, l1GenesisTime, slotDuration: Number(slotDuration), rollupManaLimit },
558
+ worldStateSynchronizer,
559
+ archiver,
560
+ dateProvider,
561
+ telemetry,
562
+ debugLogStore,
563
+ );
504
564
 
505
- sequencer = await SequencerClient.new(config, {
506
- ...deps,
507
- epochCache,
508
- l1TxUtils,
509
- validatorClient,
510
- p2pClient,
511
- worldStateSynchronizer,
512
- slasherClient,
513
- checkpointsBuilder,
514
- l2BlockSource: archiver,
515
- l1ToL2MessageSource: archiver,
516
- telemetry,
517
- dateProvider,
518
- blobClient,
519
- nodeKeyStore: keyStoreManager!,
520
- });
521
- }
565
+ sequencer = await SequencerClient.new(config, {
566
+ ...deps,
567
+ epochCache,
568
+ l1TxUtils,
569
+ funderL1TxUtils,
570
+ validatorClient,
571
+ p2pClient,
572
+ worldStateSynchronizer,
573
+ slasherClient,
574
+ checkpointsBuilder,
575
+ l2BlockSource: archiver,
576
+ l1ToL2MessageSource: archiver,
577
+ telemetry,
578
+ dateProvider,
579
+ blobClient,
580
+ nodeKeyStore: keyStoreManager!,
581
+ globalVariableBuilder,
582
+ });
583
+ }
522
584
 
523
- if (!options.dontStartSequencer && sequencer) {
524
- await sequencer.start();
525
- log.verbose(`Sequencer started`);
526
- } else if (sequencer) {
527
- log.warn(`Sequencer created but not started`);
528
- }
585
+ if (!options.dontStartSequencer && sequencer) {
586
+ await sequencer.start();
587
+ started.push(sequencer);
588
+ log.verbose(`Sequencer started`);
589
+ } else if (sequencer) {
590
+ log.warn(`Sequencer created but not started`);
591
+ }
529
592
 
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,
593
+ // Create prover node subsystem if enabled
594
+ let proverNode: ProverNode | undefined;
595
+ if (config.enableProverNode) {
596
+ proverNode = await createProverNode(config, {
597
+ ...deps.proverNodeDeps,
598
+ telemetry,
599
+ dateProvider,
600
+ archiver,
601
+ worldStateSynchronizer,
602
+ p2pClient,
603
+ epochCache,
604
+ blobClient,
605
+ keyStoreManager,
606
+ });
607
+
608
+ if (!options.dontStartProverNode) {
609
+ await proverNode.start();
610
+ started.push(proverNode);
611
+ log.info(`Prover node subsystem started`);
612
+ } else {
613
+ log.info(`Prover node subsystem created but not started`);
614
+ }
615
+ }
616
+
617
+ const node = new AztecNodeService(
618
+ config,
619
+ p2pClient,
620
+ archiver,
621
+ archiver,
622
+ archiver,
537
623
  archiver,
538
624
  worldStateSynchronizer,
539
- p2pClient,
625
+ sequencer,
626
+ proverNode,
627
+ slasherClient,
628
+ validatorsSentinel,
629
+ epochPruneWatcher,
630
+ ethereumChain.chainInfo.id,
631
+ config.rollupVersion,
632
+ globalVariableBuilder,
633
+ feeProvider,
540
634
  epochCache,
635
+ packageVersion,
636
+ peerProofVerifier,
637
+ rpcProofVerifier,
638
+ telemetry,
639
+ log,
541
640
  blobClient,
641
+ validatorClient,
542
642
  keyStoreManager,
543
- });
643
+ debugLogStore,
644
+ );
544
645
 
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`);
646
+ return node;
647
+ } catch (err) {
648
+ log.error('Failed during node creation, stopping started resources', err);
649
+ for (const resource of started.reverse()) {
650
+ await tryStop(resource);
550
651
  }
652
+ throw err;
551
653
  }
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(
561
- config,
562
- p2pClient,
563
- archiver,
564
- archiver,
565
- archiver,
566
- archiver,
567
- worldStateSynchronizer,
568
- sequencer,
569
- proverNode,
570
- slasherClient,
571
- validatorsSentinel,
572
- epochPruneWatcher,
573
- ethereumChain.chainInfo.id,
574
- config.rollupVersion,
575
- globalVariableBuilder,
576
- epochCache,
577
- packageVersion,
578
- proofVerifier,
579
- telemetry,
580
- log,
581
- blobClient,
582
- validatorClient,
583
- keyStoreManager,
584
- debugLogStore,
585
- );
586
-
587
- return node;
588
654
  }
589
655
 
590
656
  /**
@@ -720,12 +786,17 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
720
786
  return (await this.blockSource.getCheckpointedBlocks(from, limit)) ?? [];
721
787
  }
722
788
 
723
- /**
724
- * Method to fetch the current min L2 fees.
725
- * @returns The current min L2 fees.
726
- */
789
+ public getCheckpointsDataForEpoch(epochNumber: EpochNumber) {
790
+ return this.blockSource.getCheckpointsDataForEpoch(epochNumber);
791
+ }
792
+
727
793
  public async getCurrentMinFees(): Promise<GasFees> {
728
- return await this.globalVariableBuilder.getCurrentMinFees();
794
+ return await this.feeProvider.getCurrentMinFees();
795
+ }
796
+
797
+ /** Returns predicted min fees for the current slot and next N slots. */
798
+ public async getPredictedMinFees(manaUsage?: ManaUsageEstimate): Promise<GasFees[]> {
799
+ return await this.feeProvider.getPredictedMinFees(manaUsage);
729
800
  }
730
801
 
731
802
  public async getMaxPriorityFees(): Promise<GasFees> {
@@ -793,18 +864,22 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
793
864
  page?: number,
794
865
  referenceBlock?: BlockHash,
795
866
  ): Promise<TxScopedL2Log[][]> {
867
+ let upToBlockNumber: BlockNumber | undefined;
796
868
  if (referenceBlock) {
797
869
  const initialBlockHash = await this.#getInitialHeaderHash();
798
- if (!referenceBlock.equals(initialBlockHash)) {
870
+ if (referenceBlock.equals(initialBlockHash)) {
871
+ upToBlockNumber = BlockNumber(0);
872
+ } else {
799
873
  const header = await this.blockSource.getBlockHeaderByHash(referenceBlock);
800
874
  if (!header) {
801
875
  throw new Error(
802
876
  `Block ${referenceBlock.toString()} not found in the node. This might indicate a reorg has occurred.`,
803
877
  );
804
878
  }
879
+ upToBlockNumber = header.globalVariables.blockNumber;
805
880
  }
806
881
  }
807
- return this.logsSource.getPrivateLogsByTags(tags, page);
882
+ return this.logsSource.getPrivateLogsByTags(tags, page, upToBlockNumber);
808
883
  }
809
884
 
810
885
  public async getPublicLogsByTagsFromContract(
@@ -813,18 +888,22 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
813
888
  page?: number,
814
889
  referenceBlock?: BlockHash,
815
890
  ): Promise<TxScopedL2Log[][]> {
891
+ let upToBlockNumber: BlockNumber | undefined;
816
892
  if (referenceBlock) {
817
893
  const initialBlockHash = await this.#getInitialHeaderHash();
818
- if (!referenceBlock.equals(initialBlockHash)) {
894
+ if (referenceBlock.equals(initialBlockHash)) {
895
+ upToBlockNumber = BlockNumber(0);
896
+ } else {
819
897
  const header = await this.blockSource.getBlockHeaderByHash(referenceBlock);
820
898
  if (!header) {
821
899
  throw new Error(
822
900
  `Block ${referenceBlock.toString()} not found in the node. This might indicate a reorg has occurred.`,
823
901
  );
824
902
  }
903
+ upToBlockNumber = header.globalVariables.blockNumber;
825
904
  }
826
905
  }
827
- return this.logsSource.getPublicLogsByTagsFromContract(contractAddress, tags, page);
906
+ return this.logsSource.getPublicLogsByTagsFromContract(contractAddress, tags, page, upToBlockNumber);
828
907
  }
829
908
 
830
909
  /**
@@ -910,7 +989,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
910
989
  await tryStop(this.validatorsSentinel);
911
990
  await tryStop(this.epochPruneWatcher);
912
991
  await tryStop(this.slasherClient);
913
- await tryStop(this.proofVerifier);
992
+ await Promise.all([tryStop(this.peerProofVerifier), tryStop(this.rpcProofVerifier)]);
914
993
  await tryStop(this.sequencer);
915
994
  await tryStop(this.proverNode);
916
995
  await tryStop(this.p2pClient);
@@ -966,7 +1045,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
966
1045
  treeId: MerkleTreeId,
967
1046
  leafValues: Fr[],
968
1047
  ): Promise<(DataInBlock<bigint> | undefined)[]> {
969
- const committedDb = await this.#getWorldState(referenceBlock);
1048
+ const committedDb = await this.getWorldState(referenceBlock);
970
1049
  const maybeIndices = await committedDb.findLeafIndices(
971
1050
  treeId,
972
1051
  leafValues.map(x => x.toBuffer()),
@@ -1000,7 +1079,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1000
1079
  );
1001
1080
 
1002
1081
  // Build a map from block number to block hash
1003
- const blockNumberToHash = new Map<BlockNumber, Fr>();
1082
+ const blockNumberToHash = new Map<BlockNumber, BlockHash>();
1004
1083
  for (let i = 0; i < uniqueBlockNumbers.length; i++) {
1005
1084
  const blockHash = blockHashes[i];
1006
1085
  if (blockHash === undefined) {
@@ -1018,13 +1097,13 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1018
1097
  if (blockNumber === undefined) {
1019
1098
  throw new Error(`Block number not found for leaf index ${index} in tree ${MerkleTreeId[treeId]}`);
1020
1099
  }
1021
- const blockHash = blockNumberToHash.get(blockNumber);
1022
- if (blockHash === undefined) {
1100
+ const l2BlockHash = blockNumberToHash.get(blockNumber);
1101
+ if (l2BlockHash === undefined) {
1023
1102
  throw new Error(`Block hash not found for block number ${blockNumber}`);
1024
1103
  }
1025
1104
  return {
1026
1105
  l2BlockNumber: blockNumber,
1027
- l2BlockHash: new BlockHash(blockHash),
1106
+ l2BlockHash,
1028
1107
  data: index,
1029
1108
  };
1030
1109
  });
@@ -1034,7 +1113,15 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1034
1113
  referenceBlock: BlockParameter,
1035
1114
  blockHash: BlockHash,
1036
1115
  ): Promise<MembershipWitness<typeof ARCHIVE_HEIGHT> | undefined> {
1037
- const committedDb = await this.#getWorldState(referenceBlock);
1116
+ // The Noir circuit checks the archive membership proof against `anchor_block_header.last_archive.root`,
1117
+ // which is the archive tree root BEFORE the anchor block was added (i.e. the state after block N-1).
1118
+ // So we need the world state at block N-1, not block N, to produce a sibling path matching that root.
1119
+ const referenceBlockNumber = await this.resolveBlockNumber(referenceBlock);
1120
+ if (referenceBlockNumber === BlockNumber.ZERO) {
1121
+ // Block 0 (the initial block) has an empty archive, so no membership witness can exist.
1122
+ return undefined;
1123
+ }
1124
+ const committedDb = await this.getWorldState(BlockNumber(referenceBlockNumber - 1));
1038
1125
  const [pathAndIndex] = await committedDb.findSiblingPaths<MerkleTreeId.ARCHIVE>(MerkleTreeId.ARCHIVE, [blockHash]);
1039
1126
  return pathAndIndex === undefined
1040
1127
  ? undefined
@@ -1045,7 +1132,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1045
1132
  referenceBlock: BlockParameter,
1046
1133
  noteHash: Fr,
1047
1134
  ): Promise<MembershipWitness<typeof NOTE_HASH_TREE_HEIGHT> | undefined> {
1048
- const committedDb = await this.#getWorldState(referenceBlock);
1135
+ const committedDb = await this.getWorldState(referenceBlock);
1049
1136
  const [pathAndIndex] = await committedDb.findSiblingPaths<MerkleTreeId.NOTE_HASH_TREE>(
1050
1137
  MerkleTreeId.NOTE_HASH_TREE,
1051
1138
  [noteHash],
@@ -1059,7 +1146,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1059
1146
  referenceBlock: BlockParameter,
1060
1147
  l1ToL2Message: Fr,
1061
1148
  ): Promise<[bigint, SiblingPath<typeof L1_TO_L2_MSG_TREE_HEIGHT>] | undefined> {
1062
- const db = await this.#getWorldState(referenceBlock);
1149
+ const db = await this.getWorldState(referenceBlock);
1063
1150
  const [witness] = await db.findSiblingPaths(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, [l1ToL2Message]);
1064
1151
  if (!witness) {
1065
1152
  return undefined;
@@ -1092,19 +1179,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1092
1179
  public async getL2ToL1Messages(epoch: EpochNumber): Promise<Fr[][][][]> {
1093
1180
  // Assumes `getCheckpointedBlocksForEpoch` returns blocks in ascending order of block number.
1094
1181
  const checkpointedBlocks = await this.blockSource.getCheckpointedBlocksForEpoch(epoch);
1095
- const blocksInCheckpoints: L2Block[][] = [];
1096
- let previousSlotNumber = SlotNumber.ZERO;
1097
- let checkpointIndex = -1;
1098
- for (const checkpointedBlock of checkpointedBlocks) {
1099
- const block = checkpointedBlock.block;
1100
- const slotNumber = block.header.globalVariables.slotNumber;
1101
- if (slotNumber !== previousSlotNumber) {
1102
- checkpointIndex++;
1103
- blocksInCheckpoints.push([]);
1104
- previousSlotNumber = slotNumber;
1105
- }
1106
- blocksInCheckpoints[checkpointIndex].push(block);
1107
- }
1182
+ const blocksInCheckpoints = chunkBy(checkpointedBlocks, cb => cb.block.header.globalVariables.slotNumber).map(
1183
+ group => group.map(cb => cb.block),
1184
+ );
1108
1185
  return blocksInCheckpoints.map(blocks =>
1109
1186
  blocks.map(block => block.body.txEffects.map(txEffect => txEffect.l2ToL1Msgs)),
1110
1187
  );
@@ -1114,7 +1191,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1114
1191
  referenceBlock: BlockParameter,
1115
1192
  nullifier: Fr,
1116
1193
  ): Promise<NullifierMembershipWitness | undefined> {
1117
- const db = await this.#getWorldState(referenceBlock);
1194
+ const db = await this.getWorldState(referenceBlock);
1118
1195
  const [witness] = await db.findSiblingPaths(MerkleTreeId.NULLIFIER_TREE, [nullifier.toBuffer()]);
1119
1196
  if (!witness) {
1120
1197
  return undefined;
@@ -1129,33 +1206,20 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1129
1206
  return new NullifierMembershipWitness(index, leafPreimage as NullifierLeafPreimage, path);
1130
1207
  }
1131
1208
 
1132
- /**
1133
- * Returns a low nullifier membership witness for a given nullifier at a given block.
1134
- * @param referenceBlock - The block parameter (block number, block hash, or 'latest') at which to get the data
1135
- * (which contains the root of the nullifier tree in which we are searching for the nullifier).
1136
- * @param nullifier - Nullifier we try to find the low nullifier witness for.
1137
- * @returns The low nullifier membership witness (if found).
1138
- * @remarks Low nullifier witness can be used to perform a nullifier non-inclusion proof by leveraging the "linked
1139
- * list structure" of leaves and proving that a lower nullifier is pointing to a bigger next value than the nullifier
1140
- * we are trying to prove non-inclusion for.
1141
- *
1142
- * Note: This function returns the membership witness of the nullifier itself and not the low nullifier when
1143
- * the nullifier already exists in the tree. This is because the `getPreviousValueIndex` function returns the
1144
- * index of the nullifier itself when it already exists in the tree.
1145
- * TODO: This is a confusing behavior and we should eventually address that.
1146
- */
1147
1209
  public async getLowNullifierMembershipWitness(
1148
1210
  referenceBlock: BlockParameter,
1149
1211
  nullifier: Fr,
1150
1212
  ): Promise<NullifierMembershipWitness | undefined> {
1151
- const committedDb = await this.#getWorldState(referenceBlock);
1213
+ const committedDb = await this.getWorldState(referenceBlock);
1152
1214
  const findResult = await committedDb.getPreviousValueIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBigInt());
1153
1215
  if (!findResult) {
1154
1216
  return undefined;
1155
1217
  }
1156
1218
  const { index, alreadyPresent } = findResult;
1157
1219
  if (alreadyPresent) {
1158
- this.log.warn(`Nullifier ${nullifier.toBigInt()} already exists in the tree`);
1220
+ throw new Error(
1221
+ `Cannot prove nullifier non-inclusion: nullifier ${nullifier.toBigInt()} already exists in the tree`,
1222
+ );
1159
1223
  }
1160
1224
  const preimageData = (await committedDb.getLeafPreimage(MerkleTreeId.NULLIFIER_TREE, index))!;
1161
1225
 
@@ -1164,7 +1228,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1164
1228
  }
1165
1229
 
1166
1230
  async getPublicDataWitness(referenceBlock: BlockParameter, leafSlot: Fr): Promise<PublicDataWitness | undefined> {
1167
- const committedDb = await this.#getWorldState(referenceBlock);
1231
+ const committedDb = await this.getWorldState(referenceBlock);
1168
1232
  const lowLeafResult = await committedDb.getPreviousValueIndex(MerkleTreeId.PUBLIC_DATA_TREE, leafSlot.toBigInt());
1169
1233
  if (!lowLeafResult) {
1170
1234
  return undefined;
@@ -1179,7 +1243,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1179
1243
  }
1180
1244
 
1181
1245
  public async getPublicStorageAt(referenceBlock: BlockParameter, contract: AztecAddress, slot: Fr): Promise<Fr> {
1182
- const committedDb = await this.#getWorldState(referenceBlock);
1246
+ const committedDb = await this.getWorldState(referenceBlock);
1183
1247
  const leafSlot = await computePublicDataTreeLeafSlot(contract, slot);
1184
1248
 
1185
1249
  const lowLeafResult = await committedDb.getPreviousValueIndex(MerkleTreeId.PUBLIC_DATA_TREE, leafSlot.toBigInt());
@@ -1319,7 +1383,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1319
1383
  { isSimulation, skipFeeEnforcement }: { isSimulation?: boolean; skipFeeEnforcement?: boolean } = {},
1320
1384
  ): Promise<TxValidationResult> {
1321
1385
  const db = this.worldStateSynchronizer.getCommitted();
1322
- const verifier = isSimulation ? undefined : this.proofVerifier;
1386
+ const verifier = isSimulation ? undefined : this.rpcProofVerifier;
1323
1387
 
1324
1388
  // We accept transactions if they are not expired by the next slot (checked based on the ExpirationTimestamp field)
1325
1389
  const { ts: nextSlotTimestamp } = this.epochCache.getEpochAndSlotInNextL1Slot();
@@ -1368,7 +1432,15 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1368
1432
  archiver.updateConfig(config);
1369
1433
  }
1370
1434
  if (newConfig.realProofs !== this.config.realProofs) {
1371
- this.proofVerifier = config.realProofs ? await BBCircuitVerifier.new(newConfig) : new TestCircuitVerifier();
1435
+ await Promise.all([tryStop(this.peerProofVerifier), tryStop(this.rpcProofVerifier)]);
1436
+ if (newConfig.realProofs) {
1437
+ this.peerProofVerifier = await BatchChonkVerifier.new(newConfig, newConfig.bbChonkVerifyMaxBatch, 'peer');
1438
+ const rpcVerifier = await BBCircuitVerifier.new(newConfig);
1439
+ this.rpcProofVerifier = new QueuedIVCVerifier(rpcVerifier, newConfig.numConcurrentIVCVerifiers);
1440
+ } else {
1441
+ this.peerProofVerifier = new TestCircuitVerifier();
1442
+ this.rpcProofVerifier = new TestCircuitVerifier();
1443
+ }
1372
1444
  }
1373
1445
 
1374
1446
  this.config = newConfig;
@@ -1443,7 +1515,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1443
1515
  return Promise.resolve();
1444
1516
  }
1445
1517
 
1446
- public async rollbackTo(targetBlock: BlockNumber, force?: boolean): Promise<void> {
1518
+ public async rollbackTo(targetBlock: BlockNumber, force?: boolean, resumeSync = true): Promise<void> {
1447
1519
  const archiver = this.blockSource as Archiver;
1448
1520
  if (!('rollbackTo' in archiver)) {
1449
1521
  throw new Error('Archiver implementation does not support rollbacks.');
@@ -1473,9 +1545,13 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1473
1545
  this.log.error(`Error during rollback`, err);
1474
1546
  throw err;
1475
1547
  } finally {
1476
- this.log.info(`Resuming world state and archiver sync.`);
1477
- this.worldStateSynchronizer.resumeSync();
1478
- archiver.resume();
1548
+ if (resumeSync) {
1549
+ this.log.info(`Resuming world state and archiver sync.`);
1550
+ this.worldStateSynchronizer.resumeSync();
1551
+ archiver.resume();
1552
+ } else {
1553
+ this.log.info(`Sync left paused after rollback (resumeSync=false).`);
1554
+ }
1479
1555
  }
1480
1556
  }
1481
1557
 
@@ -1492,19 +1568,12 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1492
1568
  return Promise.resolve();
1493
1569
  }
1494
1570
 
1495
- public getSlashPayloads(): Promise<SlashPayloadRound[]> {
1496
- if (!this.slasherClient) {
1497
- throw new Error(`Slasher client not enabled`);
1498
- }
1499
- return this.slasherClient.getSlashPayloads();
1500
- }
1501
-
1502
1571
  public getSlashOffenses(round: bigint | 'all' | 'current'): Promise<Offense[]> {
1503
1572
  if (!this.slasherClient) {
1504
1573
  throw new Error(`Slasher client not enabled`);
1505
1574
  }
1506
1575
  if (round === 'all') {
1507
- return this.slasherClient.getPendingOffenses();
1576
+ return this.slasherClient.getOffenses();
1508
1577
  } else {
1509
1578
  return this.slasherClient.gatherOffensesForRound(round === 'current' ? undefined : BigInt(round));
1510
1579
  }
@@ -1598,6 +1667,40 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1598
1667
  this.log.info('Keystore reloaded: coinbase, feeRecipient, and attester keys updated');
1599
1668
  }
1600
1669
 
1670
+ public async mineBlock(): Promise<void> {
1671
+ if (!this.sequencer) {
1672
+ throw new BadRequestError('Cannot mine block: no sequencer is running');
1673
+ }
1674
+
1675
+ const currentBlockNumber = await this.getBlockNumber();
1676
+
1677
+ // Use slot duration + 50% buffer as the timeout so this works on running networks too
1678
+ const { slotDuration } = await this.blockSource.getL1Constants();
1679
+ const timeoutSeconds = Math.ceil(slotDuration * 1.5);
1680
+
1681
+ // Temporarily set minTxsPerBlock to 0 so the sequencer produces a block even with no txs
1682
+ const originalMinTxsPerBlock = this.sequencer.getSequencer().getConfig().minTxsPerBlock;
1683
+ this.sequencer.updateConfig({ minTxsPerBlock: 0 });
1684
+
1685
+ try {
1686
+ // Trigger the sequencer to produce a block immediately
1687
+ void this.sequencer.trigger();
1688
+
1689
+ // Wait for the new L2 block to appear
1690
+ await retryUntil(
1691
+ async () => {
1692
+ const newBlockNumber = await this.getBlockNumber();
1693
+ return newBlockNumber > currentBlockNumber ? true : undefined;
1694
+ },
1695
+ 'mineBlock',
1696
+ timeoutSeconds,
1697
+ 0.1,
1698
+ );
1699
+ } finally {
1700
+ this.sequencer.updateConfig({ minTxsPerBlock: originalMinTxsPerBlock });
1701
+ }
1702
+ }
1703
+
1601
1704
  #getInitialHeaderHash(): Promise<BlockHash> {
1602
1705
  if (!this.initialHeaderHashPromise) {
1603
1706
  this.initialHeaderHashPromise = this.worldStateSynchronizer.getCommitted().getInitialHeader().hash();
@@ -1610,7 +1713,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1610
1713
  * @param block - The block parameter (block number, block hash, or 'latest') at which to get the data.
1611
1714
  * @returns An instance of a committed MerkleTreeOperations
1612
1715
  */
1613
- async #getWorldState(block: BlockParameter) {
1716
+ protected async getWorldState(block: BlockParameter) {
1614
1717
  let blockSyncedTo: BlockNumber = BlockNumber.ZERO;
1615
1718
  try {
1616
1719
  // Attempt to sync the world state if necessary
@@ -1624,35 +1727,67 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1624
1727
  return this.worldStateSynchronizer.getCommitted();
1625
1728
  }
1626
1729
 
1730
+ // Get the block number, either directly from the parameter or by quering the archiver with the block hash
1731
+ let blockNumber: BlockNumber;
1627
1732
  if (BlockHash.isBlockHash(block)) {
1628
1733
  const initialBlockHash = await this.#getInitialHeaderHash();
1629
1734
  if (block.equals(initialBlockHash)) {
1630
- // Block source doesn't handle initial header so we need to handle the case separately.
1631
- return this.worldStateSynchronizer.getSnapshot(BlockNumber.ZERO);
1735
+ // Block 0 is a first-class historical block: its state lives in the trees' persisted
1736
+ // block-0 payload. Resolving the genesis hash to block number 0 lets the snapshot path
1737
+ // pin reads to genesis state even after the node has advanced past it.
1738
+ blockNumber = BlockNumber.ZERO;
1739
+ } else {
1740
+ const header = await this.blockSource.getBlockHeaderByHash(block);
1741
+ if (!header) {
1742
+ throw new Error(
1743
+ `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.`,
1744
+ );
1745
+ }
1746
+ blockNumber = header.getBlockNumber();
1632
1747
  }
1748
+ } else {
1749
+ blockNumber = block as BlockNumber;
1750
+ }
1633
1751
 
1634
- const header = await this.blockSource.getBlockHeaderByHash(block);
1635
- if (!header) {
1752
+ // Check it's within world state sync range
1753
+ if (blockNumber > blockSyncedTo) {
1754
+ throw new Error(`Queried block ${block} not yet synced by the node (node is synced upto ${blockSyncedTo}).`);
1755
+ }
1756
+ this.log.debug(`Using snapshot for block ${blockNumber}, world state synced upto ${blockSyncedTo}`);
1757
+
1758
+ const snapshot = this.worldStateSynchronizer.getSnapshot(blockNumber);
1759
+
1760
+ // Double-check world-state synced to the same block hash as was requested
1761
+ if (BlockHash.isBlockHash(block)) {
1762
+ const blockHash = await snapshot.getLeafValue(MerkleTreeId.ARCHIVE, BigInt(blockNumber));
1763
+ if (!blockHash || !block.equals(blockHash)) {
1764
+ const initialBlockHash = await this.#getInitialHeaderHash();
1636
1765
  throw new Error(
1637
- `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.`,
1766
+ `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.`,
1638
1767
  );
1639
1768
  }
1640
- const blockNumber = header.getBlockNumber();
1641
- this.log.debug(`Using snapshot for block ${blockNumber}, world state synced upto ${blockSyncedTo}`);
1642
- return this.worldStateSynchronizer.getSnapshot(blockNumber);
1643
1769
  }
1644
1770
 
1645
- // Block number provided
1646
- {
1647
- const blockNumber = block as BlockNumber;
1771
+ return snapshot;
1772
+ }
1648
1773
 
1649
- if (blockNumber > blockSyncedTo) {
1650
- throw new Error(`Queried block ${block} not yet synced by the node (node is synced upto ${blockSyncedTo}).`);
1774
+ /** Resolves a block parameter to a block number. */
1775
+ protected async resolveBlockNumber(block: BlockParameter): Promise<BlockNumber> {
1776
+ if (block === 'latest') {
1777
+ return BlockNumber(await this.blockSource.getBlockNumber());
1778
+ }
1779
+ if (BlockHash.isBlockHash(block)) {
1780
+ const initialBlockHash = await this.#getInitialHeaderHash();
1781
+ if (block.equals(initialBlockHash)) {
1782
+ return BlockNumber.ZERO;
1651
1783
  }
1652
-
1653
- this.log.debug(`Using snapshot for block ${blockNumber}, world state synced upto ${blockSyncedTo}`);
1654
- return this.worldStateSynchronizer.getSnapshot(blockNumber);
1784
+ const header = await this.blockSource.getBlockHeaderByHash(block);
1785
+ if (!header) {
1786
+ throw new Error(`Block hash ${block.toString()} not found.`);
1787
+ }
1788
+ return header.getBlockNumber();
1655
1789
  }
1790
+ return block as BlockNumber;
1656
1791
  }
1657
1792
 
1658
1793
  /**