@aztec/aztec-node 0.0.1-commit.c2595eba → 0.0.1-commit.c2eed6949

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,20 +1,17 @@
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
- import {
5
- ARCHIVE_HEIGHT,
6
- type L1_TO_L2_MSG_TREE_HEIGHT,
7
- type NOTE_HASH_TREE_HEIGHT,
8
- type NULLIFIER_TREE_HEIGHT,
9
- type PUBLIC_DATA_TREE_HEIGHT,
10
- } from '@aztec/constants';
5
+ import { Blob } from '@aztec/blob-lib';
6
+ import { ARCHIVE_HEIGHT, type L1_TO_L2_MSG_TREE_HEIGHT, type NOTE_HASH_TREE_HEIGHT } from '@aztec/constants';
11
7
  import { EpochCache, type EpochCacheInterface } from '@aztec/epoch-cache';
12
8
  import { createEthereumChain } from '@aztec/ethereum/chain';
13
- import { getPublicClient } from '@aztec/ethereum/client';
9
+ import { getPublicClient, makeL1HttpTransport } from '@aztec/ethereum/client';
14
10
  import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts';
15
11
  import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses';
12
+ import type { L1TxUtils } from '@aztec/ethereum/l1-tx-utils';
16
13
  import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
17
- import { compactArray, pick } from '@aztec/foundation/collection';
14
+ import { chunkBy, compactArray, pick, unique } from '@aztec/foundation/collection';
18
15
  import { Fr } from '@aztec/foundation/curves/bn254';
19
16
  import { EthAddress } from '@aztec/foundation/eth-address';
20
17
  import { BadRequestError } from '@aztec/foundation/json-rpc';
@@ -22,14 +19,19 @@ import { type Logger, createLogger } from '@aztec/foundation/log';
22
19
  import { count } from '@aztec/foundation/string';
23
20
  import { DateProvider, Timer } from '@aztec/foundation/timer';
24
21
  import { MembershipWitness, SiblingPath } from '@aztec/foundation/trees';
25
- import { KeystoreManager, loadKeystores, mergeKeystores } from '@aztec/node-keystore';
22
+ import { type KeyStore, KeystoreManager, loadKeystores, mergeKeystores } from '@aztec/node-keystore';
26
23
  import { trySnapshotSync, uploadSnapshot } from '@aztec/node-lib/actions';
24
+ import { createForwarderL1TxUtilsFromSigners, createL1TxUtilsFromSigners } from '@aztec/node-lib/factories';
27
25
  import {
28
- createForwarderL1TxUtilsFromEthSigner,
29
- createL1TxUtilsWithBlobsFromEthSigner,
30
- } from '@aztec/node-lib/factories';
31
- import { type P2P, type P2PClientDeps, createP2PClient, getDefaultAllowedSetupFunctions } from '@aztec/p2p';
26
+ type P2P,
27
+ type P2PClientDeps,
28
+ createP2PClient,
29
+ createTxValidatorForAcceptingTxsOverRPC,
30
+ getDefaultAllowedSetupFunctions,
31
+ } from '@aztec/p2p';
32
32
  import { ProtocolContractAddress } from '@aztec/protocol-contracts';
33
+ import { type ProverNode, type ProverNodeDeps, createProverNode } from '@aztec/prover-node';
34
+ import { createKeyStoreForProver } from '@aztec/prover-node/config';
33
35
  import { GlobalVariableBuilder, SequencerClient, type SequencerPublisher } from '@aztec/sequencer-client';
34
36
  import { PublicProcessorFactory } from '@aztec/simulator/server';
