@aztec/sequencer-client 0.0.1-commit.2d9cb6034 → 0.0.1-commit.2e20a94

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 (66) hide show
  1. package/dest/client/sequencer-client.d.ts +3 -1
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +3 -4
  4. package/dest/config.d.ts +1 -1
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +8 -11
  7. package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
  8. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
  9. package/dest/global_variable_builder/fee_predictor.js +128 -0
  10. package/dest/global_variable_builder/fee_provider.d.ts +21 -0
  11. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
  12. package/dest/global_variable_builder/fee_provider.js +58 -0
  13. package/dest/global_variable_builder/global_builder.d.ts +4 -9
  14. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  15. package/dest/global_variable_builder/global_builder.js +3 -41
  16. package/dest/global_variable_builder/index.d.ts +3 -1
  17. package/dest/global_variable_builder/index.d.ts.map +1 -1
  18. package/dest/global_variable_builder/index.js +2 -0
  19. package/dest/publisher/config.d.ts +1 -1
  20. package/dest/publisher/config.d.ts.map +1 -1
  21. package/dest/publisher/config.js +2 -2
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +3 -4
  23. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher-factory.d.ts +1 -3
  25. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  26. package/dest/publisher/sequencer-publisher-factory.js +0 -1
  27. package/dest/publisher/sequencer-publisher.d.ts +16 -35
  28. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  29. package/dest/publisher/sequencer-publisher.js +60 -92
  30. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  31. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  32. package/dest/sequencer/chain_state_overrides.js +39 -0
  33. package/dest/sequencer/checkpoint_proposal_job.d.ts +13 -21
  34. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  35. package/dest/sequencer/checkpoint_proposal_job.js +143 -152
  36. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  37. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  38. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  39. package/dest/sequencer/metrics.d.ts +5 -10
  40. package/dest/sequencer/metrics.d.ts.map +1 -1
  41. package/dest/sequencer/metrics.js +12 -20
  42. package/dest/sequencer/sequencer.d.ts +5 -2
  43. package/dest/sequencer/sequencer.d.ts.map +1 -1
  44. package/dest/sequencer/sequencer.js +31 -23
  45. package/dest/sequencer/timetable.d.ts +14 -1
  46. package/dest/sequencer/timetable.d.ts.map +1 -1
  47. package/dest/sequencer/timetable.js +45 -36
  48. package/package.json +27 -27
  49. package/src/client/sequencer-client.ts +5 -7
  50. package/src/config.ts +8 -11
  51. package/src/global_variable_builder/README.md +44 -0
  52. package/src/global_variable_builder/fee_predictor.ts +172 -0
  53. package/src/global_variable_builder/fee_provider.ts +75 -0
  54. package/src/global_variable_builder/global_builder.ts +7 -54
  55. package/src/global_variable_builder/index.ts +2 -0
  56. package/src/publisher/config.ts +6 -4
  57. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
  58. package/src/publisher/sequencer-publisher-factory.ts +0 -3
  59. package/src/publisher/sequencer-publisher.ts +106 -152
  60. package/src/sequencer/README.md +83 -13
  61. package/src/sequencer/chain_state_overrides.ts +87 -0
  62. package/src/sequencer/checkpoint_proposal_job.ts +185 -201
  63. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  64. package/src/sequencer/metrics.ts +15 -25
  65. package/src/sequencer/sequencer.ts +44 -26
  66. package/src/sequencer/timetable.ts +57 -45
@@ -1,5 +1,5 @@
1
1
  import type { EpochCache } from '@aztec/epoch-cache';
