@aztec/validator-client 0.0.1-commit.64b6bbb → 0.0.1-commit.684755437

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.
Files changed (46) hide show
  1. package/README.md +42 -0
  2. package/dest/block_proposal_handler.d.ts +3 -3
  3. package/dest/block_proposal_handler.d.ts.map +1 -1
  4. package/dest/block_proposal_handler.js +63 -58
  5. package/dest/checkpoint_builder.d.ts +15 -5
  6. package/dest/checkpoint_builder.d.ts.map +1 -1
  7. package/dest/checkpoint_builder.js +80 -25
  8. package/dest/config.d.ts +1 -1
  9. package/dest/config.d.ts.map +1 -1
  10. package/dest/config.js +22 -1
  11. package/dest/duties/validation_service.d.ts +2 -2
  12. package/dest/duties/validation_service.d.ts.map +1 -1
  13. package/dest/duties/validation_service.js +5 -11
  14. package/dest/factory.d.ts +1 -1
  15. package/dest/factory.d.ts.map +1 -1
  16. package/dest/factory.js +2 -1
  17. package/dest/index.d.ts +1 -2
  18. package/dest/index.d.ts.map +1 -1
  19. package/dest/index.js +0 -1
  20. package/dest/metrics.d.ts +9 -1
  21. package/dest/metrics.d.ts.map +1 -1
  22. package/dest/metrics.js +12 -0
  23. package/dest/validator.d.ts +13 -5
  24. package/dest/validator.d.ts.map +1 -1
  25. package/dest/validator.js +85 -24
  26. package/package.json +19 -19
  27. package/src/block_proposal_handler.ts +75 -76
  28. package/src/checkpoint_builder.ts +90 -14
  29. package/src/config.ts +22 -1
  30. package/src/duties/validation_service.ts +11 -10
  31. package/src/factory.ts +1 -0
  32. package/src/index.ts +0 -1
  33. package/src/metrics.ts +18 -0
  34. package/src/validator.ts +86 -18
  35. package/dest/tx_validator/index.d.ts +0 -3
  36. package/dest/tx_validator/index.d.ts.map +0 -1
  37. package/dest/tx_validator/index.js +0 -2
  38. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  39. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  40. package/dest/tx_validator/nullifier_cache.js +0 -24
  41. package/dest/tx_validator/tx_validator_factory.d.ts +0 -19
  42. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  43. package/dest/tx_validator/tx_validator_factory.js +0 -54
  44. package/src/tx_validator/index.ts +0 -2
  45. package/src/tx_validator/nullifier_cache.ts +0 -30
  46. package/src/tx_validator/tx_validator_factory.ts +0 -154
package/dest/validator.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { getBlobsPerL1Block } from '@aztec/blob-lib';
2
- import { BlockNumber } from '@aztec/foundation/branded-types';
2
+ import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
3
+ import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types';
3
4
  import { TimeoutError } from '@aztec/foundation/error';
4
5
  import { createLogger } from '@aztec/foundation/log';
5
6
  import { retryUntil } from '@aztec/foundation/retry';
@@ -8,11 +9,12 @@ import { sleep } from '@aztec/foundation/sleep';
8
9
  import { DateProvider } from '@aztec/foundation/timer';
9
10
  import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
10
11
  import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
