@aztec/validator-client 0.0.1-commit.9d2bcf6d → 0.0.1-commit.9ef841308

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 (51) hide show
  1. package/README.md +60 -18
  2. package/dest/checkpoint_builder.d.ts +21 -8
  3. package/dest/checkpoint_builder.d.ts.map +1 -1
  4. package/dest/checkpoint_builder.js +124 -46
  5. package/dest/config.d.ts +1 -1
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +26 -6
  8. package/dest/duties/validation_service.d.ts +2 -2
  9. package/dest/duties/validation_service.d.ts.map +1 -1
  10. package/dest/duties/validation_service.js +6 -12
  11. package/dest/factory.d.ts +7 -4
  12. package/dest/factory.d.ts.map +1 -1
  13. package/dest/factory.js +6 -5
  14. package/dest/index.d.ts +2 -3
  15. package/dest/index.d.ts.map +1 -1
  16. package/dest/index.js +1 -2
  17. package/dest/key_store/ha_key_store.js +1 -1
  18. package/dest/metrics.d.ts +10 -2
  19. package/dest/metrics.d.ts.map +1 -1
  20. package/dest/metrics.js +12 -0
  21. package/dest/proposal_handler.d.ts +94 -0
  22. package/dest/proposal_handler.d.ts.map +1 -0
  23. package/dest/{block_proposal_handler.js → proposal_handler.js} +377 -67
  24. package/dest/validator.d.ts +35 -21
  25. package/dest/validator.d.ts.map +1 -1
  26. package/dest/validator.js +177 -218
  27. package/package.json +19 -19
  28. package/src/checkpoint_builder.ts +142 -39
  29. package/src/config.ts +26 -6
  30. package/src/duties/validation_service.ts +12 -11
  31. package/src/factory.ts +9 -3
  32. package/src/index.ts +1 -2
  33. package/src/key_store/ha_key_store.ts +1 -1
  34. package/src/metrics.ts +19 -1
  35. package/src/proposal_handler.ts +907 -0
  36. package/src/validator.ts +240 -248
  37. package/dest/block_proposal_handler.d.ts +0 -63
  38. package/dest/block_proposal_handler.d.ts.map +0 -1
  39. package/dest/tx_validator/index.d.ts +0 -3
  40. package/dest/tx_validator/index.d.ts.map +0 -1
  41. package/dest/tx_validator/index.js +0 -2
  42. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  43. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  44. package/dest/tx_validator/nullifier_cache.js +0 -24
  45. package/dest/tx_validator/tx_validator_factory.d.ts +0 -19
  46. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  47. package/dest/tx_validator/tx_validator_factory.js +0 -54
  48. package/src/block_proposal_handler.ts +0 -555
  49. package/src/tx_validator/index.ts +0 -2
  50. package/src/tx_validator/nullifier_cache.ts +0 -30
  51. package/src/tx_validator/tx_validator_factory.ts +0 -154
@@ -63,19 +63,24 @@ function _ts_dispose_resources(env) {
63
63
  return next();
64
64
  })(env);
65
65
  }
66
+ import { encodeCheckpointBlobDataFromBlocks, getBlobsPerL1Block } from '@aztec/blob-lib';
66
67
  import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
68
+ import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
67
69
  import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
68
- import { chunkBy } from '@aztec/foundation/collection';
70
+ import { pick } from '@aztec/foundation/collection';
69
71
  import { Fr } from '@aztec/foundation/curves/bn254';
70
72
  import { TimeoutError } from '@aztec/foundation/error';
71
73
  import { createLogger } from '@aztec/foundation/log';
72
74
  import { retryUntil } from '@aztec/foundation/retry';
73
75
  import { DateProvider, Timer } from '@aztec/foundation/timer';
76
+ import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
74
77
  import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
