@aztec/validator-client 0.0.1-commit.1142ef1 → 0.0.1-commit.1bea0213

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 (53) hide show
  1. package/README.md +41 -15
  2. package/dest/block_proposal_handler.d.ts +7 -6
  3. package/dest/block_proposal_handler.d.ts.map +1 -1
  4. package/dest/block_proposal_handler.js +23 -29
  5. package/dest/checkpoint_builder.d.ts +18 -21
  6. package/dest/checkpoint_builder.d.ts.map +1 -1
  7. package/dest/checkpoint_builder.js +17 -12
  8. package/dest/config.d.ts +1 -1
  9. package/dest/config.d.ts.map +1 -1
  10. package/dest/config.js +6 -11
  11. package/dest/duties/validation_service.d.ts +19 -6
  12. package/dest/duties/validation_service.d.ts.map +1 -1
  13. package/dest/duties/validation_service.js +72 -19
  14. package/dest/factory.d.ts +2 -2
  15. package/dest/factory.d.ts.map +1 -1
  16. package/dest/factory.js +1 -1
  17. package/dest/key_store/ha_key_store.d.ts +99 -0
  18. package/dest/key_store/ha_key_store.d.ts.map +1 -0
  19. package/dest/key_store/ha_key_store.js +208 -0
  20. package/dest/key_store/index.d.ts +2 -1
  21. package/dest/key_store/index.d.ts.map +1 -1
  22. package/dest/key_store/index.js +1 -0
  23. package/dest/key_store/interface.d.ts +36 -6
  24. package/dest/key_store/interface.d.ts.map +1 -1
  25. package/dest/key_store/local_key_store.d.ts +10 -5
  26. package/dest/key_store/local_key_store.d.ts.map +1 -1
  27. package/dest/key_store/local_key_store.js +8 -4
  28. package/dest/key_store/node_keystore_adapter.d.ts +18 -5
  29. package/dest/key_store/node_keystore_adapter.d.ts.map +1 -1
  30. package/dest/key_store/node_keystore_adapter.js +18 -4
  31. package/dest/key_store/web3signer_key_store.d.ts +10 -5
  32. package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
  33. package/dest/key_store/web3signer_key_store.js +8 -4
  34. package/dest/tx_validator/tx_validator_factory.d.ts +1 -1
  35. package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
  36. package/dest/tx_validator/tx_validator_factory.js +2 -1
  37. package/dest/validator.d.ts +9 -8
  38. package/dest/validator.d.ts.map +1 -1
  39. package/dest/validator.js +68 -60
  40. package/package.json +19 -17
  41. package/src/block_proposal_handler.ts +34 -36
  42. package/src/checkpoint_builder.ts +37 -20
  43. package/src/config.ts +5 -10
  44. package/src/duties/validation_service.ts +91 -23
  45. package/src/factory.ts +1 -0
  46. package/src/key_store/ha_key_store.ts +269 -0
  47. package/src/key_store/index.ts +1 -0
  48. package/src/key_store/interface.ts +44 -5
  49. package/src/key_store/local_key_store.ts +13 -4
  50. package/src/key_store/node_keystore_adapter.ts +27 -4
  51. package/src/key_store/web3signer_key_store.ts +17 -4
  52. package/src/tx_validator/tx_validator_factory.ts +2 -0
  53. package/src/validator.ts +85 -69
package/dest/validator.js CHANGED
@@ -8,11 +8,15 @@ import { sleep } from '@aztec/foundation/sleep';
8
8
  import { DateProvider } from '@aztec/foundation/timer';
9
9
  import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
10
10
  import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
11
+ import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
11
12
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
12
13
  import { getTelemetryClient } from '@aztec/telemetry-client';
14
+ import { createHASigner } from '@aztec/validator-ha-signer/factory';
15
+ import { DutyType } from '@aztec/validator-ha-signer/types';
13
16
  import { EventEmitter } from 'events';
14
17
  import { BlockProposalHandler } from './block_proposal_handler.js';
15
18
  import { ValidationService } from './duties/validation_service.js';
19
+ import { HAKeyStore } from './key_store/ha_key_store.js';
16
20
  import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
17
21
  import { ValidatorMetrics } from './metrics.js';
18
22
  // We maintain a set of proposers who have proposed invalid blocks.
@@ -107,13 +111,23 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
107
111
  this.log.error(`Error updating epoch committee`, err);
108
112
  }
109
113
  }
110
- static new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
114
+ static async new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
111
115
  const metrics = new ValidatorMetrics(telemetry);