11
- import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
12
+ import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
13
+ import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
12
14
  import { accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
13
15
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
14
16
  import { getTelemetryClient } from '@aztec/telemetry-client';
15
- import { createHASigner } from '@aztec/validator-ha-signer/factory';
17
+ import { createHASigner, createLocalSignerWithProtection } from '@aztec/validator-ha-signer/factory';
16
18
  import { DutyType } from '@aztec/validator-ha-signer/types';
17
19
  import { EventEmitter } from 'events';
18
20
  import { BlockProposalHandler } from './block_proposal_handler.js';
@@ -41,6 +43,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
41
43
  l1ToL2MessageSource;
42
44
  config;
43
45
  blobClient;
46
+ slashingProtectionSigner;
44
47
  dateProvider;
45
48
  tracer;
46
49
  validationService;
@@ -52,10 +55,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
52
55
  /** Tracks the last checkpoint proposal we created. */ lastProposedCheckpoint;
53
56
  lastEpochForCommitteeUpdateLoop;
54
57
  epochCacheUpdateLoop;
58
+ /** Tracks the last epoch in which each attester successfully submitted at least one attestation. */ lastAttestedEpochByAttester;
55
59
  proposersOfInvalidBlocks;
56
60
  /** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */ lastAttestedProposal;
57
- constructor(keyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
58
- super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockProposalHandler = blockProposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.proposersOfInvalidBlocks = new Set();
61
+ constructor(keyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, slashingProtectionSigner, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
62
+ super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockProposalHandler = blockProposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.slashingProtectionSigner = slashingProtectionSigner, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.lastAttestedEpochByAttester = new Map(), this.proposersOfInvalidBlocks = new Set();
59
63
  // Create child logger with fisherman prefix if in fisherman mode
60
64
  this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
61
65
  this.tracer = telemetry.getTracer('Validator');
@@ -94,6 +98,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
94
98
  this.log.trace(`No committee found for slot`);
95
99
  return;
96
100
  }
101
+ this.metrics.setCurrentEpoch(epoch);
97
102
  if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
98
103
  const me = this.getValidatorAddresses();
99
104
  const committeeSet = new Set(committee.map((v)=>v.toString()));
@@ -112,20 +117,33 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
112
117
  static async new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
113
118
  const metrics = new ValidatorMetrics(telemetry);
114
119
  const blockProposalValidator = new BlockProposalValidator(epochCache, {
115
- txsPermitted: !config.disableTransactions
120
+ txsPermitted: !config.disableTransactions,
121
+ maxTxsPerBlock: config.validateMaxTxsPerBlock
116
122
  });
117
123
  const blockProposalHandler = new BlockProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, metrics, dateProvider, telemetry);
118
- let validatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
124
+ const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
125
+ let slashingProtectionSigner;
119
126
  if (config.haSigningEnabled) {
127
+ // Multi-node HA mode: use PostgreSQL-backed distributed locking.
120
128
  // If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
121
129
  const haConfig = {
122
130
  ...config,
123
131
  maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000
124
132
  };
125
- const { signer } = await createHASigner(haConfig);
126
- validatorKeyStore = new HAKeyStore(validatorKeyStore, signer);
127
- }
128
- const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider, telemetry);
133
+ ({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
134
+ telemetryClient: telemetry,
135
+ dateProvider
136
+ }));
137
+ } else {
138
+ // Single-node mode: use LMDB-backed local signing protection.
139
+ // This prevents double-signing if the node crashes and restarts mid-proposal.
140
+ ({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
141
+ telemetryClient: telemetry,
142
+ dateProvider
143
+ }));
144
+ }
145
+ const validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
146
+ const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, slashingProtectionSigner, dateProvider, telemetry);
129
147
  return validator;
130
148
  }
131
149
  getValidatorAddresses() {
@@ -152,6 +170,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
152
170
  ...config
153
171
  };
154
172
  }
173
+ reloadKeystore(newManager) {
174
+ const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
175
+ this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
176
+ this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
177
+ }
155
178
  async start() {
156
179
  if (this.epochCacheUpdateLoop.isRunning()) {
157
180
  this.log.warn(`Validator client already started`);
@@ -299,18 +322,21 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
299
322
  });
300
323
  return undefined;
301
324
  }
325
+ // Validate fee asset price modifier is within allowed range
326
+ if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
327
+ this.log.warn(`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${slotNumber}`);
328
+ return undefined;
329
+ }
302
330
  // Check that I have any address in current committee before attesting
303
331
  const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
304
332
  const partOfCommittee = inCommittee.length > 0;
305
333
  const proposalInfo = {
306
334
  slotNumber,
307
335
  archive: proposal.archive.toString(),
308
- proposer: proposer.toString(),
309
- txCount: proposal.txHashes.length
336
+ proposer: proposer.toString()
310
337
  };
311
338
  this.log.info(`Received checkpoint proposal for slot ${slotNumber}`, {
312
339
  ...proposalInfo,
313
- txHashes: proposal.txHashes.map((t)=>t.toString()),
314
340
  fishermanMode: this.config.fishermanMode || false
315
341
  });
316
342
  // Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
@@ -340,6 +366,16 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
340
366
  fishermanMode: this.config.fishermanMode || false
341
367
  });