2
- import { type FeeHeader, RollupContract } from '@aztec/ethereum/contracts';
2
+ import type { SimulationOverridesPlan } from '@aztec/ethereum/contracts';
3
3
  import {
4
4
  BlockNumber,
5
5
  CheckpointNumber,
@@ -57,6 +57,11 @@ import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validato
57
57
 
58
58
  import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
59
59
  import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
60
+ import {
61
+ buildPipelinedParentSimulationOverridesPlan,
62
+ buildSubmissionSimulationOverridesPlan,
63
+ } from './chain_state_overrides.js';
64
+ import type { CheckpointProposalJobMetricsRecorder } from './checkpoint_proposal_job_metrics.js';
60
65
  import { CheckpointVoter } from './checkpoint_voter.js';
61
66
  import { SequencerInterruptedError } from './errors.js';
62
67
  import type { SequencerEvents } from './events.js';
@@ -68,7 +73,14 @@ import { SequencerState } from './utils.js';
68
73
  /** How much time to sleep while waiting for min transactions to accumulate for a block */
69
74
  const TXS_POLLING_MS = 500;
70
75
 
71
- /** Result from proposeCheckpoint when a checkpoint was successfully built and attested. */
76
+ /** Result from proposeCheckpoint when a checkpoint was successfully built and broadcast. */
77
+ type CheckpointProposalBroadcast = {
78
+ checkpoint: Checkpoint;
79
+ proposal: CheckpointProposal;
80
+ blockProposedAt: number;
81
+ };
82
+
83
+ /** Result after attestation collection and signing, ready for L1 submission. */
72
84
  type CheckpointProposalResult = {
73
85
  checkpoint: Checkpoint;
74
86
  attestations: CommitteeAttestationsAndSigners;
@@ -87,8 +99,8 @@ export class CheckpointProposalJob implements Traceable {
87
99
  /** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */
88
100
  private pendingL1Submission: Promise<void> | undefined;
89
101
 
90
- /** Fee header override computed during proposeCheckpoint, reused in enqueueCheckpointForSubmission. */
91
- private computedForceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
102
+ /** Pipelined parent chain state used while building and later submitting this checkpoint. */
103
+ private pipelinedParentSimulationOverridesPlan?: SimulationOverridesPlan;
92
104
 
93
105
  constructor(
94
106
  private readonly slotNow: SlotNumber,
@@ -116,6 +128,7 @@ export class CheckpointProposalJob implements Traceable {
116
128
  private readonly epochCache: EpochCache,
117
129
  private readonly dateProvider: DateProvider,
118
130
  private readonly metrics: SequencerMetrics,
131
+ private readonly checkpointMetrics: CheckpointProposalJobMetricsRecorder,
119
132
  private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
120
133
  private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
121
134
  public readonly tracer: Tracer,
@@ -136,8 +149,9 @@ export class CheckpointProposalJob implements Traceable {
136
149
 
137
150
  /**
138
151
  * Executes the checkpoint proposal job.
139
- * Builds blocks, collects attestations, enqueues requests, and schedules L1 submission as a
140
- * background task so the work loop can return to IDLE immediately.
152
+ * Builds blocks, assembles checkpoint, and broadcasts the proposal (blocking).
153
+ * Attestation collection, signing, and L1 submission are backgrounded so the
154
+ * work loop can return to IDLE immediately for consecutive slot proposals.
141
155
  * Returns the built checkpoint if successful, undefined otherwise.
142
156
  */
143
157
  @trackSpan('CheckpointProposalJob.execute')
@@ -157,73 +171,99 @@ export class CheckpointProposalJob implements Traceable {
157
171
  this.log,
158
172
  ).enqueueVotes();
159
173
 
160
- // Build and propose the checkpoint. Builds blocks, broadcasts, collects attestations, and signs.
161
- // Does NOT enqueue to L1 yet that happens after the pipeline sleep.
162
- const proposalResult = await this.proposeCheckpoint();
163
- const checkpoint = proposalResult?.checkpoint;
174
+ // Build blocks, assemble checkpoint, and broadcast proposal (BLOCKING).
175
+ // Returns after broadcastattestation collection is deferred.
176
+ const broadcast = await this.proposeCheckpoint();
164
177
 
165
- // Wait until the voting promises have resolved, so all requests are enqueued (not sent)
166
- await Promise.all(votesPromises);
167
-
168
- if (checkpoint) {
169
- this.metrics.recordCheckpointProposalSuccess();
178
+ if (!broadcast) {
179
+ await Promise.all(votesPromises);
180
+ // Still submit votes even without a checkpoint
181
+ if (!this.config.fishermanMode) {
182
+ this.pendingL1Submission = this.publisher.sendRequestsAt(this.dateProvider.nowAsDate()).then(() => {});
183
+ }
184
+ return undefined;
170
185
  }
171
186
 
187
+ const { checkpoint } = broadcast;
188
+ this.metrics.recordCheckpointProposalSuccess();
189
+
172
190
  // Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
173
191
  if (this.config.fishermanMode) {
174
192
  await this.handleCheckpointEndAsFisherman(checkpoint);
175
- return;
193
+ return checkpoint;
176
194
  }
177
195
 
178
- // Enqueue the checkpoint for L1 submission
179
- if (proposalResult) {
196
+ // Background the attestation signing → L1 pipeline so the work loop is unblocked
197
+ this.pendingL1Submission = this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises);
198
+
199
+ // Return the built checkpoint immediately — the work loop is now unblocked
200
+ return checkpoint;
201
+ }
202
+
203
+ /**
204
+ * Background pipeline: collects attestations, signs them, enqueues the checkpoint, and submits to L1.
205
+ * Runs as a fire-and-forget task stored in `pendingL1Submission` so the work loop is unblocked.
206
+ */
207
+ private async waitForAttestationsAndEnqueueSubmissionAsync(
208
+ broadcast: CheckpointProposalBroadcast,
209
+ votesPromises: Promise<unknown>[],
210
+ ): Promise<void> {
211
+ const { checkpoint, proposal, blockProposedAt } = broadcast;
212
+ try {
213
+ await Promise.all(votesPromises);
214
+
215
+ this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
216
+ const attestations = await this.waitForAttestations(proposal);
217
+
218
+ this.checkpointMetrics.recordCheckpointAttestationDelay(this.dateProvider.now() - blockProposedAt);
219
+
220
+ // Proposer must sign over the attestations before pushing them to L1
221
+ const signer = this.proposer ?? this.publisher.getSenderAddress();
222
+ let attestationsSignature: Signature;
180
223
  try {
181
- await this.enqueueCheckpointForSubmission(proposalResult);
224
+ attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
225
+ attestations,
226
+ signer,
227
+ this.targetSlot,
228
+ this.checkpointNumber,
229
+ );
182
230
  } catch (err) {
183
- this.log.error(`Failed to enqueue checkpoint for L1 submission at slot ${this.targetSlot}`, err);
184
- // Continue to sendRequestsAt so votes are still sent
231
+ if (this.handleHASigningError(err, 'Attestations signature')) {
232
+ return;
233
+ }
234
+ throw err;
185
235
  }
186
- }
187
236
 
188
- // Compute the earliest time to submit: pipeline slot start when pipelining, now otherwise.
189
- const submitAfter = this.epochCache.isProposerPipeliningEnabled()
190
- ? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000)
191
- : new Date(this.dateProvider.now());
192
-
193
- // TODO(https://github.com/AztecProtocol/aztec-packages/pull/21250): should discard the pending submission if a reorg occurs underneath
194
-
195
- // Schedule L1 submission in the background so the work loop returns immediately.
196
- // The publisher will sleep until submitAfter, then send the bundled requests.
197
- // The promise is stored so it can be awaited during shutdown.
198
- this.pendingL1Submission = this.publisher
199
- .sendRequestsAt(submitAfter)
200
- .then(async l1Response => {
201
- const proposedAction = l1Response?.successfulActions.find(a => a === 'propose');
202
- if (proposedAction) {
203
- this.eventEmitter.emit('checkpoint-published', { checkpoint: this.checkpointNumber, slot: this.targetSlot });
204
- const coinbase = checkpoint?.header.coinbase;
205
- await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
206
- } else if (checkpoint) {
207
- this.eventEmitter.emit('checkpoint-publish-failed', { ...l1Response, slot: this.targetSlot });
208
-
209
- if (this.epochCache.isProposerPipeliningEnabled()) {
210
- this.metrics.recordPipelineDiscard();
211
- }
212
- }
213
- })
214
- .catch(err => {
215
- this.log.error(`Background L1 submission failed for slot ${this.targetSlot}`, err);
216
- if (checkpoint) {
217
- this.eventEmitter.emit('checkpoint-publish-failed', { slot: this.targetSlot });
218
-
219
- if (this.epochCache.isProposerPipeliningEnabled()) {
220
- this.metrics.recordPipelineDiscard();
221
- }
222
- }
223
- });
237
+ // Enqueue the checkpoint for L1 submission
238
+ await this.enqueueCheckpointForSubmission({ checkpoint, attestations, attestationsSignature });
224
239
 
225
- // Return the built checkpoint immediately the work loop is now unblocked
226
- return checkpoint;
240
+ // Compute the earliest time to submit: pipeline slot start when pipelining, now otherwise.
241
+ const submitAfter = this.epochCache.isProposerPipeliningEnabled()
242
+ ? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000)
243
+ : new Date(this.dateProvider.now());
244
+
245
+ const l1Response = await this.publisher.sendRequestsAt(submitAfter);
246
+ const proposedAction = l1Response?.successfulActions.find(a => a === 'propose');
247
+ if (proposedAction) {
248
+ this.eventEmitter.emit('checkpoint-published', { checkpoint: this.checkpointNumber, slot: this.targetSlot });
249
+ const coinbase = checkpoint.header.coinbase;
250
+ await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
251
+ } else {
252
+ this.eventEmitter.emit('checkpoint-publish-failed', { ...l1Response, slot: this.targetSlot });
253
+ if (this.epochCache.isProposerPipeliningEnabled()) {
254
+ this.metrics.recordPipelineDiscard();
255
+ }
256
+ }
257
+ } catch (err) {
258
+ if (err instanceof SequencerInterruptedError) {
259
+ return;
260
+ }
261
+ this.log.error(`Background attestation/L1 pipeline failed for slot ${this.targetSlot}`, err);
262
+ this.eventEmitter.emit('checkpoint-publish-failed', { slot: this.targetSlot });
263
+ if (this.epochCache.isProposerPipeliningEnabled()) {
264
+ this.metrics.recordPipelineDiscard();
265
+ }
266
+ }
227
267
  }
228
268
 
229
269
  /** Enqueues the checkpoint for L1 submission. Called after pipeline sleep in execute(). */
@@ -249,10 +289,17 @@ export class CheckpointProposalJob implements Traceable {
249
289
  }
250
290
  }
251
291
 
292
+ const isPipelining = this.epochCache.isProposerPipeliningEnabled();
293
+ const submissionSimulationOverridesPlan = buildSubmissionSimulationOverridesPlan({
294
+ pipelinedParentPlan: this.pipelinedParentSimulationOverridesPlan,
295
+ invalidateToPendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
296
+ lastArchiveRoot: checkpoint.header.lastArchiveRoot,
297
+ pipeliningEnabled: isPipelining,
298
+ });
299
+
252
300
  await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
253
301
  txTimeoutAt,
254
- forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
255
- forceProposedFeeHeader: this.computedForceProposedFeeHeader,
302
+ ...(submissionSimulationOverridesPlan ? { simulationOverridesPlan: submissionSimulationOverridesPlan } : {}),
256
303
  });