112
116
  const blockProposalValidator = new BlockProposalValidator(epochCache, {
113
117
  txsPermitted: !config.disableTransactions
114
118
  });
115
- const blockProposalHandler = new BlockProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, config, metrics, dateProvider, telemetry);
116
- const validator = new ValidatorClient(NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager), epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider, telemetry);
119
+ const blockProposalHandler = new BlockProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, metrics, dateProvider, telemetry);
120
+ let validatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
121
+ if (config.haSigningEnabled) {
122
+ // If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
123
+ const haConfig = {
124
+ ...config,
125
+ maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000
126
+ };
127
+ const { signer } = await createHASigner(haConfig);
128
+ validatorKeyStore = new HAKeyStore(validatorKeyStore, signer);
129
+ }
130
+ const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider, telemetry);
117
131
  return validator;
118
132
  }
119
133
  getValidatorAddresses() {
@@ -122,8 +136,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
122
136
  getBlockProposalHandler() {
123
137
  return this.blockProposalHandler;
124
138
  }
125
- signWithAddress(addr, msg) {
126
- return this.keyStore.signTypedDataWithAddress(addr, msg);
139
+ signWithAddress(addr, msg, context) {
140
+ return this.keyStore.signTypedDataWithAddress(addr, msg, context);
127
141
  }
128
142
  getCoinbaseForAttestor(attestor) {
129
143
  return this.keyStore.getCoinbaseAddress(attestor);
@@ -145,6 +159,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
145
159
  this.log.warn(`Validator client already started`);
146
160
  return;
147
161
  }
162
+ await this.keyStore.start();
148
163
  await this.registerHandlers();
149
164
  const myAddresses = this.getValidatorAddresses();
150
165
  const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
@@ -157,6 +172,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
157
172
  }
158
173
  async stop() {
159
174
  await this.epochCacheUpdateLoop.stop();
175
+ await this.keyStore.stop();
160
176
  }
161
177
  /** Register handlers on the p2p client */ async registerHandlers() {
162
178
  if (!this.hasRegisteredHandlers) {
@@ -181,6 +197,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
181
197
  * @returns true if the proposal is valid, false otherwise
182
198
  */ async validateBlockProposal(proposal, proposalSender) {
183
199
  const slotNumber = proposal.slotNumber;
200
+ // Note: During escape hatch, we still want to "validate" proposals for observability,
201
+ // but we intentionally reject them and disable slashing invalid block and attestation flow.
202
+ const escapeHatchOpen = await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber);
184
203
  const proposer = proposal.getSender();
185
204
  // Reject proposals with invalid signatures
186
205
  if (!proposer) {
@@ -203,7 +222,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
203
222
  // In fisherman mode, we always reexecute to validate proposals.
204
223
  const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
205
224
  const shouldReexecute = fishermanMode || slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute || partOfCommittee && validatorReexecute || alwaysReexecuteBlockProposals || this.blobClient.canUpload();
206
- const validationResult = await this.blockProposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute);
225
+ const validationResult = await this.blockProposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute && !escapeHatchOpen);
207
226
  if (!validationResult.isValid) {
208
227
  this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
209
228
  const reason = validationResult.reason || 'unknown';
@@ -222,7 +241,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
222
241
  this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
223
242
  }
224
243
  // Slash invalid block proposals (can happen even when not in committee)
225
- if (validationResult.reason && SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) && slashBroadcastedInvalidBlockPenalty > 0n) {
244
+ if (!escapeHatchOpen && validationResult.reason && SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) && slashBroadcastedInvalidBlockPenalty > 0n) {
226
245
  this.log.warn(`Slashing proposer for invalid block proposal`, proposalInfo);
227
246
  this.slashInvalidBlock(proposal);
228
247
  }
@@ -231,8 +250,13 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
231
250
  this.log.info(`Validated block proposal for slot ${slotNumber}`, {
232
251
  ...proposalInfo,
233
252
  inCommittee: partOfCommittee,
234
- fishermanMode: this.config.fishermanMode || false
253
+ fishermanMode: this.config.fishermanMode || false,
254
+ escapeHatchOpen
235
255
  });
256
+ if (escapeHatchOpen) {
257
+ this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
258
+ return false;
259
+ }
236
260
  // TODO(palla/mbps): Remove this once checkpoint validation is stable.
237
261
  // Track that we successfully validated a block for this slot, so we can attest to checkpoint proposals for it.
238
262
  this.validatedBlockSlots.add(slotNumber);
@@ -246,6 +270,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
246
270
  */ async attestToCheckpointProposal(proposal, _proposalSender) {
247
271
  const slotNumber = proposal.slotNumber;
248
272
  const proposer = proposal.getSender();
273
+ // If escape hatch is open for this slot's epoch, do not attest.
274
+ if (await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber)) {
275
+ this.log.warn(`Escape hatch open for slot ${slotNumber}, skipping checkpoint attestation handling`);
276
+ return undefined;
277
+ }
249
278
  // Reject proposals with invalid signatures
250
279
  if (!proposer) {
251
280
  this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
@@ -362,18 +391,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
362
391
  reason: 'last_block_not_found'
363
392
  };
364
393
  }