342
368
  this.metrics.incSuccessfulAttestations(inCommittee.length);
369
+ // Track epoch participation per attester: count each (attester, epoch) pair at most once
370
+ const proposalEpoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
371
+ for (const attester of inCommittee){
372
+ const key = attester.toString();
373
+ const lastEpoch = this.lastAttestedEpochByAttester.get(key);
374
+ if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
375
+ this.lastAttestedEpochByAttester.set(key, proposalEpoch);
376
+ this.metrics.incAttestedEpochCount(attester);
377
+ }
378
+ }
343
379
  // Determine which validators should attest
344
380
  let attestors;
345
381
  if (partOfCommittee) {
@@ -395,7 +431,10 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
395
431
  * @returns Validation result with isValid flag and reason if invalid.
396
432
  */ async validateCheckpointProposal(proposal, proposalInfo) {
397
433
  const slot = proposal.slotNumber;
398
- const timeoutSeconds = 10; // TODO(palla/mbps): This should map to the timetable settings
434
+ // Timeout block syncing at the start of the next slot
435
+ const config = this.checkpointsBuilder.getConfig();
436
+ const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
437
+ const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
399
438
  // Wait for last block to sync by archive
400
439
  let lastBlockHeader;
401
440
  try {
@@ -433,6 +472,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
433
472
  reason: 'no_blocks_for_slot'
434
473
  };
435
474
  }
475
+ // Ensure the last block for this slot matches the archive in the checkpoint proposal
476
+ if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
477
+ this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
478
+ return {
479
+ isValid: false,
480
+ reason: 'last_block_archive_mismatch'
481
+ };
482
+ }
436
483
  this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
437
484
  ...proposalInfo,
438
485
  blockNumbers: blocks.map((b)=>b.number)
@@ -443,18 +490,15 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
443
490
  const checkpointNumber = firstBlock.checkpointNumber;
444
491
  // Get L1-to-L2 messages for this checkpoint
445
492
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
446
- // Compute the previous checkpoint out hashes for the epoch.
447
- // TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
448
- // actual checkpoints and the blocks/txs in them.
493
+ // Collect the out hashes of all the checkpoints before this one in the same epoch
449
494
  const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
450
- const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch)).filter((b)=>b.number < checkpointNumber).sort((a, b)=>a.number - b.number);
451
- const previousCheckpointOutHashes = previousCheckpoints.map((c)=>c.getCheckpointOutHash());
495
+ const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
452
496
  // Fork world state at the block before the first block
453
497
  const parentBlockNumber = BlockNumber(firstBlock.number - 1);
454
498
  const fork = await this.worldState.fork(parentBlockNumber);
455
499
  try {
456
500
  // Create checkpoint builder with all existing blocks
457
- const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
501
+ const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, proposal.feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
458
502
  // Complete the checkpoint to get computed values
459
503
  const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
460
504
  // Compare checkpoint header with proposal
@@ -502,6 +546,22 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
502
546
  reason: 'out_hash_mismatch'
503
547
  };
504
548
  }
549
+ // Final round of validations on the checkpoint, just in case.
550
+ try {
551
+ validateCheckpoint(computedCheckpoint, {
552
+ rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
553
+ maxDABlockGas: this.config.validateMaxDABlockGas,
554
+ maxL2BlockGas: this.config.validateMaxL2BlockGas,
555
+ maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
556
+ maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint
557
+ });
558
+ } catch (err) {
559
+ this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
560
+ return {
561
+ isValid: false,
562
+ reason: 'checkpoint_validation_failed'
563
+ };
564
+ }
505
565
  this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
506
566
  return {
507
567
  isValid: true
@@ -518,6 +578,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
518
578
  chainId: gv.chainId,
519
579
  version: gv.version,
520
580
  slotNumber: gv.slotNumber,
581
+ timestamp: gv.timestamp,
521
582
  coinbase: gv.coinbase,
522
583
  feeRecipient: gv.feeRecipient,
523
584
  gasFees: gv.gasFees
@@ -538,7 +599,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
538
599
  return;
539
600
  }
540
601
  const blobFields = blocks.flatMap((b)=>b.toBlobFields());
541
- const blobs = getBlobsPerL1Block(blobFields);
602
+ const blobs = await getBlobsPerL1Block(blobFields);
542
603
  await this.blobClient.sendBlobsToFilestore(blobs);
