@aztec/sequencer-client 0.0.1-commit.b9865e97 → 0.0.1-commit.be03c316

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 (101) hide show
  1. package/README.md +27 -27
  2. package/dest/client/sequencer-client.d.ts +16 -3
  3. package/dest/client/sequencer-client.d.ts.map +1 -1
  4. package/dest/client/sequencer-client.js +17 -12
  5. package/dest/config.d.ts +6 -2
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +23 -14
  8. package/dest/global_variable_builder/fee_predictor.d.ts +1 -1
  9. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -1
  10. package/dest/global_variable_builder/fee_predictor.js +11 -1
  11. package/dest/global_variable_builder/fee_provider.d.ts +1 -1
  12. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -1
  13. package/dest/global_variable_builder/fee_provider.js +27 -5
  14. package/dest/global_variable_builder/global_builder.d.ts +3 -16
  15. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  16. package/dest/global_variable_builder/global_builder.js +2 -25
  17. package/dest/index.d.ts +2 -2
  18. package/dest/index.d.ts.map +1 -1
  19. package/dest/index.js +1 -1
  20. package/dest/publisher/config.d.ts +5 -1
  21. package/dest/publisher/config.d.ts.map +1 -1
  22. package/dest/publisher/config.js +11 -1
  23. package/dest/publisher/l1_to_l2_messaging.d.ts +21 -0
  24. package/dest/publisher/l1_to_l2_messaging.d.ts.map +1 -0
  25. package/dest/publisher/l1_to_l2_messaging.js +70 -0
  26. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +48 -8
  27. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
  28. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +68 -1
  29. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +2 -2
  30. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -1
  31. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +4 -2
  32. package/dest/publisher/l1_tx_failed_store/index.d.ts +2 -2
  33. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -1
  34. package/dest/publisher/l1_tx_failed_store/index.js +1 -0
  35. package/dest/publisher/sequencer-publisher.d.ts +34 -6
  36. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  37. package/dest/publisher/sequencer-publisher.js +362 -61
  38. package/dest/publisher/write_json.d.ts +11 -0
  39. package/dest/publisher/write_json.d.ts.map +1 -0
  40. package/dest/publisher/write_json.js +57 -0
  41. package/dest/sequencer/automine/automine_factory.d.ts +5 -3
  42. package/dest/sequencer/automine/automine_factory.d.ts.map +1 -1
  43. package/dest/sequencer/automine/automine_factory.js +2 -1
  44. package/dest/sequencer/automine/automine_sequencer.d.ts +43 -5
  45. package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -1
  46. package/dest/sequencer/automine/automine_sequencer.js +199 -18
  47. package/dest/sequencer/automine/index.d.ts +3 -0
  48. package/dest/sequencer/automine/index.d.ts.map +1 -0
  49. package/dest/sequencer/automine/index.js +2 -0
  50. package/dest/sequencer/checkpoint_proposal_job.d.ts +24 -16
  51. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  52. package/dest/sequencer/checkpoint_proposal_job.js +98 -90
  53. package/dest/sequencer/events.d.ts +16 -5
  54. package/dest/sequencer/events.d.ts.map +1 -1
  55. package/dest/sequencer/index.d.ts +1 -3
  56. package/dest/sequencer/index.d.ts.map +1 -1
  57. package/dest/sequencer/index.js +0 -2
  58. package/dest/sequencer/requests_tracker.d.ts +22 -0
  59. package/dest/sequencer/requests_tracker.d.ts.map +1 -0
  60. package/dest/sequencer/requests_tracker.js +33 -0
  61. package/dest/sequencer/sequencer.d.ts +110 -36
  62. package/dest/sequencer/sequencer.d.ts.map +1 -1
  63. package/dest/sequencer/sequencer.js +329 -174
  64. package/dest/test/index.d.ts +3 -3
  65. package/dest/test/index.d.ts.map +1 -1
  66. package/dest/test/utils.d.ts +15 -1
  67. package/dest/test/utils.d.ts.map +1 -1
  68. package/dest/test/utils.js +18 -1
  69. package/package.json +30 -28
  70. package/src/client/sequencer-client.ts +20 -13
  71. package/src/config.ts +25 -12
  72. package/src/global_variable_builder/fee_predictor.ts +11 -1
  73. package/src/global_variable_builder/fee_provider.ts +27 -5
  74. package/src/global_variable_builder/global_builder.ts +2 -34
  75. package/src/index.ts +1 -10
  76. package/src/publisher/config.ts +22 -1
  77. package/src/publisher/l1_to_l2_messaging.ts +85 -0
  78. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +114 -7
  79. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +4 -3
  80. package/src/publisher/l1_tx_failed_store/index.ts +1 -1
  81. package/src/publisher/sequencer-publisher.ts +383 -72
  82. package/src/publisher/write_json.ts +78 -0
  83. package/src/sequencer/automine/README.md +18 -4
  84. package/src/sequencer/automine/automine_factory.ts +8 -1
  85. package/src/sequencer/automine/automine_sequencer.ts +221 -20
  86. package/src/sequencer/automine/index.ts +6 -0
  87. package/src/sequencer/checkpoint_proposal_job.ts +119 -100
  88. package/src/sequencer/events.ts +16 -4
  89. package/src/sequencer/index.ts +0 -2
  90. package/src/sequencer/requests_tracker.ts +43 -0
  91. package/src/sequencer/sequencer.ts +360 -187
  92. package/src/test/index.ts +2 -2
  93. package/src/test/utils.ts +33 -0
  94. package/dest/sequencer/chain_state_overrides.d.ts +0 -61
  95. package/dest/sequencer/chain_state_overrides.d.ts.map +0 -1
  96. package/dest/sequencer/chain_state_overrides.js +0 -98
  97. package/dest/sequencer/timetable.d.ts +0 -98
  98. package/dest/sequencer/timetable.d.ts.map +0 -1
  99. package/dest/sequencer/timetable.js +0 -227
  100. package/src/sequencer/chain_state_overrides.ts +0 -162
  101. package/src/sequencer/timetable.ts +0 -288
