@aztec/sequencer-client 0.0.1-commit.db765a8 → 0.0.1-commit.df81a97b5

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 (54) hide show
  1. package/dest/client/sequencer-client.d.ts +4 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +46 -23
  4. package/dest/config.d.ts +25 -5
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +21 -12
  7. package/dest/global_variable_builder/global_builder.d.ts +13 -7
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +22 -21
  10. package/dest/global_variable_builder/index.d.ts +2 -2
  11. package/dest/global_variable_builder/index.d.ts.map +1 -1
  12. package/dest/publisher/config.d.ts +13 -1
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +17 -2
  15. package/dest/publisher/sequencer-publisher-factory.d.ts +3 -3
  16. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  17. package/dest/publisher/sequencer-publisher-factory.js +2 -2
  18. package/dest/publisher/sequencer-publisher.d.ts +9 -4
  19. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  20. package/dest/publisher/sequencer-publisher.js +33 -9
  21. package/dest/sequencer/checkpoint_proposal_job.d.ts +13 -7
  22. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  23. package/dest/sequencer/checkpoint_proposal_job.js +166 -115
  24. package/dest/sequencer/events.d.ts +2 -1
  25. package/dest/sequencer/events.d.ts.map +1 -1
  26. package/dest/sequencer/metrics.d.ts +5 -1
  27. package/dest/sequencer/metrics.d.ts.map +1 -1
  28. package/dest/sequencer/metrics.js +11 -0
  29. package/dest/sequencer/sequencer.d.ts +11 -8
  30. package/dest/sequencer/sequencer.d.ts.map +1 -1
  31. package/dest/sequencer/sequencer.js +71 -61
  32. package/dest/sequencer/timetable.d.ts +4 -3
  33. package/dest/sequencer/timetable.d.ts.map +1 -1
  34. package/dest/sequencer/timetable.js +6 -7
  35. package/dest/sequencer/types.d.ts +2 -2
  36. package/dest/sequencer/types.d.ts.map +1 -1
  37. package/dest/test/mock_checkpoint_builder.d.ts +7 -9
  38. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  39. package/dest/test/mock_checkpoint_builder.js +39 -30
  40. package/package.json +27 -28
  41. package/src/client/sequencer-client.ts +56 -21
  42. package/src/config.ts +28 -14
  43. package/src/global_variable_builder/global_builder.ts +22 -23
  44. package/src/global_variable_builder/index.ts +1 -1
  45. package/src/publisher/config.ts +32 -0
  46. package/src/publisher/sequencer-publisher-factory.ts +3 -3
  47. package/src/publisher/sequencer-publisher.ts +39 -11
  48. package/src/sequencer/checkpoint_proposal_job.ts +219 -131
  49. package/src/sequencer/events.ts +1 -1
  50. package/src/sequencer/metrics.ts +14 -0
  51. package/src/sequencer/sequencer.ts +97 -68
  52. package/src/sequencer/timetable.ts +7 -7
  53. package/src/sequencer/types.ts +1 -1
  54. package/src/test/mock_checkpoint_builder.ts +51 -48
@@ -1,5 +1,3 @@
1
- import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
2
- import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
3
1
  import type { EpochCache } from '@aztec/epoch-cache';