365
- // Get the last full block to determine checkpoint number
366
- const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
367
- if (!lastBlock) {
368
- this.log.warn(`Last block ${lastBlockHeader.getBlockNumber()} not found`, proposalInfo);
369
- return {
370
- isValid: false,
371
- reason: 'last_block_not_found'
372
- };
373
- }
374
- const checkpointNumber = lastBlock.checkpointNumber;
375
394
  // Get all full blocks for the slot and checkpoint
376
- const blocks = await this.getBlocksForSlot(slot, lastBlockHeader, checkpointNumber);
395
+ const blocks = await this.blockSource.getBlocksForSlot(slot);
377
396
  if (blocks.length === 0) {
378
397
  this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
379
398
  return {
@@ -388,14 +407,21 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
388
407
  // Get checkpoint constants from first block
389
408
  const firstBlock = blocks[0];
390
409
  const constants = this.extractCheckpointConstants(firstBlock);
410
+ const checkpointNumber = firstBlock.checkpointNumber;
391
411
  // Get L1-to-L2 messages for this checkpoint
392
412
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
413
+ // Compute the previous checkpoint out hashes for the epoch.
414
+ // TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
415
+ // actual checkpoints and the blocks/txs in them.
416
+ const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
417
+ const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch)).filter((b)=>b.number < checkpointNumber).sort((a, b)=>a.number - b.number);
418
+ const previousCheckpointOutHashes = previousCheckpoints.map((c)=>c.getCheckpointOutHash());
393
419
  // Fork world state at the block before the first block
394
420
  const parentBlockNumber = BlockNumber(firstBlock.number - 1);
395
421
  const fork = await this.worldState.fork(parentBlockNumber);
396
422
  try {
397
423
  // Create checkpoint builder with all existing blocks
398
- const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, fork, blocks);
424
+ const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks);
399
425
  // Complete the checkpoint to get computed values
400
426
  const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
401
427
  // Compare checkpoint header with proposal
@@ -422,6 +448,20 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
422
448
  reason: 'archive_mismatch'
423
449
  };
424
450
  }
451
+ // Check that the accumulated out hash matches the value in the proposal.
452
+ const computedOutHash = computedCheckpoint.getCheckpointOutHash();
453
+ const proposalOutHash = proposal.checkpointHeader.epochOutHash;
454
+ if (!computedOutHash.equals(proposalOutHash)) {
455
+ this.log.warn(`Epoch out hash mismatch`, {
456
+ proposalOutHash: proposalOutHash.toString(),
457
+ computedOutHash: computedOutHash.toString(),
458
+ ...proposalInfo
459
+ });
460
+ return {
461
+ isValid: false,
462
+ reason: 'out_hash_mismatch'
463
+ };
464
+ }
425
465
  this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
426
466
  return {
427
467
  isValid: true
@@ -431,36 +471,6 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
431
471
  }
432
472
  }
433
473
  /**
434
- * Get all full blocks for a given slot and checkpoint by walking backwards from the last block.
435
- * Returns blocks in ascending order (earliest to latest).
436
- * TODO(palla/mbps): Add getL2BlocksForSlot() to L2BlockSource interface for efficiency.
437
- */ async getBlocksForSlot(slot, lastBlockHeader, checkpointNumber) {
438
- const blocks = [];
439
- let currentHeader = lastBlockHeader;
440
- const { genesisArchiveRoot } = await this.blockSource.getGenesisValues();
441
- while(currentHeader.getSlot() === slot){
442
- const block = await this.blockSource.getL2BlockNew(currentHeader.getBlockNumber());
443
- if (!block) {
444
- this.log.warn(`Block ${currentHeader.getBlockNumber()} not found while getting blocks for slot ${slot}`);
445
- break;
446
- }
447
- if (block.checkpointNumber !== checkpointNumber) {
448
- break;
449
- }
450
- blocks.unshift(block);
451
- const prevArchive = currentHeader.lastArchive.root;
452
- if (prevArchive.equals(genesisArchiveRoot)) {
453
- break;
454
- }
455
- const prevHeader = await this.blockSource.getBlockHeaderByArchive(prevArchive);
456
- if (!prevHeader || prevHeader.getSlot() !== slot) {
457
- break;
458
- }
459
- currentHeader = prevHeader;
460
- }
461
- return blocks;
462
- }
463
- /**
464
474
  * Extract checkpoint global variables from a block.
465
475
  */ extractCheckpointConstants(block) {
466
476
  const gv = block.header.globalVariables;
@@ -482,13 +492,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
482
492
  this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
483
493
  return;
484
494
  }
