@aztec/validator-client 0.0.1-commit.1bb068fb5 → 0.0.1-commit.1de2a32

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 +62 -57
  5. package/dest/checkpoint_builder.d.ts +13 -3
  6. package/dest/checkpoint_builder.d.ts.map +1 -1
  7. package/dest/checkpoint_builder.js +71 -18
  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 +1 -1
  12. package/dest/duties/validation_service.d.ts.map +1 -1
  13. package/dest/duties/validation_service.js +2 -8
  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 +12 -4
  24. package/dest/validator.d.ts.map +1 -1
  25. package/dest/validator.js +70 -18
  26. package/package.json +19 -19
  27. package/src/block_proposal_handler.ts +74 -76
  28. package/src/checkpoint_builder.ts +83 -14
  29. package/src/config.ts +22 -1
  30. package/src/duties/validation_service.ts +2 -8
  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 +68 -16
  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
@@ -9,11 +9,12 @@ import { sleep } from '@aztec/foundation/sleep';
9
9
  import { DateProvider } from '@aztec/foundation/timer';
10
10
  import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
11
11
  import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
12
+ import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
12
13
  import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
13
14
  import { accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
14
15
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
15
16
  import { getTelemetryClient } from '@aztec/telemetry-client';
16
- import { createHASigner } from '@aztec/validator-ha-signer/factory';
17
+ import { createHASigner, createLocalSignerWithProtection } from '@aztec/validator-ha-signer/factory';
17
18
  import { DutyType } from '@aztec/validator-ha-signer/types';
18
19
  import { EventEmitter } from 'events';
19
20
  import { BlockProposalHandler } from './block_proposal_handler.js';
@@ -42,6 +43,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
42
43
  l1ToL2MessageSource;
43
44
  config;
44
45
  blobClient;
46
+ slashingProtectionSigner;
45
47
  dateProvider;
46
48
  tracer;
47
49
  validationService;
@@ -53,10 +55,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
53
55
  /** Tracks the last checkpoint proposal we created. */ lastProposedCheckpoint;
54
56
  lastEpochForCommitteeUpdateLoop;
55
57
  epochCacheUpdateLoop;
58
+ /** Tracks the last epoch in which each attester successfully submitted at least one attestation. */ lastAttestedEpochByAttester;
56
59
  proposersOfInvalidBlocks;
57
60
  /** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */ lastAttestedProposal;
58
- constructor(keyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
59
- 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();
60
63
  // Create child logger with fisherman prefix if in fisherman mode
61
64
  this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
62
65
  this.tracer = telemetry.getTracer('Validator');
@@ -95,6 +98,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
95
98
  this.log.trace(`No committee found for slot`);
96
99
  return;
97
100
  }
101
+ this.metrics.setCurrentEpoch(epoch);
98
102
  if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
99
103
  const me = this.getValidatorAddresses();
100
104
  const committeeSet = new Set(committee.map((v)=>v.toString()));
@@ -113,20 +117,33 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
113
117
  static async new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
114
118
  const metrics = new ValidatorMetrics(telemetry);
115
119
  const blockProposalValidator = new BlockProposalValidator(epochCache, {
116
- txsPermitted: !config.disableTransactions
120
+ txsPermitted: !config.disableTransactions,
121
+ maxTxsPerBlock: config.validateMaxTxsPerBlock
117
122
  });
118
123
  const blockProposalHandler = new BlockProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, metrics, dateProvider, telemetry);
119
- let validatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
124
+ const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
125
+ let slashingProtectionSigner;
120
126
  if (config.haSigningEnabled) {
127
+ // Multi-node HA mode: use PostgreSQL-backed distributed locking.
121
128
  // If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
122
129
  const haConfig = {
123
130
  ...config,
124
131
  maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000
125
132
  };
126
- const { signer } = await createHASigner(haConfig);
127
- validatorKeyStore = new HAKeyStore(validatorKeyStore, signer);
128
- }
129
- 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);
130
147
  return validator;
131
148
  }
132
149
  getValidatorAddresses() {
@@ -153,6 +170,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
153
170
  ...config
154
171
  };
155
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
+ }
156
178
  async start() {
157
179
  if (this.epochCacheUpdateLoop.isRunning()) {
158
180
  this.log.warn(`Validator client already started`);
@@ -311,12 +333,10 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
311
333
  const proposalInfo = {
312
334
  slotNumber,
313
335
  archive: proposal.archive.toString(),
314
- proposer: proposer.toString(),
315
- txCount: proposal.txHashes.length
336
+ proposer: proposer.toString()
316
337
  };
317
338
  this.log.info(`Received checkpoint proposal for slot ${slotNumber}`, {
318
339
  ...proposalInfo,
319
- txHashes: proposal.txHashes.map((t)=>t.toString()),
320
340
  fishermanMode: this.config.fishermanMode || false
321
341
  });
322
342
  // Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
@@ -346,6 +366,16 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
346
366
  fishermanMode: this.config.fishermanMode || false
347
367
  });
348
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
+ }
349
379
  // Determine which validators should attest
350
380
  let attestors;
351
381
  if (partOfCommittee) {
@@ -442,6 +472,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
442
472
  reason: 'no_blocks_for_slot'
443
473
  };
444
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
+ }
445
483
  this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
446
484
  ...proposalInfo,
447
485
  blockNumbers: blocks.map((b)=>b.number)
@@ -452,12 +490,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
452
490
  const checkpointNumber = firstBlock.checkpointNumber;
453
491
  // Get L1-to-L2 messages for this checkpoint