@@ -1,4 +1,4 @@
1
- import type { EpochCache } from '@aztec/epoch-cache';
1
+ import { type EpochCache, PROPOSER_PIPELINING_SLOT_OFFSET } from '@aztec/epoch-cache';
2
2
  import type { SimulationOverridesPlan } from '@aztec/ethereum/contracts';
3
3
  import {
4
4
  BlockNumber,
@@ -31,16 +31,18 @@ import {
31
31
  type L2BlockSink,
32
32
  type L2BlockSource,
33
33
  MaliciousCommitteeAttestationsAndSigners,
34
+ MaliciousYParityCommitteeAttestationsAndSigners,
34
35
  type ProposedCheckpointSink,
35
36
  type ValidateCheckpointResult,
36
37
  } from '@aztec/stdlib/block';
37
38
  import {
38
39
  type Checkpoint,
39
40
  type ProposedCheckpointData,
41
+ buildCheckpointSimulationOverridesPlan,
40
42
  getPreviousCheckpointOutHashes,
41
43
  validateCheckpoint,
42
44
  } from '@aztec/stdlib/checkpoint';
43
- import { computeQuorum, getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
45
+ import { computeQuorum, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
44
46
  import { Gas } from '@aztec/stdlib/gas';
45
47
  import {
46
48
  type BlockBuilderOptions,
@@ -59,6 +61,7 @@ import type {
59
61
  } from '@aztec/stdlib/p2p';
60
62
  import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
61
63
  import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
64
+ import type { ProposerTimetable } from '@aztec/stdlib/timetable';
62
65
  import { type FailedTx, Tx } from '@aztec/stdlib/tx';
63
66
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
64
67
  import { Attributes, type Traceable, type Tracer, trackSpan } from '@aztec/telemetry-client';
@@ -67,13 +70,12 @@ import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validato
67
70
 
68
71
  import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
69
72
  import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
70
- import { buildCheckpointSimulationOverridesPlan } from './chain_state_overrides.js';
71
73
  import type { CheckpointProposalJobMetricsRecorder } from './checkpoint_proposal_job_metrics.js';
72
74
  import { CheckpointVoter } from './checkpoint_voter.js';
73
75
  import { SequencerInterruptedError } from './errors.js';
74
76
  import type { SequencerEvents } from './events.js';
75
77
  import type { SequencerMetrics } from './metrics.js';
76
- import type { SequencerTimetable } from './timetable.js';
78
+ import type { RequestsTracker } from './requests_tracker.js';
77
79
  import type { SequencerRollupConstants } from './types.js';
78
80
  import { SequencerState } from './utils.js';
79
81
 
@@ -105,8 +107,6 @@ export class CheckpointProposalJob implements Traceable {
105
107
  protected readonly log: Logger;
106
108
  private readonly checkpointEventLog: Logger;
107
109
 
108
- /** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */
109
- private pendingL1Submission: Promise<void> | undefined;
110
110
  private readonly interruptibleSleep = new InterruptibleSleep();
111
111
  private interrupted = false;
112
112
 
@@ -123,7 +123,6 @@ export class CheckpointProposalJob implements Traceable {
123
123
  }
124
124
 
125
125
  constructor(
126
- private readonly slotNow: SlotNumber,
127
126
  private readonly targetSlot: SlotNumber,
128
127
  private readonly targetEpoch: EpochNumber,
129
128
  private readonly checkpointNumber: CheckpointNumber,
@@ -145,32 +144,46 @@ export class CheckpointProposalJob implements Traceable {
145
144
  private readonly l1Constants: SequencerRollupConstants,
146
145
  private readonly signatureContext: CoordinationSignatureContext,
147
146
  protected config: ResolvedSequencerConfig,
148
- protected timetable: SequencerTimetable,
147
+ protected timetable: ProposerTimetable,
149
148
  private readonly slasherClient: SlasherClientInterface | undefined,
150
149
  private readonly epochCache: EpochCache,
151
150
  private readonly dateProvider: DateProvider,
152
151
  private readonly metrics: SequencerMetrics,
153
152
  private readonly checkpointMetrics: CheckpointProposalJobMetricsRecorder,
154
153
  protected readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
155
- private readonly setStateFn: (state: SequencerState, slot?: SlotNumber, timeReferenceSlot?: SlotNumber) => void,
154
+ // Shared with the owning sequencer, which drains it during shutdown; the fire-and-forget L1
155
+ // submission this job backgrounds is tracked here rather than in a job-local tracker.
156
+ protected readonly pendingRequests: RequestsTracker,
157
+ private readonly setStateFn: (state: SequencerState, slot: SlotNumber) => void,
156
158
  public readonly tracer: Tracer,
157
159
  bindings?: LoggerBindings,
158
160
  private readonly proposedCheckpointData?: ProposedCheckpointData,
159
161
  ) {
160
162
  this.log = createLogger('sequencer:checkpoint-proposal', {
161
163
  ...bindings,
162
- instanceId: `slot-${this.slotNow}`,
164
+ instanceId: `slot-${this.getBuildSlot()}`,
163
165
  });
164
166
  this.checkpointEventLog = createLogger('sequencer:checkpoint-events', {
165
167
  ...bindings,
166
- instanceId: `slot-${this.slotNow}`,
168
+ instanceId: `slot-${this.getBuildSlot()}`,
167
169
  });
168
170
  }
169
171
 
170
- /** Awaits the pending L1 submission if one is in progress. Call during shutdown. */
171
- public async awaitPendingSubmission(): Promise<void> {
172
- this.log.info('Awaiting pending L1 payload submission');
173
- await this.pendingL1Submission;
172
+ /**
173
+ * The wall-clock slot during which this job builds, i.e. the slot one before {@link targetSlot} under
174
+ * proposer pipelining. Also the slot of the parent checkpoint this job builds on top of.
175
+ */
176
+ private getBuildSlot(): SlotNumber {
177
+ return SlotNumber(this.targetSlot - PROPOSER_PIPELINING_SLOT_OFFSET);
178
+ }
179
+
180
+ /**
181
+ * Sets the sequencer state for this job, reporting the target slot the checkpoint is being proposed for
182
+ * (not the wall-clock build slot). The slot is informational on the event payload/metrics; the job knows
183
+ * its own target slot, so callers only pass the state.
184
+ */
185
+ private setState(state: SequencerState): void {
186
+ this.setStateFn(state, this.targetSlot);
174
187
  }
175
188
 
176
189
  /** Interrupts job-owned waits, including the publisher's send-at-slot sleep, so shutdown can finish. */
@@ -241,7 +254,7 @@ export class CheckpointProposalJob implements Traceable {
241
254
  // signature verification to fail silently inside Multicall3. Delay submission to the
242
255
  // start of `targetSlot` so the tx mines in the slot the vote was signed for.
243
256
  if (!this.config.fishermanMode) {
244
- this.pendingL1Submission = this.publisher.sendRequestsAt(this.targetSlot).then(() => {});
257
+ this.pendingRequests.trackRequest(this.publisher.sendRequestsAt(this.targetSlot), () => this.interrupt());
245
258
  }
246
259
  return undefined;
247
260
  }
@@ -256,7 +269,9 @@ export class CheckpointProposalJob implements Traceable {
256
269
  }
257
270
 
258
271
  // Background the attestation → signing → L1 pipeline so the work loop is unblocked
259
- this.pendingL1Submission = this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises);
272
+ this.pendingRequests.trackRequest(this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises), () =>
273
+ this.interrupt(),
274
+ );
260
275
 
261
276
  // Return the built checkpoint immediately — the work loop is now unblocked
262
277
  return checkpoint;
@@ -264,7 +279,7 @@ export class CheckpointProposalJob implements Traceable {
264
279
 
265
280
  /**
266
281
  * Background pipeline: collects attestations, signs them, enqueues the checkpoint, and submits to L1.
267
- * Runs as a fire-and-forget task stored in `pendingL1Submission` so the work loop is unblocked.
282
+ * Runs as a fire-and-forget task tracked in the sequencer's shared tracker so the work loop is unblocked.
268
283
  */
269
284
  private async waitForAttestationsAndEnqueueSubmissionAsync(
270
285
  broadcast: CheckpointProposalBroadcast,
@@ -286,7 +301,7 @@ export class CheckpointProposalJob implements Traceable {
286
301
  }
287
302
 
288
303
  // If we failed to collect attestations, at least check if we need to issue an invalidation
289
- if (!signedAttestations && (await this.waitForSyncedL2SlotNumber(this.slotNow))) {
304
+ if (!signedAttestations && (await this.waitForSyncedL2SlotNumber(this.getBuildSlot()))) {
290
305
  const validationStatus = await this.l2BlockSource.getPendingChainValidationStatus();
291
306
  if (!validationStatus.valid) {
292
307
  this.log.warn(
@@ -355,11 +370,18 @@ export class CheckpointProposalJob implements Traceable {
355
370
  private async enqueueCheckpointForSubmission(result: CheckpointProposalResult): Promise<void> {
356
371
  const { checkpoint, attestations, attestationsSignature } = result;
357
372
 
358
- // Build-frame deadlines are measured against `slotNow`; observers still see `targetSlot`.
359
- this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot, this.slotNow);
360
- const aztecSlotDuration = this.l1Constants.slotDuration;
361
- const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
362
- const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
373
+ this.setState(SequencerState.PUBLISHING_CHECKPOINT);
374
+ // Latest L1 block the propose can still land in for the target slot: the last Ethereum block inside
375
+ // the target slot (`target_slot_start + S - E`). This is one ethereum slot later than
376
+ // `attestation_deadline` (= last_ethereum_block_in_target_slot - E), which bounds when validators must
377
+ // have signed, not when the proposer must have sent. Using the attestation deadline here is too tight:
378
+ // attestations are collected up to (and, when not enforcing, past) it, so the propose tx would be
379
+ // enqueued already expired and time out before it can mine.
380
+ const lastL1BlockInTargetSlot =
381
+ Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) +
382
+ this.l1Constants.slotDuration -
383
+ this.l1Constants.ethereumSlotDuration;
384
+ const txTimeoutAt = new Date(lastL1BlockInTargetSlot * 1000);
363
385
 
364
386
  // If we have been configured to potentially skip publishing checkpoint then roll the dice here
365
387
  if (
@@ -375,9 +397,7 @@ export class CheckpointProposalJob implements Traceable {
375
397
  }
376
398
  }
377
399
 
378
- await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
379
- txTimeoutAt,
380
- });
400
+ await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, { txTimeoutAt });
381
401
  }
382
402
 
383
403
  /**
@@ -433,8 +453,8 @@ export class CheckpointProposalJob implements Traceable {
433
453
 
434
454
  const parentCheckpointNumber = CheckpointNumber(this.checkpointNumber - 1);
435
455
 
436
- // Wait until archiver has synced L1 past the parent's slot (slotNow)
437
- if (!(await this.waitForSyncedL2SlotNumber(this.slotNow))) {
456
+ // Wait until archiver has synced L1 past the parent's slot (the build slot, one before targetSlot)
457
+ if (!(await this.waitForSyncedL2SlotNumber(this.getBuildSlot()))) {
438
458
  return false;
439
459
  }
440
460
 
@@ -541,7 +561,7 @@ export class CheckpointProposalJob implements Traceable {
541
561
  // Measure against the wall-clock slot whose build window we are currently using.
542
562
  // In pipelining mode `targetSlot` is intentionally one slot ahead, which makes the
543
563
  // target-slot boundary a full slot away from the actual build start time.
544
- const slotBoundaryMs = Number(getTimestampForSlot(this.slotNow, this.l1Constants)) * 1000;
564
+ const slotBoundaryMs = Number(getTimestampForSlot(this.getBuildSlot(), this.l1Constants)) * 1000;
545
565
  this.checkpointMetrics.recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(now - slotBoundaryMs);
546
566
  }
547
567
  this.checkpointMetrics.startCheckpointTiming(now);
@@ -551,9 +571,9 @@ export class CheckpointProposalJob implements Traceable {
551
571
  const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
552
572
 
553
573
  // Start the checkpoint
554
- this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot, this.slotNow);
574
+ this.setState(SequencerState.INITIALIZING_CHECKPOINT);
555
575
  this.logCheckpointEvent('slot-started', `Starting checkpoint proposal for slot ${this.targetSlot}`, {
556
- buildSlot: this.slotNow,
576
+ buildSlot: this.getBuildSlot(),
557
577
  submissionSlot: this.targetSlot,
558
578
  slot: this.targetSlot,
559
579
  checkpointNumber: this.checkpointNumber,
@@ -704,7 +724,7 @@ export class CheckpointProposalJob implements Traceable {
704
724
 
705
725
  // Assemble and broadcast the checkpoint proposal, including the last block that was not
706
726
  // broadcasted yet, and wait to collect the committee attestations.
707
- this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.targetSlot, this.slotNow);
727
+ this.setState(SequencerState.ASSEMBLING_CHECKPOINT);
708
728
  const checkpoint = await checkpointBuilder.completeCheckpoint();
709
729
 
710
730
  // Final validation: per-block limits are only checked if the operator set them explicitly.
@@ -725,7 +745,7 @@ export class CheckpointProposalJob implements Traceable {
725
745
  reason: 'invalid_checkpoint',
726
746
  checkpoint: checkpoint.header.toInspect(),
727
747
  });
728
- this.log.error(`Built an invalid checkpoint at slot ${this.slotNow} (skipping proposal)`, err, {
748
+ this.log.error(`Built an invalid checkpoint at slot ${this.targetSlot} (skipping proposal)`, err, {
729
749
  slot: this.targetSlot,
730
750
  checkpointNumber: this.checkpointNumber,
731
751
  blocksBuilt: blocksInCheckpoint.length,
@@ -744,7 +764,7 @@ export class CheckpointProposalJob implements Traceable {
744
764
  );
745
765
  this.logCheckpointEvent('built', `Checkpoint built for slot ${this.targetSlot}`, {
746
766
  slot: this.targetSlot,
747
- buildSlot: this.slotNow,
767
+ buildSlot: this.getBuildSlot(),
748
768
  checkpointNumber: this.checkpointNumber,
749
769
  proposer: this.proposer?.toString(),
750
770
  attestorAddress: this.attestorAddress.toString(),
@@ -868,14 +888,14 @@ export class CheckpointProposalJob implements Traceable {
868
888
  break;
869
889
  }
870
890
 
871
- const secondsIntoSlot = this.getSecondsIntoSlot();
872
- const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
891
+ const nowSeconds = this.dateProvider.now() / 1000;
892
+ const timingInfo = this.timetable.selectNextSubslot(this.targetSlot, nowSeconds);
873
893
 
874
894
  if (!timingInfo.canStart) {
875
895
  this.log.debug(`Not enough time left in slot to start another block`, {
876
896
  slot: this.targetSlot,
877
897
  blocksBuilt,
878
- secondsIntoSlot,
898
+ nowSeconds,
879
899
  });
880
900
  break;
881
901
  }
@@ -885,10 +905,7 @@ export class CheckpointProposalJob implements Traceable {
885
905
  blockTimestamp: timestamp,
886
906
  // Create an empty block if we haven't already and this is the last one
887
907
  forceCreate: timingInfo.isLastBlock && blocksBuilt === 0 && this.config.buildCheckpointIfEmpty,
888
- // Build deadline is only set if we are enforcing the timetable
889
- buildDeadline: timingInfo.deadline
890
- ? new Date((this.getSlotStartBuildTimestamp() + timingInfo.deadline) * 1000)
891
- : undefined,
908
+ buildDeadline: new Date(timingInfo.deadline * 1000),
892
909
  blockNumber,
893
910
  indexWithinCheckpoint,
894
911
  txHashesAlreadyIncluded,
@@ -896,8 +913,8 @@ export class CheckpointProposalJob implements Traceable {
896
913
 
897
914
  // If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
898
915
  if ('failure' in buildResult) {
899
- // If this was the last subslot, or we're running with a single block per slot, we're done
900
- if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
916
+ // If this was the last subslot, we're done.
917
+ if (timingInfo.isLastBlock) {
901
918
  break;
902
919
  }
903
920
  // Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
@@ -993,14 +1010,17 @@ export class CheckpointProposalJob implements Traceable {
993
1010
  );
994
1011
  }
995
1012
 
996
- /** Sleeps until it is time to produce the next block in the slot */
1013
+ /**
1014
+ * Sleeps until it is time to produce the next block in the slot.
1015
+ * @param nextSubslotStart - Absolute wall-clock timestamp in seconds of the previous sub-slot deadline.
1016
+ */
997
1017
  @trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
998
- private async waitUntilNextSubslot(nextSubslotStart: number) {
999
- this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot, this.slotNow);
1000
- this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, {
1018
+ protected async waitUntilNextSubslot(nextSubslotStart: number) {
1019
+ this.setState(SequencerState.WAITING_UNTIL_NEXT_BLOCK);
1020
+ this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s`, {
1001
1021
  slot: this.targetSlot,
1002
1022
  });
1003
- await this.waitUntilTimeInSlot(nextSubslotStart);
1023
+ await this.awaitInterruptibleSleep(Math.max(0, nextSubslotStart * 1000 - this.dateProvider.now()));
1004
1024
  }
1005
1025
 
1006
1026
  /** Builds a single block. Called from the main block building loop. */
@@ -1028,7 +1048,7 @@ export class CheckpointProposalJob implements Traceable {
1028
1048
 
1029
1049
  try {
1030
1050
  // Wait until we have enough txs to build the block
1031
- const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
1051
+ const { canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
1032
1052
  if (!canStartBuilding) {
1033
1053
  this.logCheckpointEvent('block-build-failed', `Block build failed for slot ${this.targetSlot}`, {
1034
1054
  reason: 'insufficient_txs',
@@ -1036,38 +1056,39 @@ export class CheckpointProposalJob implements Traceable {
1036
1056
  slot: this.targetSlot,
1037
1057
  checkpointNumber: this.checkpointNumber,
1038
1058
  indexWithinCheckpoint,
1039
- availableTxs,
1040
1059
  minTxs,
1041
1060
  });
1042
- this.log.warn(
1043
- `Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (got ${availableTxs} txs but needs ${minTxs})`,
1061
+ this.log.verbose(
1062
+ `Not enough age-eligible txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (needs ${minTxs} eligible)`,
1044
1063
  {
1045
1064
  reason: 'insufficient_txs',
1046
1065
  blockNumber,
1047
1066
  slot: this.targetSlot,
1048
1067
  checkpointNumber: this.checkpointNumber,
1049
1068
  indexWithinCheckpoint,
1050
- availableTxs,
1051
1069
  minTxs,
1052
1070
  },
1053
1071
  );
1054
- this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.targetSlot });
1072
+ this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, slot: this.targetSlot });
1055
1073
  this.metrics.recordBlockProposalFailed('insufficient_txs');
1056
1074
  return { failure: 'insufficient-txs' };
1057
1075
  }
1058
1076
 
1059
1077
  // Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
1060
1078
  // just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
1079
+ // Block building only executes txs, so we skip loading their proofs unless these same tx objects get attached
1080
+ // to the broadcasted proposals via publishTxsWithProposals.
1061
1081
  const pendingTxs = filter(
1062
- this.p2pClient.iterateEligiblePendingTxs(),
1082
+ this.p2pClient.iterateEligiblePendingTxs({ includeProof: !!this.config.publishTxsWithProposals }),
1063
1083
  tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
1064
1084
  );
1065
1085
 
1066
- this.log.debug(
1067
- `Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot} with ${availableTxs} available txs`,
1068
- { slot: this.targetSlot, blockNumber, indexWithinCheckpoint },
1069
- );
1070
- this.setStateFn(SequencerState.CREATING_BLOCK, this.targetSlot, this.slotNow);
1086
+ this.log.debug(`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot}`, {
1087
+ slot: this.targetSlot,
1088
+ blockNumber,
1089
+ indexWithinCheckpoint,
1090
+ });
1091
+ this.setState(SequencerState.CREATING_BLOCK);
1071
1092
 
1072
1093
  // Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
1073
1094
  // by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
@@ -1082,8 +1103,9 @@ export class CheckpointProposalJob implements Traceable {
1082
1103
  deadline: buildDeadline,
1083
1104
  isBuildingProposal: true,
1084
1105
  minValidTxs,
1085
- maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
1106
+ maxBlocksPerCheckpoint: this.timetable.getMaxBlocksPerCheckpoint(),
1086
1107
  perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
1108
+ perBlockDAAllocationMultiplier: this.config.perBlockDAAllocationMultiplier,
1087
1109
  };
1088
1110
 
1089
1111
  // Actually build the block by executing txs. The builder throws InsufficientValidTxsError
@@ -1157,7 +1179,7 @@ export class CheckpointProposalJob implements Traceable {
1157
1179
  checkpointNumber: this.checkpointNumber,
1158
1180
  indexWithinCheckpoint: block.indexWithinCheckpoint,
1159
1181
  slot: this.targetSlot,
1160
- buildSlot: this.slotNow,
1182
+ buildSlot: this.getBuildSlot(),
1161
1183
  });
1162
1184
  this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe(), this.targetSlot);
1163
1185
 
@@ -1217,44 +1239,43 @@ export class CheckpointProposalJob implements Traceable {
1217
1239
  blockNumber: BlockNumber;
1218
1240
  indexWithinCheckpoint: IndexWithinCheckpoint;
1219
1241
  buildDeadline: Date | undefined;
1220
- }): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
1242
+ }): Promise<{ canStartBuilding: boolean; minTxs: number }> {
1221
1243
  const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
1222
1244
 
1223
1245
  // We only allow a block with 0 txs in the first block of the checkpoint
1224
1246
  const minTxs = indexWithinCheckpoint > 0 && this.config.minTxsPerBlock === 0 ? 1 : this.config.minTxsPerBlock;
1225
1247
 
1226
- // Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
1248
+ // Latest time to keep waiting for txs: wait_for_txs_deadline = block_build_deadline(k) - min_block_duration.
1227
1249
  const startBuildingDeadline = buildDeadline
1228
- ? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000)
1250
+ ? new Date(buildDeadline.getTime() - this.timetable.minBlockDuration * 1000)
1229
1251
  : undefined;
1230
1252
 
1231
- let availableTxs = await this.p2pClient.getPendingTxCount();
1232
-
1233
- while (!forceCreate && availableTxs < minTxs) {
1253
+ // Gate on age-eligible txs so we don't start building on txs the builder would then filter out for being
1254
+ // too fresh. hasEligiblePendingTxs early-exits once minTxs are eligible instead of counting the whole pool.
1255
+ while (!forceCreate && !(await this.p2pClient.hasEligiblePendingTxs(minTxs))) {
1234
1256
  // If we're past deadline, or we have no deadline, give up
1235
1257
  const now = this.dateProvider.nowAsDate();
1236
1258
  if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
1237
- return { canStartBuilding: false, availableTxs, minTxs };
1259
+ return { canStartBuilding: false, minTxs };
1238
1260
  }
1239
1261
 
1240
1262
  // Wait a bit before checking again
1241
- this.setStateFn(SequencerState.WAITING_FOR_TXS, this.targetSlot, this.slotNow);
1263
+ this.setState(SequencerState.WAITING_FOR_TXS);
1242
1264
  this.log.verbose(
1243
- `Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (have ${availableTxs} but need ${minTxs})`,
1244
- { blockNumber, slot: this.targetSlot, indexWithinCheckpoint },
1265
+ `Waiting for ${minTxs} age-eligible txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot}`,
1266
+ { blockNumber, slot: this.targetSlot, indexWithinCheckpoint, minTxs },
1245
1267
  );
1246
1268
  await this.waitForTxsPollingInterval();
1247
- availableTxs = await this.p2pClient.getPendingTxCount();
1248
1269
  }
1249
1270
 
1250
- return { canStartBuilding: true, availableTxs, minTxs };
1271
+ return { canStartBuilding: true, minTxs };
1251
1272
  }
1252
1273
 
1253
1274
  private async getSignedCommitteeAttestations(
1254
1275
  broadcast: CheckpointProposalBroadcast,
1255
1276
  ): Promise<{ attestations: CommitteeAttestationsAndSigners; attestationsSignature: Signature } | undefined> {
1256
1277
  const { proposal, blockProposedAt } = broadcast;
1257
- this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot, this.slotNow);
1278
+ this.setState(SequencerState.COLLECTING_ATTESTATIONS);
1258
1279
  const attestations = await this.waitForAttestations(proposal);
1259
1280
  if (!attestations) {
1260
1281
  return undefined;
@@ -1319,12 +1340,14 @@ export class CheckpointProposalJob implements Traceable {
1319
1340
  );
1320
1341
  }
1321
1342
 
1322
- const attestationTimeAllowed = this.config.enforceTimeTable
1323
- ? this.timetable.getCheckpointAttestationDeadline()
1324
- : this.l1Constants.slotDuration;
1325
- const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
1343
+ // Hard attestation-collection cutoff = the single consensus attestation_deadline (target_slot_start + S - 2E).
1344
+ const attestationDeadlineSeconds = this.timetable.getAttestationDeadline(this.targetSlot);
1345
+ const attestationDeadline = new Date(attestationDeadlineSeconds * 1000);
1326
1346
 
1327
- this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
1347
+ this.metrics.recordRequiredAttestations(
1348
+ numberOfRequiredAttestations,
1349
+ Math.max(0, attestationDeadline.getTime() - this.dateProvider.now()),
1350
+ );
1328
1351
 
1329
1352
  const collectAttestationsTimer = new Timer();
1330
1353
  let collectedAttestationsCount: number = 0;
@@ -1361,6 +1384,7 @@ export class CheckpointProposalJob implements Traceable {
1361
1384
  this.config.injectFakeAttestation ||
1362
1385
  this.config.injectHighSValueAttestation ||
1363
1386
  this.config.injectUnrecoverableSignatureAttestation ||
1387
+ this.config.injectYParityAttestation ||
1364
1388
  this.config.shuffleAttestationOrdering
1365
1389
  ) {
1366
1390
  return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
@@ -1462,6 +1486,19 @@ export class CheckpointProposalJob implements Traceable {
1462
1486
  return new CommitteeAttestationsAndSigners(attestations, this.getSignatureContext());
1463
1487
  }
1464
1488
 
1489
+ if (this.config.injectYParityAttestation) {
1490
+ // Force every non-proposer signed slot's recovery byte to yParity (v ∈ {0, 1}) form in the packed L1
1491
+ // tuple, after packAttestations has canonicalized it. The proposer's own slot is left canonical so
1492
+ // propose() still passes verifyProposer. Models a malicious proposer landing a checkpoint L1 accepts
1493
+ // but that can never be proven (ECDSA.recover rejects v ∉ {27, 28}).
1494
+ this.log.warn(`Injecting yParity attestations in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
1495
+ return new MaliciousYParityCommitteeAttestationsAndSigners(
1496
+ attestations,
1497
+ proposerIndex,
1498
+ this.getSignatureContext(),
1499
+ );
1500
+ }
1501
+
1465
1502
  if (this.config.shuffleAttestationOrdering) {
1466
1503
  this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
1467
1504
 
@@ -1604,29 +1641,11 @@ export class CheckpointProposalJob implements Traceable {
1604
1641
  return false;
1605
1642
  }
1606
1643
 
1607
- /** Waits until a specific time within the current slot */
1608
- @trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
1609
- protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
1610
- const slotStartTimestamp = this.getSlotStartBuildTimestamp();
1611
- const targetTimestamp = slotStartTimestamp + targetSecondsIntoSlot;
1612
- const delayMs = targetTimestamp * 1000 - this.dateProvider.nowAsDate().getTime();
1613
- await this.awaitInterruptibleSleep(delayMs);
1614
- }
1615
-
1616
- /** Pause between mempool polls in waitForMinTxs. Extracted for test overriding. */
1644
+ /** Waits the polling interval for transactions. Extracted for test overriding. */
1617
1645
  protected async waitForTxsPollingInterval(): Promise<void> {
1618
1646
  await this.awaitInterruptibleSleep(TXS_POLLING_MS);
1619
1647
  }
1620
1648
 
1621
- private getSlotStartBuildTimestamp(): number {
1622
- return getSlotStartBuildTimestamp(this.slotNow, this.l1Constants);
1623
- }
1624
-
1625
- private getSecondsIntoSlot(): number {
1626
- const slotStartTimestamp = this.getSlotStartBuildTimestamp();
1627
- return Number((this.dateProvider.now() / 1000 - slotStartTimestamp).toFixed(3));
1628
- }
1629
-
1630
1649
  public getPublisher() {
1631
1650
  return this.publisher;
1632
1651
  }
@@ -5,12 +5,24 @@ import type { Action } from '../publisher/sequencer-publisher.js';
5
5
  import type { SequencerState } from './utils.js';
6
6
 
7
7
  export type SequencerEvents = {
8
+ /**
9
+ * Emitted on every sequencer state transition (including no-op transitions to the same state). The
10
+ * timing fields are anchored to the build frame of the slot being proposed for, not to wall-clock
11
+ * slot boundaries, because the proposer builds for `targetSlot` during the previous (build) slot.
12
+ *
13
+ * - `oldState` / `newState` are the previous and new {@link SequencerState}.
14
+ * - `secondsIntoBuildFrame` is the wall-clock seconds elapsed since the build-frame start of
15
+ * `targetSlot` (`now − getBuildFrameStart(targetSlot)`). Undefined for lifecycle states with no
16
+ * associated slot (e.g. IDLE/STOPPING). It can be negative if the transition happens before the
17
+ * build frame opens.
18
+ * - `targetSlot` is the slot the checkpoint is being proposed for (the submission slot, one ahead of
19
+ * the wall-clock build slot under pipelining). Undefined for lifecycle states with no slot.
20
+ */
8
21
  ['state-changed']: (args: {
9
22
  oldState: SequencerState;
10
23
  newState: SequencerState;
11
- secondsIntoSlot?: number;
12
- slot?: SlotNumber;
13
- timeReferenceSlot?: SlotNumber;
24
+ secondsIntoBuildFrame?: number;
25
+ targetSlot?: SlotNumber;
14
26
  }) => void;
15
27
  /**
16
28
  * Emitted by the sequencer once it has decided it is going to attempt to build a
@@ -37,7 +49,7 @@ export type SequencerEvents = {
37
49
  simulatedPending: CheckpointNumber | undefined;
38
50
  }) => void;
39
51
  ['proposer-rollup-check-failed']: (args: { reason: string; slot: SlotNumber }) => void;
40
- ['block-tx-count-check-failed']: (args: { minTxs: number; availableTxs: number; slot: SlotNumber }) => void;
52
+ ['block-tx-count-check-failed']: (args: { minTxs: number; slot: SlotNumber }) => void;
41
53
  ['block-build-failed']: (args: { reason: string; slot: SlotNumber }) => void;
42
54
  ['block-proposed']: (args: {
43
55
  blockNumber: BlockNumber;
@@ -1,5 +1,3 @@
1
- export * from './automine/automine_factory.js';
2
- export * from './automine/automine_sequencer.js';
3
1
  export * from './checkpoint_proposal_job.js';
4
2
  export * from './checkpoint_voter.js';
5
3
  export * from './config.js';
@@ -0,0 +1,43 @@
1
+ /** A tracked request: the awaitable to wait on, plus an optional interrupt to cancel it early. */
2
+ type TrackedRequest = { promise: Promise<unknown>; interrupt?: () => void };
3
+
4
+ /**
5
+ * Tracks fire-and-forget requests so a shutdown path can interrupt any in-flight work and await it to
6
+ * settle. Each tracked request is a promise plus an optional interrupt callback; the entry removes itself
7
+ * from the bag once its promise settles, so the tracker only ever holds requests that are still in flight.
8
+ */
9
+ export class RequestsTracker {
10
+ private readonly requests = new Map<number, TrackedRequest>();
11
+ private nextId = 0;
12
+
13
+ /** Number of in-flight requests currently tracked. */
14
+ public get size(): number {
15
+ return this.requests.size;
16
+ }
17
+
18
+ /**
19
+ * Tracks a fire-and-forget request. `interrupt`, when provided, is invoked by {@link interruptRequests}
20
+ * to cancel the in-flight work so its promise settles promptly. The entry auto-removes once `promise`
21
+ * settles, whether it resolves or rejects.
22
+ */
23
+ public trackRequest(promise: Promise<unknown>, interrupt?: () => void): void {
24
+ const id = this.nextId++;
25
+ const settled = promise.then(
26
+ () => this.requests.delete(id),
27
+ () => this.requests.delete(id),
28
+ );
29
+ this.requests.set(id, { promise: settled, interrupt });
30
+ }
31
+
32
+ /** Interrupts every in-flight request that was tracked with an interrupt callback. */
33
+ public interruptRequests(): void {
34
+ for (const { interrupt } of this.requests.values()) {
35
+ interrupt?.();
36
+ }
37
+ }
38
+
39
+ /** Awaits every currently in-flight request to settle. */
40
+ public async awaitRequests(): Promise<void> {
41
+ await Promise.all([...this.requests.values()].map(request => request.promise));
42
+ }
43
+ }