543
604
  this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
544
605
  ...proposalInfo,
@@ -626,7 +687,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
626
687
  this.lastProposedBlock = newProposal;
627
688
  return newProposal;
628
689
  }
629
- async createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options = {}) {
690
+ async createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier, lastBlockInfo, proposerAddress, options = {}) {
630
691
  // Validate that we're not creating a proposal for an older or equal slot
631
692
  if (this.lastProposedCheckpoint) {
632
693
  const lastSlot = this.lastProposedCheckpoint.slotNumber;
@@ -636,7 +697,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
636
697
  }
637
698
  }
638
699
  this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
639
- const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options);
700
+ const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier, lastBlockInfo, proposerAddress, options);
640
701
  this.lastProposedCheckpoint = newProposal;
641
702
  return newProposal;
642
703
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/validator-client",
3
- "version": "0.0.1-commit.64b6bbb",
3
+ "version": "0.0.1-commit.684755437",
4
4
  "main": "dest/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -64,30 +64,30 @@
64
64
  ]
65
65
  },
66
66
  "dependencies": {
67
- "@aztec/blob-client": "0.0.1-commit.64b6bbb",
68
- "@aztec/blob-lib": "0.0.1-commit.64b6bbb",
69
- "@aztec/constants": "0.0.1-commit.64b6bbb",
70
- "@aztec/epoch-cache": "0.0.1-commit.64b6bbb",
71
- "@aztec/ethereum": "0.0.1-commit.64b6bbb",
72
- "@aztec/foundation": "0.0.1-commit.64b6bbb",
73
- "@aztec/node-keystore": "0.0.1-commit.64b6bbb",
74
- "@aztec/noir-protocol-circuits-types": "0.0.1-commit.64b6bbb",
75
- "@aztec/p2p": "0.0.1-commit.64b6bbb",
76
- "@aztec/protocol-contracts": "0.0.1-commit.64b6bbb",
77
- "@aztec/prover-client": "0.0.1-commit.64b6bbb",
78
- "@aztec/simulator": "0.0.1-commit.64b6bbb",
79
- "@aztec/slasher": "0.0.1-commit.64b6bbb",
80
- "@aztec/stdlib": "0.0.1-commit.64b6bbb",
81
- "@aztec/telemetry-client": "0.0.1-commit.64b6bbb",
82
- "@aztec/validator-ha-signer": "0.0.1-commit.64b6bbb",
67
+ "@aztec/blob-client": "0.0.1-commit.684755437",
68
+ "@aztec/blob-lib": "0.0.1-commit.684755437",
69
+ "@aztec/constants": "0.0.1-commit.684755437",
70
+ "@aztec/epoch-cache": "0.0.1-commit.684755437",
71
+ "@aztec/ethereum": "0.0.1-commit.684755437",
72
+ "@aztec/foundation": "0.0.1-commit.684755437",
73
+ "@aztec/node-keystore": "0.0.1-commit.684755437",
74
+ "@aztec/noir-protocol-circuits-types": "0.0.1-commit.684755437",
75
+ "@aztec/p2p": "0.0.1-commit.684755437",
76
+ "@aztec/protocol-contracts": "0.0.1-commit.684755437",
77
+ "@aztec/prover-client": "0.0.1-commit.684755437",
78
+ "@aztec/simulator": "0.0.1-commit.684755437",
79
+ "@aztec/slasher": "0.0.1-commit.684755437",
80
+ "@aztec/stdlib": "0.0.1-commit.684755437",
81
+ "@aztec/telemetry-client": "0.0.1-commit.684755437",
82
+ "@aztec/validator-ha-signer": "0.0.1-commit.684755437",
83
83
  "koa": "^2.16.1",
84
84
  "koa-router": "^13.1.1",
85
85
  "tslib": "^2.4.0",
86
86
  "viem": "npm:@aztec/viem@2.38.2"
87
87
  },