454
492
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
455
- // Compute the previous checkpoint out hashes for the epoch.
456
- // TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
457
- // actual checkpoints and the blocks/txs in them.
493
+ // Collect the out hashes of all the checkpoints before this one in the same epoch
458
494
  const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
459
- const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch)).filter((b)=>b.number < checkpointNumber).sort((a, b)=>a.number - b.number);
460
- const previousCheckpointOutHashes = previousCheckpoints.map((c)=>c.getCheckpointOutHash());
495
+ const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
461
496
  // Fork world state at the block before the first block
462
497
  const parentBlockNumber = BlockNumber(firstBlock.number - 1);
463
498
  const fork = await this.worldState.fork(parentBlockNumber);
@@ -511,6 +546,22 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
511
546
  reason: 'out_hash_mismatch'
512
547
  };
513
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
+ }
514
565
  this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
515
566
  return {
516
567
  isValid: true
@@ -527,6 +578,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
527
578
  chainId: gv.chainId,
528
579
  version: gv.version,
529
580
  slotNumber: gv.slotNumber,
581
+ timestamp: gv.timestamp,
530
582
  coinbase: gv.coinbase,
531
583
  feeRecipient: gv.feeRecipient,
532
584
  gasFees: gv.gasFees
@@ -547,7 +599,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
547
599
  return;
548
600
  }
549
601
  const blobFields = blocks.flatMap((b)=>b.toBlobFields());
550
- const blobs = getBlobsPerL1Block(blobFields);
602
+ const blobs = await getBlobsPerL1Block(blobFields);
551
603
  await this.blobClient.sendBlobsToFilestore(blobs);
552
604
  this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
553
605
  ...proposalInfo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/validator-client",
3
- "version": "0.0.1-commit.1bb068fb5",
3
+ "version": "0.0.1-commit.1de2a32",
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.1bb068fb5",
68
- "@aztec/blob-lib": "0.0.1-commit.1bb068fb5",
69
- "@aztec/constants": "0.0.1-commit.1bb068fb5",
70
- "@aztec/epoch-cache": "0.0.1-commit.1bb068fb5",
71
- "@aztec/ethereum": "0.0.1-commit.1bb068fb5",
72
- "@aztec/foundation": "0.0.1-commit.1bb068fb5",
73
- "@aztec/node-keystore": "0.0.1-commit.1bb068fb5",
74
- "@aztec/noir-protocol-circuits-types": "0.0.1-commit.1bb068fb5",
75
- "@aztec/p2p": "0.0.1-commit.1bb068fb5",
76
- "@aztec/protocol-contracts": "0.0.1-commit.1bb068fb5",
77
- "@aztec/prover-client": "0.0.1-commit.1bb068fb5",
78
- "@aztec/simulator": "0.0.1-commit.1bb068fb5",
79
- "@aztec/slasher": "0.0.1-commit.1bb068fb5",
80
- "@aztec/stdlib": "0.0.1-commit.1bb068fb5",
81
- "@aztec/telemetry-client": "0.0.1-commit.1bb068fb5",
82
- "@aztec/validator-ha-signer": "0.0.1-commit.1bb068fb5",
67
+ "@aztec/blob-client": "0.0.1-commit.1de2a32",
68
+ "@aztec/blob-lib": "0.0.1-commit.1de2a32",
69
+ "@aztec/constants": "0.0.1-commit.1de2a32",
70
+ "@aztec/epoch-cache": "0.0.1-commit.1de2a32",
71
+ "@aztec/ethereum": "0.0.1-commit.1de2a32",
72
+ "@aztec/foundation": "0.0.1-commit.1de2a32",
73
+ "@aztec/node-keystore": "0.0.1-commit.1de2a32",
74
+ "@aztec/noir-protocol-circuits-types": "0.0.1-commit.1de2a32",
75
+ "@aztec/p2p": "0.0.1-commit.1de2a32",
76
+ "@aztec/protocol-contracts": "0.0.1-commit.1de2a32",
77
+ "@aztec/prover-client": "0.0.1-commit.1de2a32",
78
+ "@aztec/simulator": "0.0.1-commit.1de2a32",
79
+ "@aztec/slasher": "0.0.1-commit.1de2a32",
80
+ "@aztec/stdlib": "0.0.1-commit.1de2a32",
81
+ "@aztec/telemetry-client": "0.0.1-commit.1de2a32",
82
+ "@aztec/validator-ha-signer": "0.0.1-commit.1de2a32",
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.1bb068fb5",
90
- "@aztec/world-state": "0.0.1-commit.1bb068fb5",
89
+ "@aztec/archiver": "0.0.1-commit.1de2a32",
90
+ "@aztec/world-state": "0.0.1-commit.1de2a32",
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,
@@ -501,18 +492,25 @@ export class BlockProposalHandler {
501
492
 
502
493
  // Build the new block
503
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;
504
499
  const result = await checkpointBuilder.buildBlock(txs, blockNumber, blockHeader.globalVariables.timestamp, {
505
500
  deadline,
506
501
  expectedEndState: blockHeader.state,
502
+ maxTransactions: this.config.validateMaxTxsPerBlock,
503
+ maxBlockGas,
507
504
  });
508
505
 
509
506
  const { block, failedTxs } = result;
510
507
  const numFailedTxs = failedTxs.length;
511
508
 
512
- this.log.verbose(`Transaction re-execution complete for slot ${slot}`, {
509
+ this.log.verbose(`Block proposal ${blockNumber} at slot ${slot} transaction re-execution complete`, {
513
510
  numFailedTxs,
514
511
  numProposalTxs: txHashes.length,
515
512
  numProcessedTxs: block.body.txEffects.length,
513
+ blockNumber,
516
514
  slot,
517
515
  });
518
516