257
304
  }
258
305
 
@@ -263,8 +310,18 @@ export class CheckpointProposalJob implements Traceable {
263
310
  [Attributes.SLOT_NUMBER]: this.targetSlot,
264
311
  };
265
312
  })
266
- private async proposeCheckpoint(): Promise<CheckpointProposalResult | undefined> {
313
+ private async proposeCheckpoint(): Promise<CheckpointProposalBroadcast | undefined> {
267
314
  try {
315
+ const now = this.dateProvider.now();
316
+ if (this.epochCache.isProposerPipeliningEnabled() && this.proposedCheckpointData) {
317
+ // Measure against the wall-clock slot whose build window we are currently using.
318
+ // In pipelining mode `targetSlot` is intentionally one slot ahead, which makes the
319
+ // target-slot boundary a full slot away from the actual build start time.
320
+ const slotBoundaryMs = Number(getTimestampForSlot(this.slotNow, this.l1Constants)) * 1000;
321
+ this.checkpointMetrics.recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(now - slotBoundaryMs);
322
+ }
323
+ this.checkpointMetrics.startCheckpointTiming(now);
324
+
268
325
  // Get operator configured coinbase and fee recipient for this attestor
269
326
  const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
270
327
  const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
@@ -289,21 +346,20 @@ export class CheckpointProposalJob implements Traceable {
289
346
  // When pipelining, force the proposed checkpoint number and fee header to our parent so the
290
347
  // fee computation sees the same chain tip that L1 will see once the previous pipelined checkpoint lands.
291
348
  const isPipelining = this.epochCache.isProposerPipeliningEnabled();
292
- const parentCheckpointNumber = isPipelining ? CheckpointNumber(this.checkpointNumber - 1) : undefined;
293
-
294
- // Compute the parent's fee header override when pipelining
295
- if (isPipelining && this.proposedCheckpointData) {
296
- this.computedForceProposedFeeHeader = await this.computeForceProposedFeeHeader(parentCheckpointNumber!);
297
- }
349
+ this.pipelinedParentSimulationOverridesPlan = isPipelining
350
+ ? await buildPipelinedParentSimulationOverridesPlan({
351
+ checkpointNumber: this.checkpointNumber,
352
+ proposedCheckpointData: this.proposedCheckpointData,
353
+ rollup: this.publisher.rollupContract,
354
+ log: this.log,
355
+ })
356
+ : undefined;
298
357
 
299
358
  const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(
300
359
  coinbase,
301
360
  feeRecipient,
302
361
  this.targetSlot,
303
- {
304
- forcePendingCheckpointNumber: parentCheckpointNumber,
305
- forceProposedFeeHeader: this.computedForceProposedFeeHeader,
306
- },
362
+ this.pipelinedParentSimulationOverridesPlan,
307
363
  );
308
364
 
309
365
  // Collect L1 to L2 messages for the checkpoint and compute their hash
@@ -344,7 +400,7 @@ export class CheckpointProposalJob implements Traceable {
344
400
  };
345
401
 
346
402
  let blocksInCheckpoint: L2Block[] = [];
347
- let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
403
+ let blockPendingBroadcast: BlockProposal | undefined = undefined;
348
404
  const checkpointBuildTimer = new Timer();
349
405
 
350
406
  try {
@@ -405,14 +461,14 @@ export class CheckpointProposalJob implements Traceable {
405
461
  }
406
462
 
407
463
  // Record checkpoint-level build metrics
408
- this.metrics.recordCheckpointBuild(
464
+ this.checkpointMetrics.recordCheckpointBuild(
409
465
  checkpointBuildTimer.ms(),
410
466
  blocksInCheckpoint.length,
411
467
  checkpoint.getStats().txCount,
412
468
  Number(checkpoint.header.totalManaUsed.toBigInt()),
413
469
  );
414
470
 
415
- // Do not collect attestations nor publish to L1 in fisherman mode
471
+ // In fisherman mode, return the checkpoint without broadcasting or collecting attestations
416
472
  if (this.config.fishermanMode) {
417
473
  this.log.info(
418
474
  `Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` +
@@ -424,60 +480,27 @@ export class CheckpointProposalJob implements Traceable {
424
480
  },
425
481
  );
426
482
  this.metrics.recordCheckpointSuccess();
427
- return {
428
- checkpoint,
429
- attestations: CommitteeAttestationsAndSigners.empty(),
430
- attestationsSignature: Signature.empty(),
431
- };
483
+ // Return a broadcast result with a dummy proposal — fisherman mode skips attestation collection
484
+ return { checkpoint, proposal: undefined!, blockProposedAt: this.dateProvider.now() };
432
485
  }
433
486
 
434
- // Include the block pending broadcast in the checkpoint proposal if any
435
- const lastBlock = blockPendingBroadcast && {
436
- blockHeader: blockPendingBroadcast.block.header,
437
- indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
438
- txs: blockPendingBroadcast.txs,
439
- };
440
-
441
487
  // Create the checkpoint proposal and broadcast it
442
488
  const proposal = await this.validatorClient.createCheckpointProposal(
443
489
  checkpoint.header,
444
490
  checkpoint.archive.root,
491
+ this.checkpointNumber,
445
492
  feeAssetPriceModifier,
446
- lastBlock,
493
+ blockPendingBroadcast,
447
494
  this.proposer,
448
495
  checkpointProposalOptions,
449
496
  );
450
497
 
451
498
  const blockProposedAt = this.dateProvider.now();
452
499
  await this.p2pClient.broadcastCheckpointProposal(proposal);
500
+ this.checkpointMetrics.noteCheckpointBroadcast(this.dateProvider.now());
453
501
 
454
- this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
455
- const attestations = await this.waitForAttestations(proposal);
456
- const blockAttestedAt = this.dateProvider.now();
457
-
458
- this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
459
-
460
- // Proposer must sign over the attestations before pushing them to L1
461
- const signer = this.proposer ?? this.publisher.getSenderAddress();
462
- let attestationsSignature: Signature;
463
- try {
464
- attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
465
- attestations,
466
- signer,
467
- this.targetSlot,
468
- this.checkpointNumber,
469
- );
470
- } catch (err) {
471
- // We shouldn't really get here since we yield to another HA node
472
- // as soon as we see these errors when creating block or checkpoint proposals.
473
- if (this.handleHASigningError(err, 'Attestations signature')) {
474
- return undefined;
475
- }
476
- throw err;
477
- }
478
-
479
- // Return the result for the caller to enqueue after the pipeline sleep
480
- return { checkpoint, attestations, attestationsSignature };
502
+ // Return immediately after broadcast — attestation collection happens in the background
503
+ return { checkpoint, proposal, blockProposedAt };
481
504
  } catch (err) {
482
505
  if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
483
506
  // swallow this error. It's already been logged by a function deeper in the stack
@@ -500,14 +523,14 @@ export class CheckpointProposalJob implements Traceable {
500
523
  blockProposalOptions: BlockProposalOptions,
501
524
  ): Promise<{
502
525
  blocksInCheckpoint: L2Block[];
503
- blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined;
526
+ blockPendingBroadcast: BlockProposal | undefined;
504
527
  }> {
505
528
  const blocksInCheckpoint: L2Block[] = [];
506
529
  const txHashesAlreadyIncluded = new Set<string>();
507
530
  const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
508
531
 
509
532
  // Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
510
- let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
533
+ let blockPendingBroadcast: BlockProposal | undefined = undefined;
511
534
 
512
535
  while (true) {
513
536
  const blocksBuilt = blocksInCheckpoint.length;
@@ -540,19 +563,20 @@ export class CheckpointProposalJob implements Traceable {
540
563
  txHashesAlreadyIncluded,
541
564
  });
542
565
 
543
- // TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
544
- if (!buildResult && timingInfo.isLastBlock) {
545
- // If no block was produced due to not enough txs and this was the last subslot, exit
546
- break;
547
- } else if (!buildResult && timingInfo.deadline !== undefined) {
548
- // But if there is still time for more blocks, wait until the next subslot and try again
566
+ // If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
567
+ if ('failure' in buildResult) {
568
+ // If this was the last subslot, or we're running with a single block per slot, we're done
569
+ if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
570
+ break;
571
+ }
572
+ // Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
549
573
  await this.waitUntilNextSubslot(timingInfo.deadline);
550
574
  continue;
551
- } else if (!buildResult) {
552
- // Exit if there is no possibility of building more blocks
553
- break;
554
- } else if ('error' in buildResult) {
555
- // If there was an error building the block, just exit the loop and give up the rest of the slot
575
+ }
576
+
577
+ // If there was an error building the block, we just exit the loop and give up the rest of the slot.
578
+ // We don't want to risk building more blocks if something went wrong.
579
+ if ('error' in buildResult) {
556
580
  if (!(buildResult.error instanceof SequencerInterruptedError)) {
557
581
  this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
558
582
  slot: this.targetSlot,
@@ -564,33 +588,35 @@ export class CheckpointProposalJob implements Traceable {
564
588
  }
565
589
 
566
590
  const { block, usedTxs } = buildResult;
591
+ this.checkpointMetrics.noteCheckpointBlockBuilt(this.dateProvider.now(), {
592
+ isFirstBlock: blocksBuilt === 0,
593
+ isLastBlock: timingInfo.isLastBlock,
594
+ });
595
+
567
596
  blocksInCheckpoint.push(block);
568
597
  usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
569
598
 
570
- // If this is the last block, sync it to the archiver and exit the loop
571
- // so we can build the checkpoint and start collecting attestations.
599
+ // Sign the block proposal. This will throw if HA signing fails.
600
+ const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
601
+
602
+ // Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal,
603
+ // so we avoid polluting our archive with a block that would fail.
604
+ // We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
605
+ // If this throws, we abort the entire checkpoint.
606
+ await this.syncProposedBlockToArchiver(block);
607
+
608
+ // If this is the last block, do not broadcast it, since it will be included in the checkpoint proposal.
572
609
  if (timingInfo.isLastBlock) {
573
- await this.syncProposedBlockToArchiver(block);
574
610
  this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
575
611
  slot: this.targetSlot,
576
612
  blockNumber,
577
613
  blocksBuilt,
578
614
  });
579
- blockPendingBroadcast = { block, txs: usedTxs };
615
+
616
+ blockPendingBroadcast = proposal;
580
617
  break;
581
618
  }
582
619
 
583
- // Broadcast the block proposal (unless we're in fisherman mode) unless the block is the last one,
584
- // in which case we'll broadcast it along with the checkpoint at the end of the loop.
585
- // Note that we only send the block to the archiver if we manage to create the proposal, so if there's
586
- // a HA error we don't pollute our archiver with a block that won't make it to the chain.
587
- const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
588
-
589
- // Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal.
590
- // We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
591
- // If this throws, we abort the entire checkpoint.
592
- await this.syncProposedBlockToArchiver(block);
593
-
594
620
  // Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
595
621
  proposal && (await this.p2pClient.broadcastProposal(proposal));
596
622
 
@@ -619,6 +645,7 @@ export class CheckpointProposalJob implements Traceable {
619
645
  }
620
646
  return this.validatorClient.createBlockProposal(
621
647
  block.header,
648
+ this.checkpointNumber,
622
649
  block.indexWithinCheckpoint,
623
650
  inHash,
624
651
  block.archive.root,
@@ -650,7 +677,9 @@ export class CheckpointProposalJob implements Traceable {
650
677
  buildDeadline: Date | undefined;
651
678
  txHashesAlreadyIncluded: Set<string>;
652
679
  },
653
- ): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
680
+ ): Promise<
681
+ { block: L2Block; usedTxs: Tx[] } | { failure: 'insufficient-txs' | 'insufficient-valid-txs' } | { error: Error }
682
+ > {
654
683
  const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
655
684
  opts;
656
685
 
@@ -669,7 +698,7 @@ export class CheckpointProposalJob implements Traceable {
669
698
  );
670
699
  this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.targetSlot });
671
700
  this.metrics.recordBlockProposalFailed('insufficient_txs');
672
- return undefined;
701
+ return { failure: 'insufficient-txs' };
673
702
  }
674
703
 
675
704
  // Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
@@ -732,7 +761,7 @@ export class CheckpointProposalJob implements Traceable {
732
761
  slot: this.targetSlot,
733
762
  });
734
763
  this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
735
- return undefined;
764
+ return { failure: 'insufficient-valid-txs' };
736
765
  }
737
766
 
738
767
  // Block creation succeeded, emit stats and metrics
@@ -761,7 +790,7 @@ export class CheckpointProposalJob implements Traceable {
761
790
  slot: this.targetSlot,
762
791
  buildSlot: this.slotNow,
763
792
  });
