@aztec/validator-client 0.0.1-commit.fffb133c → 0.0.1-dev

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 +62 -18
  2. package/dest/checkpoint_builder.d.ts +26 -14
  3. package/dest/checkpoint_builder.d.ts.map +1 -1
  4. package/dest/checkpoint_builder.js +136 -45
  5. package/dest/config.d.ts +1 -1
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +26 -3
  8. package/dest/duties/validation_service.d.ts +3 -4
  9. package/dest/duties/validation_service.d.ts.map +1 -1
  10. package/dest/duties/validation_service.js +11 -22
  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.d.ts +1 -1
  18. package/dest/key_store/ha_key_store.d.ts.map +1 -1
  19. package/dest/key_store/ha_key_store.js +3 -3
  20. package/dest/metrics.d.ts +12 -3
  21. package/dest/metrics.d.ts.map +1 -1
  22. package/dest/metrics.js +46 -5
  23. package/dest/proposal_handler.d.ts +94 -0
  24. package/dest/proposal_handler.d.ts.map +1 -0
  25. package/dest/{block_proposal_handler.js → proposal_handler.js} +376 -69
  26. package/dest/validator.d.ts +37 -24
  27. package/dest/validator.d.ts.map +1 -1
  28. package/dest/validator.js +167 -215
  29. package/package.json +19 -17
  30. package/src/checkpoint_builder.ts +183 -50
  31. package/src/config.ts +26 -3
  32. package/src/duties/validation_service.ts +18 -24
  33. package/src/factory.ts +9 -3
  34. package/src/index.ts +1 -2
  35. package/src/key_store/ha_key_store.ts +3 -3
  36. package/src/metrics.ts +63 -6
  37. package/src/proposal_handler.ts +903 -0
  38. package/src/validator.ts +228 -248
  39. package/dest/block_proposal_handler.d.ts +0 -64
  40. package/dest/block_proposal_handler.d.ts.map +0 -1
  41. package/dest/tx_validator/index.d.ts +0 -3
  42. package/dest/tx_validator/index.d.ts.map +0 -1
  43. package/dest/tx_validator/index.js +0 -2
  44. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  45. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  46. package/dest/tx_validator/nullifier_cache.js +0 -24
  47. package/dest/tx_validator/tx_validator_factory.d.ts +0 -18
  48. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  49. package/dest/tx_validator/tx_validator_factory.js +0 -54
  50. package/src/block_proposal_handler.ts +0 -554
  51. package/src/tx_validator/index.ts +0 -2
  52. package/src/tx_validator/nullifier_cache.ts +0 -30
  53. package/src/tx_validator/tx_validator_factory.ts +0 -135
@@ -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,19 +203,34 @@ 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
+ // TODO(@Maddiaa0): This may break staggered slots.
211
+ const blockSourceSync = await this.waitForBlockSourceSync(slotNumber);
212
+ if (!blockSourceSync) {
213
+ this.log.warn(`Block source is not synced, skipping processing`, proposalInfo);
214
+ return {
215
+ isValid: false,
216
+ reason: 'block_source_not_synced'
217
+ };
218
+ }
219
+ // Check that the parent proposal is a block we know, otherwise reexecution would fail.
220
+ // If we don't find it immediately, we keep retrying for a while; it may be we still
221
+ // need to process other block proposals to get to it.
222
+ const parentBlock = await this.getParentBlock(proposal);
223
+ if (parentBlock === undefined) {
170
224
  this.log.warn(`Parent block for proposal not found, skipping processing`, proposalInfo);
171
225
  return {
172
226
  isValid: false,
173
227
  reason: 'parent_block_not_found'
174
228
  };
175
229
  }
176
- // Check that the parent block's slot is less than the proposal's slot (should not happen, but we check anyway)
177
- if (parentBlockHeader !== 'genesis' && parentBlockHeader.getSlot() >= slotNumber) {
178
- this.log.warn(`Parent block slot is greater than or equal to proposal slot, skipping processing`, {
179
- parentBlockSlot: parentBlockHeader.getSlot().toString(),
230
+ // Check that the parent block's slot is not greater than the proposal's slot.
231
+ if (parentBlock !== 'genesis' && parentBlock.header.getSlot() > slotNumber) {
232
+ this.log.warn(`Parent block slot is greater than proposal slot, skipping processing`, {
233
+ parentBlockSlot: parentBlock.header.getSlot().toString(),
180
234
  proposalSlot: slotNumber.toString(),
181
235
  ...proposalInfo
182
236
  });
@@ -186,7 +240,8 @@ export class BlockProposalHandler {
186
240
  };
187
241
  }
188
242
  // Compute the block number based on the parent block
189
- const blockNumber = parentBlockHeader === 'genesis' ? BlockNumber(INITIAL_L2_BLOCK_NUM) : BlockNumber(parentBlockHeader.getBlockNumber() + 1);
243
+ const blockNumber = parentBlock === 'genesis' ? BlockNumber(INITIAL_L2_BLOCK_NUM) : BlockNumber(parentBlock.header.getBlockNumber() + 1);
244
+ proposalInfo.blockNumber = blockNumber;
190
245
  // Check that this block number does not exist already
191
246
  const existingBlock = await this.blockSource.getBlockHeader(blockNumber);
192
247
  if (existingBlock) {
@@ -203,8 +258,16 @@ export class BlockProposalHandler {
203
258
  pinnedPeer: proposalSender,
204
259
  deadline: this.getReexecutionDeadline(slotNumber, config)
205
260
  });
261
+ // If reexecution is disabled, bail. We were just interested in triggering tx collection.
262
+ if (!shouldReexecute) {
263
+ this.log.info(`Received valid block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`, proposalInfo);
264
+ return {
265
+ isValid: true,
266
+ blockNumber
267
+ };
268
+ }
206
269
  // Compute the checkpoint number for this block and validate checkpoint consistency
207
- const checkpointResult = await this.computeCheckpointNumber(proposal, parentBlockHeader, proposalInfo);
270
+ const checkpointResult = this.computeCheckpointNumber(proposal, parentBlock, proposalInfo);
208
271
  if (checkpointResult.reason) {
209
272
  return {
210
273
  isValid: false,
@@ -213,6 +276,7 @@ export class BlockProposalHandler {
213
276
  };
214
277
  }
215
278
  const checkpointNumber = checkpointResult.checkpointNumber;
279
+ proposalInfo.checkpointNumber = checkpointNumber;
216
280
  // Check that I have the same set of l1ToL2Messages as the proposal
217
281
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
218
282
  const computedInHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
@@ -241,35 +305,32 @@ export class BlockProposalHandler {
241
305
  reason: 'txs_not_available'
242
306
  };
243
307
  }
308
+ // Collect the out hashes of all the checkpoints before this one in the same epoch
309
+ const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
310
+ const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
244
311
  // Try re-executing the transactions in the proposal if needed
245
312
  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
- }
313
+ try {
314
+ this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
315
+ reexecutionResult = await this.reexecuteTransactions(proposal, blockNumber, checkpointNumber, txs, l1ToL2Messages, previousCheckpointOutHashes);
316
+ } catch (error) {
317
+ this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
318
+ const reason = this.getReexecuteFailureReason(error);
319
+ return {
320
+ isValid: false,
321
+ blockNumber,
322
+ reason,
323
+ reexecutionResult
324
+ };
267
325
  }
268
326
  // If we succeeded, push this block into the archiver (unless disabled)
269
327
  if (reexecutionResult?.block && this.config.skipPushProposedBlocksToArchiver === false) {
270
328
  await this.blockSource.addBlock(reexecutionResult?.block);
271
329
  }
272
- this.log.info(`Successfully processed block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`, proposalInfo);
330
+ this.log.info(`Successfully re-executed block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`, {
331
+ ...proposalInfo,
332
+ ...pick(reexecutionResult, 'reexecutionTimeMs', 'totalManaUsed')
333
+ });
273
334
  return {
274
335
  isValid: true,
275
336
  blockNumber,
@@ -288,7 +349,7 @@ export class BlockProposalHandler {
288
349
  const currentTime = this.dateProvider.now();
289
350
  const timeoutDurationMs = deadline.getTime() - currentTime;
290
351
  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));
352
+ 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
353
  } catch (err) {
293
354
  if (err instanceof TimeoutError) {
294
355
  this.log.debug(`Timed out getting parent block by archive root`, {
@@ -302,8 +363,8 @@ export class BlockProposalHandler {
302
363
  return undefined;
303
364
  }
304
365
  }
305
- async computeCheckpointNumber(proposal, parentBlockHeader, proposalInfo) {
306
- if (parentBlockHeader === 'genesis') {
366
+ computeCheckpointNumber(proposal, parentBlock, proposalInfo) {
367
+ if (parentBlock === 'genesis') {
307
368
  // First block is in checkpoint 1
308
369
  if (proposal.indexWithinCheckpoint !== 0) {
309
370
  this.log.warn(`First block proposal has non-zero indexWithinCheckpoint`, proposalInfo);
@@ -315,20 +376,9 @@ export class BlockProposalHandler {
315
376
  checkpointNumber: CheckpointNumber.INITIAL
316
377
  };
317
378
  }
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
379
  if (proposal.indexWithinCheckpoint === 0) {
330
380
  // If this is the first block in a new checkpoint, increment the checkpoint number
331
- if (!(proposal.blockHeader.getSlot() > parentBlockHeader.getSlot())) {
381
+ if (!(proposal.blockHeader.getSlot() > parentBlock.header.getSlot())) {
332
382
  this.log.warn(`Slot should be greater than parent block slot for first block in checkpoint`, proposalInfo);
333
383
  return {
334
384
  reason: 'invalid_proposal'
@@ -345,7 +395,7 @@ export class BlockProposalHandler {
345
395
  reason: 'invalid_proposal'
346
396
  };
347
397
  }
348
- if (proposal.blockHeader.getSlot() !== parentBlockHeader.getSlot()) {
398
+ if (proposal.blockHeader.getSlot() !== parentBlock.header.getSlot()) {
349
399
  this.log.warn(`Slot should be equal to parent block slot for non-first block in checkpoint`, proposalInfo);
350
400
  return {
351
401
  reason: 'invalid_proposal'
@@ -445,8 +495,39 @@ export class BlockProposalHandler {
445
495
  const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
446
496
  return new Date(nextSlotTimestampSeconds * 1000);
447
497
  }
498
+ /** Waits for the block source to sync L1 data up to at least the slot before the given one. */ async waitForBlockSourceSync(slot) {
499
+ const deadline = this.getReexecutionDeadline(slot, this.checkpointsBuilder.getConfig());
500
+ const timeoutMs = deadline.getTime() - this.dateProvider.now();
501
+ if (slot === 0) {
502
+ return true;
503
+ }
504
+ // Make a quick check before triggering an archiver sync
505
+ const syncedSlot = await this.blockSource.getSyncedL2SlotNumber();
506
+ if (syncedSlot !== undefined && syncedSlot + 1 >= slot) {
507
+ return true;
508
+ }
509
+ try {
510
+ // Trigger an immediate sync of the block source, and wait until it reports being synced to the required slot
511
+ return await retryUntil(async ()=>{
512
+ await this.blockSource.syncImmediate();
513
+ const syncedSlot = await this.blockSource.getSyncedL2SlotNumber();
514
+ return syncedSlot !== undefined && syncedSlot + 1 >= slot;
515
+ }, 'wait for block source sync', timeoutMs / 1000, 0.5);
516
+ } catch (err) {
517
+ if (err instanceof TimeoutError) {
518
+ this.log.warn(`Timed out waiting for block source to sync to slot ${slot}`);
519
+ return false;
520
+ } else {
521
+ throw err;
522
+ }
523
+ }
524
+ }
448
525
  getReexecuteFailureReason(err) {
449
- if (err instanceof ReExStateMismatchError) {
526
+ if (err instanceof TransactionsNotAvailableError) {
527
+ return 'txs_not_available';
528
+ } else if (err instanceof ReExInitialStateMismatchError) {
529
+ return 'initial_state_mismatch';
530
+ } else if (err instanceof ReExStateMismatchError) {
450
531
  return 'state_mismatch';
451
532
  } else if (err instanceof ReExFailedTxsError) {
452
533
  return 'failed_txs';
@@ -478,30 +559,44 @@ export class BlockProposalHandler {
478
559
  const priorBlocks = allBlocksInSlot.filter((b)=>b.number < blockNumber && b.header.getSlot() === slot);
479
560
  // Fork before the block to be built
480
561
  const parentBlockNumber = BlockNumber(blockNumber - 1);
481
- const fork = _ts_add_disposable_resource(env, await this.worldState.fork(parentBlockNumber), false);
482
- // Build checkpoint constants from proposal (excludes blockNumber and timestamp which are per-block)
562
+ await this.worldState.syncImmediate(parentBlockNumber);
563
+ const fork = _ts_add_disposable_resource(env, await this.worldState.fork(parentBlockNumber), true);
564
+ // Verify the fork's archive root matches the proposal's expected last archive.
565
+ // If they don't match, our world state synced to a different chain and reexecution would fail.
566
+ const forkArchiveRoot = new Fr((await fork.getTreeInfo(MerkleTreeId.ARCHIVE)).root);
567
+ if (!forkArchiveRoot.equals(proposal.blockHeader.lastArchive.root)) {
568
+ throw new ReExInitialStateMismatchError(proposal.blockHeader.lastArchive.root, forkArchiveRoot);
569
+ }
570
+ // Build checkpoint constants from proposal (excludes blockNumber which is per-block)
483
571
  const constants = {
484
572
  chainId: new Fr(config.l1ChainId),
485
573
  version: new Fr(config.rollupVersion),
486
574
  slotNumber: slot,
575
+ timestamp: blockHeader.globalVariables.timestamp,
487
576
  coinbase: blockHeader.globalVariables.coinbase,
488
577
  feeRecipient: blockHeader.globalVariables.feeRecipient,
489
578
  gasFees: blockHeader.globalVariables.gasFees
490
579
  };
491
580
  // Create checkpoint builder with prior blocks
492
- const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, priorBlocks);
581
+ const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, 0n, l1ToL2Messages, previousCheckpointOutHashes, fork, priorBlocks, this.log.getBindings());
493
582
  // Build the new block
494
583
  const deadline = this.getReexecutionDeadline(slot, config);
584
+ const maxBlockGas = this.config.validateMaxL2BlockGas !== undefined || this.config.validateMaxDABlockGas !== undefined ? new Gas(this.config.validateMaxDABlockGas ?? Infinity, this.config.validateMaxL2BlockGas ?? Infinity) : undefined;
495
585
  const result = await checkpointBuilder.buildBlock(txs, blockNumber, blockHeader.globalVariables.timestamp, {
586
+ isBuildingProposal: false,
587
+ minValidTxs: 0,
496
588
  deadline,
497
- expectedEndState: blockHeader.state
589
+ expectedEndState: blockHeader.state,
590
+ maxTransactions: this.config.validateMaxTxsPerBlock,
591
+ maxBlockGas
498
592
  });
499
593
  const { block, failedTxs } = result;
500
594
  const numFailedTxs = failedTxs.length;
501
- this.log.verbose(`Transaction re-execution complete for slot ${slot}`, {
595
+ this.log.verbose(`Block proposal ${blockNumber} at slot ${slot} transaction re-execution complete`, {
502
596
  numFailedTxs,
503
597
  numProposalTxs: txHashes.length,
504
598
  numProcessedTxs: block.body.txEffects.length,
599
+ blockNumber,
505
600
  slot
506
601
  });
507
602
  if (numFailedTxs > 0) {
@@ -539,7 +634,219 @@ export class BlockProposalHandler {
539
634
  env.error = e;
540
635
  env.hasError = true;
541
636
  } finally{
542
- _ts_dispose_resources(env);
637
+ const result = _ts_dispose_resources(env);
638
+ if (result) await result;
639
+ }
640
+ }
641
+ /**
642
+ * Validates a checkpoint proposal and uploads blobs if configured.
643
+ * Used by both non-validator nodes (via register) and the validator client (via delegation).
644
+ */ async handleCheckpointProposal(proposal, proposalInfo) {
645
+ const proposer = proposal.getSender();
646
+ if (!proposer) {
647
+ this.log.warn(`Received checkpoint proposal with invalid signature for slot ${proposal.slotNumber}`);
648
+ return {
649
+ isValid: false,
650
+ reason: 'invalid_signature'
651
+ };
652
+ }
653
+ if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
654
+ this.log.warn(`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${proposal.slotNumber}`);
655
+ return {
656
+ isValid: false,
657
+ reason: 'invalid_fee_asset_price_modifier'
658
+ };
659
+ }
660
+ const result = await this.validateCheckpointProposal(proposal, proposalInfo);
661
+ // Upload blobs to filestore if validation passed (fire and forget)
662
+ if (result.isValid) {
663
+ this.tryUploadBlobsForCheckpoint(proposal, proposalInfo);
664
+ }
665
+ return result;
666
+ }
667
+ /**
668
+ * Validates a checkpoint proposal by building the full checkpoint and comparing it with the proposal.
669
+ * @returns Validation result with isValid flag and reason if invalid.
670
+ */ async validateCheckpointProposal(proposal, proposalInfo) {
671
+ const slot = proposal.slotNumber;
672
+ // Timeout block syncing at the start of the next slot
673
+ const config = this.checkpointsBuilder.getConfig();
674
+ const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
675
+ const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
676
+ // Wait for last block to sync by archive
677
+ let lastBlockHeader;
678
+ try {
679
+ lastBlockHeader = await retryUntil(async ()=>{
680
+ await this.blockSource.syncImmediate();
681
+ return this.blockSource.getBlockHeaderByArchive(proposal.archive);
682
+ }, `waiting for block with archive ${proposal.archive.toString()} for slot ${slot}`, timeoutSeconds, 0.5);
683
+ } catch (err) {
684
+ if (err instanceof TimeoutError) {
685
+ this.log.warn(`Timed out waiting for block with archive matching checkpoint proposal`, proposalInfo);
686
+ return {
687
+ isValid: false,
688
+ reason: 'last_block_not_found'
689
+ };
690
+ }
691
+ this.log.error(`Error fetching last block for checkpoint proposal`, err, proposalInfo);
692
+ return {
693
+ isValid: false,
694
+ reason: 'block_fetch_error'
695
+ };
696
+ }
697
+ if (!lastBlockHeader) {
698
+ this.log.warn(`Last block not found for checkpoint proposal`, proposalInfo);
699
+ return {
700
+ isValid: false,
701
+ reason: 'last_block_not_found'
702
+ };
703
+ }
704
+ // Get all full blocks for the slot and checkpoint
705
+ const blocks = await this.blockSource.getBlocksForSlot(slot);
706
+ if (blocks.length === 0) {
707
+ this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
708
+ return {
709
+ isValid: false,
710
+ reason: 'no_blocks_for_slot'
711
+ };
712
+ }
713
+ // Ensure the last block for this slot matches the archive in the checkpoint proposal
714
+ if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
715
+ this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
716
+ return {
717
+ isValid: false,
718
+ reason: 'last_block_archive_mismatch'
719
+ };
720
+ }
721
+ this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
722
+ ...proposalInfo,
723
+ blockNumbers: blocks.map((b)=>b.number)
724
+ });
725
+ // Get checkpoint constants from first block
726
+ const firstBlock = blocks[0];
727
+ const constants = this.extractCheckpointConstants(firstBlock);
728
+ const checkpointNumber = firstBlock.checkpointNumber;
729
+ // Get L1-to-L2 messages for this checkpoint
730
+ const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
731
+ // Collect the out hashes of all the checkpoints before this one in the same epoch
732
+ const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
733
+ const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
734
+ // Fork world state at the block before the first block
735
+ const parentBlockNumber = BlockNumber(firstBlock.number - 1);
736
+ const fork = await this.worldState.fork(parentBlockNumber);
737
+ try {
738
+ // Create checkpoint builder with all existing blocks
739
+ const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, proposal.feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
740
+ // Complete the checkpoint to get computed values
741
+ const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
742
+ // Compare checkpoint header with proposal
743
+ if (!computedCheckpoint.header.equals(proposal.checkpointHeader)) {
744
+ this.log.warn(`Checkpoint header mismatch`, {
745
+ ...proposalInfo,
746
+ computed: computedCheckpoint.header.toInspect(),
747
+ proposal: proposal.checkpointHeader.toInspect()
748
+ });
749
+ return {
750
+ isValid: false,
751
+ reason: 'checkpoint_header_mismatch'
752
+ };
753
+ }
754
+ // Compare archive root with proposal
755
+ if (!computedCheckpoint.archive.root.equals(proposal.archive)) {
756
+ this.log.warn(`Archive root mismatch`, {
757
+ ...proposalInfo,
758
+ computed: computedCheckpoint.archive.root.toString(),
759
+ proposal: proposal.archive.toString()
760
+ });
761
+ return {
762
+ isValid: false,
763
+ reason: 'archive_mismatch'
764
+ };
765
+ }
766
+ // Check that the accumulated epoch out hash matches the value in the proposal.
767
+ // The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
768
+ const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
769
+ const computedEpochOutHash = accumulateCheckpointOutHashes([
770
+ ...previousCheckpointOutHashes,
771
+ checkpointOutHash
772
+ ]);
773
+ const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
774
+ if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
775
+ this.log.warn(`Epoch out hash mismatch`, {
776
+ proposalEpochOutHash: proposalEpochOutHash.toString(),
777
+ computedEpochOutHash: computedEpochOutHash.toString(),
778
+ checkpointOutHash: checkpointOutHash.toString(),
779
+ previousCheckpointOutHashes: previousCheckpointOutHashes.map((h)=>h.toString()),
780
+ ...proposalInfo
781
+ });
782
+ return {
783
+ isValid: false,
784
+ reason: 'out_hash_mismatch'
785
+ };
786
+ }
787
+ // Final round of validations on the checkpoint, just in case.
788
+ try {
789
+ validateCheckpoint(computedCheckpoint, {
790
+ rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
791
+ maxDABlockGas: this.config.validateMaxDABlockGas,
792
+ maxL2BlockGas: this.config.validateMaxL2BlockGas,
793
+ maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
794
+ maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint
795
+ });
796
+ } catch (err) {
797
+ this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
798
+ return {
799
+ isValid: false,
800
+ reason: 'checkpoint_validation_failed'
801
+ };
802
+ }
803
+ this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
804
+ return {
805
+ isValid: true
806
+ };
807
+ } finally{
808
+ await fork.close();
809
+ }
810
+ }
811
+ /** Extracts checkpoint global variables from a block. */ extractCheckpointConstants(block) {
812
+ const gv = block.header.globalVariables;
813
+ return {
814
+ chainId: gv.chainId,
815
+ version: gv.version,
816
+ slotNumber: gv.slotNumber,
817
+ timestamp: gv.timestamp,
818
+ coinbase: gv.coinbase,
819
+ feeRecipient: gv.feeRecipient,
820
+ gasFees: gv.gasFees
821
+ };
822
+ }
823
+ /** Triggers blob upload for a checkpoint if the blob client can upload (fire and forget). */ tryUploadBlobsForCheckpoint(proposal, proposalInfo) {
824
+ if (this.blobClient.canUpload()) {
825
+ void this.uploadBlobsForCheckpoint(proposal, proposalInfo);
826
+ }
827
+ }
828
+ /** Uploads blobs for a checkpoint to the filestore. */ async uploadBlobsForCheckpoint(proposal, proposalInfo) {
829
+ try {
830
+ const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
831
+ if (!lastBlockHeader) {
832
+ this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
833
+ return;
834
+ }
835
+ const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
836
+ if (blocks.length === 0) {
837
+ this.log.warn(`No blocks found for blob upload`, proposalInfo);
838
+ return;
839
+ }
840
+ const blockBlobData = blocks.map((b)=>b.toBlockBlobData());
841
+ const blobFields = encodeCheckpointBlobDataFromBlocks(blockBlobData);
842
+ const blobs = await getBlobsPerL1Block(blobFields);
843
+ await this.blobClient.sendBlobsToFilestore(blobs);
844
+ this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
845
+ ...proposalInfo,
846
+ numBlobs: blobs.length
847
+ });
848
+ } catch (err) {
849
+ this.log.warn(`Failed to upload blobs for checkpoint: ${err}`, proposalInfo);
543
850
  }
544
851
  }
545
852
  }