35
37
  import {
@@ -41,7 +43,14 @@ import {
41
43
  } from '@aztec/slasher';
42
44
  import { CollectionLimitsConfig, PublicSimulatorConfig } from '@aztec/stdlib/avm';
43
45
  import { AztecAddress } from '@aztec/stdlib/aztec-address';
44
- import { BlockHash, type BlockParameter, type DataInBlock, L2Block, type L2BlockSource } from '@aztec/stdlib/block';
46
+ import {
47
+ type BlockData,
48
+ BlockHash,
49
+ type BlockParameter,
50
+ type DataInBlock,
51
+ L2Block,
52
+ type L2BlockSource,
53
+ } from '@aztec/stdlib/block';
45
54
  import type { PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
46
55
  import type {
47
56
  ContractClassPublic,
@@ -69,9 +78,9 @@ import {
69
78
  type WorldStateSynchronizer,
70
79
  tryStop,
71
80
  } from '@aztec/stdlib/interfaces/server';
72
- import type { LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs';
81
+ import type { DebugLogStore, LogFilter, SiloedTag, Tag, TxScopedL2Log } from '@aztec/stdlib/logs';
82
+ import { InMemoryDebugLogStore, NullDebugLogStore } from '@aztec/stdlib/logs';
73
83
  import { InboxLeaf, type L1ToL2MessageSource } from '@aztec/stdlib/messaging';
74
- import { P2PClientType } from '@aztec/stdlib/p2p';
75
84
  import type { Offense, SlashPayloadRound } from '@aztec/stdlib/slashing';
76
85
  import type { NullifierLeafPreimage, PublicDataTreeLeaf, PublicDataTreeLeafPreimage } from '@aztec/stdlib/trees';
77
86
  import { MerkleTreeId, NullifierMembershipWitness, PublicDataWitness } from '@aztec/stdlib/trees';
@@ -103,11 +112,11 @@ import {
103
112
  ValidatorClient,
104
113
  createBlockProposalHandler,
105
114
  createValidatorClient,
106
- createValidatorForAcceptingTxs,
107
115
  } from '@aztec/validator-client';
116
+ import type { SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
108
117
  import { createWorldStateSynchronizer } from '@aztec/world-state';
109
118
 
110
- import { createPublicClient, fallback, http } from 'viem';
119
+ import { createPublicClient } from 'viem';
111
120
 
112
121
  import { createSentinel } from '../sentinel/factory.js';
113
122
  import { Sentinel } from '../sentinel/sentinel.js';
@@ -135,6 +144,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
135
144
  protected readonly l1ToL2MessageSource: L1ToL2MessageSource,
136
145
  protected readonly worldStateSynchronizer: WorldStateSynchronizer,
137
146
  protected readonly sequencer: SequencerClient | undefined,
147
+ protected readonly proverNode: ProverNode | undefined,
138
148
  protected readonly slasherClient: SlasherClientInterface | undefined,
139
149
  protected readonly validatorsSentinel: Sentinel | undefined,
140
150
  protected readonly epochPruneWatcher: EpochPruneWatcher | undefined,
@@ -143,16 +153,32 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
143
153
  protected readonly globalVariableBuilder: GlobalVariableBuilderInterface,
144
154
  protected readonly epochCache: EpochCacheInterface,
145
155
  protected readonly packageVersion: string,
146
- private proofVerifier: ClientProtocolCircuitVerifier,
156
+ private peerProofVerifier: ClientProtocolCircuitVerifier,
157
+ private rpcProofVerifier: ClientProtocolCircuitVerifier,
147
158
  private telemetry: TelemetryClient = getTelemetryClient(),
148
159
  private log = createLogger('node'),
149
160
  private blobClient?: BlobClientInterface,
161
+ private validatorClient?: ValidatorClient,
162
+ private keyStoreManager?: KeystoreManager,
163
+ private debugLogStore: DebugLogStore = new NullDebugLogStore(),
150
164
  ) {
151
165
  this.metrics = new NodeMetrics(telemetry, 'AztecNodeService');
152
166
  this.tracer = telemetry.getTracer('AztecNodeService');
153
167
 
154
168
  this.log.info(`Aztec Node version: ${this.packageVersion}`);
155
169
  this.log.info(`Aztec Node started on chain 0x${l1ChainId.toString(16)}`, config.l1Contracts);
170
+
171
+ // A defensive check that protects us against introducing a bug in the complex `createAndSync` function. We must
172
+ // never have debugLogStore enabled when not in test mode because then we would be accumulating debug logs in
173
+ // memory which could be a DoS vector on the sequencer (since no fees are paid for debug logs).
174
+ if (debugLogStore.isEnabled && config.realProofs) {
175
+ throw new Error('debugLogStore should never be enabled when realProofs are set');
176
+ }
177
+ }
178
+
179
+ /** @internal Exposed for testing — returns the RPC proof verifier. */
180
+ public getProofVerifier(): ClientProtocolCircuitVerifier {
181
+ return this.rpcProofVerifier;
156
182
  }
157
183
 
158
184
  public async getWorldStateSyncStatus(): Promise<WorldStateSyncStatus> {
@@ -176,11 +202,14 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
176
202
  logger?: Logger;
177
203
  publisher?: SequencerPublisher;
178
204
  dateProvider?: DateProvider;
179
- p2pClientDeps?: P2PClientDeps<P2PClientType.Full>;
205
+ p2pClientDeps?: P2PClientDeps;
206
+ proverNodeDeps?: Partial<ProverNodeDeps>;
207
+ slashingProtectionDb?: SlashingProtectionDatabase;
180
208
  } = {},
181
209
  options: {
182
210
  prefilledPublicData?: PublicDataTreeLeaf[];
183
211
  dontStartSequencer?: boolean;
212
+ dontStartProverNode?: boolean;
184
213
  } = {},
185
214
  ): Promise<AztecNodeService> {
186
215
  const config = { ...inputConfig }; // Copy the config so we dont mutate the input object
@@ -190,16 +219,29 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
190
219
  const dateProvider = deps.dateProvider ?? new DateProvider();
191
220
  const ethereumChain = createEthereumChain(config.l1RpcUrls, config.l1ChainId);
192
221
 
193
- // Build a key store from file if given or from environment otherwise
222
+ // Build a key store from file if given or from environment otherwise.
223
+ // We keep the raw KeyStore available so we can merge with prover keys if enableProverNode is set.
194
224
  let keyStoreManager: KeystoreManager | undefined;
195
225
  const keyStoreProvided = config.keyStoreDirectory !== undefined && config.keyStoreDirectory.length > 0;
196
226
  if (keyStoreProvided) {
197
227
  const keyStores = loadKeystores(config.keyStoreDirectory!);
198
228
  keyStoreManager = new KeystoreManager(mergeKeystores(keyStores));
199
229
  } else {
200
- const keyStore = createKeyStoreForValidator(config);
201
- if (keyStore) {
202
- keyStoreManager = new KeystoreManager(keyStore);
230
+ const rawKeyStores: KeyStore[] = [];
231
+ const validatorKeyStore = createKeyStoreForValidator(config);
232
+ if (validatorKeyStore) {
233
+ rawKeyStores.push(validatorKeyStore);
234
+ }
235
+ if (config.enableProverNode) {
236
+ const proverKeyStore = createKeyStoreForProver(config);
237
+ if (proverKeyStore) {
238
+ rawKeyStores.push(proverKeyStore);
239
+ }
240
+ }
241
+ if (rawKeyStores.length > 0) {
242
+ keyStoreManager = new KeystoreManager(
243
+ rawKeyStores.length === 1 ? rawKeyStores[0] : mergeKeystores(rawKeyStores),
244
+ );
203
245
  }
204
246
  }
205
247
 
@@ -210,10 +252,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
210
252
  if (keyStoreManager === undefined) {
211
253
  throw new Error('Failed to create key store, a requirement for running a validator');
212
254
  }
213
- if (!keyStoreProvided) {
214
- log.warn(
215
- 'KEY STORE CREATED FROM ENVIRONMENT, IT IS RECOMMENDED TO USE A FILE-BASED KEY STORE IN PRODUCTION ENVIRONMENTS',
216
- );
255
+ if (!keyStoreProvided && process.env.NODE_ENV !== 'test') {
256
+ log.warn("Keystore created from env: it's recommended to use a file-based key store for production");
217
257
  }
218
258
  ValidatorClient.validateKeyStoreConfiguration(keyStoreManager, log);
219
259
  }
@@ -227,7 +267,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
227
267
 
228
268
  const publicClient = createPublicClient({
229
269
  chain: ethereumChain.chainInfo,
230
- transport: fallback(config.l1RpcUrls.map((url: string) => http(url, { batch: false }))),
270
+ transport: makeL1HttpTransport(config.l1RpcUrls, { timeout: config.l1HttpTimeoutMS }),
231
271
  pollingInterval: config.viemPollingIntervalMS,
232
272
  });
233
273
 
@@ -241,10 +281,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
241
281
  config.l1Contracts = { ...config.l1Contracts, ...l1ContractsAddresses };
242
282
 
243
283
  const rollupContract = new RollupContract(publicClient, config.l1Contracts.rollupAddress.toString());
244
- const [l1GenesisTime, slotDuration, rollupVersionFromRollup] = await Promise.all([
284
+ const [l1GenesisTime, slotDuration, rollupVersionFromRollup, rollupManaLimit] = await Promise.all([
245
285
  rollupContract.getL1GenesisTime(),
246
286
  rollupContract.getSlotDuration(),
247
287
  rollupContract.getVersion(),
288
+ rollupContract.getManaLimit().then(Number),
248
289
  ] as const);
249
290
 
250
291
  config.rollupVersion ??= Number(rollupVersionFromRollup);
@@ -255,7 +296,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
255
296
  );
256
297
  }
257
298
 
258
- const blobClient = await createBlobClientWithFileStores(config, createLogger('node:blob-client:client'));
299
+ const blobClient = await createBlobClientWithFileStores(config, log.createChild('blob-client'));
259
300
 
260
301
  // attempt snapshot sync if possible
261
302
  await trySnapshotSync(config, log);
@@ -275,21 +316,40 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
275
316
  options.prefilledPublicData,
276
317
  telemetry,
277
318
  );
278
- const circuitVerifier =
279
- config.realProofs || config.debugForceTxProofVerification
280
- ? await BBCircuitVerifier.new(config)
281
- : new TestCircuitVerifier(config.proverTestVerificationDelayMs);
319
+ const useRealVerifiers = config.realProofs || config.debugForceTxProofVerification;
320
+ let peerProofVerifier: ClientProtocolCircuitVerifier;
321
+ let rpcProofVerifier: ClientProtocolCircuitVerifier;
322
+ if (useRealVerifiers) {
323
+ peerProofVerifier = await BatchChonkVerifier.new(config, config.bbChonkVerifyMaxBatch, 'peer');
324
+ const rpcVerifier = await BBCircuitVerifier.new(config);
325
+ rpcProofVerifier = new QueuedIVCVerifier(rpcVerifier, config.numConcurrentIVCVerifiers);
326
+ } else {
327
+ peerProofVerifier = new TestCircuitVerifier(config.proverTestVerificationDelayMs);
328
+ rpcProofVerifier = new TestCircuitVerifier(config.proverTestVerificationDelayMs);
329
+ }
330
+
331
+ let debugLogStore: DebugLogStore;
282
332
  if (!config.realProofs) {
283
333
  log.warn(`Aztec node is accepting fake proofs`);
334
+
335
+ debugLogStore = new InMemoryDebugLogStore();
336
+ log.info(
337
+ 'Aztec node started in test mode (realProofs set to false) hence debug logs from public functions will be collected and served',
338
+ );
339
+ } else {
340
+ debugLogStore = new NullDebugLogStore();
341
+ }
342
+
343
+ const proverOnly = config.enableProverNode && config.disableValidator;
344
+ if (proverOnly) {
345
+ log.info('Starting in prover-only mode: skipping validator, sequencer, sentinel, and slasher subsystems');
284
346
  }
285
- const proofVerifier = new QueuedIVCVerifier(config, circuitVerifier);
286
347
 
287
348
  // create the tx pool and the p2p client, which will need the l2 block source
288
349
  const p2pClient = await createP2PClient(
289
- P2PClientType.Full,
290
350
  config,
291
351
  archiver,
292
- proofVerifier,
352
+ peerProofVerifier,
293
353
  worldStateSynchronizer,
294
354
  epochCache,
295
355
  packageVersion,
@@ -298,49 +358,60 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
298
358
  deps.p2pClientDeps,
299
359
  );
300
360
 
301
- // We should really not be modifying the config object
302
- config.txPublicSetupAllowList = config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions());
361
+ // We'll accumulate sentinel watchers here
362
+ const watchers: Watcher[] = [];
303
363
 
304
- // Create FullNodeCheckpointsBuilder for validator and non-validator block proposal handling
364
+ // Create FullNodeCheckpointsBuilder for block proposal handling and tx validation.
365
+ // Override maxTxsPerCheckpoint with the validator-specific limit if set.
305
366
  const validatorCheckpointsBuilder = new FullNodeCheckpointsBuilder(
306
- { ...config, l1GenesisTime, slotDuration: Number(slotDuration) },
367
+ {
368
+ ...config,
369
+ l1GenesisTime,
370
+ slotDuration: Number(slotDuration),
371
+ rollupManaLimit,
372
+ maxTxsPerCheckpoint: config.validateMaxTxsPerCheckpoint,
373
+ },
307
374
  worldStateSynchronizer,
308
375
  archiver,
309
376
  dateProvider,
310
377
  telemetry,
311
378
  );
312
379
 
313
- // We'll accumulate sentinel watchers here
314
- const watchers: Watcher[] = [];
380
+ let validatorClient: ValidatorClient | undefined;
315
381
 
316
- // Create validator client if required
317
- const validatorClient = await createValidatorClient(config, {
318
- checkpointsBuilder: validatorCheckpointsBuilder,
319
- worldState: worldStateSynchronizer,
320
- p2pClient,
321
- telemetry,
322
- dateProvider,
323
- epochCache,
324
- blockSource: archiver,
325
- l1ToL2MessageSource: archiver,
326
- keyStoreManager,
327
- blobClient,
328
- });
382
+ if (!proverOnly) {
383
+ // Create validator client if required
384
+ validatorClient = await createValidatorClient(config, {
385
+ checkpointsBuilder: validatorCheckpointsBuilder,
386
+ worldState: worldStateSynchronizer,
387
+ p2pClient,
388
+ telemetry,
389
+ dateProvider,
390
+ epochCache,
391
+ blockSource: archiver,
392
+ l1ToL2MessageSource: archiver,
393
+ keyStoreManager,
394
+ blobClient,
395
+ slashingProtectionDb: deps.slashingProtectionDb,
396
+ });
329
397
 
330
- // If we have a validator client, register it as a source of offenses for the slasher,
331
- // and have it register callbacks on the p2p client *before* we start it, otherwise messages
332
- // like attestations or auths will fail.
333
- if (validatorClient) {
334
- watchers.push(validatorClient);
335
- if (!options.dontStartSequencer) {
336
- await validatorClient.registerHandlers();
398
+ // If we have a validator client, register it as a source of offenses for the slasher,
399
+ // and have it register callbacks on the p2p client *before* we start it, otherwise messages
400
+ // like attestations or auths will fail.
401
+ if (validatorClient) {
402
+ watchers.push(validatorClient);
403
+ if (!options.dontStartSequencer) {
404
+ await validatorClient.registerHandlers();
405
+ }
337
406
  }
338
407
  }
339
408
 
340
- // If there's no validator client but alwaysReexecuteBlockProposals is enabled,
341
- // create a BlockProposalHandler to reexecute block proposals for monitoring
342
- if (!validatorClient && config.alwaysReexecuteBlockProposals) {
343
- log.info('Setting up block proposal reexecution for monitoring');
409
+ // If there's no validator client, create a BlockProposalHandler to handle block proposals
410
+ // for monitoring or reexecution. Reexecution (default) allows us to follow the pending chain,
411
+ // while non-reexecution is used for validating the proposals and collecting their txs.
412
+ if (!validatorClient) {
413
+ const reexecute = !!config.alwaysReexecuteBlockProposals;
414
+ log.info(`Setting up block proposal handler` + (reexecute ? ' with reexecution of proposals' : ''));
344
415
  createBlockProposalHandler(config, {
345
416
  checkpointsBuilder: validatorCheckpointsBuilder,
346
417
  worldState: worldStateSynchronizer,
@@ -350,7 +421,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
350
421
  p2pClient,
351
422
  dateProvider,
352
423
  telemetry,
353
- }).registerForReexecution(p2pClient);
424
+ }).register(p2pClient, reexecute);
354
425
  }
355
426
 
356
427
  // Start world state and wait for it to sync to the archiver.
@@ -359,29 +430,33 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
359
430
  // Start p2p. Note that it depends on world state to be running.
360
431
  await p2pClient.start();
361
432
 
362
- const validatorsSentinel = await createSentinel(epochCache, archiver, p2pClient, config);
363
- if (validatorsSentinel && config.slashInactivityPenalty > 0n) {
364
- watchers.push(validatorsSentinel);
365
- }
366
-
433
+ let validatorsSentinel: Awaited<ReturnType<typeof createSentinel>> | undefined;
367
434
  let epochPruneWatcher: EpochPruneWatcher | undefined;
368
- if (config.slashPrunePenalty > 0n || config.slashDataWithholdingPenalty > 0n) {
369
- epochPruneWatcher = new EpochPruneWatcher(
370
- archiver,
371
- archiver,
372
- epochCache,
373
- p2pClient.getTxProvider(),
374
- validatorCheckpointsBuilder,
375
- config,
376
- );
377
- watchers.push(epochPruneWatcher);
378
- }
379
-
380
- // We assume we want to slash for invalid attestations unless all max penalties are set to 0
381
435
  let attestationsBlockWatcher: AttestationsBlockWatcher | undefined;
382
- if (config.slashProposeInvalidAttestationsPenalty > 0n || config.slashAttestDescendantOfInvalidPenalty > 0n) {
383
- attestationsBlockWatcher = new AttestationsBlockWatcher(archiver, epochCache, config);
384
- watchers.push(attestationsBlockWatcher);
436
+
437
+ if (!proverOnly) {
438
+ validatorsSentinel = await createSentinel(epochCache, archiver, p2pClient, config);
439
+ if (validatorsSentinel && config.slashInactivityPenalty > 0n) {
440
+ watchers.push(validatorsSentinel);
441
+ }
442
+
443
+ if (config.slashPrunePenalty > 0n || config.slashDataWithholdingPenalty > 0n) {
444
+ epochPruneWatcher = new EpochPruneWatcher(
445
+ archiver,
446
+ archiver,
447
+ epochCache,
448
+ p2pClient.getTxProvider(),
449
+ validatorCheckpointsBuilder,
450
+ config,
451
+ );
452
+ watchers.push(epochPruneWatcher);
453
+ }
454
+
455
+ // We assume we want to slash for invalid attestations unless all max penalties are set to 0
456
+ if (config.slashProposeInvalidAttestationsPenalty > 0n || config.slashAttestDescendantOfInvalidPenalty > 0n) {
457
+ attestationsBlockWatcher = new AttestationsBlockWatcher(archiver, epochCache, config);
458
+ watchers.push(attestationsBlockWatcher);
459
+ }
385
460
  }
386
461
 
387
462
  // Start p2p-related services once the archiver has completed sync
@@ -396,6 +471,14 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
396
471
  })
397
472
  .catch(err => log.error('Failed to start p2p services after archiver sync', err));
398
473
 
474
+ const globalVariableBuilder = new GlobalVariableBuilder(dateProvider, publicClient, {
475
+ l1Contracts: config.l1Contracts,
476
+ ethereumSlotDuration: config.ethereumSlotDuration,
477
+ rollupVersion: BigInt(config.rollupVersion),
478
+ l1GenesisTime,
479
+ slotDuration: Number(slotDuration),
480
+ });
481
+
399
482
  // Validator enabled, create/start relevant service
400
483
  let sequencer: SequencerClient | undefined;
401
484
  let slasherClient: SlasherClientInterface | undefined;
@@ -418,34 +501,49 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
418
501
  );
419
502
  await slasherClient.start();
420
503
 
421
- const l1TxUtils = config.publisherForwarderAddress
422
- ? await createForwarderL1TxUtilsFromEthSigner(
504
+ const l1TxUtils = config.sequencerPublisherForwarderAddress
505
+ ? await createForwarderL1TxUtilsFromSigners(
423
506
  publicClient,
424
507
  keyStoreManager!.createAllValidatorPublisherSigners(),
425
- config.publisherForwarderAddress,
508
+ config.sequencerPublisherForwarderAddress,
426
509
  { ...config, scope: 'sequencer' },
427
- { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider },
510
+ { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider, kzg: Blob.getViemKzgInstance() },
428
511
  )
429
- : await createL1TxUtilsWithBlobsFromEthSigner(
512
+ : await createL1TxUtilsFromSigners(
430
513
  publicClient,
431
514
  keyStoreManager!.createAllValidatorPublisherSigners(),
432
515
  { ...config, scope: 'sequencer' },
433
- { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider },
516
+ { telemetry, logger: log.createChild('l1-tx-utils'), dateProvider, kzg: Blob.getViemKzgInstance() },
434
517
  );
435
518
 
519
+ // Create a funder L1TxUtils from the keystore funding account (if configured)
520
+ const fundingSigner = keyStoreManager?.createFundingSigner();
521
+ let funderL1TxUtils: L1TxUtils | undefined;
522
+ if (fundingSigner) {
523
+ const [funder] = await createL1TxUtilsFromSigners(
524
+ publicClient,
525
+ [fundingSigner],
526
+ { ...config, scope: 'sequencer' },
527
+ { telemetry, logger: log.createChild('l1-tx-utils:funder'), dateProvider },
528
+ );
529
+ funderL1TxUtils = funder;
530
+ }
531
+
436
532
  // Create and start the sequencer client
437
533
  const checkpointsBuilder = new CheckpointsBuilder(
438
- { ...config, l1GenesisTime, slotDuration: Number(slotDuration) },
534
+ { ...config, l1GenesisTime, slotDuration: Number(slotDuration), rollupManaLimit },
439
535
  worldStateSynchronizer,
440
536
  archiver,
441
537
  dateProvider,
442
538
  telemetry,
539
+ debugLogStore,
443
540
  );
444
541
 
445
542
  sequencer = await SequencerClient.new(config, {
446
543
  ...deps,
447
544
  epochCache,
448
545
  l1TxUtils,
546
+ funderL1TxUtils,
449
547
  validatorClient,
450
548
  p2pClient,
451
549
  worldStateSynchronizer,
@@ -457,6 +555,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
457
555
  dateProvider,
458
556
  blobClient,
459
557
  nodeKeyStore: keyStoreManager!,
558
+ globalVariableBuilder,
460
559
  });
461
560
  }
462
561
 
@@ -467,14 +566,30 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
467
566
  log.warn(`Sequencer created but not started`);
468
567
  }
469
568
 
470
- const globalVariableBuilder = new GlobalVariableBuilder({
471
- ...config,
472
- rollupVersion: BigInt(config.rollupVersion),
473
- l1GenesisTime,
474
- slotDuration: Number(slotDuration),
475
- });
569
+ // Create prover node subsystem if enabled
570
+ let proverNode: ProverNode | undefined;
571
+ if (config.enableProverNode) {
572
+ proverNode = await createProverNode(config, {
573
+ ...deps.proverNodeDeps,
574
+ telemetry,
575
+ dateProvider,
576
+ archiver,
577
+ worldStateSynchronizer,
578
+ p2pClient,
579
+ epochCache,
580
+ blobClient,
581
+ keyStoreManager,
582
+ });
583
+
584
+ if (!options.dontStartProverNode) {
585
+ await proverNode.start();
586
+ log.info(`Prover node subsystem started`);
587
+ } else {
588
+ log.info(`Prover node subsystem created but not started`);
589
+ }
590
+ }
476
591
 
477
- return new AztecNodeService(
592
+ const node = new AztecNodeService(
478
593
  config,
479
594
  p2pClient,
480
595
  archiver,
@@ -483,6 +598,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
483
598
  archiver,
484
599
  worldStateSynchronizer,
485
600
  sequencer,
601
+ proverNode,
486
602
  slasherClient,
487
603
  validatorsSentinel,
488
604
  epochPruneWatcher,
@@ -491,11 +607,17 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
491
607
  globalVariableBuilder,
492
608
  epochCache,
493
609
  packageVersion,
494
- proofVerifier,
610
+ peerProofVerifier,
611
+ rpcProofVerifier,
495
612
  telemetry,
496
613
  log,
497
614
  blobClient,
615
+ validatorClient,
616
+ keyStoreManager,
617
+ debugLogStore,
498
618
  );
619
+
620
+ return node;
499
621
  }
500
622
 
501
623
  /**
@@ -506,6 +628,11 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
506
628
  return this.sequencer;
507
629
  }
508
630
 
631
+ /** Returns the prover node subsystem, if enabled. */
632
+ public getProverNode(): ProverNode | undefined {
633
+ return this.proverNode;
634
+ }
635
+
509
636
  public getBlockSource(): L2BlockSource {
510
637
  return this.blockSource;
511
638
  }
@@ -531,7 +658,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
531
658
  }
532
659
 
533
660
  public async getAllowedPublicSetup(): Promise<AllowedElement[]> {
534
- return this.config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions());
661
+ return [...(await getDefaultAllowedSetupFunctions()), ...(this.config.txPublicSetupAllowListExtend ?? [])];
535
662
  }
536
663
 
537
664
  /**
@@ -559,6 +686,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
559
686
  enr,
560
687
  l1ContractAddresses: contractAddresses,
561
688
  protocolContractAddresses: protocolContractAddresses,
689
+ realProofs: !!this.config.realProofs,
562
690
  };
563
691
 
564
692
  return nodeInfo;
@@ -625,6 +753,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
625
753
  return (await this.blockSource.getCheckpointedBlocks(from, limit)) ?? [];
626
754
  }
627
755
 
756
+ public getCheckpointsDataForEpoch(epochNumber: EpochNumber) {
757
+ return this.blockSource.getCheckpointsDataForEpoch(epochNumber);
758
+ }
759
+
628
760
  /**
629
761
  * Method to fetch the current min L2 fees.
630
762
  * @returns The current min L2 fees.
@@ -657,6 +789,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
657
789
  return await this.blockSource.getCheckpointedL2BlockNumber();
658
790
  }
659
791
 
792
+ public getCheckpointNumber(): Promise<CheckpointNumber> {
793
+ return this.blockSource.getCheckpointNumber();
794
+ }
795
+
660
796
  /**
661
797
  * Method to fetch the version of the package.
662
798
  * @returns The node package version
@@ -694,18 +830,22 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
694
830
  page?: number,
695
831
  referenceBlock?: BlockHash,
696
832
  ): Promise<TxScopedL2Log[][]> {
833
+ let upToBlockNumber: BlockNumber | undefined;
697
834
  if (referenceBlock) {
698
835
  const initialBlockHash = await this.#getInitialHeaderHash();
699
- if (!referenceBlock.equals(initialBlockHash)) {
836
+ if (referenceBlock.equals(initialBlockHash)) {
837
+ upToBlockNumber = BlockNumber(0);
838
+ } else {
700
839
  const header = await this.blockSource.getBlockHeaderByHash(referenceBlock);
701
840
  if (!header) {
702
841
  throw new Error(
703
842
  `Block ${referenceBlock.toString()} not found in the node. This might indicate a reorg has occurred.`,
704
843
  );
705
844
  }
845
+ upToBlockNumber = header.globalVariables.blockNumber;
706
846
  }
707
847
  }
708
- return this.logsSource.getPrivateLogsByTags(tags, page);
848
+ return this.logsSource.getPrivateLogsByTags(tags, page, upToBlockNumber);
709
849
  }
710
850
 
711
851
  public async getPublicLogsByTagsFromContract(
@@ -714,18 +854,22 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
714
854
  page?: number,
715
855
  referenceBlock?: BlockHash,
716
856
  ): Promise<TxScopedL2Log[][]> {
857
+ let upToBlockNumber: BlockNumber | undefined;
717
858
  if (referenceBlock) {
718
859
  const initialBlockHash = await this.#getInitialHeaderHash();
719
- if (!referenceBlock.equals(initialBlockHash)) {
860
+ if (referenceBlock.equals(initialBlockHash)) {
861
+ upToBlockNumber = BlockNumber(0);
862
+ } else {
720
863
  const header = await this.blockSource.getBlockHeaderByHash(referenceBlock);
721
864
  if (!header) {
722
865
  throw new Error(
723
866
  `Block ${referenceBlock.toString()} not found in the node. This might indicate a reorg has occurred.`,
724
867
  );
725
868
  }
869
+ upToBlockNumber = header.globalVariables.blockNumber;
726
870
  }
727
871
  }
728
- return this.logsSource.getPublicLogsByTagsFromContract(contractAddress, tags, page);
872
+ return this.logsSource.getPublicLogsByTagsFromContract(contractAddress, tags, page, upToBlockNumber);
729
873
  }
730
874
 
731
875
  /**
@@ -767,8 +911,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
767
911
  }
768
912
 
769
913
  await this.p2pClient!.sendTx(tx);
770
- this.metrics.receivedTx(timer.ms(), true);
771
- this.log.info(`Received tx ${txHash}`, { txHash });
914
+ const duration = timer.ms();
915
+ this.metrics.receivedTx(duration, true);
916
+ this.log.info(`Received tx ${txHash} in ${duration}ms`, { txHash });
772
917
  }
773
918
 
774
919
  public async getTxReceipt(txHash: TxHash): Promise<TxReceipt> {
@@ -780,18 +925,22 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
780
925
  // Then get the actual tx from the archiver, which tracks every tx in a mined block.
781
926
  const settledTxReceipt = await this.blockSource.getSettledTxReceipt(txHash);
782
927
 
928
+ let receipt: TxReceipt;
783
929
  if (settledTxReceipt) {
784
- // If the archiver has the receipt then return it.
785
- return settledTxReceipt;
930
+ receipt = settledTxReceipt;
786
931
  } else if (isKnownToPool) {
787
932
  // If the tx is in the pool but not in the archiver, it's pending.
788
933
  // This handles race conditions between archiver and p2p, where the archiver
789
934
  // has pruned the block in which a tx was mined, but p2p has not caught up yet.
790
- return new TxReceipt(txHash, TxStatus.PENDING, undefined, undefined);
935
+ receipt = new TxReceipt(txHash, TxStatus.PENDING, undefined, undefined);
791
936
  } else {
792
937
  // Otherwise, if we don't know the tx, we consider it dropped.
793
- return new TxReceipt(txHash, TxStatus.DROPPED, undefined, 'Tx dropped by P2P node');
938
+ receipt = new TxReceipt(txHash, TxStatus.DROPPED, undefined, 'Tx dropped by P2P node');
794
939
  }
940
+
941
+ this.debugLogStore.decorateReceiptWithLogs(txHash.toString(), receipt);
942
+
943
+ return receipt;
795
944
  }
796
945
 
797
946
  public getTxEffect(txHash: TxHash): Promise<IndexedTxEffect | undefined> {
@@ -806,8 +955,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
806
955
  await tryStop(this.validatorsSentinel);
807
956
  await tryStop(this.epochPruneWatcher);
808
957
  await tryStop(this.slasherClient);
809
- await tryStop(this.proofVerifier);
958
+ await Promise.all([tryStop(this.peerProofVerifier), tryStop(this.rpcProofVerifier)]);
810
959
  await tryStop(this.sequencer);
960
+ await tryStop(this.proverNode);
811
961
  await tryStop(this.p2pClient);
812
962
  await tryStop(this.worldStateSynchronizer);
813
963
  await tryStop(this.blockSource);
@@ -857,100 +1007,98 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
857
1007
  }
858
1008
 
859
1009
  public async findLeavesIndexes(
860
- block: BlockParameter,
1010
+ referenceBlock: BlockParameter,
861
1011
  treeId: MerkleTreeId,
862
1012
  leafValues: Fr[],
863
1013
  ): Promise<(DataInBlock<bigint> | undefined)[]> {
864
- const committedDb = await this.#getWorldState(block);
1014
+ const committedDb = await this.getWorldState(referenceBlock);
865
1015
  const maybeIndices = await committedDb.findLeafIndices(
866
1016
  treeId,
867
1017
  leafValues.map(x => x.toBuffer()),
868
1018
  );
869
- // We filter out undefined values
870
- const indices = maybeIndices.filter(x => x !== undefined) as bigint[];
1019
+ // Filter out undefined values to query block numbers only for found leaves
1020
+ const definedIndices = maybeIndices.filter(x => x !== undefined);
871
1021
 
872
- // Now we find the block numbers for the indices
873
- const blockNumbers = await committedDb.getBlockNumbersForLeafIndices(treeId, indices);
1022
+ // Now we find the block numbers for the defined indices
1023
+ const blockNumbers = await committedDb.getBlockNumbersForLeafIndices(treeId, definedIndices);
874
1024
 
875
- // If any of the block numbers are undefined, we throw an error.
876
- for (let i = 0; i < indices.length; i++) {
877
- if (blockNumbers[i] === undefined) {
878
- throw new Error(`Block number is undefined for leaf index ${indices[i]} in tree ${MerkleTreeId[treeId]}`);
1025
+ // Build a map from leaf index to block number
1026
+ const indexToBlockNumber = new Map<bigint, BlockNumber>();
1027
+ for (let i = 0; i < definedIndices.length; i++) {
1028
+ const blockNumber = blockNumbers[i];
1029
+ if (blockNumber === undefined) {
1030
+ throw new Error(
1031
+ `Block number is undefined for leaf index ${definedIndices[i]} in tree ${MerkleTreeId[treeId]}`,
1032
+ );
879
1033
  }
1034
+ indexToBlockNumber.set(definedIndices[i], blockNumber);
880
1035
  }
881
1036
 
882
1037
  // Get unique block numbers in order to optimize num calls to getLeafValue function.
883
- const uniqueBlockNumbers = [...new Set(blockNumbers.filter(x => x !== undefined))];
1038
+ const uniqueBlockNumbers = [...new Set(indexToBlockNumber.values())];
884
1039
 
885
- // Now we obtain the block hashes from the archive tree by calling await `committedDb.getLeafValue(treeId, index)`
886
- // (note that block number corresponds to the leaf index in the archive tree).
1040
+ // Now we obtain the block hashes from the archive tree (block number = leaf index in archive tree).
887
1041
  const blockHashes = await Promise.all(
888
1042
  uniqueBlockNumbers.map(blockNumber => {
889
1043
  return committedDb.getLeafValue(MerkleTreeId.ARCHIVE, BigInt(blockNumber));
890
1044
  }),
891
1045
  );
892
1046
 
893
- // If any of the block hashes are undefined, we throw an error.
1047
+ // Build a map from block number to block hash
1048
+ const blockNumberToHash = new Map<BlockNumber, Fr>();
894
1049
  for (let i = 0; i < uniqueBlockNumbers.length; i++) {
895
- if (blockHashes[i] === undefined) {
1050
+ const blockHash = blockHashes[i];
1051
+ if (blockHash === undefined) {
896
1052
  throw new Error(`Block hash is undefined for block number ${uniqueBlockNumbers[i]}`);
897
1053
  }
1054
+ blockNumberToHash.set(uniqueBlockNumbers[i], blockHash);
898
1055
  }
899
1056
 
900
1057
  // Create DataInBlock objects by combining indices, blockNumbers and blockHashes and return them.
901
- return maybeIndices.map((index, i) => {
1058
+ return maybeIndices.map(index => {
902
1059
  if (index === undefined) {
903
1060
  return undefined;
904
1061
  }
905
- const blockNumber = blockNumbers[i];
1062
+ const blockNumber = indexToBlockNumber.get(index);
906
1063
  if (blockNumber === undefined) {
907
- return undefined;
1064
+ throw new Error(`Block number not found for leaf index ${index} in tree ${MerkleTreeId[treeId]}`);
908
1065
  }
909
- const blockHashIndex = uniqueBlockNumbers.indexOf(blockNumber);
910
- const blockHash = blockHashes[blockHashIndex];
911
- if (!blockHash) {
912
- return undefined;
1066
+ const blockHash = blockNumberToHash.get(blockNumber);
1067
+ if (blockHash === undefined) {
1068
+ throw new Error(`Block hash not found for block number ${blockNumber}`);
913
1069
  }
914
1070
  return {
915
- l2BlockNumber: BlockNumber(Number(blockNumber)),
1071
+ l2BlockNumber: blockNumber,
916
1072
  l2BlockHash: new BlockHash(blockHash),
917
1073
  data: index,
918
1074
  };
919
1075
  });
920
1076
  }
921
1077
 
922
- public async getNullifierSiblingPath(
923
- block: BlockParameter,
924
- leafIndex: bigint,
925
- ): Promise<SiblingPath<typeof NULLIFIER_TREE_HEIGHT>> {
926
- const committedDb = await this.#getWorldState(block);
927
- return committedDb.getSiblingPath(MerkleTreeId.NULLIFIER_TREE, leafIndex);
928
- }
929
-
930
- public async getNoteHashSiblingPath(
931
- block: BlockParameter,
932
- leafIndex: bigint,
933
- ): Promise<SiblingPath<typeof NOTE_HASH_TREE_HEIGHT>> {
934
- const committedDb = await this.#getWorldState(block);
935
- return committedDb.getSiblingPath(MerkleTreeId.NOTE_HASH_TREE, leafIndex);
936
- }
937
-
938
- public async getArchiveMembershipWitness(
939
- block: BlockParameter,
940
- archive: Fr,
1078
+ public async getBlockHashMembershipWitness(
1079
+ referenceBlock: BlockParameter,
1080
+ blockHash: BlockHash,
941
1081
  ): Promise<MembershipWitness<typeof ARCHIVE_HEIGHT> | undefined> {
942
- const committedDb = await this.#getWorldState(block);
943
- const [pathAndIndex] = await committedDb.findSiblingPaths<MerkleTreeId.ARCHIVE>(MerkleTreeId.ARCHIVE, [archive]);
1082
+ // The Noir circuit checks the archive membership proof against `anchor_block_header.last_archive.root`,
1083
+ // which is the archive tree root BEFORE the anchor block was added (i.e. the state after block N-1).
1084
+ // So we need the world state at block N-1, not block N, to produce a sibling path matching that root.
1085
+ const referenceBlockNumber = await this.resolveBlockNumber(referenceBlock);
1086
+ if (referenceBlockNumber === BlockNumber.ZERO) {
1087
+ // Block 0 (the initial block) has an empty archive, so no membership witness can exist.
1088
+ return undefined;
1089
+ }
1090
+ const committedDb = await this.getWorldState(BlockNumber(referenceBlockNumber - 1));
1091
+ const [pathAndIndex] = await committedDb.findSiblingPaths<MerkleTreeId.ARCHIVE>(MerkleTreeId.ARCHIVE, [blockHash]);
944
1092
  return pathAndIndex === undefined
945
1093
  ? undefined
946
1094
  : MembershipWitness.fromSiblingPath(pathAndIndex.index, pathAndIndex.path);
947
1095
  }
948
1096
 
949
1097
  public async getNoteHashMembershipWitness(
950
- block: BlockParameter,
1098
+ referenceBlock: BlockParameter,
951
1099
  noteHash: Fr,
952
1100
  ): Promise<MembershipWitness<typeof NOTE_HASH_TREE_HEIGHT> | undefined> {
953
- const committedDb = await this.#getWorldState(block);
1101
+ const committedDb = await this.getWorldState(referenceBlock);
954
1102
  const [pathAndIndex] = await committedDb.findSiblingPaths<MerkleTreeId.NOTE_HASH_TREE>(
955
1103
  MerkleTreeId.NOTE_HASH_TREE,
956
1104
  [noteHash],
@@ -961,10 +1109,10 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
961
1109
  }
962
1110
 
963
1111
  public async getL1ToL2MessageMembershipWitness(
964
- block: BlockParameter,
1112
+ referenceBlock: BlockParameter,
965
1113
  l1ToL2Message: Fr,
966
1114
  ): Promise<[bigint, SiblingPath<typeof L1_TO_L2_MSG_TREE_HEIGHT>] | undefined> {
967
- const db = await this.#getWorldState(block);
1115
+ const db = await this.getWorldState(referenceBlock);
968
1116
  const [witness] = await db.findSiblingPaths(MerkleTreeId.L1_TO_L2_MESSAGE_TREE, [l1ToL2Message]);
969
1117
  if (!witness) {
970
1118
  return undefined;
@@ -974,11 +1122,9 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
974
1122
  return [witness.index, witness.path];
975
1123
  }
976
1124
 
977
- public async getL1ToL2MessageBlock(l1ToL2Message: Fr): Promise<BlockNumber | undefined> {
1125
+ public async getL1ToL2MessageCheckpoint(l1ToL2Message: Fr): Promise<CheckpointNumber | undefined> {
978
1126
  const messageIndex = await this.l1ToL2MessageSource.getL1ToL2MessageIndex(l1ToL2Message);
979
- return messageIndex
980
- ? BlockNumber.fromCheckpointNumber(InboxLeaf.checkpointNumberFromIndex(messageIndex))
981
- : undefined;
1127
+ return messageIndex ? InboxLeaf.checkpointNumberFromIndex(messageIndex) : undefined;
982
1128
  }
983
1129
 
984
1130
  /**
@@ -999,45 +1145,19 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
999
1145
  public async getL2ToL1Messages(epoch: EpochNumber): Promise<Fr[][][][]> {
1000
1146
  // Assumes `getCheckpointedBlocksForEpoch` returns blocks in ascending order of block number.
1001
1147
  const checkpointedBlocks = await this.blockSource.getCheckpointedBlocksForEpoch(epoch);
1002
- const blocksInCheckpoints: L2Block[][] = [];
1003
- let previousSlotNumber = SlotNumber.ZERO;
1004
- let checkpointIndex = -1;
1005
- for (const checkpointedBlock of checkpointedBlocks) {
1006
- const block = checkpointedBlock.block;
1007
- const slotNumber = block.header.globalVariables.slotNumber;
1008
- if (slotNumber !== previousSlotNumber) {
1009
- checkpointIndex++;
1010
- blocksInCheckpoints.push([]);
1011
- previousSlotNumber = slotNumber;
1012
- }
1013
- blocksInCheckpoints[checkpointIndex].push(block);
1014
- }
1148
+ const blocksInCheckpoints = chunkBy(checkpointedBlocks, cb => cb.block.header.globalVariables.slotNumber).map(
1149
+ group => group.map(cb => cb.block),
1150
+ );
1015
1151
  return blocksInCheckpoints.map(blocks =>
1016
1152
  blocks.map(block => block.body.txEffects.map(txEffect => txEffect.l2ToL1Msgs)),
1017
1153
  );
1018
1154
  }
1019
1155
 
1020
- public async getArchiveSiblingPath(
1021
- block: BlockParameter,
1022
- leafIndex: bigint,
1023
- ): Promise<SiblingPath<typeof ARCHIVE_HEIGHT>> {
1024
- const committedDb = await this.#getWorldState(block);
1025
- return committedDb.getSiblingPath(MerkleTreeId.ARCHIVE, leafIndex);
1026
- }
1027
-
1028
- public async getPublicDataSiblingPath(
1029
- block: BlockParameter,
1030
- leafIndex: bigint,
1031
- ): Promise<SiblingPath<typeof PUBLIC_DATA_TREE_HEIGHT>> {
1032
- const committedDb = await this.#getWorldState(block);
1033
- return committedDb.getSiblingPath(MerkleTreeId.PUBLIC_DATA_TREE, leafIndex);
1034
- }
1035
-
1036
1156
  public async getNullifierMembershipWitness(
1037
- block: BlockParameter,
1157
+ referenceBlock: BlockParameter,
1038
1158
  nullifier: Fr,
1039
1159
  ): Promise<NullifierMembershipWitness | undefined> {
1040
- const db = await this.#getWorldState(block);
1160
+ const db = await this.getWorldState(referenceBlock);
1041
1161
  const [witness] = await db.findSiblingPaths(MerkleTreeId.NULLIFIER_TREE, [nullifier.toBuffer()]);
1042
1162
  if (!witness) {
1043
1163
  return undefined;
@@ -1052,32 +1172,20 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1052
1172
  return new NullifierMembershipWitness(index, leafPreimage as NullifierLeafPreimage, path);
1053
1173
  }
1054
1174
 
1055
- /**
1056
- * Returns a low nullifier membership witness for a given nullifier at a given block.
1057
- * @param block - The block parameter (block number, block hash, or 'latest') at which to get the data.
1058
- * @param nullifier - Nullifier we try to find the low nullifier witness for.
1059
- * @returns The low nullifier membership witness (if found).
1060
- * @remarks Low nullifier witness can be used to perform a nullifier non-inclusion proof by leveraging the "linked
1061
- * list structure" of leaves and proving that a lower nullifier is pointing to a bigger next value than the nullifier
1062
- * we are trying to prove non-inclusion for.
1063
- *
1064
- * Note: This function returns the membership witness of the nullifier itself and not the low nullifier when
1065
- * the nullifier already exists in the tree. This is because the `getPreviousValueIndex` function returns the
1066
- * index of the nullifier itself when it already exists in the tree.
1067
- * TODO: This is a confusing behavior and we should eventually address that.
1068
- */
1069
1175
  public async getLowNullifierMembershipWitness(
1070
- block: BlockParameter,
1176
+ referenceBlock: BlockParameter,
1071
1177
  nullifier: Fr,
1072
1178
  ): Promise<NullifierMembershipWitness | undefined> {
1073
- const committedDb = await this.#getWorldState(block);
1179
+ const committedDb = await this.getWorldState(referenceBlock);
1074
1180
  const findResult = await committedDb.getPreviousValueIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBigInt());