764
- this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
793
+ this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe(), this.targetSlot);
765
794
 
766
795
  return { block, usedTxs };
767
796
  } catch (err: any) {
@@ -868,12 +897,12 @@ export class CheckpointProposalJob implements Traceable {
868
897
 
869
898
  if (this.config.skipCollectingAttestations) {
870
899
  this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
871
- const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
900
+ const attestations = await this.validatorClient?.collectOwnAttestations(proposal, this.checkpointNumber);
872
901
  return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
873
902
  }
874
903
 
875
904
  const attestationTimeAllowed = this.config.enforceTimeTable
876
- ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_CHECKPOINT)!
905
+ ? this.timetable.getCheckpointAttestationDeadline()
877
906
  : this.l1Constants.slotDuration;
878
907
  const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
879
908
 
@@ -886,6 +915,7 @@ export class CheckpointProposalJob implements Traceable {
886
915
  proposal,
887
916
  numberOfRequiredAttestations,
888
917
  attestationDeadline,
918
+ this.checkpointNumber,
889
919
  );
890
920
 
891
921
  collectedAttestationsCount = attestations.length;
@@ -1012,9 +1042,13 @@ export class CheckpointProposalJob implements Traceable {
1012
1042
  * Adds the proposed block to the archiver so it's available via P2P.
1013
1043
  * Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
1014
1044
  * would never receive its own block without this explicit sync.
1045
+ *
1046
+ * In fisherman mode we skip this push: the fisherman builds blocks locally for validation
1047
+ * and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
1048
+ * whenever the real proposer's block arrives from L1.
1015
1049
  */
1016
1050
  private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
1017
- if (this.config.skipPushProposedBlocksToArchiver) {
1051
+ if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
1018
1052
  this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
1019
1053
  blockNumber: block.number,
1020
1054
  slot: block.header.globalVariables.slotNumber,
@@ -1075,56 +1109,6 @@ export class CheckpointProposalJob implements Traceable {
1075
1109
  return false;
1076
1110
  }
1077
1111
 
1078
- /**
1079
- * In times of congestion we need to simulate using the correct fee header override for the previous block
1080
- * We calculate the correct fee header values.
1081
- *
1082
- * If we are in block 1, or the checkpoint we are querying does not exist, we return undefined. However
1083
- * If we are pipelining - where this function is called, the grandparentCheckpointNumber should always exist
1084
- * @param parentCheckpointNumber
1085
- * @returns
1086
- */
1087
- protected async computeForceProposedFeeHeader(parentCheckpointNumber: CheckpointNumber): Promise<
1088
- | {
1089
- checkpointNumber: CheckpointNumber;
1090
- feeHeader: FeeHeader;
1091
- }
1092
- | undefined
1093
- > {
1094
- if (!this.proposedCheckpointData) {
1095
- return undefined;
1096
- }
1097
-
1098
- const rollup = this.publisher.rollupContract;
1099
- const grandparentCheckpointNumber = CheckpointNumber(this.checkpointNumber - 2);
1100
- try {
1101
- const [grandparentCheckpoint, manaTarget] = await Promise.all([
1102
- rollup.getCheckpoint(grandparentCheckpointNumber),
1103
- rollup.getManaTarget(),
1104
- ]);
1105
-
1106
- if (!grandparentCheckpoint || !grandparentCheckpoint.feeHeader) {
1107
- this.log.error(
1108
- `Grandparent checkpoint or its feeHeader is undefined for checkpointNumber=${grandparentCheckpointNumber.toString()}`,
1109
- );
1110
- return undefined;
1111
- } else {
1112
- const parentFeeHeader = RollupContract.computeChildFeeHeader(
1113
- grandparentCheckpoint.feeHeader,
1114
- this.proposedCheckpointData.totalManaUsed,
1115
- this.proposedCheckpointData.feeAssetPriceModifier,
1116
- manaTarget,
1117
- );
1118
- return { checkpointNumber: parentCheckpointNumber, feeHeader: parentFeeHeader };
1119
- }
1120
- } catch (err) {
1121
- this.log.error(
1122
- `Failed to fetch grandparent checkpoint or mana target for checkpointNumber=${grandparentCheckpointNumber.toString()}: ${err}`,
1123
- );
1124
- return undefined;
1125
- }
1126
- }
1127
-
1128
1112
  /** Waits until a specific time within the current slot */
1129
1113
  @trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
1130
1114
  protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {