@aztec/sequencer-client 0.0.1-commit.6c91f13 → 0.0.1-commit.6d63667d

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 (72) hide show
  1. package/dest/client/sequencer-client.d.ts +4 -5
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +1 -1
  4. package/dest/config.d.ts +1 -1
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +6 -1
  7. package/dest/global_variable_builder/global_builder.d.ts +4 -4
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +13 -13
  10. package/dest/index.d.ts +2 -3
  11. package/dest/index.d.ts.map +1 -1
  12. package/dest/index.js +1 -2
  13. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  14. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  15. package/dest/publisher/sequencer-publisher-metrics.js +23 -86
  16. package/dest/publisher/sequencer-publisher.d.ts +17 -17
  17. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  18. package/dest/publisher/sequencer-publisher.js +481 -67
  19. package/dest/sequencer/checkpoint_proposal_job.d.ts +40 -12
  20. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  21. package/dest/sequencer/checkpoint_proposal_job.js +605 -56
  22. package/dest/sequencer/checkpoint_voter.d.ts +3 -2
  23. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  24. package/dest/sequencer/checkpoint_voter.js +34 -10
  25. package/dest/sequencer/index.d.ts +1 -3
  26. package/dest/sequencer/index.d.ts.map +1 -1
  27. package/dest/sequencer/index.js +0 -2
  28. package/dest/sequencer/metrics.d.ts +4 -4
  29. package/dest/sequencer/metrics.d.ts.map +1 -1
  30. package/dest/sequencer/metrics.js +48 -129
  31. package/dest/sequencer/sequencer.d.ts +27 -15
  32. package/dest/sequencer/sequencer.d.ts.map +1 -1
  33. package/dest/sequencer/sequencer.js +491 -43
  34. package/dest/test/index.d.ts +2 -3
  35. package/dest/test/index.d.ts.map +1 -1
  36. package/dest/test/mock_checkpoint_builder.d.ts +23 -11
  37. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  38. package/dest/test/mock_checkpoint_builder.js +50 -9
  39. package/dest/test/utils.d.ts +13 -9
  40. package/dest/test/utils.d.ts.map +1 -1
  41. package/dest/test/utils.js +26 -17
  42. package/package.json +30 -28
  43. package/src/client/sequencer-client.ts +4 -5
  44. package/src/config.ts +5 -0
  45. package/src/global_variable_builder/global_builder.ts +13 -13
  46. package/src/index.ts +1 -9
  47. package/src/publisher/sequencer-publisher-metrics.ts +17 -69
  48. package/src/publisher/sequencer-publisher.ts +118 -91
  49. package/src/sequencer/checkpoint_proposal_job.ts +257 -86
  50. package/src/sequencer/checkpoint_voter.ts +32 -7
  51. package/src/sequencer/index.ts +0 -2
  52. package/src/sequencer/metrics.ts +48 -138
  53. package/src/sequencer/sequencer.ts +132 -43
  54. package/src/test/index.ts +1 -2
  55. package/src/test/mock_checkpoint_builder.ts +91 -29
  56. package/src/test/utils.ts +55 -28
  57. package/dest/sequencer/block_builder.d.ts +0 -26
  58. package/dest/sequencer/block_builder.d.ts.map +0 -1
  59. package/dest/sequencer/block_builder.js +0 -129
  60. package/dest/sequencer/checkpoint_builder.d.ts +0 -63
  61. package/dest/sequencer/checkpoint_builder.d.ts.map +0 -1
  62. package/dest/sequencer/checkpoint_builder.js +0 -131
  63. package/dest/tx_validator/nullifier_cache.d.ts +0 -14
  64. package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
  65. package/dest/tx_validator/nullifier_cache.js +0 -24
  66. package/dest/tx_validator/tx_validator_factory.d.ts +0 -18
  67. package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
  68. package/dest/tx_validator/tx_validator_factory.js +0 -53
  69. package/src/sequencer/block_builder.ts +0 -217
  70. package/src/sequencer/checkpoint_builder.ts +0 -217
  71. package/src/tx_validator/nullifier_cache.ts +0 -30
  72. package/src/tx_validator/tx_validator_factory.ts +0 -133
@@ -12,7 +12,7 @@ import type { DateProvider } from '@aztec/foundation/timer';
12
12
  import type { TypedEventEmitter } from '@aztec/foundation/types';
13
13
  import type { P2P } from '@aztec/p2p';