4
2
  import {
5
3
  BlockNumber,
@@ -32,17 +30,22 @@ import {
32
30
  type L2BlockSource,
33
31
  MaliciousCommitteeAttestationsAndSigners,
34
32
  } from '@aztec/stdlib/block';
35
- import type { Checkpoint } from '@aztec/stdlib/checkpoint';
36
- import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
33
+ import { type Checkpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
34
+ import { getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
37
35
  import { Gas } from '@aztec/stdlib/gas';
38
36
  import {
39
- NoValidTxsError,
40
- type PublicProcessorLimits,
37
+ type BlockBuilderOptions,
38
+ InsufficientValidTxsError,
41
39
  type ResolvedSequencerConfig,
42
40
  type WorldStateSynchronizer,
43
41
  } from '@aztec/stdlib/interfaces/server';
44
42
  import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
45
- import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
43
+ import type {
44
+ BlockProposal,
45
+ BlockProposalOptions,
46
+ CheckpointProposal,
47
+ CheckpointProposalOptions,
48
+ } from '@aztec/stdlib/p2p';
46
49
  import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
47
50
  import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
48
51
  import { type FailedTx, Tx } from '@aztec/stdlib/tx';
@@ -74,8 +77,10 @@ export class CheckpointProposalJob implements Traceable {
74
77
  protected readonly log: Logger;
75
78
 
76
79
  constructor(
77
- private readonly epoch: EpochNumber,
78
- private readonly slot: SlotNumber,
80
+ private readonly slotNow: SlotNumber,
81
+ private readonly targetSlot: SlotNumber,
82
+ private readonly epochNow: EpochNumber,
83
+ private readonly targetEpoch: EpochNumber,
79
84
  private readonly checkpointNumber: CheckpointNumber,
80
85
  private readonly syncedToBlockNumber: BlockNumber,
81
86
  // TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
@@ -103,7 +108,20 @@ export class CheckpointProposalJob implements Traceable {
103
108
  public readonly tracer: Tracer,
104
109
  bindings?: LoggerBindings,
105
110
  ) {
106
- this.log = createLogger('sequencer:checkpoint-proposal', { ...bindings, instanceId: `slot-${slot}` });
111
+ this.log = createLogger('sequencer:checkpoint-proposal', {
112
+ ...bindings,
113
+ instanceId: `slot-${this.slotNow}`,
114
+ });
115
+ }
116
+
117
+ /** The wall-clock slot during which the proposer builds. */
118
+ private get slot(): SlotNumber {
119
+ return this.slotNow;
120
+ }
121
+
122
+ /** The wall-clock epoch. */
123
+ private get epoch(): EpochNumber {
124
+ return this.epochNow;
107
125
  }
108
126
 
109
127
  /**
@@ -116,7 +134,7 @@ export class CheckpointProposalJob implements Traceable {
116
134
  // In fisherman mode, we simulate slashing but don't actually publish to L1
117
135
  // These are constant for the whole slot, so we only enqueue them once
118
136
  const votesPromises = new CheckpointVoter(
119
- this.slot,
137
+ this.targetSlot,
120
138
  this.publisher,
121
139
  this.attestorAddress,
122
140
  this.validatorClient,
@@ -143,6 +161,29 @@ export class CheckpointProposalJob implements Traceable {
143
161
  return;
144
162
  }
145
163
 
164
+ // If pipelining, wait until the submission slot so L1 recognizes the pipelined proposer
165
+ if (this.epochCache.isProposerPipeliningEnabled()) {
166
+ const submissionSlotTimestamp =
167
+ getTimestampForSlot(this.targetSlot, this.l1Constants) - BigInt(this.l1Constants.ethereumSlotDuration);
168
+ this.log.info(`Waiting until submission slot ${this.targetSlot} for L1 submission`, {
169
+ slot: this.slot,
170
+ submissionSlot: this.targetSlot,
171
+ submissionSlotTimestamp,
172
+ });
173
+ await sleepUntil(new Date(Number(submissionSlotTimestamp) * 1000), this.dateProvider.nowAsDate());
174
+
175
+ // After waking, verify the parent checkpoint wasn't pruned during the sleep.
176
+ // We check L1's pending tip directly instead of canProposeAt, which also validates the proposer
177
+ // identity and would fail because the timestamp resolves to a different slot's proposer.
178
+ const l1Tips = await this.publisher.rollupContract.getTips();
179
+ if (l1Tips.pending < this.checkpointNumber - 1) {
180
+ this.log.warn(
181
+ `Parent checkpoint was pruned during pipelining sleep (L1 pending=${l1Tips.pending}, expected>=${this.checkpointNumber - 1}), skipping L1 submission for checkpoint ${this.checkpointNumber}`,
182
+ );
183
+ return undefined;
184
+ }
185
+ }
186
+
146
187
  // Then send everything to L1
147
188
  const l1Response = await this.publisher.sendRequests();
148
189
  const proposedAction = l1Response?.successfulActions.find(a => a === 'propose');
@@ -161,7 +202,7 @@ export class CheckpointProposalJob implements Traceable {
161
202
  return {
162
203
  // nullish operator needed for tests
163
204
  [Attributes.COINBASE]: this.validatorClient.getCoinbaseForAttestor(this.attestorAddress)?.toString(),
164
- [Attributes.SLOT_NUMBER]: this.slot,
205
+ [Attributes.SLOT_NUMBER]: this.targetSlot,
165
206
  };
166
207
  })
167
208
  private async proposeCheckpoint(): Promise<Checkpoint | undefined> {
@@ -171,8 +212,15 @@ export class CheckpointProposalJob implements Traceable {
171
212
  const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
172
213
 
173
214
  // Start the checkpoint
174
- this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.slot);
175
- this.metrics.incOpenSlot(this.slot, this.proposer?.toString() ?? 'unknown');
215
+ this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot);
216
+ this.log.info(`Starting checkpoint proposal`, {
217
+ buildSlot: this.slot,
218
+ submissionSlot: this.targetSlot,
219
+ pipelining: this.epochCache.isProposerPipeliningEnabled(),
220
+ proposer: this.proposer?.toString(),
221
+ coinbase: coinbase.toString(),
222
+ });
223
+ this.metrics.incOpenSlot(this.targetSlot, this.proposer?.toString() ?? 'unknown');
176
224
 
177
225
  // Enqueues checkpoint invalidation (constant for the whole slot)
178
226
  if (this.invalidateCheckpoint && !this.config.skipInvalidateBlockAsProposer) {
@@ -183,7 +231,7 @@ export class CheckpointProposalJob implements Traceable {
183
231
  const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(
184
232
  coinbase,
185
233
  feeRecipient,
186
- this.slot,
234
+ this.targetSlot,
187
235
  );
188
236
 
189
237
  // Collect L1 to L2 messages for the checkpoint and compute their hash
@@ -191,7 +239,7 @@ export class CheckpointProposalJob implements Traceable {
191
239
  const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
192
240
 
193
241
  // Collect the out hashes of all the checkpoints before this one in the same epoch
194
- const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.epoch))
242
+ const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.targetEpoch))
195
243
  .filter(c => c.checkpointNumber < this.checkpointNumber)
196
244
  .map(c => c.checkpointOutHash);
197
245
 
@@ -248,8 +296,8 @@ export class CheckpointProposalJob implements Traceable {
248
296
  }
249
297
 
250
298
  if (blocksInCheckpoint.length === 0) {
251
- this.log.warn(`No blocks were built for slot ${this.slot}`, { slot: this.slot });
252
- this.eventEmitter.emit('checkpoint-empty', { slot: this.slot });
299
+ this.log.warn(`No blocks were built for slot ${this.targetSlot}`, { slot: this.targetSlot });
300
+ this.eventEmitter.emit('checkpoint-empty', { slot: this.targetSlot });
253
301
  return undefined;
254
302
  }
255
303
 
@@ -257,16 +305,33 @@ export class CheckpointProposalJob implements Traceable {
257
305
  if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
258
306
  this.log.warn(
259
307
  `Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
260
- { slot: this.slot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
308
+ { slot: this.targetSlot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
261
309
  );
262
310
  return undefined;
263
311
  }
264
312
 
265
313
  // Assemble and broadcast the checkpoint proposal, including the last block that was not
266
314
  // broadcasted yet, and wait to collect the committee attestations.
267
- this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
315
+ this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.targetSlot);
268
316
  const checkpoint = await checkpointBuilder.completeCheckpoint();
269
317
 
318
+ // Final validation: per-block limits are only checked if the operator set them explicitly.
319
+ // Otherwise, checkpoint-level budgets were already enforced by the redistribution logic.
320
+ try {
321
+ validateCheckpoint(checkpoint, {
322
+ rollupManaLimit: this.l1Constants.rollupManaLimit,
323
+ maxL2BlockGas: this.config.maxL2BlockGas,
324
+ maxDABlockGas: this.config.maxDABlockGas,
325
+ maxTxsPerBlock: this.config.maxTxsPerBlock,
326
+ maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
327
+ });
328
+ } catch (err) {
329
+ this.log.error(`Built an invalid checkpoint at slot ${this.slot} (skipping proposal)`, err, {
330
+ checkpoint: checkpoint.header.toInspect(),
331
+ });
332
+ return undefined;
333
+ }
334
+
270
335
  // Record checkpoint-level build metrics
271
336
  this.metrics.recordCheckpointBuild(
272
337
  checkpointBuildTimer.ms(),
@@ -278,10 +343,10 @@ export class CheckpointProposalJob implements Traceable {
278
343
  // Do not collect attestations nor publish to L1 in fisherman mode
279
344
  if (this.config.fishermanMode) {
280
345
  this.log.info(
281
- `Built checkpoint for slot ${this.slot} with ${blocksInCheckpoint.length} blocks. ` +
346
+ `Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` +
282
347
  `Skipping proposal in fisherman mode.`,
283
348
  {
284
- slot: this.slot,
349
+ slot: this.targetSlot,
285
350
  checkpoint: checkpoint.header.toInspect(),
286
351
  blocksBuilt: blocksInCheckpoint.length,
287
352
  },
@@ -310,7 +375,7 @@ export class CheckpointProposalJob implements Traceable {
310
375
  const blockProposedAt = this.dateProvider.now();
311
376
  await this.p2pClient.broadcastCheckpointProposal(proposal);
312
377
 
313
- this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.slot);
378
+ this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
314
379
  const attestations = await this.waitForAttestations(proposal);
315
380
  const blockAttestedAt = this.dateProvider.now();
316
381
 
@@ -323,7 +388,7 @@ export class CheckpointProposalJob implements Traceable {
323
388
  attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
324
389
  attestations,
325
390
  signer,
326
- this.slot,
391
+ this.targetSlot,
327
392
  this.checkpointNumber,
328
393
  );
329
394
  } catch (err) {
@@ -336,10 +401,10 @@ export class CheckpointProposalJob implements Traceable {
336
401
  }
337
402
 
338
403
  // Enqueue publishing the checkpoint to L1
339
- this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.slot);
404
+ this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot);
340
405
  const aztecSlotDuration = this.l1Constants.slotDuration;
341
- const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
342
- const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
406
+ const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
407
+ const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
343
408
 
344
409
  // If we have been configured to potentially skip publishing checkpoint then roll the dice here
345
410
  if (
@@ -389,9 +454,6 @@ export class CheckpointProposalJob implements Traceable {
389
454
  const txHashesAlreadyIncluded = new Set<string>();
390
455
  const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
391
456
 
392
- // Remaining blob fields available for blocks (checkpoint end marker already subtracted)
393
- let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
394
-
395
457
  // Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
396
458
  let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
397
459
 
@@ -405,7 +467,7 @@ export class CheckpointProposalJob implements Traceable {
405
467
 
406
468
  if (!timingInfo.canStart) {
407
469
  this.log.debug(`Not enough time left in slot to start another block`, {
408
- slot: this.slot,
470
+ slot: this.targetSlot,
409
471
  blocksBuilt,
410
472
  secondsIntoSlot,
411
473
  });
@@ -424,7 +486,6 @@ export class CheckpointProposalJob implements Traceable {
424
486
  blockNumber,
425
487
  indexWithinCheckpoint,
426
488
  txHashesAlreadyIncluded,
427
- remainingBlobFields,
428
489
  });
429
490
 
430
491
  // TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
@@ -441,8 +502,8 @@ export class CheckpointProposalJob implements Traceable {
441
502
  } else if ('error' in buildResult) {
442
503
  // If there was an error building the block, just exit the loop and give up the rest of the slot
443
504
  if (!(buildResult.error instanceof SequencerInterruptedError)) {
444
- this.log.warn(`Halting block building for slot ${this.slot}`, {
445
- slot: this.slot,
505
+ this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
506
+ slot: this.targetSlot,
446
507
  blocksBuilt,
447
508
  error: buildResult.error,
448
509
  });
@@ -450,26 +511,16 @@ export class CheckpointProposalJob implements Traceable {
450
511
  break;
451
512
  }
452
513
 
453
- const { block, usedTxs, remainingBlobFields: newRemainingBlobFields } = buildResult;
514
+ const { block, usedTxs } = buildResult;
454
515
  blocksInCheckpoint.push(block);
455
-
456
- // Update remaining blob fields for the next block
457
- remainingBlobFields = newRemainingBlobFields;
458
-
459
- // Sync the proposed block to the archiver to make it available
460
- // Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
461
- // Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
462
- // Fire and forget - don't block the critical path, but log errors
463
- this.syncProposedBlockToArchiver(block).catch(err => {
464
- this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
465
- });
466
-
467
516
  usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
468
517
 
469
- // If this is the last block, exit the loop now so we start collecting attestations
518
+ // If this is the last block, sync it to the archiver and exit the loop
519
+ // so we can build the checkpoint and start collecting attestations.
470
520
  if (timingInfo.isLastBlock) {
471
- this.log.verbose(`Completed final block ${blockNumber} for slot ${this.slot}`, {
472
- slot: this.slot,
521
+ await this.syncProposedBlockToArchiver(block);
522
+ this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
523
+ slot: this.targetSlot,
473
524
  blockNumber,
474
525
  blocksBuilt,
475
526
  });
@@ -477,38 +528,61 @@ export class CheckpointProposalJob implements Traceable {
477
528
  break;
478
529
  }
479
530
 
480
- // For non-last blocks, broadcast the block proposal (unless we're in fisherman mode)
481
- // If the block is the last one, we'll broadcast it along with the checkpoint at the end of the loop
482
- if (!this.config.fishermanMode) {
483
- const proposal = await this.validatorClient.createBlockProposal(
484
- block.header,
485
- block.indexWithinCheckpoint,
486
- inHash,
487
- block.archive.root,
488
- usedTxs,
489
- this.proposer,
490
- blockProposalOptions,
491
- );
492
- await this.p2pClient.broadcastProposal(proposal);
493
- }
531
+ // Broadcast the block proposal (unless we're in fisherman mode) unless the block is the last one,
532
+ // in which case we'll broadcast it along with the checkpoint at the end of the loop.
533
+ // Note that we only send the block to the archiver if we manage to create the proposal, so if there's
534
+ // a HA error we don't pollute our archiver with a block that won't make it to the chain.
535
+ const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
536
+
537
+ // Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal.
538
+ // We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
539
+ // If this throws, we abort the entire checkpoint.
540
+ await this.syncProposedBlockToArchiver(block);
541
+
542
+ // Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
543
+ proposal && (await this.p2pClient.broadcastProposal(proposal));
494
544
 
495
545
  // Wait until the next block's start time
496
546
  await this.waitUntilNextSubslot(timingInfo.deadline);
497
547
  }
498
548
 
499
- this.log.verbose(`Block building loop completed for slot ${this.slot}`, {
500
- slot: this.slot,
549
+ this.log.verbose(`Block building loop completed for slot ${this.targetSlot}`, {
550
+ slot: this.targetSlot,
501
551
  blocksBuilt: blocksInCheckpoint.length,
502
552
  });
503
553
 
504
554
  return { blocksInCheckpoint, blockPendingBroadcast };
505
555
  }
506
556
 
557
+ /** Creates a block proposal for a given block via the validator client (unless in fisherman mode) */
558
+ private createBlockProposal(
559
+ block: L2Block,
560
+ inHash: Fr,
561
+ usedTxs: Tx[],
562
+ blockProposalOptions: BlockProposalOptions,
563
+ ): Promise<BlockProposal | undefined> {
564
+ if (this.config.fishermanMode) {
565
+ this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`);
566
+ return Promise.resolve(undefined);
567
+ }
568
+ return this.validatorClient.createBlockProposal(
569
+ block.header,
570
+ block.indexWithinCheckpoint,
571
+ inHash,
572
+ block.archive.root,
573
+ usedTxs,
574
+ this.proposer,
575
+ blockProposalOptions,
576
+ );
577
+ }
578
+
507
579
  /** Sleeps until it is time to produce the next block in the slot */
508
580
  @trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
509
581
  private async waitUntilNextSubslot(nextSubslotStart: number) {
510
- this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.slot);
511
- this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, { slot: this.slot });
582
+ this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot);
583
+ this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, {
584
+ slot: this.targetSlot,
585
+ });
512
586
  await this.waitUntilTimeInSlot(nextSubslotStart);
513
587
  }
514
588
 
@@ -523,34 +597,25 @@ export class CheckpointProposalJob implements Traceable {
523
597
  indexWithinCheckpoint: IndexWithinCheckpoint;
524
598
  buildDeadline: Date | undefined;
525
599
  txHashesAlreadyIncluded: Set<string>;
526
- remainingBlobFields: number;
527
600
  },
528
- ): Promise<{ block: L2Block; usedTxs: Tx[]; remainingBlobFields: number } | { error: Error } | undefined> {
529
- const {
530
- blockTimestamp,
531
- forceCreate,
532
- blockNumber,
533
- indexWithinCheckpoint,
534
- buildDeadline,
535
- txHashesAlreadyIncluded,
536
- remainingBlobFields,
537
- } = opts;
601
+ ): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
602
+ const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
603
+ opts;
538
604
 
539
605
  this.log.verbose(
540
- `Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
606
+ `Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot}`,
541
607
  { ...checkpointBuilder.getConstantData(), ...opts },
542
608
  );
543
609
 
544
610
  try {
545
611
  // Wait until we have enough txs to build the block
546
- const minTxs = this.config.minTxsPerBlock;
547
- const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
612
+ const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
548
613
  if (!canStartBuilding) {
549
614
  this.log.warn(
550
- `Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`,
551
- { blockNumber, slot: this.slot, indexWithinCheckpoint },
615
+ `Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (got ${availableTxs} txs but needs ${minTxs})`,
616
+ { blockNumber, slot: this.targetSlot, indexWithinCheckpoint },
552
617
  );
553
- this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.slot });
618
+ this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.targetSlot });
554
619
  this.metrics.recordBlockProposalFailed('insufficient_txs');
555
620
  return undefined;
556
621
  }
@@ -563,24 +628,31 @@ export class CheckpointProposalJob implements Traceable {
563
628
  );
564
629
 
565
630
  this.log.debug(
566
- `Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.slot} with ${availableTxs} available txs`,
567
- { slot: this.slot, blockNumber, indexWithinCheckpoint },
631
+ `Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot} with ${availableTxs} available txs`,
632
+ { slot: this.targetSlot, blockNumber, indexWithinCheckpoint },
568
633
  );
569
- this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
570
-
571
- // Calculate blob fields limit for txs (remaining capacity - this block's end overhead)
572
- const blockEndOverhead = getNumBlockEndBlobFields(indexWithinCheckpoint === 0);
573
- const maxBlobFieldsForTxs = remainingBlobFields - blockEndOverhead;
634
+ this.setStateFn(SequencerState.CREATING_BLOCK, this.targetSlot);
574
635
 
575
- const blockBuilderOptions: PublicProcessorLimits = {
636
+ // Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
637
+ // by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
638
+ // minValidTxs is passed into the builder so it can reject the block *before* updating state.
639
+ const minValidTxs = forceCreate ? 0 : (this.config.minValidTxsPerBlock ?? minTxs);
640
+ const blockBuilderOptions: BlockBuilderOptions = {
576
641
  maxTransactions: this.config.maxTxsPerBlock,
577
- maxBlockSize: this.config.maxBlockSizeInBytes,
578
- maxBlockGas: new Gas(this.config.maxDABlockGas, this.config.maxL2BlockGas),
579
- maxBlobFields: maxBlobFieldsForTxs,
642
+ maxBlockGas:
643
+ this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
644
+ ? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
645
+ : undefined,
580
646
  deadline: buildDeadline,
647
+ isBuildingProposal: true,
648
+ minValidTxs,
649
+ maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
650
+ perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
581
651
  };
582
652
 
583
- // Actually build the block by executing txs
653
+ // Actually build the block by executing txs. The builder throws InsufficientValidTxsError
654
+ // if the number of successfully processed txs is below minValidTxs, ensuring state is not
655
+ // updated for blocks that will be discarded.
584
656
  const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
585
657
  checkpointBuilder,
586
658
  pendingTxs,
@@ -592,22 +664,27 @@ export class CheckpointProposalJob implements Traceable {
592
664
  // If any txs failed during execution, drop them from the mempool so we don't pick them up again
593
665
  await this.dropFailedTxsFromP2P(buildResult.failedTxs);
594
666
 
595
- // Check if we have created a block with enough txs. If there were invalid txs in the pool, or if execution took
596
- // too long, then we may not get to minTxsPerBlock after executing public functions.
597
- const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
598
- const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
599
- if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
667
+ if (buildResult.status === 'insufficient-valid-txs') {
600
668
  this.log.warn(
601
- `Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
602
- { slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint, minValidTxs, buildResult: buildResult.status },
669
+ `Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.targetSlot} has too few valid txs to be proposed`,
670
+ {
671
+ slot: this.targetSlot,
672
+ blockNumber,
673
+ numTxs: buildResult.processedCount,
674
+ indexWithinCheckpoint,
675
+ minValidTxs,
676
+ },
603
677
  );
604
- this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
678
+ this.eventEmitter.emit('block-build-failed', {
679
+ reason: `Insufficient valid txs`,
680
+ slot: this.targetSlot,
681
+ });
605
682
  this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
606
683
  return undefined;
607
684
  }
608
685
 
609
686
  // Block creation succeeded, emit stats and metrics
610
- const { publicGas, block, publicProcessorDuration, usedTxs, usedTxBlobFields, blockBuildDuration } = buildResult;
687
+ const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
611
688
 
612
689
  const blockStats = {
613
690
  eventName: 'l2-block-built',
@@ -618,33 +695,40 @@ export class CheckpointProposalJob implements Traceable {
618
695
 
619
696
  const blockHash = await block.hash();
620
697
  const txHashes = block.body.txEffects.map(tx => tx.txHash);
621
- const manaPerSec = publicGas.l2Gas / (blockBuildDuration / 1000);
698
+ const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
622
699
 
623
700
  this.log.info(
624
- `Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
701
+ `Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot} with ${numTxs} txs`,
625
702
  { blockHash, txHashes, manaPerSec, ...blockStats },
626
703
  );
627
704
 
628
- this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
629
- this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
705
+ this.eventEmitter.emit('block-proposed', {
706
+ blockNumber: block.number,
707
+ slot: this.targetSlot,
708
+ buildSlot: this.slotNow,
709
+ });
710
+ this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
630
711
 
631
- return { block, usedTxs, remainingBlobFields: maxBlobFieldsForTxs - usedTxBlobFields };
712
+ return { block, usedTxs };
632
713
  } catch (err: any) {
633
- this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
634
- this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
714
+ this.eventEmitter.emit('block-build-failed', {
715
+ reason: err.message,
716
+ slot: this.targetSlot,
717
+ });
718
+ this.log.error(`Error building block`, err, { blockNumber, slot: this.targetSlot });
635
719
  this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
636
720
  this.metrics.recordFailedBlock();
637
721
  return { error: err };
638
722
  }
639
723
  }
640
724
 
641
- /** Uses the checkpoint builder to build a block, catching specific txs */
725
+ /** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
642
726
  private async buildSingleBlockWithCheckpointBuilder(
643
727
  checkpointBuilder: CheckpointBuilder,
644
728
  pendingTxs: AsyncIterable<Tx>,
645
729
  blockNumber: BlockNumber,
646
730
  blockTimestamp: bigint,
647
- blockBuilderOptions: PublicProcessorLimits,
731
+ blockBuilderOptions: BlockBuilderOptions,
648
732
  ) {
649
733
  try {
650
734
  const workTimer = new Timer();
@@ -652,8 +736,12 @@ export class CheckpointProposalJob implements Traceable {
652
736
  const blockBuildDuration = workTimer.ms();
653
737
  return { ...result, blockBuildDuration, status: 'success' as const };
654
738
  } catch (err: unknown) {
655
- if (isErrorClass(err, NoValidTxsError)) {
656
- return { failedTxs: err.failedTxs, status: 'no-valid-txs' as const };
739
+ if (isErrorClass(err, InsufficientValidTxsError)) {
740
+ return {
741
+ failedTxs: err.failedTxs,
742
+ processedCount: err.processedCount,
743
+ status: 'insufficient-valid-txs' as const,
744
+ };
657
745
  }
658
746
  throw err;
659
747
  }
@@ -666,7 +754,7 @@ export class CheckpointProposalJob implements Traceable {
666
754
  blockNumber: BlockNumber;
667
755
  indexWithinCheckpoint: IndexWithinCheckpoint;
668
756
  buildDeadline: Date | undefined;
669
- }): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
757
+ }): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
670
758
  const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
671
759
 
672
760
  // We only allow a block with 0 txs in the first block of the checkpoint
@@ -683,20 +771,20 @@ export class CheckpointProposalJob implements Traceable {
683
771
  // If we're past deadline, or we have no deadline, give up
684
772
  const now = this.dateProvider.nowAsDate();
685
773
  if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
686
- return { canStartBuilding: false, availableTxs: availableTxs };
774
+ return { canStartBuilding: false, availableTxs, minTxs };
687
775
  }
688
776
 
689
777
  // Wait a bit before checking again
690
- this.setStateFn(SequencerState.WAITING_FOR_TXS, this.slot);
778
+ this.setStateFn(SequencerState.WAITING_FOR_TXS, this.targetSlot);
691
779
  this.log.verbose(
692
- `Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (have ${availableTxs} but need ${minTxs})`,
693
- { blockNumber, slot: this.slot, indexWithinCheckpoint },
780
+ `Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (have ${availableTxs} but need ${minTxs})`,
781
+ { blockNumber, slot: this.targetSlot, indexWithinCheckpoint },
694
782
  );
695
783
  await this.waitForTxsPollingInterval();
696
784
  availableTxs = await this.p2pClient.getPendingTxCount();
697
785
  }
698
786
 
699
- return { canStartBuilding: true, availableTxs };
787
+ return { canStartBuilding: true, availableTxs, minTxs };
700
788
  }
701
789
 
702
790
  /**
@@ -890,19 +978,19 @@ export class CheckpointProposalJob implements Traceable {
890
978
  private async handleCheckpointEndAsFisherman(checkpoint: Checkpoint | undefined) {
891
979
  // Perform L1 fee analysis before clearing requests
892
980
  // The callback is invoked asynchronously after the next block is mined
893
- const feeAnalysis = await this.publisher.analyzeL1Fees(this.slot, analysis =>
981
+ const feeAnalysis = await this.publisher.analyzeL1Fees(this.targetSlot, analysis =>
894
982
  this.metrics.recordFishermanFeeAnalysis(analysis),
895
983
  );
896
984
 
897
985
  if (checkpoint) {
898
- this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.slot}`, {
986
+ this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.targetSlot}`, {
899
987
  ...checkpoint.toCheckpointInfo(),
900
988
  ...checkpoint.getStats(),
901
989
  feeAnalysisId: feeAnalysis?.id,
902
990
  });
903
991
  } else {
904
- this.log.warn(`Validation block building FAILED for slot ${this.slot}`, {
905
- slot: this.slot,
992
+ this.log.warn(`Validation block building FAILED for slot ${this.targetSlot}`, {
993
+ slot: this.targetSlot,
906
994
  feeAnalysisId: feeAnalysis?.id,
907
995
  });
908
996
  this.metrics.recordCheckpointProposalFailed('block_build_failed');
@@ -916,15 +1004,15 @@ export class CheckpointProposalJob implements Traceable {
916
1004
  */
917
1005
  private handleHASigningError(err: any, errorContext: string): boolean {
918
1006
  if (err instanceof DutyAlreadySignedError) {
919
- this.log.info(`${errorContext} for slot ${this.slot} already signed by another HA node, yielding`, {
920
- slot: this.slot,
1007
+ this.log.info(`${errorContext} for slot ${this.targetSlot} already signed by another HA node, yielding`, {
1008
+ slot: this.targetSlot,
921
1009
  signedByNode: err.signedByNode,
922
1010
  });
923
1011
  return true;
924
1012
  }
925
1013
  if (err instanceof SlashingProtectionError) {
926
- this.log.info(`${errorContext} for slot ${this.slot} blocked by slashing protection, yielding`, {
927
- slot: this.slot,
1014
+ this.log.info(`${errorContext} for slot ${this.targetSlot} blocked by slashing protection, yielding`, {
1015
+ slot: this.targetSlot,
928
1016
  existingMessageHash: err.existingMessageHash,
929
1017
  attemptedMessageHash: err.attemptedMessageHash,
930
1018
  });
@@ -13,7 +13,7 @@ export type SequencerEvents = {
13
13
  ['proposer-rollup-check-failed']: (args: { reason: string; slot: SlotNumber }) => void;
14
14
  ['block-tx-count-check-failed']: (args: { minTxs: number; availableTxs: number; slot: SlotNumber }) => void;
15
15
  ['block-build-failed']: (args: { reason: string; slot: SlotNumber }) => void;
16
- ['block-proposed']: (args: { blockNumber: BlockNumber; slot: SlotNumber }) => void;
16
+ ['block-proposed']: (args: { blockNumber: BlockNumber; slot: SlotNumber; buildSlot: SlotNumber }) => void;
17
17
  ['checkpoint-empty']: (args: { slot: SlotNumber }) => void;
18
18
  ['checkpoint-publish-failed']: (args: {
19
19
  slot: SlotNumber;