485
- // Get the last full block to determine checkpoint number
486
- const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
487
- if (!lastBlock) {
488
- this.log.warn(`Failed to get last block for blob upload`, proposalInfo);
489
- return;
490
- }
491
- const blocks = await this.getBlocksForSlot(proposal.slotNumber, lastBlockHeader, lastBlock.checkpointNumber);
495
+ const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
492
496
  if (blocks.length === 0) {
493
497
  this.log.warn(`No blocks found for blob upload`, proposalInfo);
494
498
  return;
@@ -547,8 +551,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
547
551
  async broadcastBlockProposal(proposal) {
548
552
  await this.p2pClient.broadcastProposal(proposal);
549
553
  }
550
- async signAttestationsAndSigners(attestationsAndSigners, proposer) {
551
- return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
554
+ async signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber) {
555
+ return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
552
556
  }
553
557
  async collectOwnAttestations(proposal) {
554
558
  const slot = proposal.slotNumber;
@@ -630,7 +634,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
630
634
  return Buffer.alloc(0);
631
635
  }
632
636
  const payloadToSign = authRequest.getPayloadToSign();
633
- const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign);
637
+ // AUTH_REQUEST doesn't require HA protection - multiple signatures are safe
638
+ const context = {
639
+ dutyType: DutyType.AUTH_REQUEST
640
+ };
641
+ const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign, context);
634
642
  const authResponse = new AuthResponse(statusMessage, signature);
635
643
  return authResponse.toBuffer();
636
644
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/validator-client",
3
- "version": "0.0.1-commit.1142ef1",
3
+ "version": "0.0.1-commit.1bea0213",
4
4
  "main": "dest/index.js",
5
5
  "type": "module",
6
6
  "exports": {
@@ -64,31 +64,33 @@
64
64
  ]
65
65
  },
66
66
  "dependencies": {
67
- "@aztec/blob-client": "0.0.1-commit.1142ef1",
68
- "@aztec/blob-lib": "0.0.1-commit.1142ef1",
69
- "@aztec/constants": "0.0.1-commit.1142ef1",
70
- "@aztec/epoch-cache": "0.0.1-commit.1142ef1",
71
- "@aztec/ethereum": "0.0.1-commit.1142ef1",
72
- "@aztec/foundation": "0.0.1-commit.1142ef1",
73
- "@aztec/node-keystore": "0.0.1-commit.1142ef1",
74
- "@aztec/noir-protocol-circuits-types": "0.0.1-commit.1142ef1",
75
- "@aztec/p2p": "0.0.1-commit.1142ef1",
76
- "@aztec/protocol-contracts": "0.0.1-commit.1142ef1",
77
- "@aztec/prover-client": "0.0.1-commit.1142ef1",
78
- "@aztec/simulator": "0.0.1-commit.1142ef1",
79
- "@aztec/slasher": "0.0.1-commit.1142ef1",
80
- "@aztec/stdlib": "0.0.1-commit.1142ef1",
81
- "@aztec/telemetry-client": "0.0.1-commit.1142ef1",
67
+ "@aztec/blob-client": "0.0.1-commit.1bea0213",
68
+ "@aztec/blob-lib": "0.0.1-commit.1bea0213",
69
+ "@aztec/constants": "0.0.1-commit.1bea0213",
70
+ "@aztec/epoch-cache": "0.0.1-commit.1bea0213",
71
+ "@aztec/ethereum": "0.0.1-commit.1bea0213",
72
+ "@aztec/foundation": "0.0.1-commit.1bea0213",
73
+ "@aztec/node-keystore": "0.0.1-commit.1bea0213",
74
+ "@aztec/noir-protocol-circuits-types": "0.0.1-commit.1bea0213",
75
+ "@aztec/p2p": "0.0.1-commit.1bea0213",
76
+ "@aztec/protocol-contracts": "0.0.1-commit.1bea0213",
77
+ "@aztec/prover-client": "0.0.1-commit.1bea0213",
78
+ "@aztec/simulator": "0.0.1-commit.1bea0213",
79
+ "@aztec/slasher": "0.0.1-commit.1bea0213",
80
+ "@aztec/stdlib": "0.0.1-commit.1bea0213",
81
+ "@aztec/telemetry-client": "0.0.1-commit.1bea0213",
82
+ "@aztec/validator-ha-signer": "0.0.1-commit.1bea0213",
82
83
  "koa": "^2.16.1",
83
84
  "koa-router": "^13.1.1",
84
85
  "tslib": "^2.4.0",
85
86
  "viem": "npm:@aztec/viem@2.38.2"
86
87
  },