88
88
  "devDependencies": {
89
- "@aztec/archiver": "0.0.1-commit.64b6bbb",
90
- "@aztec/world-state": "0.0.1-commit.64b6bbb",
89
+ "@aztec/archiver": "0.0.1-commit.684755437",
90
+ "@aztec/world-state": "0.0.1-commit.684755437",
91
91
  "@electric-sql/pglite": "^0.3.14",
92
92
  "@jest/globals": "^30.0.0",
93
93
  "@types/jest": "^30.0.0",
@@ -1,7 +1,7 @@
1
1
  import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
2
2
  import type { EpochCache } from '@aztec/epoch-cache';
3
3
  import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
4
- import { chunkBy } from '@aztec/foundation/collection';
4
+ import { pick } from '@aztec/foundation/collection';
5
5
  import { Fr } from '@aztec/foundation/curves/bn254';
6
6
  import { TimeoutError } from '@aztec/foundation/error';
7
7
  import { createLogger } from '@aztec/foundation/log';
@@ -9,16 +9,13 @@ import { retryUntil } from '@aztec/foundation/retry';
9
9
  import { DateProvider, Timer } from '@aztec/foundation/timer';
10
10
  import type { P2P, PeerId } from '@aztec/p2p';
11
11
  import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
12
- import type { L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
12
+ import type { BlockData, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
13
13
  import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
14
+ import { Gas } from '@aztec/stdlib/gas';
14
15
  import type { ITxProvider, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
15
- import {
16
- type L1ToL2MessageSource,
17
- computeCheckpointOutHash,
18
- computeInHashFromL1ToL2Messages,
19
- } from '@aztec/stdlib/messaging';
16
+ import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
20
17
  import type { BlockProposal } from '@aztec/stdlib/p2p';
21
- import { BlockHeader, type CheckpointGlobalVariables, type FailedTx, type Tx } from '@aztec/stdlib/tx';
18
+ import type { CheckpointGlobalVariables, FailedTx, Tx } from '@aztec/stdlib/tx';
22
19
  import {
23
20
  ReExFailedTxsError,
24
21
  ReExStateMismatchError,
@@ -92,25 +89,28 @@ export class BlockProposalHandler {
92
89
  this.tracer = telemetry.getTracer('BlockProposalHandler');
93
90
  }
94
91
 
95
- registerForReexecution(p2pClient: P2P): BlockProposalHandler {
96
- // Non-validator handler that re-executes for monitoring but does not attest.
92
+ register(p2pClient: P2P, shouldReexecute: boolean): BlockProposalHandler {
93
+ // Non-validator handler that processes or re-executes for monitoring but does not attest.
97
94
  // Returns boolean indicating whether the proposal was valid.
98
95
  const handler = async (proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> => {
99
96
  try {
100
- const result = await this.handleBlockProposal(proposal, proposalSender, true);
97
+ const { slotNumber, blockNumber } = proposal;
98
+ const result = await this.handleBlockProposal(proposal, proposalSender, shouldReexecute);
101
99
  if (result.isValid) {
102
- this.log.info(`Non-validator reexecution completed for slot ${proposal.slotNumber}`, {
100
+ this.log.info(`Non-validator block proposal ${blockNumber} at slot ${slotNumber} handled`, {
103
101
  blockNumber: result.blockNumber,
102
+ slotNumber,
104
103
  reexecutionTimeMs: result.reexecutionResult?.reexecutionTimeMs,
105
104
  totalManaUsed: result.reexecutionResult?.totalManaUsed,
106
105
  numTxs: result.reexecutionResult?.block?.body?.txEffects?.length ?? 0,
106
+ reexecuted: shouldReexecute,
107
107
  });
108
108
  return true;
109
109
  } else {
110
- this.log.warn(`Non-validator reexecution failed for slot ${proposal.slotNumber}`, {
111
- blockNumber: result.blockNumber,
112
- reason: result.reason,
113
- });
110
+ this.log.warn(
111
+ `Non-validator block proposal ${blockNumber} at slot ${slotNumber} failed processing with ${result.reason}`,
112
+ { blockNumber: result.blockNumber, slotNumber, reason: result.reason },
113
+ );
114
114
  return false;
115
115
  }
116
116
  } catch (error) {
@@ -153,16 +153,16 @@ export class BlockProposalHandler {
153
153
  }
154
154
 
155
155
  // Check that the parent proposal is a block we know, otherwise reexecution would fail
156
- const parentBlockHeader = await this.getParentBlock(proposal);
157
- if (parentBlockHeader === undefined) {
156
+ const parentBlock = await this.getParentBlock(proposal);
157
+ if (parentBlock === undefined) {
158
158
  this.log.warn(`Parent block for proposal not found, skipping processing`, proposalInfo);
159
159
  return { isValid: false, reason: 'parent_block_not_found' };
160
160
  }
161
161
 
162
162
  // Check that the parent block's slot is not greater than the proposal's slot.
163
- if (parentBlockHeader !== 'genesis' && parentBlockHeader.getSlot() > slotNumber) {
163
+ if (parentBlock !== 'genesis' && parentBlock.header.getSlot() > slotNumber) {
164
164
  this.log.warn(`Parent block slot is greater than proposal slot, skipping processing`, {
165
- parentBlockSlot: parentBlockHeader.getSlot().toString(),
165
+ parentBlockSlot: parentBlock.header.getSlot().toString(),
166
166
  proposalSlot: slotNumber.toString(),
167
167
  ...proposalInfo,
168
168
  });
@@ -171,9 +171,9 @@ export class BlockProposalHandler {
171
171
 
172
172
  // Compute the block number based on the parent block
173
173
  const blockNumber =
174
- parentBlockHeader === 'genesis'
174
+ parentBlock === 'genesis'
175
175
  ? BlockNumber(INITIAL_L2_BLOCK_NUM)
176
- : BlockNumber(parentBlockHeader.getBlockNumber() + 1);
176
+ : BlockNumber(parentBlock.header.getBlockNumber() + 1);
177
177
 
178
178
  // Check that this block number does not exist already
179
179
  const existingBlock = await this.blockSource.getBlockHeader(blockNumber);
@@ -189,8 +189,17 @@ export class BlockProposalHandler {
189
189
  deadline: this.getReexecutionDeadline(slotNumber, config),
190
190
  });
191
191
 
192
+ // If reexecution is disabled, bail. We are just interested in triggering tx collection.
193
+ if (!shouldReexecute) {
194
+ this.log.info(
195
+ `Received valid block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`,
196
+ proposalInfo,
197
+ );
198
+ return { isValid: true, blockNumber };
199
+ }
200
+
192
201
  // Compute the checkpoint number for this block and validate checkpoint consistency
193
- const checkpointResult = await this.computeCheckpointNumber(proposal, parentBlockHeader, proposalInfo);
202
+ const checkpointResult = this.computeCheckpointNumber(proposal, parentBlock, proposalInfo);
194
203
  if (checkpointResult.reason) {
195
204
  return { isValid: false, blockNumber, reason: checkpointResult.reason };
196
205
  }
@@ -215,36 +224,28 @@ export class BlockProposalHandler {
215
224
  return { isValid: false, blockNumber, reason: 'txs_not_available' };
216
225
  }
217
226
 
227
+ // Collect the out hashes of all the checkpoints before this one in the same epoch
228
+ const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
229
+ const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch))
230
+ .filter(c => c.checkpointNumber < checkpointNumber)
231
+ .map(c => c.checkpointOutHash);
232
+
218
233
  // Try re-executing the transactions in the proposal if needed
219
234
  let reexecutionResult;
220
- if (shouldReexecute) {
221
- // Compute the previous checkpoint out hashes for the epoch.
222
- // TODO(leila/mbps): There can be a more efficient way to get the previous checkpoint out
223
- // hashes without having to fetch all the blocks.
224
- const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
225
- const checkpointedBlocks = (await this.blockSource.getCheckpointedBlocksForEpoch(epoch))
226
- .filter(b => b.block.number < blockNumber)
227
- .sort((a, b) => a.block.number - b.block.number);
228
- const blocksByCheckpoint = chunkBy(checkpointedBlocks, b => b.checkpointNumber);
229
- const previousCheckpointOutHashes = blocksByCheckpoint.map(checkpointBlocks =>
230
- computeCheckpointOutHash(checkpointBlocks.map(b => b.block.body.txEffects.map(tx => tx.l2ToL1Msgs))),
235
+ try {
236
+ this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
237
+ reexecutionResult = await this.reexecuteTransactions(
238
+ proposal,
239
+ blockNumber,
240
+ checkpointNumber,
241
+ txs,
242
+ l1ToL2Messages,
243
+ previousCheckpointOutHashes,
231
244
  );
232
-
233
- try {
234
- this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
235
- reexecutionResult = await this.reexecuteTransactions(
236
- proposal,
237
- blockNumber,
238
- checkpointNumber,
239
- txs,
240
- l1ToL2Messages,
241
- previousCheckpointOutHashes,
242
- );
243
- } catch (error) {
244
- this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
245
- const reason = this.getReexecuteFailureReason(error);
246
- return { isValid: false, blockNumber, reason, reexecutionResult };
247
- }
245
+ } catch (error) {
246
+ this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
247
+ const reason = this.getReexecuteFailureReason(error);
248
+ return { isValid: false, blockNumber, reason, reexecutionResult };
248
249
  }
249
250
 
250
251
  // If we succeeded, push this block into the archiver (unless disabled)
@@ -253,14 +254,14 @@ export class BlockProposalHandler {
253
254
  }
254
255
 
255
256
  this.log.info(
256
- `Successfully processed block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`,
257
- proposalInfo,
257
+ `Successfully re-executed block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`,
258
+ { ...proposalInfo, ...pick(reexecutionResult, 'reexecutionTimeMs', 'totalManaUsed') },
258
259
  );
259
260
 
260
261
  return { isValid: true, blockNumber, reexecutionResult };
261
262
  }
262
263
 
263
- private async getParentBlock(proposal: BlockProposal): Promise<'genesis' | BlockHeader | undefined> {
264
+ private async getParentBlock(proposal: BlockProposal): Promise<'genesis' | BlockData | undefined> {
264
265
  const parentArchive = proposal.blockHeader.lastArchive.root;
265
266
  const slot = proposal.slotNumber;
266
267
  const config = this.checkpointsBuilder.getConfig();
@@ -276,12 +277,11 @@ export class BlockProposalHandler {
276
277
 
277
278
  try {
278
279
  return (
279
- (await this.blockSource.getBlockHeaderByArchive(parentArchive)) ??
280
+ (await this.blockSource.getBlockDataByArchive(parentArchive)) ??
280
281
  (timeoutDurationMs <= 0
281
282
  ? undefined
282
283
  : await retryUntil(
283
- () =>
284
- this.blockSource.syncImmediate().then(() => this.blockSource.getBlockHeaderByArchive(parentArchive)),
284
+ () => this.blockSource.syncImmediate().then(() => this.blockSource.getBlockDataByArchive(parentArchive)),
285
285
  'force archiver sync',
286
286
  timeoutDurationMs / 1000,
287
287
  0.5,
@@ -297,12 +297,12 @@ export class BlockProposalHandler {
297
297
  }
298
298
  }
299
299
 
300
- private async computeCheckpointNumber(
300
+ private computeCheckpointNumber(
301
301
  proposal: BlockProposal,
302
- parentBlockHeader: 'genesis' | BlockHeader,
302
+ parentBlock: 'genesis' | BlockData,
303
303
  proposalInfo: object,
304
- ): Promise<CheckpointComputationResult> {
305
- if (parentBlockHeader === 'genesis') {
304
+ ): CheckpointComputationResult {
305
+ if (parentBlock === 'genesis') {
306
306
  // First block is in checkpoint 1
307
307
  if (proposal.indexWithinCheckpoint !== 0) {
308
308
  this.log.warn(`First block proposal has non-zero indexWithinCheckpoint`, proposalInfo);
@@ -311,19 +311,9 @@ export class BlockProposalHandler {
311
311
  return { checkpointNumber: CheckpointNumber.INITIAL };
312
312
  }
313
313
 
314
- // Get the parent block to find its checkpoint number
315
- // TODO(palla/mbps): The block header should include the checkpoint number to avoid this lookup,
316
- // or at least the L2BlockSource should return a different struct that includes it.
317
- const parentBlockNumber = parentBlockHeader.getBlockNumber();
318
- const parentBlock = await this.blockSource.getL2Block(parentBlockNumber);
319
- if (!parentBlock) {
320
- this.log.warn(`Parent block ${parentBlockNumber} not found in archiver`, proposalInfo);
321
- return { reason: 'invalid_proposal' };
322
- }
323
-
324
314
  if (proposal.indexWithinCheckpoint === 0) {
325
315
  // If this is the first block in a new checkpoint, increment the checkpoint number
326
- if (!(proposal.blockHeader.getSlot() > parentBlockHeader.getSlot())) {
316
+ if (!(proposal.blockHeader.getSlot() > parentBlock.header.getSlot())) {
327
317
  this.log.warn(`Slot should be greater than parent block slot for first block in checkpoint`, proposalInfo);
328
318
  return { reason: 'invalid_proposal' };
329
319
  }
@@ -335,7 +325,7 @@ export class BlockProposalHandler {
335
325
  this.log.warn(`Non-sequential indexWithinCheckpoint`, proposalInfo);
336
326
  return { reason: 'invalid_proposal' };
337
327
  }
338
- if (proposal.blockHeader.getSlot() !== parentBlockHeader.getSlot()) {
328
+ if (proposal.blockHeader.getSlot() !== parentBlock.header.getSlot()) {
339
329
  this.log.warn(`Slot should be equal to parent block slot for non-first block in checkpoint`, proposalInfo);
340
330
  return { reason: 'invalid_proposal' };
341
331
  }
@@ -356,7 +346,7 @@ export class BlockProposalHandler {
356
346
  */
357
347
  private validateNonFirstBlockInCheckpoint(
358
348
  proposal: BlockProposal,
359
- parentBlock: L2Block,
349
+ parentBlock: BlockData,
360
350
  proposalInfo: object,
361
351
  ): CheckpointComputationResult | undefined {
362
352
  const proposalGlobals = proposal.blockHeader.globalVariables;
@@ -475,13 +465,14 @@ export class BlockProposalHandler {
475
465
  // Fork before the block to be built
476
466
  const parentBlockNumber = BlockNumber(blockNumber - 1);
477
467
  await this.worldState.syncImmediate(parentBlockNumber);
478
- using fork = await this.worldState.fork(parentBlockNumber);
468
+ await using fork = await this.worldState.fork(parentBlockNumber);
479
469
 
480
- // Build checkpoint constants from proposal (excludes blockNumber and timestamp which are per-block)
470
+ // Build checkpoint constants from proposal (excludes blockNumber which is per-block)
481
471
  const constants: CheckpointGlobalVariables = {
482
472
  chainId: new Fr(config.l1ChainId),
483
473
  version: new Fr(config.rollupVersion),
484
474
  slotNumber: slot,
475
+ timestamp: blockHeader.globalVariables.timestamp,
485
476
  coinbase: blockHeader.globalVariables.coinbase,
486
477
  feeRecipient: blockHeader.globalVariables.feeRecipient,
487
478
  gasFees: blockHeader.globalVariables.gasFees,
@@ -491,6 +482,7 @@ export class BlockProposalHandler {
491
482
  const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
492
483
  checkpointNumber,
493
484
  constants,
485
+ 0n, // only takes effect in the following checkpoint.
494
486
  l1ToL2Messages,
495
487
  previousCheckpointOutHashes,
496
488
  fork,
@@ -500,18 +492,25 @@ export class BlockProposalHandler {
500
492
 
501
493
  // Build the new block
502
494
  const deadline = this.getReexecutionDeadline(slot, config);
495
+ const maxBlockGas =
496
+ this.config.validateMaxL2BlockGas !== undefined || this.config.validateMaxDABlockGas !== undefined
497
+ ? new Gas(this.config.validateMaxDABlockGas ?? Infinity, this.config.validateMaxL2BlockGas ?? Infinity)
498
+ : undefined;
503
499
  const result = await checkpointBuilder.buildBlock(txs, blockNumber, blockHeader.globalVariables.timestamp, {
504
500
  deadline,
505
501
  expectedEndState: blockHeader.state,
502
+ maxTransactions: this.config.validateMaxTxsPerBlock,
503
+ maxBlockGas,
506
504
  });
507
505
 
508
506
  const { block, failedTxs } = result;
509
507
  const numFailedTxs = failedTxs.length;
510
508
 
511
- this.log.verbose(`Transaction re-execution complete for slot ${slot}`, {
509
+ this.log.verbose(`Block proposal ${blockNumber} at slot ${slot} transaction re-execution complete`, {
512
510
  numFailedTxs,
513
511
  numProposalTxs: txHashes.length,
514
512
  numProcessedTxs: block.body.txEffects.length,
513
+ blockNumber,
515
514
  slot,
516
515
  });
517
516