75
- import { computeCheckpointOutHash, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
76
- import { ReExFailedTxsError, ReExStateMismatchError, ReExTimeoutError, TransactionsNotAvailableError } from '@aztec/stdlib/validators';
78
+ import { Gas } from '@aztec/stdlib/gas';
79
+ import { accumulateCheckpointOutHashes, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
80
+ import { MerkleTreeId } from '@aztec/stdlib/trees';
81
+ import { ReExFailedTxsError, ReExInitialStateMismatchError, ReExStateMismatchError, ReExTimeoutError, TransactionsNotAvailableError } from '@aztec/stdlib/validators';
77
82
  import { getTelemetryClient } from '@aztec/telemetry-client';
78
- export class BlockProposalHandler {
83
+ /** Handles block and checkpoint proposals for both validator and non-validator nodes. */ export class ProposalHandler {
79
84
  checkpointsBuilder;
80
85
  worldState;
81
86
  blockSource;
@@ -84,11 +89,12 @@ export class BlockProposalHandler {
84
89
  blockProposalValidator;
85
90
  epochCache;
86
91
  config;
92
+ blobClient;
87
93
  metrics;
88
94
  dateProvider;
89
95
  log;
90
96
  tracer;
91
- constructor(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, metrics, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator:block-proposal-handler')){
97
+ constructor(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, blobClient, metrics, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator:proposal-handler')){
92
98
  this.checkpointsBuilder = checkpointsBuilder;
93
99
  this.worldState = worldState;
94
100
  this.blockSource = blockSource;
@@ -97,31 +103,39 @@ export class BlockProposalHandler {
97
103
  this.blockProposalValidator = blockProposalValidator;
98
104
  this.epochCache = epochCache;
99
105
  this.config = config;
106
+ this.blobClient = blobClient;
100
107
  this.metrics = metrics;
101
108
  this.dateProvider = dateProvider;
102
109
  this.log = log;
103
110
  if (config.fishermanMode) {
104
111
  this.log = this.log.createChild('[FISHERMAN]');
105
112
  }
106
- this.tracer = telemetry.getTracer('BlockProposalHandler');
113
+ this.tracer = telemetry.getTracer('ProposalHandler');
107
114
  }
108
- registerForReexecution(p2pClient) {
109
- // Non-validator handler that re-executes for monitoring but does not attest.
115
+ /**
116
+ * Registers non-validator handlers for block and checkpoint proposals on the p2p client.
117
+ * Block proposals are always registered. Checkpoint proposals are registered if the blob client can upload.
118
+ */ register(p2pClient, shouldReexecute) {
119
+ // Non-validator handler that processes or re-executes for monitoring but does not attest.
110
120
  // Returns boolean indicating whether the proposal was valid.
111
- const handler = async (proposal, proposalSender)=>{
121
+ const blockHandler = async (proposal, proposalSender)=>{
112
122
  try {
113
- const result = await this.handleBlockProposal(proposal, proposalSender, true);
123
+ const { slotNumber, blockNumber } = proposal;
124
+ const result = await this.handleBlockProposal(proposal, proposalSender, shouldReexecute);
114
125
  if (result.isValid) {
115
- this.log.info(`Non-validator reexecution completed for slot ${proposal.slotNumber}`, {
126
+ this.log.info(`Non-validator block proposal ${blockNumber} at slot ${slotNumber} handled`, {
116
127
  blockNumber: result.blockNumber,
128
+ slotNumber,
117
129
  reexecutionTimeMs: result.reexecutionResult?.reexecutionTimeMs,
118
130
  totalManaUsed: result.reexecutionResult?.totalManaUsed,
119
- numTxs: result.reexecutionResult?.block?.body?.txEffects?.length ?? 0
131
+ numTxs: result.reexecutionResult?.block?.body?.txEffects?.length ?? 0,
132
+ reexecuted: shouldReexecute
120
133
  });
121
134
  return true;
122
135
  } else {
123
- this.log.warn(`Non-validator reexecution failed for slot ${proposal.slotNumber}`, {
136
+ this.log.warn(`Non-validator block proposal ${blockNumber} at slot ${slotNumber} failed processing with ${result.reason}`, {
124
137
  blockNumber: result.blockNumber,
138
+ slotNumber,
125
139
  reason: result.reason
126
140
  });
127
141
  return false;
@@ -131,7 +145,30 @@ export class BlockProposalHandler {
131
145
  return false;
132
146
  }
133
147
  };
134
- p2pClient.registerBlockProposalHandler(handler);
148
+ p2pClient.registerBlockProposalHandler(blockHandler);
149
+ // Register checkpoint proposal handler if blob uploads are enabled and we are reexecuting
150
+ if (this.blobClient.canUpload() && shouldReexecute) {
151
+ const checkpointHandler = async (checkpoint, _sender)=>{
152
+ try {
153
+ const proposalInfo = {
154
+ proposalSlotNumber: checkpoint.slotNumber,
155
+ archive: checkpoint.archive.toString(),
156
+ proposer: checkpoint.getSender()?.toString()
157
+ };
158
+ const result = await this.handleCheckpointProposal(checkpoint, proposalInfo);
159
+ if (result.isValid) {
160
+ this.log.info(`Non-validator checkpoint proposal at slot ${checkpoint.slotNumber} handled`, proposalInfo);
161
+ } else {
162
+ this.log.warn(`Non-validator checkpoint proposal at slot ${checkpoint.slotNumber} failed: ${result.reason}`, proposalInfo);
163
+ }
164
+ } catch (error) {
165
+ this.log.error('Error processing checkpoint proposal in non-validator handler', error);
166
+ }
167
+ // Non-validators don't attest
168
+ return undefined;
169
+ };
170
+ p2pClient.registerCheckpointProposalHandler(checkpointHandler);
171
+ }
135
172
  return this;
136
173
  }
137
174
  async handleBlockProposal(proposal, proposalSender, shouldReexecute) {
@@ -148,7 +185,9 @@ export class BlockProposalHandler {
148
185
  }
149
186
  const proposalInfo = {
150
187
  ...proposal.toBlockInfo(),
151
- proposer: proposer.toString()
188
+ proposer: proposer.toString(),
189
+ blockNumber: undefined,
190
+ checkpointNumber: undefined
152
191
  };
153
192
  this.log.info(`Processing proposal for slot ${slotNumber}`, {
154
193
  ...proposalInfo,
@@ -164,9 +203,28 @@ export class BlockProposalHandler {
164
203
  reason: 'invalid_proposal'
165
204
  };
166
205
  }
167
- // Check that the parent proposal is a block we know, otherwise reexecution would fail
168
- const parentBlockHeader = await this.getParentBlock(proposal);
169
- if (parentBlockHeader === undefined) {
206
+ // Ensure the block source is synced before checking for existing blocks,
207
+ // since a pending checkpoint prune may remove blocks we'd otherwise find.
208
+ // This affects mostly the block_number_already_exists check, since a pending
209
+ // checkpoint prune could remove a block that would conflict with this proposal.
210
+ // When pipelining is enabled, the proposer builds ahead of L1 submission, so the
211
+ // block source won't have synced to the proposed slot yet. Skip the sync wait to
212
+ // avoid eating into the attestation window.
213
+ if (!this.epochCache.isProposerPipeliningEnabled()) {
214
+ const blockSourceSync = await this.waitForBlockSourceSync(slotNumber);
215
+ if (!blockSourceSync) {
216
+ this.log.warn(`Block source is not synced, skipping processing`, proposalInfo);
217
+ return {
218
+ isValid: false,
219
+ reason: 'block_source_not_synced'
220
+ };
221
+ }
222
+ }
223
+ // Check that the parent proposal is a block we know, otherwise reexecution would fail.
224
+ // If we don't find it immediately, we keep retrying for a while; it may be we still
225
+ // need to process other block proposals to get to it.
226
+ const parentBlock = await this.getParentBlock(proposal);
227
+ if (parentBlock === undefined) {
170
228
  this.log.warn(`Parent block for proposal not found, skipping processing`, proposalInfo);
171
229
  return {
172
230
  isValid: false,
@@ -174,9 +232,9 @@ export class BlockProposalHandler {
174
232
  };
175
233
  }
176
234
  // Check that the parent block's slot is not greater than the proposal's slot.
177
- if (parentBlockHeader !== 'genesis' && parentBlockHeader.getSlot() > slotNumber) {
235
+ if (parentBlock !== 'genesis' && parentBlock.header.getSlot() > slotNumber) {
178
236
  this.log.warn(`Parent block slot is greater than proposal slot, skipping processing`, {
179
- parentBlockSlot: parentBlockHeader.getSlot().toString(),
237
+ parentBlockSlot: parentBlock.header.getSlot().toString(),
180
238
  proposalSlot: slotNumber.toString(),
181
239
  ...proposalInfo
182
240
  });
@@ -186,7 +244,8 @@ export class BlockProposalHandler {
186
244
  };
187
245
  }
188
246
  // Compute the block number based on the parent block
189
- const blockNumber = parentBlockHeader === 'genesis' ? BlockNumber(INITIAL_L2_BLOCK_NUM) : BlockNumber(parentBlockHeader.getBlockNumber() + 1);
247
+ const blockNumber = parentBlock === 'genesis' ? BlockNumber(INITIAL_L2_BLOCK_NUM) : BlockNumber(parentBlock.header.getBlockNumber() + 1);
248
+ proposalInfo.blockNumber = blockNumber;
190
249
  // Check that this block number does not exist already
191
250
  const existingBlock = await this.blockSource.getBlockHeader(blockNumber);
192
251
  if (existingBlock) {
@@ -203,8 +262,16 @@ export class BlockProposalHandler {
203
262
  pinnedPeer: proposalSender,
204
263
  deadline: this.getReexecutionDeadline(slotNumber, config)
205
264
  });
265
+ // If reexecution is disabled, bail. We were just interested in triggering tx collection.
266
+ if (!shouldReexecute) {
267
+ this.log.info(`Received valid block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`, proposalInfo);
268
+ return {
269
+ isValid: true,
270
+ blockNumber
271
+ };
272
+ }
206
273
  // Compute the checkpoint number for this block and validate checkpoint consistency
207
- const checkpointResult = await this.computeCheckpointNumber(proposal, parentBlockHeader, proposalInfo);
274
+ const checkpointResult = this.computeCheckpointNumber(proposal, parentBlock, proposalInfo);
208
275
  if (checkpointResult.reason) {
209
276
  return {
210
277
  isValid: false,
@@ -213,6 +280,7 @@ export class BlockProposalHandler {
213
280
  };
214
281
  }
215
282
  const checkpointNumber = checkpointResult.checkpointNumber;
283
+ proposalInfo.checkpointNumber = checkpointNumber;
216
284
  // Check that I have the same set of l1ToL2Messages as the proposal
217
285
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
218
286
  const computedInHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
@@ -241,35 +309,32 @@ export class BlockProposalHandler {
241
309
  reason: 'txs_not_available'
242
310
  };
243
311
  }
312
+ // Collect the out hashes of all the checkpoints before this one in the same epoch
313
+ const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
314
+ const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
244
315
  // Try re-executing the transactions in the proposal if needed
245
316
  let reexecutionResult;
246
- if (shouldReexecute) {
247
- // Compute the previous checkpoint out hashes for the epoch.
248
- // TODO(leila/mbps): There can be a more efficient way to get the previous checkpoint out
249
- // hashes without having to fetch all the blocks.
250
- const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
251
- const checkpointedBlocks = (await this.blockSource.getCheckpointedBlocksForEpoch(epoch)).filter((b)=>b.block.number < blockNumber).sort((a, b)=>a.block.number - b.block.number);
252
- const blocksByCheckpoint = chunkBy(checkpointedBlocks, (b)=>b.checkpointNumber);
253
- const previousCheckpointOutHashes = blocksByCheckpoint.map((checkpointBlocks)=>computeCheckpointOutHash(checkpointBlocks.map((b)=>b.block.body.txEffects.map((tx)=>tx.l2ToL1Msgs))));
254
- try {
255
- this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
256
- reexecutionResult = await this.reexecuteTransactions(proposal, blockNumber, checkpointNumber, txs, l1ToL2Messages, previousCheckpointOutHashes);
257
- } catch (error) {
258
- this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
259
- const reason = this.getReexecuteFailureReason(error);
260
- return {
261
- isValid: false,
262
- blockNumber,
263
- reason,
264
- reexecutionResult
265
- };
266
- }
317
+ try {
318
+ this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
319
+ reexecutionResult = await this.reexecuteTransactions(proposal, blockNumber, checkpointNumber, txs, l1ToL2Messages, previousCheckpointOutHashes);
320
+ } catch (error) {
321
+ this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
322
+ const reason = this.getReexecuteFailureReason(error);
323
+ return {
324
+ isValid: false,
325
+ blockNumber,
326
+ reason,
327
+ reexecutionResult
328
+ };
267
329
  }
268
330
  // If we succeeded, push this block into the archiver (unless disabled)
269
331
  if (reexecutionResult?.block && this.config.skipPushProposedBlocksToArchiver === false) {
270
332
  await this.blockSource.addBlock(reexecutionResult?.block);
271
333
  }
272
- this.log.info(`Successfully processed block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`, proposalInfo);
334
+ this.log.info(`Successfully re-executed block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`, {
335
+ ...proposalInfo,
336
+ ...pick(reexecutionResult, 'reexecutionTimeMs', 'totalManaUsed')
337
+ });
273
338
  return {
274
339
  isValid: true,
275
340
  blockNumber,
@@ -288,7 +353,7 @@ export class BlockProposalHandler {
288
353
  const currentTime = this.dateProvider.now();
289
354
  const timeoutDurationMs = deadline.getTime() - currentTime;
290
355
  try {
291
- return await this.blockSource.getBlockHeaderByArchive(parentArchive) ?? (timeoutDurationMs <= 0 ? undefined : await retryUntil(()=>this.blockSource.syncImmediate().then(()=>this.blockSource.getBlockHeaderByArchive(parentArchive)), 'force archiver sync', timeoutDurationMs / 1000, 0.5));
356
+ return await this.blockSource.getBlockDataByArchive(parentArchive) ?? (timeoutDurationMs <= 0 ? undefined : await retryUntil(()=>this.blockSource.syncImmediate().then(()=>this.blockSource.getBlockDataByArchive(parentArchive)), 'force archiver sync', timeoutDurationMs / 1000, 0.5));
292
357
  } catch (err) {
293
358
  if (err instanceof TimeoutError) {
294
359
  this.log.debug(`Timed out getting parent block by archive root`, {
@@ -302,8 +367,8 @@ export class BlockProposalHandler {
302
367
  return undefined;
303
368
  }
304
369
  }
305
- async computeCheckpointNumber(proposal, parentBlockHeader, proposalInfo) {
306
- if (parentBlockHeader === 'genesis') {
370
+ computeCheckpointNumber(proposal, parentBlock, proposalInfo) {
371
+ if (parentBlock === 'genesis') {
307
372
  // First block is in checkpoint 1
308
373
  if (proposal.indexWithinCheckpoint !== 0) {
309
374
  this.log.warn(`First block proposal has non-zero indexWithinCheckpoint`, proposalInfo);
@@ -315,20 +380,9 @@ export class BlockProposalHandler {
315
380
  checkpointNumber: CheckpointNumber.INITIAL
316
381
  };
317
382
  }
318
- // Get the parent block to find its checkpoint number
319
- // TODO(palla/mbps): The block header should include the checkpoint number to avoid this lookup,
320
- // or at least the L2BlockSource should return a different struct that includes it.
321
- const parentBlockNumber = parentBlockHeader.getBlockNumber();
322
- const parentBlock = await this.blockSource.getL2Block(parentBlockNumber);
323
- if (!parentBlock) {
324
- this.log.warn(`Parent block ${parentBlockNumber} not found in archiver`, proposalInfo);
325
- return {
326
- reason: 'invalid_proposal'
327
- };
328
- }
329
383
  if (proposal.indexWithinCheckpoint === 0) {
330
384
  // If this is the first block in a new checkpoint, increment the checkpoint number
331
- if (!(proposal.blockHeader.getSlot() > parentBlockHeader.getSlot())) {
385
+ if (!(proposal.blockHeader.getSlot() > parentBlock.header.getSlot())) {
332
386
  this.log.warn(`Slot should be greater than parent block slot for first block in checkpoint`, proposalInfo);
333
387
  return {
334
388
  reason: 'invalid_proposal'
@@ -345,7 +399,7 @@ export class BlockProposalHandler {
345
399
  reason: 'invalid_proposal'
346
400
  };
347
401
  }
348
- if (proposal.blockHeader.getSlot() !== parentBlockHeader.getSlot()) {
402
+ if (proposal.blockHeader.getSlot() !== parentBlock.header.getSlot()) {
349
403
  this.log.warn(`Slot should be equal to parent block slot for non-first block in checkpoint`, proposalInfo);
350
404
  return {
351
405
  reason: 'invalid_proposal'
@@ -445,8 +499,39 @@ export class BlockProposalHandler {
445
499
  const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
446
500
  return new Date(nextSlotTimestampSeconds * 1000);
447
501
  }
502
+ /** Waits for the block source to sync L1 data up to at least the slot before the given one. */ async waitForBlockSourceSync(slot) {
503
+ const deadline = this.getReexecutionDeadline(slot, this.checkpointsBuilder.getConfig());
504
+ const timeoutMs = deadline.getTime() - this.dateProvider.now();
505
+ if (slot === 0) {
506
+ return true;
507
+ }
508
+ // Make a quick check before triggering an archiver sync
509
+ const syncedSlot = await this.blockSource.getSyncedL2SlotNumber();
510
+ if (syncedSlot !== undefined && syncedSlot + 1 >= slot) {
511
+ return true;
512
+ }
513
+ try {
514
+ // Trigger an immediate sync of the block source, and wait until it reports being synced to the required slot
515
+ return await retryUntil(async ()=>{
516
+ await this.blockSource.syncImmediate();
517
+ const syncedSlot = await this.blockSource.getSyncedL2SlotNumber();
518
+ return syncedSlot !== undefined && syncedSlot + 1 >= slot;
519
+ }, 'wait for block source sync', timeoutMs / 1000, 0.5);
520
+ } catch (err) {
521
+ if (err instanceof TimeoutError) {
522
+ this.log.warn(`Timed out waiting for block source to sync to slot ${slot}`);
523
+ return false;
524
+ } else {
525
+ throw err;
526
+ }
527
+ }
528
+ }
448
529
  getReexecuteFailureReason(err) {
449
- if (err instanceof ReExStateMismatchError) {
530
+ if (err instanceof TransactionsNotAvailableError) {
531
+ return 'txs_not_available';
532
+ } else if (err instanceof ReExInitialStateMismatchError) {
533
+ return 'initial_state_mismatch';
534
+ } else if (err instanceof ReExStateMismatchError) {
450
535
  return 'state_mismatch';
451
536
  } else if (err instanceof ReExFailedTxsError) {
452
537
  return 'failed_txs';
@@ -479,30 +564,43 @@ export class BlockProposalHandler {
479
564
  // Fork before the block to be built
480
565
  const parentBlockNumber = BlockNumber(blockNumber - 1);
481
566
  await this.worldState.syncImmediate(parentBlockNumber);
482
- const fork = _ts_add_disposable_resource(env, await this.worldState.fork(parentBlockNumber), false);
483
- // Build checkpoint constants from proposal (excludes blockNumber and timestamp which are per-block)
567
+ const fork = _ts_add_disposable_resource(env, await this.worldState.fork(parentBlockNumber), true);
568
+ // Verify the fork's archive root matches the proposal's expected last archive.
569
+ // If they don't match, our world state synced to a different chain and reexecution would fail.
570
+ const forkArchiveRoot = new Fr((await fork.getTreeInfo(MerkleTreeId.ARCHIVE)).root);
571
+ if (!forkArchiveRoot.equals(proposal.blockHeader.lastArchive.root)) {
572
+ throw new ReExInitialStateMismatchError(proposal.blockHeader.lastArchive.root, forkArchiveRoot);
573
+ }
574
+ // Build checkpoint constants from proposal (excludes blockNumber which is per-block)
484
575
  const constants = {
485
576
  chainId: new Fr(config.l1ChainId),
486
577
  version: new Fr(config.rollupVersion),
487
578
  slotNumber: slot,
579
+ timestamp: blockHeader.globalVariables.timestamp,
488
580
  coinbase: blockHeader.globalVariables.coinbase,
489
581
  feeRecipient: blockHeader.globalVariables.feeRecipient,
490
582
  gasFees: blockHeader.globalVariables.gasFees
491
583
  };
492
584
  // Create checkpoint builder with prior blocks
493
- const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, priorBlocks, this.log.getBindings());
585
+ const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, 0n, l1ToL2Messages, previousCheckpointOutHashes, fork, priorBlocks, this.log.getBindings());
494
586
  // Build the new block
495
587
  const deadline = this.getReexecutionDeadline(slot, config);
588
+ const maxBlockGas = this.config.validateMaxL2BlockGas !== undefined || this.config.validateMaxDABlockGas !== undefined ? new Gas(this.config.validateMaxDABlockGas ?? Infinity, this.config.validateMaxL2BlockGas ?? Infinity) : undefined;
496
589
  const result = await checkpointBuilder.buildBlock(txs, blockNumber, blockHeader.globalVariables.timestamp, {
590
+ isBuildingProposal: false,
591
+ minValidTxs: 0,
497
592
  deadline,
498
- expectedEndState: blockHeader.state
593
+ expectedEndState: blockHeader.state,
594
+ maxTransactions: this.config.validateMaxTxsPerBlock,
595
+ maxBlockGas
499
596
  });
500
597
  const { block, failedTxs } = result;
501
598
  const numFailedTxs = failedTxs.length;
502
- this.log.verbose(`Transaction re-execution complete for slot ${slot}`, {
599
+ this.log.verbose(`Block proposal ${blockNumber} at slot ${slot} transaction re-execution complete`, {
503
600
  numFailedTxs,
504
601
  numProposalTxs: txHashes.length,
505
602
  numProcessedTxs: block.body.txEffects.length,
603
+ blockNumber,
506
604
  slot
507
605
  });
508
606
  if (numFailedTxs > 0) {
@@ -540,7 +638,219 @@ export class BlockProposalHandler {
540
638
  env.error = e;
541
639
  env.hasError = true;
542
640
  } finally{
543
- _ts_dispose_resources(env);
641
+ const result = _ts_dispose_resources(env);
642
+ if (result) await result;
643
+ }
644
+ }
645
+ /**
646
+ * Validates a checkpoint proposal and uploads blobs if configured.
647
+ * Used by both non-validator nodes (via register) and the validator client (via delegation).
648
+ */ async handleCheckpointProposal(proposal, proposalInfo) {
649
+ const proposer = proposal.getSender();
650
+ if (!proposer) {
651
+ this.log.warn(`Received checkpoint proposal with invalid signature for slot ${proposal.slotNumber}`);
652
+ return {
653
+ isValid: false,
654
+ reason: 'invalid_signature'
655
+ };
656
+ }
657
+ if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
658
+ this.log.warn(`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${proposal.slotNumber}`);
659
+ return {
660
+ isValid: false,
661
+ reason: 'invalid_fee_asset_price_modifier'
662
+ };
663
+ }
664
+ const result = await this.validateCheckpointProposal(proposal, proposalInfo);
665
+ // Upload blobs to filestore if validation passed (fire and forget)
666
+ if (result.isValid) {
667
+ this.tryUploadBlobsForCheckpoint(proposal, proposalInfo);
668
+ }
669
+ return result;
670
+ }
671
+ /**
672
+ * Validates a checkpoint proposal by building the full checkpoint and comparing it with the proposal.
673
+ * @returns Validation result with isValid flag and reason if invalid.
674
+ */ async validateCheckpointProposal(proposal, proposalInfo) {
675
+ const slot = proposal.slotNumber;
676
+ // Timeout block syncing at the start of the next slot
677
+ const config = this.checkpointsBuilder.getConfig();
678
+ const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
679
+ const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
680
+ // Wait for last block to sync by archive
681
+ let lastBlockHeader;
682
+ try {
683
+ lastBlockHeader = await retryUntil(async ()=>{
684
+ await this.blockSource.syncImmediate();
685
+ return this.blockSource.getBlockHeaderByArchive(proposal.archive);
686
+ }, `waiting for block with archive ${proposal.archive.toString()} for slot ${slot}`, timeoutSeconds, 0.5);
687
+ } catch (err) {
688
+ if (err instanceof TimeoutError) {
689
+ this.log.warn(`Timed out waiting for block with archive matching checkpoint proposal`, proposalInfo);
690
+ return {
691
+ isValid: false,
692
+ reason: 'last_block_not_found'
693
+ };
694
+ }
695
+ this.log.error(`Error fetching last block for checkpoint proposal`, err, proposalInfo);
696
+ return {
697
+ isValid: false,
698
+ reason: 'block_fetch_error'
699
+ };
700
+ }
701
+ if (!lastBlockHeader) {
702
+ this.log.warn(`Last block not found for checkpoint proposal`, proposalInfo);
703
+ return {
704
+ isValid: false,
705
+ reason: 'last_block_not_found'
706
+ };
707
+ }
708
+ // Get all full blocks for the slot and checkpoint
709
+ const blocks = await this.blockSource.getBlocksForSlot(slot);
710
+ if (blocks.length === 0) {
711
+ this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
712
+ return {
713
+ isValid: false,
714
+ reason: 'no_blocks_for_slot'
715
+ };
716
+ }
717
+ // Ensure the last block for this slot matches the archive in the checkpoint proposal
718
+ if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
719
+ this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
720
+ return {
721
+ isValid: false,
722
+ reason: 'last_block_archive_mismatch'
723
+ };
724
+ }
725
+ this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
726
+ ...proposalInfo,
727
+ blockNumbers: blocks.map((b)=>b.number)
728
+ });
729
+ // Get checkpoint constants from first block
730
+ const firstBlock = blocks[0];
731
+ const constants = this.extractCheckpointConstants(firstBlock);
732
+ const checkpointNumber = firstBlock.checkpointNumber;
733
+ // Get L1-to-L2 messages for this checkpoint
734
+ const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
735
+ // Collect the out hashes of all the checkpoints before this one in the same epoch
736
+ const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
737
+ const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
738
+ // Fork world state at the block before the first block
739
+ const parentBlockNumber = BlockNumber(firstBlock.number - 1);
740
+ const fork = await this.worldState.fork(parentBlockNumber);
741
+ try {
742
+ // Create checkpoint builder with all existing blocks
743
+ const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, proposal.feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
744
+ // Complete the checkpoint to get computed values
745
+ const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
746
+ // Compare checkpoint header with proposal
747
+ if (!computedCheckpoint.header.equals(proposal.checkpointHeader)) {
748
+ this.log.warn(`Checkpoint header mismatch`, {
749
+ ...proposalInfo,
750
+ computed: computedCheckpoint.header.toInspect(),
751
+ proposal: proposal.checkpointHeader.toInspect()
752
+ });
753
+ return {
754
+ isValid: false,
755
+ reason: 'checkpoint_header_mismatch'
756
+ };
757
+ }
758
+ // Compare archive root with proposal
759
+ if (!computedCheckpoint.archive.root.equals(proposal.archive)) {
760
+ this.log.warn(`Archive root mismatch`, {
761
+ ...proposalInfo,
762
+ computed: computedCheckpoint.archive.root.toString(),
763
+ proposal: proposal.archive.toString()
764
+ });
765
+ return {
766
+ isValid: false,
767
+ reason: 'archive_mismatch'
768
+ };
769
+ }
770
+ // Check that the accumulated epoch out hash matches the value in the proposal.
771
+ // The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
772
+ const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
773
+ const computedEpochOutHash = accumulateCheckpointOutHashes([
774
+ ...previousCheckpointOutHashes,
775
+ checkpointOutHash
776
+ ]);
777
+ const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
778
+ if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
779
+ this.log.warn(`Epoch out hash mismatch`, {
780
+ proposalEpochOutHash: proposalEpochOutHash.toString(),
781
+ computedEpochOutHash: computedEpochOutHash.toString(),
782
+ checkpointOutHash: checkpointOutHash.toString(),
783
+ previousCheckpointOutHashes: previousCheckpointOutHashes.map((h)=>h.toString()),
784
+ ...proposalInfo
785
+ });
786
+ return {
787
+ isValid: false,
788
+ reason: 'out_hash_mismatch'
789
+ };
790
+ }
791
+ // Final round of validations on the checkpoint, just in case.
792
+ try {
793
+ validateCheckpoint(computedCheckpoint, {
794
+ rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
795
+ maxDABlockGas: this.config.validateMaxDABlockGas,
796
+ maxL2BlockGas: this.config.validateMaxL2BlockGas,
797
+ maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
798
+ maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint
799
+ });
800
+ } catch (err) {
801
+ this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
802
+ return {
803
+ isValid: false,
804
+ reason: 'checkpoint_validation_failed'
805
+ };
806
+ }
807
+ this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
808
+ return {
809
+ isValid: true
810
+ };
811
+ } finally{
812
+ await fork.close();
813
+ }
814
+ }
815
+ /** Extracts checkpoint global variables from a block. */ extractCheckpointConstants(block) {
816
+ const gv = block.header.globalVariables;
817
+ return {
818
+ chainId: gv.chainId,
819
+ version: gv.version,
820
+ slotNumber: gv.slotNumber,
821
+ timestamp: gv.timestamp,
822
+ coinbase: gv.coinbase,
823
+ feeRecipient: gv.feeRecipient,
824
+ gasFees: gv.gasFees
825
+ };
826
+ }
827
+ /** Triggers blob upload for a checkpoint if the blob client can upload (fire and forget). */ tryUploadBlobsForCheckpoint(proposal, proposalInfo) {
828
+ if (this.blobClient.canUpload()) {
829
+ void this.uploadBlobsForCheckpoint(proposal, proposalInfo);
830
+ }
831
+ }
832
+ /** Uploads blobs for a checkpoint to the filestore. */ async uploadBlobsForCheckpoint(proposal, proposalInfo) {
833
+ try {
834
+ const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
835
+ if (!lastBlockHeader) {
836
+ this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
837
+ return;
838
+ }
839
+ const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
840
+ if (blocks.length === 0) {
841
+ this.log.warn(`No blocks found for blob upload`, proposalInfo);
842
+ return;
843
+ }
844
+ const blockBlobData = blocks.map((b)=>b.toBlockBlobData());
845
+ const blobFields = encodeCheckpointBlobDataFromBlocks(blockBlobData);
846
+ const blobs = await getBlobsPerL1Block(blobFields);
847
+ await this.blobClient.sendBlobsToFilestore(blobs);
848
+ this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
849
+ ...proposalInfo,
850
+ numBlobs: blobs.length
851
+ });
852
+ } catch (err) {
853
+ this.log.warn(`Failed to upload blobs for checkpoint: ${err}`, proposalInfo);
544
854
  }
545
855
  }
546
856
  }