14
14
  import type { SlasherClientInterface } from '@aztec/slasher';
15
- import type { L2BlockNew, L2BlockSource, ValidateBlockResult } from '@aztec/stdlib/block';
15
+ import type { L2Block, L2BlockSink, L2BlockSource, ValidateCheckpointResult } from '@aztec/stdlib/block';
16
16
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
17
17
  import { getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
18
18
  import {
@@ -24,16 +24,15 @@ import {
24
24
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
25
25
  import { pickFromSchema } from '@aztec/stdlib/schemas';
26
26
  import { MerkleTreeId } from '@aztec/stdlib/trees';
27
- import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
28
- import type { ValidatorClient } from '@aztec/validator-client';
27
+ import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
28
+ import { FullNodeCheckpointsBuilder, type ValidatorClient } from '@aztec/validator-client';
29
29
 
30
30
  import EventEmitter from 'node:events';
31
31
 
32
32
  import { DefaultSequencerConfig } from '../config.js';
33
33
  import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
34
34
  import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
35
- import type { InvalidateBlockRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
36
- import { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
35
+ import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
37
36
  import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
38
37
  import { CheckpointVoter } from './checkpoint_voter.js';
39
38
  import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
@@ -58,8 +57,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
58
57
  private state = SequencerState.STOPPED;
59
58
  private metrics: SequencerMetrics;
60
59
 
61
- /** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */
62
- private lastSlotForVoteWhenSyncFailed: SlotNumber | undefined;
60
+ /** The last slot for which we attempted to perform our voting duties with degraded block production */
61
+ private lastSlotForFallbackVote: SlotNumber | undefined;
62
+
63
+ /** The last slot for which we logged "no committee" warning, to avoid spam */
64
+ private lastSlotForNoCommitteeWarning: SlotNumber | undefined;
63
65
 
64
66
  /** The last slot for which we triggered a checkpoint proposal job, to prevent duplicate attempts. */
65
67
  private lastSlotForCheckpointProposalJob: SlotNumber | undefined;
@@ -91,7 +93,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
91
93
  protected p2pClient: P2P,
92
94
  protected worldState: WorldStateSynchronizer,
93
95
  protected slasherClient: SlasherClientInterface | undefined,
94
- protected l2BlockSource: L2BlockSource,
96
+ protected l2BlockSource: L2BlockSource & L2BlockSink,
95
97
  protected l1ToL2MessageSource: L1ToL2MessageSource,
96
98
  protected checkpointsBuilder: FullNodeCheckpointsBuilder,
97
99
  protected l1Constants: SequencerRollupConstants,
@@ -160,7 +162,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
160
162
  this.log.info('Stopped sequencer');
161
163
  }
162
164
 
163
- @trackSpan('Sequencer.work')
164
165
  /** Main sequencer loop with a try/catch */
165
166
  protected async safeWork() {
166
167
  try {
@@ -198,12 +199,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
198
199
  * - Collect attestations for the final block
199
200
  * - Submit checkpoint
200
201
  */
202
+ @trackSpan('Sequencer.work')
201
203
  protected async work() {
202
204
  this.setState(SequencerState.SYNCHRONIZING, undefined);
203
205
  const { slot, ts, now, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
204
206
 
205
207
  // Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
206
- const checkpointProposalJob = await this.prepareCheckpointProposal(slot, ts, now);
208
+ const checkpointProposalJob = await this.prepareCheckpointProposal(epoch, slot, ts, now);
207
209
  if (!checkpointProposalJob) {
208
210
  return;
209
211
  }
@@ -233,7 +235,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
233
235
  * This is the initial step in the main loop.
234
236
  * @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
235
237
  */
238
+ @trackSpan('Sequencer.prepareCheckpointProposal')
236
239
  private async prepareCheckpointProposal(
240
+ epoch: EpochNumber,
237
241
  slot: SlotNumber,
238
242
  ts: bigint,
239
243
  now: bigint,
@@ -263,8 +267,27 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
263
267
  return undefined;
264
268
  }
265
269
 
266
- // TODO(palla/mbps): Compute proper checkpoint number
267
- const checkpointNumber = CheckpointNumber.fromBlockNumber(BlockNumber(syncedTo.blockNumber + 1));
270
+ // If escape hatch is open for this epoch, do not start checkpoint proposal work and do not attempt invalidations.
271
+ // Still perform governance/slashing voting (as proposer) once per slot.
272
+ const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(epoch);
273
+
274
+ if (isEscapeHatchOpen) {
275
+ this.setState(SequencerState.PROPOSER_CHECK, slot);
276
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
277
+ if (canPropose) {
278
+ await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
279
+ } else {
280
+ this.log.trace(`Escape hatch open but we are not proposer, skipping vote-only actions`, {
281
+ slot,
282
+ epoch,
283
+ proposer,
284
+ });
285
+ }
286
+ return undefined;
287
+ }
288
+
289
+ // Next checkpoint follows from the last synced one
290
+ const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
268
291
 
269
292
  const logCtx = {
270
293
  now,
@@ -280,9 +303,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
280
303
  this.setState(SequencerState.PROPOSER_CHECK, slot);
281
304
  const [canPropose, proposer] = await this.checkCanPropose(slot);
282
305
 
283
- // If we are not a proposer check if we should invalidate a invalid block, and bail
306
+ // If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
284
307
  if (!canPropose) {
285
- await this.considerInvalidatingBlock(syncedTo, slot);
308
+ await this.considerInvalidatingCheckpoint(syncedTo, slot);
286
309
  return undefined;
287
310
  }
288
311
 
@@ -312,15 +335,14 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
312
335
  }
313
336
 
314
337
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
315
- // TODO(palla/mbps): We need to invalidate checkpoints, not blocks
316
- const invalidateBlock = await publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
338
+ const invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
317
339
 
318
340
  // Check with the rollup contract if we can indeed propose at the next L2 slot. This check should not fail
319
341
  // if all the previous checks are good, but we do it just in case.
320
342
  const canProposeCheck = await publisher.canProposeAtNextEthBlock(
321
343
  syncedTo.archive,
322
344
  proposer ?? EthAddress.ZERO,
323
- invalidateBlock,
345
+ invalidateCheckpoint,
324
346
  );
325
347
 
326
348
  if (canProposeCheck === undefined) {
@@ -358,39 +380,44 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
358
380
 
359
381
  // Create and return the checkpoint proposal job
360
382
  return this.createCheckpointProposalJob(
383
+ epoch,
361
384
  slot,
362
385
  checkpointNumber,
363
386
  syncedTo.blockNumber,
364
387
  proposer,
365
388
  publisher,
366
389
  attestorAddress,
367
- invalidateBlock,
390
+ invalidateCheckpoint,
368
391
  );
369
392
  }
370
393
 
371
394
  protected createCheckpointProposalJob(
395
+ epoch: EpochNumber,
372
396
  slot: SlotNumber,
373
397
  checkpointNumber: CheckpointNumber,
374
398
  syncedToBlockNumber: BlockNumber,
375
399
  proposer: EthAddress | undefined,
376
400
  publisher: SequencerPublisher,
377
401
  attestorAddress: EthAddress,
378
- invalidateBlock: InvalidateBlockRequest | undefined,
402
+ invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
379
403
  ): CheckpointProposalJob {
380
404
  return new CheckpointProposalJob(
405
+ epoch,
381
406
  slot,
382
407
  checkpointNumber,
383
408
  syncedToBlockNumber,
384
409
  proposer,
385
410
  publisher,
386
411
  attestorAddress,
387
- invalidateBlock,
412
+ invalidateCheckpoint,
388
413
  this.validatorClient,
389
414
  this.globalsBuilder,
390
415
  this.p2pClient,
391
416
  this.worldState,
392
417
  this.l1ToL2MessageSource,
418
+ this.l2BlockSource,
393
419
  this.checkpointsBuilder,
420
+ this.l2BlockSource,
394
421
  this.l1Constants,
395
422
  this.config,
396
423
  this.timetable,
@@ -400,7 +427,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
400
427
  this.metrics,
401
428
  this,
402
429
  this.setState.bind(this),
403
- this.log,
430
+ this.tracer,
431
+ this.log.getBindings(),
404
432
  );
405
433
  }
406
434
 
@@ -469,9 +497,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
469
497
  number: syncSummary.latestBlockNumber,
470
498
  hash: syncSummary.latestBlockHash,
471
499
  })),
472
- this.l2BlockSource.getL2Tips().then(t => t.latest),
500
+ this.l2BlockSource.getL2Tips().then(t => t.proposed),
473
501
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
474
- this.l1ToL2MessageSource.getL2Tips().then(t => t.latest),
502
+ this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
475
503
  this.l2BlockSource.getPendingChainValidationStatus(),
476
504
  ] as const);
477
505
 
@@ -479,6 +507,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
479
507
 
480
508
  // Handle zero as a special case, since the block hash won't match across services if we're changing the prefilled data for the genesis block,
481
509
  // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
510
+ // TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
482
511
  const result =
483
512
  (l2BlockSource.number === 0 && worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0) ||
484
513
  (worldState.hash === l2BlockSource.hash &&
@@ -494,10 +523,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
494
523
  const blockNumber = worldState.number;
495
524
  if (blockNumber < INITIAL_L2_BLOCK_NUM) {
496
525
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
497
- return { blockNumber: BlockNumber(INITIAL_L2_BLOCK_NUM - 1), archive, l1Timestamp, pendingChainValidationStatus };
526
+ return {
527
+ checkpointNumber: CheckpointNumber.ZERO,
528
+ blockNumber: BlockNumber.ZERO,
529
+ archive,
530
+ l1Timestamp,
531
+ pendingChainValidationStatus,
532
+ };
498
533
  }
499
534
 
500
- const block = await this.l2BlockSource.getL2BlockNew(blockNumber);
535
+ const block = await this.l2BlockSource.getL2Block(blockNumber);
501
536
  if (!block) {
502
537
  // this shouldn't really happen because a moment ago we checked that all components were in sync
503
538
  this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
@@ -507,6 +542,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
507
542
  return {
508
543
  block,
509
544
  blockNumber: block.number,
545
+ checkpointNumber: block.checkpointNumber,
510
546
  archive: block.archive.root,
511
547
  l1Timestamp,
512
548
  pendingChainValidationStatus,
@@ -524,7 +560,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
524
560
  proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
525
561
  } catch (e) {
526
562
  if (e instanceof NoCommitteeError) {
527
- this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
563
+ if (this.lastSlotForNoCommitteeWarning !== slot) {
564
+ this.lastSlotForNoCommitteeWarning = slot;
565
+ this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
566
+ }
528
567
  return [false, undefined];
529
568
  }
530
569
  this.log.error(`Error getting proposer for slot ${slot}`, e);
@@ -555,11 +594,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
555
594
  * Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
556
595
  * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
557
596
  */
597
+ @trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
558
598
  protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
559
599
  const { slot } = args;
560
600
 
561
601
  // Prevent duplicate attempts in the same slot
562
- if (this.lastSlotForVoteWhenSyncFailed === slot) {
602
+ if (this.lastSlotForFallbackVote === slot) {
563
603
  this.log.trace(`Already attempted to vote in slot ${slot} (skipping)`);
564
604
  return;
565
605
  }
@@ -591,7 +631,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
591
631
  }
592
632
 
593
633
  // Mark this slot as attempted
594
- this.lastSlotForVoteWhenSyncFailed = slot;
634
+ this.lastSlotForFallbackVote = slot;
595
635
 
596
636
  // Get a publisher for voting
597
637
  const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
@@ -625,13 +665,61 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
625
665
  await publisher.sendRequests();
626
666
  }
627
667
 
668
+ /**
669
+ * Tries to vote on slashing actions and governance proposals when escape hatch is open.
670
+ * This allows the sequencer to participate in voting without performing checkpoint proposal work.
671
+ */
672
+ @trackSpan('Sequencer.tryVoteWhenEscapeHatchOpen', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
673
+ protected async tryVoteWhenEscapeHatchOpen(args: {
674
+ slot: SlotNumber;
675
+ proposer: EthAddress | undefined;
676
+ }): Promise<void> {
677
+ const { slot, proposer } = args;
678
+
679
+ // Prevent duplicate attempts in the same slot
680
+ if (this.lastSlotForFallbackVote === slot) {
681
+ this.log.trace(`Already attempted to vote in slot ${slot} (escape hatch open, skipping)`);
682
+ return;
683
+ }
684
+
685
+ // Mark this slot as attempted
686
+ this.lastSlotForFallbackVote = slot;
687
+
688
+ const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
689
+
690
+ this.log.debug(`Escape hatch open for slot ${slot}, attempting vote-only actions`, { slot, attestorAddress });
691
+
692
+ const voter = new CheckpointVoter(
693
+ slot,
694
+ publisher,
695
+ attestorAddress,
696
+ this.validatorClient,
697
+ this.slasherClient,
698
+ this.l1Constants,
699
+ this.config,
700
+ this.metrics,
701
+ this.log,
702
+ );
703
+
704
+ const votesPromises = voter.enqueueVotes();
705
+ const votes = await Promise.all(votesPromises);
706
+
707
+ if (votes.every(p => !p)) {
708
+ this.log.debug(`No votes to enqueue for slot ${slot} (escape hatch open)`);
709
+ return;
710
+ }
711
+
712
+ this.log.info(`Voting in slot ${slot} (escape hatch open)`, { slot });
713
+ await publisher.sendRequests();
714
+ }
715
+
628
716
  /**
629
717
  * Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
630
718
  * has been there without being invalidated and whether the sequencer is in the committee or not. We always
631
719
  * have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
632
720
  * and if they fail, any sequencer will try as well.
633
721
  */
634
- protected async considerInvalidatingBlock(
722
+ protected async considerInvalidatingCheckpoint(
635
723
  syncedTo: SequencerSyncCheckResult,
636
724
  currentSlot: SlotNumber,
637
725
  ): Promise<void> {
@@ -640,18 +728,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
640
728
  return;
641
729
  }
642
730
 
643
- const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
644
- const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
645
- const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
731
+ const invalidCheckpointNumber = pendingChainValidationStatus.checkpoint.checkpointNumber;
732
+ const invalidCheckpointTimestamp = pendingChainValidationStatus.checkpoint.timestamp;
733
+ const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidCheckpointTimestamp);
646
734
  const ourValidatorAddresses = this.validatorClient.getValidatorAddresses();
647
735
 
648
736
  const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } =
649
737
  this.config;
650
738
 
651
739
  const logData = {
652
- invalidL1Timestamp: invalidBlockTimestamp,
740
+ invalidL1Timestamp: invalidCheckpointTimestamp,
653
741
  l1Timestamp,
654
- invalidBlock: pendingChainValidationStatus.block,
742
+ invalidCheckpoint: pendingChainValidationStatus.checkpoint,
655
743
  secondsBeforeInvalidatingBlockAsCommitteeMember,
656
744
  secondsBeforeInvalidatingBlockAsNonCommitteeMember,
657
745
  ourValidatorAddresses,
@@ -697,25 +785,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
697
785
 
698
786
  const { publisher } = await this.publisherFactory.create(validatorToUse);
699
787
 
700
- const invalidateBlock = await publisher.simulateInvalidateBlock(pendingChainValidationStatus);
701
- if (!invalidateBlock) {
702
- this.log.warn(`Failed to simulate invalidate block`, logData);
788
+ const invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(pendingChainValidationStatus);
789
+ if (!invalidateCheckpoint) {
790
+ this.log.warn(`Failed to simulate invalidate checkpoint`, logData);
703
791
  return;
704
792
  }
705
793
 
706
794
  this.log.info(
707
795
  invalidateAsCommitteeMember
708
- ? `Invalidating block ${invalidBlockNumber} as committee member`
709
- : `Invalidating block ${invalidBlockNumber} as non-committee member`,
796
+ ? `Invalidating checkpoint ${invalidCheckpointNumber} as committee member`
797
+ : `Invalidating checkpoint ${invalidCheckpointNumber} as non-committee member`,
710
798
  logData,
711
799
  );
712
800
 
713
- publisher.enqueueInvalidateBlock(invalidateBlock);
801
+ publisher.enqueueInvalidateCheckpoint(invalidateCheckpoint);
714
802
 
715
803
  if (!this.config.fishermanMode) {
716
804
  await publisher.sendRequests();
717
805
  } else {
718
- this.log.info('Invalidating block in fisherman mode, clearing pending requests');
806
+ this.log.info('Invalidating checkpoint in fisherman mode, clearing pending requests');
719
807
  publisher.clearPendingRequests();
720
808
  }
721
809
  }
@@ -788,9 +876,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
788
876
  }
789
877
 
790
878
  type SequencerSyncCheckResult = {
791
- block?: L2BlockNew;
879
+ block?: L2Block;
880
+ checkpointNumber: CheckpointNumber;
792
881
  blockNumber: BlockNumber;
793
882
  archive: Fr;
794
883
  l1Timestamp: bigint;
795
- pendingChainValidationStatus: ValidateBlockResult;
884
+ pendingChainValidationStatus: ValidateCheckpointResult;
796
885
  };
package/src/test/index.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
2
2
  import type { PublisherManager } from '@aztec/ethereum/publisher-manager';
3
3
  import type { PublicProcessorFactory } from '@aztec/simulator/server';
4
- import type { ValidatorClient } from '@aztec/validator-client';
4
+ import type { FullNodeCheckpointsBuilder, ValidatorClient } from '@aztec/validator-client';
5
5
 
6
6
  import { SequencerClient } from '../client/sequencer-client.js';
7
7
  import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
8
8
  import type { SequencerPublisher } from '../publisher/sequencer-publisher.js';
9
- import type { FullNodeCheckpointsBuilder } from '../sequencer/checkpoint_builder.js';
10
9
  import { Sequencer } from '../sequencer/sequencer.js';
11
10
  import type { SequencerTimetable } from '../sequencer/timetable.js';
12
11
 
@@ -1,33 +1,32 @@
1
1
  import { type BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
2
2
  import { Fr } from '@aztec/foundation/curves/bn254';
3
- import { Timer } from '@aztec/foundation/timer';
4
- import type { FunctionsOf } from '@aztec/foundation/types';
5
- import { L2BlockNew } from '@aztec/stdlib/block';
3
+ import { L2Block } from '@aztec/stdlib/block';
6
4
  import { Checkpoint } from '@aztec/stdlib/checkpoint';
7
5
  import { Gas } from '@aztec/stdlib/gas';
8
- import type { FullNodeBlockBuilderConfig, PublicProcessorLimits } from '@aztec/stdlib/interfaces/server';
6
+ import type {
7
+ FullNodeBlockBuilderConfig,
8
+ ICheckpointBlockBuilder,
9
+ ICheckpointsBuilder,
10
+ MerkleTreeWriteOperations,
11
+ PublicProcessorLimits,
12
+ } from '@aztec/stdlib/interfaces/server';
9
13
  import { CheckpointHeader } from '@aztec/stdlib/rollup';
10
14
  import { makeAppendOnlyTreeSnapshot } from '@aztec/stdlib/testing';
11
15
  import type { CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
12
-
13
- import type {
14
- BuildBlockInCheckpointResult,
15
- CheckpointBuilder,
16
- FullNodeCheckpointsBuilder,
17
- } from '../sequencer/checkpoint_builder.js';
16
+ import type { BuildBlockInCheckpointResult } from '@aztec/validator-client';
18
17
 
19
18
  /**
20
19
  * A fake CheckpointBuilder for testing that implements the same interface as the real one.
21
20
  * Can be seeded with blocks to return sequentially on each `buildBlock` call.
22
21
  */
23
- export class MockCheckpointBuilder implements FunctionsOf<CheckpointBuilder> {
24
- private blocks: L2BlockNew[] = [];
25
- private builtBlocks: L2BlockNew[] = [];
22
+ export class MockCheckpointBuilder implements ICheckpointBlockBuilder {
23
+ private blocks: L2Block[] = [];
24
+ private builtBlocks: L2Block[] = [];
26
25
  private usedTxsPerBlock: Tx[][] = [];
27
26
  private blockIndex = 0;
28
27
 
29
28
  /** Optional function to dynamically provide the block (alternative to seedBlocks) */
30
- private blockProvider: (() => L2BlockNew) | undefined = undefined;
29
+ private blockProvider: (() => L2Block) | undefined = undefined;
31
30
 
32
31
  /** Track calls for assertions */
33
32
  public buildBlockCalls: Array<{
@@ -35,6 +34,8 @@ export class MockCheckpointBuilder implements FunctionsOf<CheckpointBuilder> {
35
34
  timestamp: bigint;
36
35
  opts: PublicProcessorLimits;
37
36
  }> = [];
37
+ /** Track all consumed transaction hashes across buildBlock calls */
38
+ public consumedTxHashes: Set<string> = new Set();
38
39
  public completeCheckpointCalled = false;
39
40
  public getCheckpointCalled = false;
40
41
 
@@ -47,7 +48,7 @@ export class MockCheckpointBuilder implements FunctionsOf<CheckpointBuilder> {
47
48
  ) {}
48
49
 
49
50
  /** Seed the builder with blocks to return on successive buildBlock calls */
50
- seedBlocks(blocks: L2BlockNew[], usedTxsPerBlock?: Tx[][]): this {
51
+ seedBlocks(blocks: L2Block[], usedTxsPerBlock?: Tx[][]): this {
51
52
  this.blocks = blocks;
52
53
  this.usedTxsPerBlock = usedTxsPerBlock ?? blocks.map(() => []);
53
54
  this.blockIndex = 0;
@@ -59,7 +60,7 @@ export class MockCheckpointBuilder implements FunctionsOf<CheckpointBuilder> {
59
60
  * Set a function that provides blocks dynamically.
60
61
  * Useful for tests where the block is determined at call time (e.g., sequencer tests).
61
62
  */
62
- setBlockProvider(provider: () => L2BlockNew): this {
63
+ setBlockProvider(provider: () => L2Block): this {
63
64
  this.blockProvider = provider;
64
65
  this.blocks = [];
65
66
  return this;
@@ -69,8 +70,8 @@ export class MockCheckpointBuilder implements FunctionsOf<CheckpointBuilder> {
69
70
  return this.constants;
70
71
  }
71
72
 
72
- buildBlock(
73
- _pendingTxs: Iterable<Tx> | AsyncIterable<Tx>,
73
+ async buildBlock(
74
+ pendingTxs: Iterable<Tx> | AsyncIterable<Tx>,
74
75
  blockNumber: BlockNumber,
75
76
  timestamp: bigint,
76
77
  opts: PublicProcessorLimits,
@@ -78,10 +79,10 @@ export class MockCheckpointBuilder implements FunctionsOf<CheckpointBuilder> {
78
79
  this.buildBlockCalls.push({ blockNumber, timestamp, opts });
79
80
 
80
81
  if (this.errorOnBuild) {
81
- return Promise.reject(this.errorOnBuild);
82
+ throw this.errorOnBuild;
82
83
  }
83
84
 
84
- let block: L2BlockNew;
85
+ let block: L2Block;
85
86
  let usedTxs: Tx[];
86
87
 
87
88
  if (this.blockProvider) {
@@ -97,15 +98,28 @@ export class MockCheckpointBuilder implements FunctionsOf<CheckpointBuilder> {
97
98
  this.builtBlocks.push(block);
98
99
  }
99
100
 
100
- return Promise.resolve({
101
+ // Check that no pending tx has already been consumed
102
+ for await (const tx of pendingTxs) {
103
+ const hash = tx.getTxHash().toString();
104
+ if (this.consumedTxHashes.has(hash)) {
105
+ throw new Error(`Transaction ${hash} was already consumed in a previous block`);
106
+ }
107
+ }
108
+
109
+ // Add used txs to consumed set
110
+ for (const tx of usedTxs) {
111
+ this.consumedTxHashes.add(tx.getTxHash().toString());
112
+ }
113
+
114
+ return {
101
115
  block,
102
116
  publicGas: Gas.empty(),
103
117
  publicProcessorDuration: 0,
104
118
  numTxs: block?.body?.txEffects?.length ?? usedTxs.length,
105
- blockBuildingTimer: new Timer(),
106
119
  usedTxs,
107
120
  failedTxs: [],
108
- });
121
+ usedTxBlobFields: block?.body?.txEffects?.reduce((sum, tx) => sum + tx.getNumBlobFields(), 0) ?? 0,
122
+ };
109
123
  }
110
124
 
111
125
  completeCheckpoint(): Promise<Checkpoint> {
@@ -147,7 +161,7 @@ export class MockCheckpointBuilder implements FunctionsOf<CheckpointBuilder> {
147
161
  * Creates a CheckpointHeader from a block's header for testing.
148
162
  * This is a simplified version that creates a minimal CheckpointHeader.
149
163
  */
150
- private createCheckpointHeader(block: L2BlockNew): CheckpointHeader {
164
+ private createCheckpointHeader(block: L2Block): CheckpointHeader {
151
165
  const header = block.header;
152
166
  const gv = header.globalVariables;
153
167
  return CheckpointHeader.empty({
@@ -169,6 +183,7 @@ export class MockCheckpointBuilder implements FunctionsOf<CheckpointBuilder> {
169
183
  this.usedTxsPerBlock = [];
170
184
  this.blockIndex = 0;
171
185
  this.buildBlockCalls = [];
186
+ this.consumedTxHashes.clear();
172
187
  this.completeCheckpointCalled = false;
173
188
  this.getCheckpointCalled = false;
174
189
  this.errorOnBuild = undefined;
@@ -181,7 +196,7 @@ export class MockCheckpointBuilder implements FunctionsOf<CheckpointBuilder> {
181
196
  * as FullNodeCheckpointsBuilder. Returns MockCheckpointBuilder instances.
182
197
  * Does NOT use jest mocks - this is a proper test double.
183
198
  */
184
- export class MockCheckpointsBuilder implements FunctionsOf<FullNodeCheckpointsBuilder> {
199
+ export class MockCheckpointsBuilder implements ICheckpointsBuilder {
185
200
  private checkpointBuilder: MockCheckpointBuilder | undefined;
186
201
 
187
202
  /** Track calls for assertions */
@@ -189,6 +204,14 @@ export class MockCheckpointsBuilder implements FunctionsOf<FullNodeCheckpointsBu
189
204
  checkpointNumber: CheckpointNumber;
190
205
  constants: CheckpointGlobalVariables;
191
206
  l1ToL2Messages: Fr[];
207
+ previousCheckpointOutHashes: Fr[];
208
+ }> = [];
209
+ public openCheckpointCalls: Array<{
210
+ checkpointNumber: CheckpointNumber;
211
+ constants: CheckpointGlobalVariables;
212
+ l1ToL2Messages: Fr[];
213
+ previousCheckpointOutHashes: Fr[];
214
+ existingBlocks: L2Block[];
192
215
  }> = [];
193
216
  public updateConfigCalls: Array<Partial<FullNodeBlockBuilderConfig>> = [];
194
217
 
@@ -218,6 +241,15 @@ export class MockCheckpointsBuilder implements FunctionsOf<FullNodeCheckpointsBu
218
241
  return this.checkpointBuilder;
219
242
  }
220
243
 
244
+ getConfig(): FullNodeBlockBuilderConfig {
245
+ return {
246
+ l1GenesisTime: 0n,
247
+ slotDuration: 24,
248
+ l1ChainId: 1,
249
+ rollupVersion: 1,
250
+ };
251
+ }
252
+
221
253
  updateConfig(config: Partial<FullNodeBlockBuilderConfig>): void {
222
254
  this.updateConfigCalls.push(config);
223
255
  }
@@ -226,22 +258,52 @@ export class MockCheckpointsBuilder implements FunctionsOf<FullNodeCheckpointsBu
226
258
  checkpointNumber: CheckpointNumber,
227
259
  constants: CheckpointGlobalVariables,
228
260
  l1ToL2Messages: Fr[],
229
- _fork: unknown,
230
- ): Promise<CheckpointBuilder> {
231
- this.startCheckpointCalls.push({ checkpointNumber, constants, l1ToL2Messages });
261
+ previousCheckpointOutHashes: Fr[],
262
+ _fork: MerkleTreeWriteOperations,
263
+ ): Promise<ICheckpointBlockBuilder> {
264
+ this.startCheckpointCalls.push({ checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes });
232
265
 
233
266
  if (!this.checkpointBuilder) {
234
267
  // Auto-create a builder if none was set
235
268
  this.checkpointBuilder = new MockCheckpointBuilder(constants, checkpointNumber);
236
269
  }
237
270
 
238
- return Promise.resolve(this.checkpointBuilder as unknown as CheckpointBuilder);
271
+ return Promise.resolve(this.checkpointBuilder);
272
+ }
273
+
274
+ openCheckpoint(
275
+ checkpointNumber: CheckpointNumber,
276
+ constants: CheckpointGlobalVariables,
277
+ l1ToL2Messages: Fr[],
278
+ previousCheckpointOutHashes: Fr[],
279
+ _fork: MerkleTreeWriteOperations,
280
+ existingBlocks: L2Block[] = [],
281
+ ): Promise<ICheckpointBlockBuilder> {
282
+ this.openCheckpointCalls.push({
283
+ checkpointNumber,
284
+ constants,
285
+ l1ToL2Messages,
286
+ previousCheckpointOutHashes,
287
+ existingBlocks,
288
+ });
289
+
290
+ if (!this.checkpointBuilder) {
291
+ // Auto-create a builder if none was set
292
+ this.checkpointBuilder = new MockCheckpointBuilder(constants, checkpointNumber);
293
+ }
294
+
295
+ return Promise.resolve(this.checkpointBuilder);
296
+ }
297
+
298
+ getFork(_blockNumber: BlockNumber): Promise<MerkleTreeWriteOperations> {
299
+ throw new Error('MockCheckpointsBuilder.getFork not implemented');
239
300
  }
240
301
 
241
302
  /** Reset for reuse in another test */
242
303
  reset(): void {
243
304
  this.checkpointBuilder = undefined;
244
305
  this.startCheckpointCalls = [];
306
+ this.openCheckpointCalls = [];
245
307
  this.updateConfigCalls = [];
246
308
  }
247
309
  }