1075
1181
  if (!findResult) {
1076
1182
  return undefined;
1077
1183
  }
1078
1184
  const { index, alreadyPresent } = findResult;
1079
1185
  if (alreadyPresent) {
1080
- this.log.warn(`Nullifier ${nullifier.toBigInt()} already exists in the tree`);
1186
+ throw new Error(
1187
+ `Cannot prove nullifier non-inclusion: nullifier ${nullifier.toBigInt()} already exists in the tree`,
1188
+ );
1081
1189
  }
1082
1190
  const preimageData = (await committedDb.getLeafPreimage(MerkleTreeId.NULLIFIER_TREE, index))!;
1083
1191
 
@@ -1085,8 +1193,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1085
1193
  return new NullifierMembershipWitness(BigInt(index), preimageData as NullifierLeafPreimage, siblingPath);
1086
1194
  }
1087
1195
 
1088
- async getPublicDataWitness(block: BlockParameter, leafSlot: Fr): Promise<PublicDataWitness | undefined> {
1089
- const committedDb = await this.#getWorldState(block);
1196
+ async getPublicDataWitness(referenceBlock: BlockParameter, leafSlot: Fr): Promise<PublicDataWitness | undefined> {
1197
+ const committedDb = await this.getWorldState(referenceBlock);
1090
1198
  const lowLeafResult = await committedDb.getPreviousValueIndex(MerkleTreeId.PUBLIC_DATA_TREE, leafSlot.toBigInt());
1091
1199
  if (!lowLeafResult) {
1092
1200
  return undefined;
@@ -1100,8 +1208,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1100
1208
  }
1101
1209
  }
1102
1210
 
1103
- public async getPublicStorageAt(block: BlockParameter, contract: AztecAddress, slot: Fr): Promise<Fr> {
1104
- const committedDb = await this.#getWorldState(block);
1211
+ public async getPublicStorageAt(referenceBlock: BlockParameter, contract: AztecAddress, slot: Fr): Promise<Fr> {
1212
+ const committedDb = await this.getWorldState(referenceBlock);
1105
1213
  const leafSlot = await computePublicDataTreeLeafSlot(contract, slot);
1106
1214
 
1107
1215
  const lowLeafResult = await committedDb.getPreviousValueIndex(MerkleTreeId.PUBLIC_DATA_TREE, leafSlot.toBigInt());
@@ -1142,6 +1250,14 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1142
1250
  return await this.blockSource.getBlockHeaderByArchive(archive);
1143
1251
  }
1144
1252
 
1253
+ public getBlockData(number: BlockNumber): Promise<BlockData | undefined> {
1254
+ return this.blockSource.getBlockData(number);
1255
+ }
1256
+
1257
+ public getBlockDataByArchive(archive: Fr): Promise<BlockData | undefined> {
1258
+ return this.blockSource.getBlockDataByArchive(archive);
1259
+ }
1260
+
1145
1261
  /**
1146
1262
  * Simulates the public part of a transaction with the current state.
1147
1263
  * @param tx - The transaction to simulate.
@@ -1165,7 +1281,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1165
1281
  }
1166
1282
 
1167
1283
  const txHash = tx.getTxHash();
1168
- const blockNumber = BlockNumber((await this.blockSource.getBlockNumber()) + 1);
1284
+ const latestBlockNumber = await this.blockSource.getBlockNumber();
1285
+ const blockNumber = BlockNumber.add(latestBlockNumber, 1);
1169
1286
 
1170
1287
  // If sequencer is not initialized, we just set these values to zero for simulation.
1171
1288
  const coinbase = EthAddress.ZERO;
@@ -1189,6 +1306,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1189
1306
  blockNumber,
1190
1307
  });
1191
1308
 
1309
+ // Ensure world-state has caught up with the latest block we loaded from the archiver
1310
+ await this.worldStateSynchronizer.syncImmediate(latestBlockNumber);
1192
1311
  const merkleTreeFork = await this.worldStateSynchronizer.fork();
1193
1312
  try {
1194
1313
  const config = PublicSimulatorConfig.from({
@@ -1204,7 +1323,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1204
1323
  const processor = publicProcessorFactory.create(merkleTreeFork, newGlobalVariables, config);
1205
1324
 
1206
1325
  // REFACTOR: Consider merging ProcessReturnValues into ProcessedTx
1207
- const [processedTxs, failedTxs, _usedTxs, returns] = await processor.process([tx]);
1326
+ const [processedTxs, failedTxs, _usedTxs, returns, debugLogs] = await processor.process([tx]);
1208
1327
  // REFACTOR: Consider returning the error rather than throwing
1209
1328
  if (failedTxs.length) {
1210
1329
  this.log.warn(`Simulated tx ${txHash} fails: ${failedTxs[0].error}`, { txHash });
@@ -1218,6 +1337,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1218
1337
  processedTx.txEffect,
1219
1338
  returns,
1220
1339
  processedTx.gasUsed,
1340
+ debugLogs,
1221
1341
  );
1222
1342
  } finally {
1223
1343
  await merkleTreeFork.close();
@@ -1229,12 +1349,13 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1229
1349
  { isSimulation, skipFeeEnforcement }: { isSimulation?: boolean; skipFeeEnforcement?: boolean } = {},
1230
1350
  ): Promise<TxValidationResult> {
1231
1351
  const db = this.worldStateSynchronizer.getCommitted();
1232
- const verifier = isSimulation ? undefined : this.proofVerifier;
1352
+ const verifier = isSimulation ? undefined : this.rpcProofVerifier;
1233
1353
 
1234
- // We accept transactions if they are not expired by the next slot (checked based on the IncludeByTimestamp field)
1354
+ // We accept transactions if they are not expired by the next slot (checked based on the ExpirationTimestamp field)
1235
1355
  const { ts: nextSlotTimestamp } = this.epochCache.getEpochAndSlotInNextL1Slot();
1236
1356
  const blockNumber = BlockNumber((await this.blockSource.getBlockNumber()) + 1);
1237
- const validator = createValidatorForAcceptingTxs(
1357
+ const l1Constants = await this.blockSource.getL1Constants();
1358
+ const validator = createTxValidatorForAcceptingTxsOverRPC(
1238
1359
  db,
1239
1360
  this.contractDataSource,
1240
1361
  verifier,
@@ -1243,10 +1364,16 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1243
1364
  blockNumber,
1244
1365
  l1ChainId: this.l1ChainId,
1245
1366
  rollupVersion: this.version,
1246
- setupAllowList: this.config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions()),
1367
+ setupAllowList: [
1368
+ ...(await getDefaultAllowedSetupFunctions()),
1369
+ ...(this.config.txPublicSetupAllowListExtend ?? []),
1370
+ ],
1247
1371
  gasFees: await this.getCurrentMinFees(),
1248
1372
  skipFeeEnforcement,
1249
1373
  txsPermitted: !this.config.disableTransactions,
1374
+ rollupManaLimit: l1Constants.rollupManaLimit,
1375
+ maxBlockL2Gas: this.config.validateMaxL2BlockGas,
1376
+ maxBlockDAGas: this.config.validateMaxDABlockGas,
1250
1377
  },
1251
1378
  this.log.getBindings(),
1252
1379
  );
@@ -1271,7 +1398,15 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1271
1398
  archiver.updateConfig(config);
1272
1399
  }
1273
1400
  if (newConfig.realProofs !== this.config.realProofs) {
1274
- this.proofVerifier = config.realProofs ? await BBCircuitVerifier.new(newConfig) : new TestCircuitVerifier();
1401
+ await Promise.all([tryStop(this.peerProofVerifier), tryStop(this.rpcProofVerifier)]);
1402
+ if (newConfig.realProofs) {
1403
+ this.peerProofVerifier = await BatchChonkVerifier.new(newConfig, newConfig.bbChonkVerifyMaxBatch, 'peer');
1404
+ const rpcVerifier = await BBCircuitVerifier.new(newConfig);
1405
+ this.rpcProofVerifier = new QueuedIVCVerifier(rpcVerifier, newConfig.numConcurrentIVCVerifiers);
1406
+ } else {
1407
+ this.peerProofVerifier = new TestCircuitVerifier();
1408
+ this.rpcProofVerifier = new TestCircuitVerifier();
1409
+ }
1275
1410
  }
1276
1411
 
1277
1412
  this.config = newConfig;
@@ -1413,6 +1548,94 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1413
1548
  }
1414
1549
  }
1415
1550
 
1551
+ public async reloadKeystore(): Promise<void> {
1552
+ if (!this.config.keyStoreDirectory?.length) {
1553
+ throw new BadRequestError(
1554
+ 'Cannot reload keystore: node is not using a file-based keystore. ' +
1555
+ 'Set KEY_STORE_DIRECTORY to use file-based keystores.',
1556
+ );
1557
+ }
1558
+ if (!this.validatorClient) {
1559
+ throw new BadRequestError('Cannot reload keystore: validator is not enabled.');
1560
+ }
1561
+
1562
+ this.log.info('Reloading keystore from disk');
1563
+
1564
+ // Re-read and validate keystore files
1565
+ const keyStores = loadKeystores(this.config.keyStoreDirectory);
1566
+ const newManager = new KeystoreManager(mergeKeystores(keyStores));
1567
+ await newManager.validateSigners();
1568
+ ValidatorClient.validateKeyStoreConfiguration(newManager, this.log);
1569
+
1570
+ // Validate that every validator's publisher keys overlap with the L1 signers
1571
+ // that were initialized at startup. Publishers cannot be hot-reloaded, so a
1572
+ // validator with a publisher key that doesn't match any existing L1 signer
1573
+ // would silently fail on every proposer slot.
1574
+ if (this.keyStoreManager && this.sequencer) {
1575
+ const oldAdapter = NodeKeystoreAdapter.fromKeyStoreManager(this.keyStoreManager);
1576
+ const availablePublishers = new Set(
1577
+ oldAdapter
1578
+ .getAttesterAddresses()
1579
+ .flatMap(a => oldAdapter.getPublisherAddresses(a).map(p => p.toString().toLowerCase())),
1580
+ );
1581
+
1582
+ const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
1583
+ for (const attester of newAdapter.getAttesterAddresses()) {
1584
+ const pubs = newAdapter.getPublisherAddresses(attester);
1585
+ if (pubs.length > 0 && !pubs.some(p => availablePublishers.has(p.toString().toLowerCase()))) {
1586
+ throw new BadRequestError(
1587
+ `Cannot reload keystore: validator ${attester} has publisher keys ` +
1588
+ `[${pubs.map(p => p.toString()).join(', ')}] but none match the L1 signers initialized at startup ` +
1589
+ `[${[...availablePublishers].join(', ')}]. Publishers cannot be hot-reloaded — ` +
1590
+ `use an existing publisher key or restart the node.`,
1591
+ );
1592
+ }
1593
+ }
1594
+ }
1595
+
1596
+ // Build adapters for old and new keystores to compute diff
1597
+ const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
1598
+ const newAddresses = newAdapter.getAttesterAddresses();
1599
+ const oldAddresses = this.keyStoreManager
1600
+ ? NodeKeystoreAdapter.fromKeyStoreManager(this.keyStoreManager).getAttesterAddresses()
1601
+ : [];
1602
+
1603
+ const oldSet = new Set(oldAddresses.map(a => a.toString()));
1604
+ const newSet = new Set(newAddresses.map(a => a.toString()));
1605
+ const added = newAddresses.filter(a => !oldSet.has(a.toString()));
1606
+ const removed = oldAddresses.filter(a => !newSet.has(a.toString()));
1607
+
1608
+ if (added.length > 0) {
1609
+ this.log.info(`Keystore reload: adding attester keys: ${added.map(a => a.toString()).join(', ')}`);
1610
+ }
1611
+ if (removed.length > 0) {
1612
+ this.log.info(`Keystore reload: removing attester keys: ${removed.map(a => a.toString()).join(', ')}`);
1613
+ }
1614
+ if (added.length === 0 && removed.length === 0) {
1615
+ this.log.info('Keystore reload: attester keys unchanged');
1616
+ }
1617
+
1618
+ // Update the validator client (coinbase, feeRecipient, attester keys)
1619
+ this.validatorClient.reloadKeystore(newManager);
1620
+
1621
+ // Update the publisher factory's keystore so newly-added validators
1622
+ // can be matched to existing publisher keys when proposing blocks.
1623
+ if (this.sequencer) {
1624
+ this.sequencer.updatePublisherNodeKeyStore(newAdapter);
1625
+ }
1626
+
1627
+ // Update slasher's "don't-slash-self" list with new validator addresses
1628
+ if (this.slasherClient && !this.config.slashSelfAllowed) {
1629
+ const slashValidatorsNever = unique(
1630
+ [...(this.config.slashValidatorsNever ?? []), ...newAddresses].map(a => a.toString()),
1631
+ ).map(EthAddress.fromString);
1632
+ this.slasherClient.updateConfig({ slashValidatorsNever });
1633
+ }
1634
+
1635
+ this.keyStoreManager = newManager;
1636
+ this.log.info('Keystore reloaded: coinbase, feeRecipient, and attester keys updated');
1637
+ }
1638
+
1416
1639
  #getInitialHeaderHash(): Promise<BlockHash> {
1417
1640
  if (!this.initialHeaderHashPromise) {
1418
1641
  this.initialHeaderHashPromise = this.worldStateSynchronizer.getCommitted().getInitialHeader().hash();
@@ -1425,7 +1648,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1425
1648
  * @param block - The block parameter (block number, block hash, or 'latest') at which to get the data.
1426
1649
  * @returns An instance of a committed MerkleTreeOperations
1427
1650
  */
1428
- async #getWorldState(block: BlockParameter) {
1651
+ protected async getWorldState(block: BlockParameter) {
1429
1652
  let blockSyncedTo: BlockNumber = BlockNumber.ZERO;
1430
1653
  try {
1431
1654
  // Attempt to sync the world state if necessary
@@ -1439,6 +1662,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1439
1662
  return this.worldStateSynchronizer.getCommitted();
1440
1663
  }
1441
1664
 
1665
+ // Get the block number, either directly from the parameter or by quering the archiver with the block hash
1666
+ let blockNumber: BlockNumber;
1442
1667
  if (BlockHash.isBlockHash(block)) {
1443
1668
  const initialBlockHash = await this.#getInitialHeaderHash();
1444
1669
  if (block.equals(initialBlockHash)) {
@@ -1452,22 +1677,50 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
1452
1677
  `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.`,
1453
1678
  );
1454
1679
  }
1455
- const blockNumber = header.getBlockNumber();
1456
- this.log.debug(`Using snapshot for block ${blockNumber}, world state synced upto ${blockSyncedTo}`);
1457
- return this.worldStateSynchronizer.getSnapshot(blockNumber);
1680
+
1681
+ blockNumber = header.getBlockNumber();
1682
+ } else {
1683
+ blockNumber = block as BlockNumber;
1684
+ }
1685
+
1686
+ // Check it's within world state sync range
1687
+ if (blockNumber > blockSyncedTo) {
1688
+ throw new Error(`Queried block ${block} not yet synced by the node (node is synced upto ${blockSyncedTo}).`);
1458
1689
  }
1690
+ this.log.debug(`Using snapshot for block ${blockNumber}, world state synced upto ${blockSyncedTo}`);
1459
1691
 
1460
- // Block number provided
1461
- {
1462
- const blockNumber = block as BlockNumber;
1692
+ const snapshot = this.worldStateSynchronizer.getSnapshot(blockNumber);
1463
1693
 
1464
- if (blockNumber > blockSyncedTo) {
1465
- throw new Error(`Queried block ${block} not yet synced by the node (node is synced upto ${blockSyncedTo}).`);
1694
+ // Double-check world-state synced to the same block hash as was requested
1695
+ if (BlockHash.isBlockHash(block)) {
1696
+ const blockHash = await snapshot.getLeafValue(MerkleTreeId.ARCHIVE, BigInt(blockNumber));
1697
+ if (!blockHash || !new BlockHash(blockHash).equals(block)) {
1698
+ throw new Error(
1699
+ `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.`,
1700
+ );
1466
1701
  }
1702
+ }
1467
1703
 
1468
- this.log.debug(`Using snapshot for block ${blockNumber}, world state synced upto ${blockSyncedTo}`);
1469
- return this.worldStateSynchronizer.getSnapshot(blockNumber);
1704
+ return snapshot;
1705
+ }
1706
+
1707
+ /** Resolves a block parameter to a block number. */
1708
+ protected async resolveBlockNumber(block: BlockParameter): Promise<BlockNumber> {
1709
+ if (block === 'latest') {
1710
+ return BlockNumber(await this.blockSource.getBlockNumber());
1711
+ }
1712
+ if (BlockHash.isBlockHash(block)) {
1713
+ const initialBlockHash = await this.#getInitialHeaderHash();
1714
+ if (block.equals(initialBlockHash)) {
1715
+ return BlockNumber.ZERO;
1716
+ }
1717
+ const header = await this.blockSource.getBlockHeaderByHash(block);
1718
+ if (!header) {
1719
+ throw new Error(`Block hash ${block.toString()} not found.`);
1720
+ }
1721
+ return header.getBlockNumber();
1470
1722
  }
1723
+ return block as BlockNumber;
1471
1724
  }
1472
1725
 
1473
1726
  /**