87
88
  "devDependencies": {
89
+ "@electric-sql/pglite": "^0.3.14",
88
90
  "@jest/globals": "^30.0.0",
89
91
  "@types/jest": "^30.0.0",
90
92
  "@types/node": "^22.15.17",
91
- "@typescript/native-preview": "7.0.0-dev.20251126.1",
93
+ "@typescript/native-preview": "7.0.0-dev.20260113.1",
92
94
  "jest": "^30.0.0",
93
95
  "jest-mock-extended": "^4.0.0",
94
96
  "ts-node": "^10.9.1",
@@ -1,5 +1,7 @@
1
1
  import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
2
+ import type { EpochCache } from '@aztec/epoch-cache';
2
3
  import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
4
+ import { chunkBy } from '@aztec/foundation/collection';
3
5
  import { Fr } from '@aztec/foundation/curves/bn254';
4
6
  import { TimeoutError } from '@aztec/foundation/error';
5
7
  import { createLogger } from '@aztec/foundation/log';
@@ -8,10 +10,14 @@ import { DateProvider, Timer } from '@aztec/foundation/timer';
8
10
  import type { P2P, PeerId } from '@aztec/p2p';
9
11
  import { TxProvider } from '@aztec/p2p';
10
12
  import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
11
- import type { L2BlockNew, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
12
- import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
13
+ import type { L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
14
+ import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
13
15
  import type { ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
14
- import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
16
+ import {
17
+ type L1ToL2MessageSource,
18
+ computeCheckpointOutHash,
19
+ computeInHashFromL1ToL2Messages,
20
+ } from '@aztec/stdlib/messaging';
15
21
  import type { BlockProposal } from '@aztec/stdlib/p2p';
16
22
  import { BlockHeader, type CheckpointGlobalVariables, type FailedTx, type Tx } from '@aztec/stdlib/tx';
17
23
  import {
@@ -39,7 +45,7 @@ export type BlockProposalValidationFailureReason =
39
45
  | 'unknown_error';
40
46
 
41
47
  type ReexecuteTransactionsResult = {
42
- block: L2BlockNew;
48
+ block: L2Block;
43
49
  failedTxs: FailedTx[];
44
50
  reexecutionTimeMs: number;
45
51
  totalManaUsed: number;
@@ -74,6 +80,7 @@ export class BlockProposalHandler {
74
80
  private l1ToL2MessageSource: L1ToL2MessageSource,
75
81
  private txProvider: TxProvider,
76
82
  private blockProposalValidator: BlockProposalValidator,
83
+ private epochCache: EpochCache,
77
84
  private config: ValidatorClientFullConfig,
78
85
  private metrics?: ValidatorMetrics,
79
86
  private dateProvider: DateProvider = new DateProvider(),
@@ -140,8 +147,8 @@ export class BlockProposalHandler {
140
147
 
141
148
  // Check that the proposal is from the current proposer, or the next proposer
142
149
  // This should have been handled by the p2p layer, but we double check here out of caution
143
- const invalidProposal = await this.blockProposalValidator.validate(proposal);
144
- if (invalidProposal) {
150
+ const validationResult = await this.blockProposalValidator.validate(proposal);
151
+ if (validationResult.result !== 'accept') {
145
152
  this.log.warn(`Proposal is not valid, skipping processing`, proposalInfo);
146
153
  return { isValid: false, reason: 'invalid_proposal' };
147
154
  }
@@ -212,6 +219,18 @@ export class BlockProposalHandler {
212
219
  // Try re-executing the transactions in the proposal if needed
213
220
  let reexecutionResult;
214
221
  if (shouldReexecute) {
222
+ // Compute the previous checkpoint out hashes for the epoch.
223
+ // TODO(leila/mbps): There can be a more efficient way to get the previous checkpoint out
224
+ // hashes without having to fetch all the blocks.
225
+ const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
226
+ const checkpointedBlocks = (await this.blockSource.getCheckpointedBlocksForEpoch(epoch))
227
+ .filter(b => b.block.number < blockNumber)
228
+ .sort((a, b) => a.block.number - b.block.number);
229
+ const blocksByCheckpoint = chunkBy(checkpointedBlocks, b => b.checkpointNumber);
230
+ const previousCheckpointOutHashes = blocksByCheckpoint.map(checkpointBlocks =>
231
+ computeCheckpointOutHash(checkpointBlocks.map(b => b.block.body.txEffects.map(tx => tx.l2ToL1Msgs))),
232
+ );
233
+
215
234
  try {
216
235
  this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
217
236
  reexecutionResult = await this.reexecuteTransactions(
@@ -220,6 +239,7 @@ export class BlockProposalHandler {
220
239
  checkpointNumber,
221
240
  txs,
222
241
  l1ToL2Messages,
242
+ previousCheckpointOutHashes,
223
243
  );
224
244
  } catch (error) {
225
245
  this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
@@ -229,7 +249,6 @@ export class BlockProposalHandler {
229
249
  }
230
250
 
231
251
  // If we succeeded, push this block into the archiver (unless disabled)
232
- // TODO(palla/mbps): Change default to false once block sync is stable.
233
252
  if (reexecutionResult?.block && this.config.skipPushProposedBlocksToArchiver === false) {
234
253
  await this.blockSource.addBlock(reexecutionResult?.block);
235
254
  }
@@ -297,7 +316,7 @@ export class BlockProposalHandler {
297
316
  // TODO(palla/mbps): The block header should include the checkpoint number to avoid this lookup,
298
317
  // or at least the L2BlockSource should return a different struct that includes it.
299
318
  const parentBlockNumber = parentBlockHeader.getBlockNumber();
300
- const parentBlock = await this.blockSource.getL2BlockNew(parentBlockNumber);
319
+ const parentBlock = await this.blockSource.getL2Block(parentBlockNumber);
301
320
  if (!parentBlock) {
302
321
  this.log.warn(`Parent block ${parentBlockNumber} not found in archiver`, proposalInfo);
303
322
  return { reason: 'invalid_proposal' };
@@ -338,7 +357,7 @@ export class BlockProposalHandler {
338
357
  */
339
358
  private validateNonFirstBlockInCheckpoint(
340
359
  proposal: BlockProposal,
341
- parentBlock: L2BlockNew,
360
+ parentBlock: L2Block,
342
361
  proposalInfo: object,
343
362
  ): CheckpointComputationResult | undefined {
344
363
  const proposalGlobals = proposal.blockHeader.globalVariables;
@@ -414,31 +433,7 @@ export class BlockProposalHandler {
414
433
 
415
434
  private getReexecutionDeadline(slot: SlotNumber, config: { l1GenesisTime: bigint; slotDuration: number }): Date {
416
435
  const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
417
- const msNeededForPropagationAndPublishing = this.config.validatorReexecuteDeadlineMs;
418
- return new Date(nextSlotTimestampSeconds * 1000 - msNeededForPropagationAndPublishing);
419
- }
420
-
421
- /**
422
- * Gets all prior blocks in the same checkpoint (same slot and checkpoint number) up to but not including upToBlockNumber.
423
- */
424
- private async getBlocksInCheckpoint(
425
- slot: SlotNumber,
426
- upToBlockNumber: BlockNumber,
427
- checkpointNumber: CheckpointNumber,
428
- ): Promise<L2BlockNew[]> {
429
- const blocks: L2BlockNew[] = [];
430
- let currentBlockNumber = BlockNumber(upToBlockNumber - 1);
431
-
432
- while (currentBlockNumber >= INITIAL_L2_BLOCK_NUM) {
433
- const block = await this.blockSource.getL2BlockNew(currentBlockNumber);
434
- if (!block || block.header.getSlot() !== slot || block.checkpointNumber !== checkpointNumber) {
435
- break;
436
- }
437
- blocks.unshift(block);
438
- currentBlockNumber = BlockNumber(currentBlockNumber - 1);
439
- }
440
-
441
- return blocks;
436
+ return new Date(nextSlotTimestampSeconds * 1000);
442
437
  }
443
438
 
444
439
  private getReexecuteFailureReason(err: any) {
@@ -459,6 +454,7 @@ export class BlockProposalHandler {
459
454
  checkpointNumber: CheckpointNumber,
460
455
  txs: Tx[],
461
456
  l1ToL2Messages: Fr[],
457
+ previousCheckpointOutHashes: Fr[],
462
458
  ): Promise<ReexecuteTransactionsResult> {
463
459
  const { blockHeader, txHashes } = proposal;
464
460
 
@@ -473,8 +469,9 @@ export class BlockProposalHandler {
473
469
  const slot = proposal.slotNumber;
474
470
  const config = this.checkpointsBuilder.getConfig();
475
471
 
476
- // Get prior blocks in this checkpoint (same slot and checkpoint number)
477
- const priorBlocks = await this.getBlocksInCheckpoint(slot, blockNumber, checkpointNumber);
472
+ // Get prior blocks in this checkpoint (same slot before current block)
473
+ const allBlocksInSlot = await this.blockSource.getBlocksForSlot(slot);
474
+ const priorBlocks = allBlocksInSlot.filter(b => b.number < blockNumber && b.header.getSlot() === slot);
478
475
 
479
476
  // Fork before the block to be built
480
477
  const parentBlockNumber = BlockNumber(blockNumber - 1);
@@ -495,6 +492,7 @@ export class BlockProposalHandler {
495
492
  checkpointNumber,
496
493
  constants,
497
494
  l1ToL2Messages,
495
+ previousCheckpointOutHashes,
498
496
  fork,
499
497
  priorBlocks,
500
498
  );
@@ -12,39 +12,42 @@ import {
12
12
  PublicProcessor,
13
13
  createPublicTxSimulatorForBlockBuilding,
14
14
  } from '@aztec/simulator/server';
15
- import { L2BlockNew } from '@aztec/stdlib/block';
15
+ import { L2Block } from '@aztec/stdlib/block';
16
16
  import { Checkpoint } from '@aztec/stdlib/checkpoint';
17
17
  import type { ContractDataSource } from '@aztec/stdlib/contract';
18
+ import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
18
19
  import { Gas } from '@aztec/stdlib/gas';
19
20
  import {
21
+ type BuildBlockInCheckpointResult,
20
22
  type FullNodeBlockBuilderConfig,
21
23
  FullNodeBlockBuilderConfigKeys,
24
+ type ICheckpointBlockBuilder,
25
+ type ICheckpointsBuilder,
22
26
  type MerkleTreeWriteOperations,
23
27
  type PublicProcessorLimits,
28
+ type WorldStateSynchronizer,
24
29
  } from '@aztec/stdlib/interfaces/server';
25
30
  import { MerkleTreeId } from '@aztec/stdlib/trees';
26
- import { type CheckpointGlobalVariables, type FailedTx, GlobalVariables, StateReference, Tx } from '@aztec/stdlib/tx';
31
+ import { type CheckpointGlobalVariables, GlobalVariables, StateReference, Tx } from '@aztec/stdlib/tx';
27
32
  import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
28
33
 
29
34
  import { createValidatorForBlockBuilding } from './tx_validator/tx_validator_factory.js';
30
35
 
36
+ // Re-export for backward compatibility
37
+ export type { BuildBlockInCheckpointResult } from '@aztec/stdlib/interfaces/server';
38
+
31
39
  const log = createLogger('checkpoint-builder');
32
40
 
33
- export interface BuildBlockInCheckpointResult {
34
- block: L2BlockNew;
35
- publicGas: Gas;
36
- publicProcessorDuration: number;
37
- numTxs: number;
38
- failedTxs: FailedTx[];
41
+ /** Result of building a block within a checkpoint. Extends the base interface with timer. */
42
+ export interface BuildBlockInCheckpointResultWithTimer extends BuildBlockInCheckpointResult {
39
43
  blockBuildingTimer: Timer;
40
- usedTxs: Tx[];
41
44
  }
42
45
 
43
46
  /**
44
47
  * Builder for a single checkpoint. Handles building blocks within the checkpoint
45
48
  * and completing it.
46
49
  */
47
- export class CheckpointBuilder {
50
+ export class CheckpointBuilder implements ICheckpointBlockBuilder {
48
51
  constructor(
49
52
  private checkpointBuilder: LightweightCheckpointBuilder,
50
53
  private fork: MerkleTreeWriteOperations,
@@ -66,11 +69,16 @@ export class CheckpointBuilder {
66
69
  blockNumber: BlockNumber,
67
70
  timestamp: bigint,
68
71
  opts: PublicProcessorLimits & { expectedEndState?: StateReference },
69
- ): Promise<BuildBlockInCheckpointResult> {
72
+ ): Promise<BuildBlockInCheckpointResultWithTimer> {
70
73
  const blockBuildingTimer = new Timer();
71
74
  const slot = this.checkpointBuilder.constants.slotNumber;
72
75
 
73
- log.verbose(`Building block ${blockNumber} for slot ${slot} within checkpoint`, { slot, blockNumber, ...opts });
76
+ log.verbose(`Building block ${blockNumber} for slot ${slot} within checkpoint`, {
77
+ slot,
78
+ blockNumber,
79
+ ...opts,
80
+ currentTime: new Date(this.dateProvider.now()),
81
+ });
74
82
 
75
83
  const constants = this.checkpointBuilder.constants;
76
84
  const globalVariables = GlobalVariables.from({
@@ -85,7 +93,7 @@ export class CheckpointBuilder {
85
93
  });
86
94
  const { processor, validator } = await this.makeBlockBuilderDeps(globalVariables, this.fork);
87
95
 
88
- const [publicProcessorDuration, [processedTxs, failedTxs, usedTxs]] = await elapsed(() =>
96
+ const [publicProcessorDuration, [processedTxs, failedTxs, usedTxs, _, usedTxBlobFields]] = await elapsed(() =>
89
97
  processor.process(pendingTxs, opts, validator),
90
98
  );
91
99
 
@@ -105,6 +113,7 @@ export class CheckpointBuilder {
105
113
  failedTxs,
106
114
  blockBuildingTimer,
107
115
  usedTxs,
116
+ usedTxBlobFields,
108
117
  };
109
118
  log.debug('Built block within checkpoint', res.block.header);
110
119
  return res;
@@ -165,12 +174,11 @@ export class CheckpointBuilder {
165
174
  }
166
175
  }
167
176
 
168
- /**
169
- * Factory for creating checkpoint builders.
170
- */
171
- export class FullNodeCheckpointsBuilder {
177
+ /** Factory for creating checkpoint builders. */
178
+ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder {
172
179
  constructor(
173
- private config: FullNodeBlockBuilderConfig,
180
+ private config: FullNodeBlockBuilderConfig & Pick<L1RollupConstants, 'l1GenesisTime' | 'slotDuration'>,
181
+ private worldState: WorldStateSynchronizer,
174
182
  private contractDataSource: ContractDataSource,
175
183
  private dateProvider: DateProvider,
176
184
  private telemetryClient: TelemetryClient = getTelemetryClient(),
@@ -191,6 +199,7 @@ export class FullNodeCheckpointsBuilder {
191
199
  checkpointNumber: CheckpointNumber,
192
200
  constants: CheckpointGlobalVariables,
193
201
  l1ToL2Messages: Fr[],
202
+ previousCheckpointOutHashes: Fr[],
194
203
  fork: MerkleTreeWriteOperations,
195
204
  ): Promise<CheckpointBuilder> {
196
205
  const stateReference = await fork.getStateReference();
@@ -208,6 +217,7 @@ export class FullNodeCheckpointsBuilder {
208
217
  checkpointNumber,
209
218
  constants,
210
219
  l1ToL2Messages,
220
+ previousCheckpointOutHashes,
211
221
  fork,
212
222
  );
213
223
 
@@ -228,14 +238,15 @@ export class FullNodeCheckpointsBuilder {
228
238
  checkpointNumber: CheckpointNumber,
229
239
  constants: CheckpointGlobalVariables,
230
240
  l1ToL2Messages: Fr[],
241
+ previousCheckpointOutHashes: Fr[],
231
242
  fork: MerkleTreeWriteOperations,
232
- existingBlocks: L2BlockNew[] = [],
243
+ existingBlocks: L2Block[] = [],
233
244
  ): Promise<CheckpointBuilder> {
234
245
  const stateReference = await fork.getStateReference();
235
246
  const archiveTree = await fork.getTreeInfo(MerkleTreeId.ARCHIVE);
236
247
 
237
248
  if (existingBlocks.length === 0) {
238
- return this.startCheckpoint(checkpointNumber, constants, l1ToL2Messages, fork);
249
+ return this.startCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork);
239
250
  }
240
251
 
241
252
  log.verbose(`Resuming checkpoint ${checkpointNumber} with ${existingBlocks.length} existing blocks`, {
@@ -251,6 +262,7 @@ export class FullNodeCheckpointsBuilder {
251
262
  checkpointNumber,
252
263
  constants,
253
264
  l1ToL2Messages,
265
+ previousCheckpointOutHashes,
254
266
  fork,
255
267
  existingBlocks,
256
268
  );
@@ -264,4 +276,9 @@ export class FullNodeCheckpointsBuilder {
264
276
  this.telemetryClient,
265
277
  );
266
278
  }
279
+
280
+ /** Returns a fork of the world state at the given block number. */
281
+ getFork(blockNumber: BlockNumber): Promise<MerkleTreeWriteOperations> {
282
+ return this.worldState.fork(blockNumber);
283
+ }
267
284
  }