@aztec/sequencer-client 0.0.1-commit.e6bd8901 → 0.0.1-commit.ec7ac5448

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 (93) hide show
  1. package/dest/client/sequencer-client.d.ts +17 -7
  2. package/dest/client/sequencer-client.d.ts.map +1 -1
  3. package/dest/client/sequencer-client.js +63 -30
  4. package/dest/config.d.ts +26 -7
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +47 -33
  7. package/dest/global_variable_builder/global_builder.d.ts +15 -10
  8. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  9. package/dest/global_variable_builder/global_builder.js +26 -26
  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 +47 -17
  13. package/dest/publisher/config.d.ts.map +1 -1
  14. package/dest/publisher/config.js +121 -42
  15. package/dest/publisher/index.d.ts +2 -1
  16. package/dest/publisher/index.d.ts.map +1 -1
  17. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  18. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  19. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  20. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
  21. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  22. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
  23. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  24. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  25. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
  26. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  27. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  28. package/dest/publisher/l1_tx_failed_store/index.js +2 -0
  29. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -5
  30. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  31. package/dest/publisher/sequencer-publisher-factory.js +27 -3
  32. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  33. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  34. package/dest/publisher/sequencer-publisher-metrics.js +12 -4
  35. package/dest/publisher/sequencer-publisher.d.ts +75 -49
  36. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  37. package/dest/publisher/sequencer-publisher.js +470 -156
  38. package/dest/sequencer/chain_state_overrides.d.ts +25 -0
  39. package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
  40. package/dest/sequencer/chain_state_overrides.js +39 -0
  41. package/dest/sequencer/checkpoint_proposal_job.d.ts +51 -13
  42. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  43. package/dest/sequencer/checkpoint_proposal_job.js +414 -239
  44. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  45. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  46. package/dest/sequencer/checkpoint_voter.js +2 -5
  47. package/dest/sequencer/events.d.ts +2 -1
  48. package/dest/sequencer/events.d.ts.map +1 -1
  49. package/dest/sequencer/metrics.d.ts +25 -6
  50. package/dest/sequencer/metrics.d.ts.map +1 -1
  51. package/dest/sequencer/metrics.js +134 -31
  52. package/dest/sequencer/sequencer.d.ts +44 -17
  53. package/dest/sequencer/sequencer.d.ts.map +1 -1
  54. package/dest/sequencer/sequencer.js +157 -94
  55. package/dest/sequencer/timetable.d.ts +17 -6
  56. package/dest/sequencer/timetable.d.ts.map +1 -1
  57. package/dest/sequencer/timetable.js +51 -46
  58. package/dest/sequencer/types.d.ts +2 -2
  59. package/dest/sequencer/types.d.ts.map +1 -1
  60. package/dest/test/index.d.ts +3 -5
  61. package/dest/test/index.d.ts.map +1 -1
  62. package/dest/test/mock_checkpoint_builder.d.ts +14 -11
  63. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  64. package/dest/test/mock_checkpoint_builder.js +63 -40
  65. package/dest/test/utils.d.ts +3 -3
  66. package/dest/test/utils.d.ts.map +1 -1
  67. package/dest/test/utils.js +5 -4
  68. package/package.json +27 -28
  69. package/src/client/sequencer-client.ts +81 -30
  70. package/src/config.ts +65 -43
  71. package/src/global_variable_builder/global_builder.ts +31 -28
  72. package/src/global_variable_builder/index.ts +1 -1
  73. package/src/publisher/config.ts +153 -43
  74. package/src/publisher/index.ts +3 -0
  75. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  76. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
  77. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
  78. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  79. package/src/publisher/sequencer-publisher-factory.ts +38 -9
  80. package/src/publisher/sequencer-publisher-metrics.ts +7 -3
  81. package/src/publisher/sequencer-publisher.ts +531 -214
  82. package/src/sequencer/README.md +83 -13
  83. package/src/sequencer/chain_state_overrides.ts +87 -0
  84. package/src/sequencer/checkpoint_proposal_job.ts +547 -274
  85. package/src/sequencer/checkpoint_voter.ts +1 -12
  86. package/src/sequencer/events.ts +1 -1
  87. package/src/sequencer/metrics.ts +153 -33
  88. package/src/sequencer/sequencer.ts +228 -107
  89. package/src/sequencer/timetable.ts +70 -57
  90. package/src/sequencer/types.ts +1 -1
  91. package/src/test/index.ts +2 -4
  92. package/src/test/mock_checkpoint_builder.ts +84 -56
  93. package/src/test/utils.ts +5 -2
@@ -1,16 +1,26 @@
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
- import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
2
+ import type { SimulationOverridesPlan } from '@aztec/ethereum/contracts';
3
+ import {
4
+ BlockNumber,
5
+ CheckpointNumber,
6
+ EpochNumber,
7
+ IndexWithinCheckpoint,
8
+ SlotNumber,
9
+ } from '@aztec/foundation/branded-types';
5
10
  import { randomInt } from '@aztec/foundation/crypto/random';
11
+ import {
12
+ flipSignature,
13
+ generateRecoverableSignature,
14
+ generateUnrecoverableSignature,
15
+ } from '@aztec/foundation/crypto/secp256k1-signer';
6
16
  import { Fr } from '@aztec/foundation/curves/bn254';
7
17
  import { EthAddress } from '@aztec/foundation/eth-address';
8
18
  import { Signature } from '@aztec/foundation/eth-signature';
9
19
  import { filter } from '@aztec/foundation/iterator';
10
- import type { Logger } from '@aztec/foundation/log';
20
+ import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
11
21
  import { sleep, sleepUntil } from '@aztec/foundation/sleep';
12
22
  import { type DateProvider, Timer } from '@aztec/foundation/timer';
13
- import { type TypedEventEmitter, unfreeze } from '@aztec/foundation/types';
23
+ import { type TypedEventEmitter, isErrorClass, unfreeze } from '@aztec/foundation/types';
14
24
  import type { P2P } from '@aztec/p2p';
15
25
  import type { SlasherClientInterface } from '@aztec/slasher';
16
26
  import {
@@ -21,17 +31,23 @@ import {
21
31
  type L2BlockSource,
22
32
  MaliciousCommitteeAttestationsAndSigners,
23
33
  } from '@aztec/stdlib/block';
24
- import type { Checkpoint } from '@aztec/stdlib/checkpoint';
25
- import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
34
+ import { type Checkpoint, type ProposedCheckpointData, validateCheckpoint } from '@aztec/stdlib/checkpoint';
35
+ import { computeQuorum, getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
26
36
  import { Gas } from '@aztec/stdlib/gas';
27
- import type {
28
- PublicProcessorLimits,
29
- ResolvedSequencerConfig,
30
- WorldStateSynchronizer,
37
+ import {
38
+ type BlockBuilderOptions,
39
+ InsufficientValidTxsError,
40
+ type ResolvedSequencerConfig,
41
+ type WorldStateSynchronizer,
31
42
  } from '@aztec/stdlib/interfaces/server';
32
43
  import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
33
- import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
34
- import { orderAttestations } from '@aztec/stdlib/p2p';
44
+ import type {
45
+ BlockProposal,
46
+ BlockProposalOptions,
47
+ CheckpointProposal,
48
+ CheckpointProposalOptions,
49
+ } from '@aztec/stdlib/p2p';
50
+ import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
35
51
  import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
36
52
  import { type FailedTx, Tx } from '@aztec/stdlib/tx';
37
53
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
@@ -41,6 +57,10 @@ import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validato
41
57
 
42
58
  import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
43
59
  import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
60
+ import {
61
+ buildPipelinedParentSimulationOverridesPlan,
62
+ buildSubmissionSimulationOverridesPlan,
63
+ } from './chain_state_overrides.js';
44
64
  import { CheckpointVoter } from './checkpoint_voter.js';
45
65
  import { SequencerInterruptedError } from './errors.js';
46
66
  import type { SequencerEvents } from './events.js';
@@ -52,6 +72,20 @@ import { SequencerState } from './utils.js';
52
72
  /** How much time to sleep while waiting for min transactions to accumulate for a block */
53
73
  const TXS_POLLING_MS = 500;
54
74
 
75
+ /** Result from proposeCheckpoint when a checkpoint was successfully built and broadcast. */
76
+ type CheckpointProposalBroadcast = {
77
+ checkpoint: Checkpoint;
78
+ proposal: CheckpointProposal;
79
+ blockProposedAt: number;
80
+ };
81
+
82
+ /** Result after attestation collection and signing, ready for L1 submission. */
83
+ type CheckpointProposalResult = {
84
+ checkpoint: Checkpoint;
85
+ attestations: CommitteeAttestationsAndSigners;
86
+ attestationsSignature: Signature;
87
+ };
88
+
55
89
  /**
56
90
  * Handles the execution of a checkpoint proposal after the initial preparation phase.
57
91
  * This includes building blocks, collecting attestations, and publishing the checkpoint to L1,
@@ -59,9 +93,18 @@ const TXS_POLLING_MS = 500;
59
93
  * the Sequencer once the check for being the proposer for the slot has succeeded.
60
94
  */
61
95
  export class CheckpointProposalJob implements Traceable {
96
+ protected readonly log: Logger;
97
+
98
+ /** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */
99
+ private pendingL1Submission: Promise<void> | undefined;
100
+
101
+ /** Pipelined parent chain state used while building and later submitting this checkpoint. */
102
+ private pipelinedParentSimulationOverridesPlan?: SimulationOverridesPlan;
103
+
62
104
  constructor(
63
- private readonly epoch: EpochNumber,
64
- private readonly slot: SlotNumber,
105
+ private readonly slotNow: SlotNumber,
106
+ private readonly targetSlot: SlotNumber,
107
+ private readonly targetEpoch: EpochNumber,
65
108
  private readonly checkpointNumber: CheckpointNumber,
66
109
  private readonly syncedToBlockNumber: BlockNumber,
67
110
  // TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
@@ -86,13 +129,28 @@ export class CheckpointProposalJob implements Traceable {
86
129
  private readonly metrics: SequencerMetrics,
87
130
  private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
88
131
  private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
89
- protected readonly log: Logger,
90
132
  public readonly tracer: Tracer,
91
- ) {}
133
+ bindings?: LoggerBindings,
134
+ private readonly proposedCheckpointData?: ProposedCheckpointData,
135
+ ) {
136
+ this.log = createLogger('sequencer:checkpoint-proposal', {
137
+ ...bindings,
138
+ instanceId: `slot-${this.slotNow}`,
139
+ });
140
+ }
141
+
142
+ /** Awaits the pending L1 submission if one is in progress. Call during shutdown. */
143
+ public async awaitPendingSubmission(): Promise<void> {
144
+ this.log.info('Awaiting pending L1 payload submission');
145
+ await this.pendingL1Submission;
146
+ }
92
147
 
93
148
  /**
94
149
  * Executes the checkpoint proposal job.
95
- * Returns the published checkpoint if successful, undefined otherwise.
150
+ * Builds blocks, assembles checkpoint, and broadcasts the proposal (blocking).
151
+ * Attestation collection, signing, and L1 submission are backgrounded so the
152
+ * work loop can return to IDLE immediately for consecutive slot proposals.
153
+ * Returns the built checkpoint if successful, undefined otherwise.
96
154
  */
97
155
  @trackSpan('CheckpointProposalJob.execute')
98
156
  public async execute(): Promise<Checkpoint | undefined> {
@@ -100,7 +158,7 @@ export class CheckpointProposalJob implements Traceable {
100
158
  // In fisherman mode, we simulate slashing but don't actually publish to L1
101
159
  // These are constant for the whole slot, so we only enqueue them once
102
160
  const votesPromises = new CheckpointVoter(
103
- this.slot,
161
+ this.targetSlot,
104
162
  this.publisher,
105
163
  this.attestorAddress,
106
164
  this.validatorClient,
@@ -111,63 +169,185 @@ export class CheckpointProposalJob implements Traceable {
111
169
  this.log,
112
170
  ).enqueueVotes();
113
171
 
114
- // Build and propose the checkpoint. This will enqueue the request on the publisher if a checkpoint is built.
115
- const checkpoint = await this.proposeCheckpoint();
116
-
117
- // Wait until the voting promises have resolved, so all requests are enqueued (not sent)
118
- await Promise.all(votesPromises);
172
+ // Build blocks, assemble checkpoint, and broadcast proposal (BLOCKING).
173
+ // Returns after broadcast — attestation collection is deferred.
174
+ const broadcast = await this.proposeCheckpoint();
119
175
 
120
- if (checkpoint) {
121
- this.metrics.recordBlockProposalSuccess();
176
+ if (!broadcast) {
177
+ await Promise.all(votesPromises);
178
+ // Still submit votes even without a checkpoint
179
+ if (!this.config.fishermanMode) {
180
+ this.pendingL1Submission = this.publisher.sendRequestsAt(this.dateProvider.nowAsDate()).then(() => {});
181
+ }
182
+ return undefined;
122
183
  }
123
184
 
185
+ const { checkpoint } = broadcast;
186
+ this.metrics.recordCheckpointProposalSuccess();
187
+
124
188
  // Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
125
189
  if (this.config.fishermanMode) {
126
190
  await this.handleCheckpointEndAsFisherman(checkpoint);
127
- return;
191
+ return checkpoint;
128
192
  }
129
193
 
130
- // Then send everything to L1
131
- const l1Response = await this.publisher.sendRequests();
132
- const proposedAction = l1Response?.successfulActions.find(a => a === 'propose');
133
- if (proposedAction) {
134
- this.eventEmitter.emit('checkpoint-published', { checkpoint: this.checkpointNumber, slot: this.slot });
135
- const coinbase = checkpoint?.header.coinbase;
136
- await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
137
- return checkpoint;
138
- } else if (checkpoint) {
139
- this.eventEmitter.emit('checkpoint-publish-failed', { ...l1Response, slot: this.slot });
140
- return undefined;
194
+ // Background the attestation signing → L1 pipeline so the work loop is unblocked
195
+ this.pendingL1Submission = this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises);
196
+
197
+ // Return the built checkpoint immediately — the work loop is now unblocked
198
+ return checkpoint;
199
+ }
200
+
201
+ /**
202
+ * Background pipeline: collects attestations, signs them, enqueues the checkpoint, and submits to L1.
203
+ * Runs as a fire-and-forget task stored in `pendingL1Submission` so the work loop is unblocked.
204
+ */
205
+ private async waitForAttestationsAndEnqueueSubmissionAsync(
206
+ broadcast: CheckpointProposalBroadcast,
207
+ votesPromises: Promise<unknown>[],
208
+ ): Promise<void> {
209
+ const { checkpoint, proposal, blockProposedAt } = broadcast;
210
+ try {
211
+ await Promise.all(votesPromises);
212
+
213
+ this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
214
+ const attestations = await this.waitForAttestations(proposal);
215
+
216
+ this.metrics.recordCheckpointAttestationDelay(this.dateProvider.now() - blockProposedAt);
217
+
218
+ // Proposer must sign over the attestations before pushing them to L1
219
+ const signer = this.proposer ?? this.publisher.getSenderAddress();
220
+ let attestationsSignature: Signature;
221
+ try {
222
+ attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
223
+ attestations,
224
+ signer,
225
+ this.targetSlot,
226
+ this.checkpointNumber,
227
+ );
228
+ } catch (err) {
229
+ if (this.handleHASigningError(err, 'Attestations signature')) {
230
+ return;
231
+ }
232
+ throw err;
233
+ }
234
+
235
+ // Enqueue the checkpoint for L1 submission
236
+ await this.enqueueCheckpointForSubmission({ checkpoint, attestations, attestationsSignature });
237
+
238
+ // Compute the earliest time to submit: pipeline slot start when pipelining, now otherwise.
239
+ const submitAfter = this.epochCache.isProposerPipeliningEnabled()
240
+ ? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000)
241
+ : new Date(this.dateProvider.now());
242
+
243
+ const l1Response = await this.publisher.sendRequestsAt(submitAfter);
244
+ const proposedAction = l1Response?.successfulActions.find(a => a === 'propose');
245
+ if (proposedAction) {
246
+ this.eventEmitter.emit('checkpoint-published', { checkpoint: this.checkpointNumber, slot: this.targetSlot });
247
+ const coinbase = checkpoint.header.coinbase;
248
+ await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
249
+ } else {
250
+ this.eventEmitter.emit('checkpoint-publish-failed', { ...l1Response, slot: this.targetSlot });
251
+ if (this.epochCache.isProposerPipeliningEnabled()) {
252
+ this.metrics.recordPipelineDiscard();
253
+ }
254
+ }
255
+ } catch (err) {
256
+ if (err instanceof SequencerInterruptedError) {
257
+ return;
258
+ }
259
+ this.log.error(`Background attestation/L1 pipeline failed for slot ${this.targetSlot}`, err);
260
+ this.eventEmitter.emit('checkpoint-publish-failed', { slot: this.targetSlot });
261
+ if (this.epochCache.isProposerPipeliningEnabled()) {
262
+ this.metrics.recordPipelineDiscard();
263
+ }
264
+ }
265
+ }
266
+
267
+ /** Enqueues the checkpoint for L1 submission. Called after pipeline sleep in execute(). */
268
+ private async enqueueCheckpointForSubmission(result: CheckpointProposalResult): Promise<void> {
269
+ const { checkpoint, attestations, attestationsSignature } = result;
270
+
271
+ this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot);
272
+ const aztecSlotDuration = this.l1Constants.slotDuration;
273
+ const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
274
+ const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
275
+
276
+ // If we have been configured to potentially skip publishing checkpoint then roll the dice here
277
+ if (
278
+ this.config.skipPublishingCheckpointsPercent !== undefined &&
279
+ this.config.skipPublishingCheckpointsPercent > 0
280
+ ) {
281
+ const roll = Math.max(0, randomInt(100));
282
+ if (roll < this.config.skipPublishingCheckpointsPercent) {
283
+ this.log.warn(
284
+ `Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${roll}`,
285
+ );
286
+ return;
287
+ }
141
288
  }
289
+
290
+ const isPipelining = this.epochCache.isProposerPipeliningEnabled();
291
+ const submissionSimulationOverridesPlan = buildSubmissionSimulationOverridesPlan({
292
+ pipelinedParentPlan: this.pipelinedParentSimulationOverridesPlan,
293
+ invalidateToPendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
294
+ lastArchiveRoot: checkpoint.header.lastArchiveRoot,
295
+ pipeliningEnabled: isPipelining,
296
+ });
297
+
298
+ await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
299
+ txTimeoutAt,
300
+ ...(submissionSimulationOverridesPlan ? { simulationOverridesPlan: submissionSimulationOverridesPlan } : {}),
301
+ });
142
302
  }
143
303
 
144
304
  @trackSpan('CheckpointProposalJob.proposeCheckpoint', function () {
145
305
  return {
146
306
  // nullish operator needed for tests
147
307
  [Attributes.COINBASE]: this.validatorClient.getCoinbaseForAttestor(this.attestorAddress)?.toString(),
148
- [Attributes.SLOT_NUMBER]: this.slot,
308
+ [Attributes.SLOT_NUMBER]: this.targetSlot,
149
309
  };
150
310
  })
151
- private async proposeCheckpoint(): Promise<Checkpoint | undefined> {
311
+ private async proposeCheckpoint(): Promise<CheckpointProposalBroadcast | undefined> {
152
312
  try {
153
313
  // Get operator configured coinbase and fee recipient for this attestor
154
314
  const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
155
315
  const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
156
316
 
157
317
  // Start the checkpoint
158
- this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.slot);
159
- this.metrics.incOpenSlot(this.slot, this.proposer?.toString() ?? 'unknown');
318
+ this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot);
319
+ this.log.info(`Starting checkpoint proposal`, {
320
+ buildSlot: this.slotNow,
321
+ submissionSlot: this.targetSlot,
322
+ pipelining: this.epochCache.isProposerPipeliningEnabled(),
323
+ proposer: this.proposer?.toString(),
324
+ coinbase: coinbase.toString(),
325
+ });
326
+ this.metrics.incOpenSlot(this.targetSlot, this.proposer?.toString() ?? 'unknown');
160
327
 
161
328
  // Enqueues checkpoint invalidation (constant for the whole slot)
162
329
  if (this.invalidateCheckpoint && !this.config.skipInvalidateBlockAsProposer) {
163
330
  this.publisher.enqueueInvalidateCheckpoint(this.invalidateCheckpoint);
164
331
  }
165
332
 
166
- // Create checkpoint builder for the slot
333
+ // Create checkpoint builder for the slot.
334
+ // When pipelining, force the proposed checkpoint number and fee header to our parent so the
335
+ // fee computation sees the same chain tip that L1 will see once the previous pipelined checkpoint lands.
336
+ const isPipelining = this.epochCache.isProposerPipeliningEnabled();
337
+ this.pipelinedParentSimulationOverridesPlan = isPipelining
338
+ ? await buildPipelinedParentSimulationOverridesPlan({
339
+ checkpointNumber: this.checkpointNumber,
340
+ proposedCheckpointData: this.proposedCheckpointData,
341
+ rollup: this.publisher.rollupContract,
342
+ log: this.log,
343
+ })
344
+ : undefined;
345
+
167
346
  const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(
168
347
  coinbase,
169
348
  feeRecipient,
170
- this.slot,
349
+ this.targetSlot,
350
+ this.pipelinedParentSimulationOverridesPlan,
171
351
  );
172
352
 
173
353
  // Collect L1 to L2 messages for the checkpoint and compute their hash
@@ -175,21 +355,25 @@ export class CheckpointProposalJob implements Traceable {
175
355
  const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
176
356
 
177
357
  // Collect the out hashes of all the checkpoints before this one in the same epoch
178
- const previousCheckpoints = (await this.l2BlockSource.getCheckpointsForEpoch(this.epoch)).filter(
179
- c => c.number < this.checkpointNumber,
180
- );
181
- const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
358
+ const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.targetEpoch))
359
+ .filter(c => c.checkpointNumber < this.checkpointNumber)
360
+ .map(c => c.checkpointOutHash);
361
+
362
+ // Get the fee asset price modifier from the oracle
363
+ const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
182
364
 
183
365
  // Create a long-lived forked world state for the checkpoint builder
184
- using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
366
+ await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
185
367
 
186
368
  // Create checkpoint builder for the entire slot
187
369
  const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
188
370
  this.checkpointNumber,
189
371
  checkpointGlobalVariables,
372
+ feeAssetPriceModifier,
190
373
  l1ToL2Messages,
191
374
  previousCheckpointOutHashes,
192
375
  fork,
376
+ this.log.getBindings(),
193
377
  );
194
378
 
195
379
  // Options for the validator client when creating block and checkpoint proposals
@@ -204,7 +388,8 @@ export class CheckpointProposalJob implements Traceable {
204
388
  };
205
389
 
206
390
  let blocksInCheckpoint: L2Block[] = [];
207
- let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
391
+ let blockPendingBroadcast: BlockProposal | undefined = undefined;
392
+ const checkpointBuildTimer = new Timer();
208
393
 
209
394
  try {
210
395
  // Main loop: build blocks for the checkpoint
@@ -220,62 +405,79 @@ export class CheckpointProposalJob implements Traceable {
220
405
  // These errors are expected in HA mode, so we yield and let another HA node handle the slot
221
406
  // The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
222
407
  // which is normal for block building (may have picked different txs)
223
- if (err instanceof DutyAlreadySignedError) {
224
- this.log.info(`Checkpoint proposal for slot ${this.slot} already signed by another HA node, yielding`, {
225
- slot: this.slot,
226
- signedByNode: err.signedByNode,
227
- });
228
- return undefined;
229
- }
230
- if (err instanceof SlashingProtectionError) {
231
- this.log.info(`Checkpoint proposal for slot ${this.slot} blocked by slashing protection, yielding`, {
232
- slot: this.slot,
233
- existingMessageHash: err.existingMessageHash,
234
- attemptedMessageHash: err.attemptedMessageHash,
235
- });
408
+ if (this.handleHASigningError(err, 'Block proposal')) {
236
409
  return undefined;
237
410
  }
238
411
  throw err;
239
412
  }
240
413
 
241
414
  if (blocksInCheckpoint.length === 0) {
242
- this.log.warn(`No blocks were built for slot ${this.slot}`, { slot: this.slot });
243
- this.eventEmitter.emit('checkpoint-empty', { slot: this.slot });
415
+ this.log.warn(`No blocks were built for slot ${this.targetSlot}`, { slot: this.targetSlot });
416
+ this.eventEmitter.emit('checkpoint-empty', { slot: this.targetSlot });
417
+ return undefined;
418
+ }
419
+
420
+ const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
421
+ if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
422
+ this.log.warn(
423
+ `Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
424
+ { slot: this.targetSlot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
425
+ );
244
426
  return undefined;
245
427
  }
246
428
 
247
429
  // Assemble and broadcast the checkpoint proposal, including the last block that was not
248
430
  // broadcasted yet, and wait to collect the committee attestations.
249
- this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
431
+ this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.targetSlot);
250
432
  const checkpoint = await checkpointBuilder.completeCheckpoint();
251
433
 
252
- // Do not collect attestations nor publish to L1 in fisherman mode
434
+ // Final validation: per-block limits are only checked if the operator set them explicitly.
435
+ // Otherwise, checkpoint-level budgets were already enforced by the redistribution logic.
436
+ try {
437
+ validateCheckpoint(checkpoint, {
438
+ rollupManaLimit: this.l1Constants.rollupManaLimit,
439
+ maxL2BlockGas: this.config.maxL2BlockGas,
440
+ maxDABlockGas: this.config.maxDABlockGas,
441
+ maxTxsPerBlock: this.config.maxTxsPerBlock,
442
+ maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
443
+ });
444
+ } catch (err) {
445
+ this.log.error(`Built an invalid checkpoint at slot ${this.slotNow} (skipping proposal)`, err, {
446
+ checkpoint: checkpoint.header.toInspect(),
447
+ });
448
+ return undefined;
449
+ }
450
+
451
+ // Record checkpoint-level build metrics
452
+ this.metrics.recordCheckpointBuild(
453
+ checkpointBuildTimer.ms(),
454
+ blocksInCheckpoint.length,
455
+ checkpoint.getStats().txCount,
456
+ Number(checkpoint.header.totalManaUsed.toBigInt()),
457
+ );
458
+
459
+ // In fisherman mode, return the checkpoint without broadcasting or collecting attestations
253
460
  if (this.config.fishermanMode) {
254
461
  this.log.info(
255
- `Built checkpoint for slot ${this.slot} with ${blocksInCheckpoint.length} blocks. ` +
462
+ `Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` +
256
463
  `Skipping proposal in fisherman mode.`,
257
464
  {
258
- slot: this.slot,
465
+ slot: this.targetSlot,
259
466
  checkpoint: checkpoint.header.toInspect(),
260
467
  blocksBuilt: blocksInCheckpoint.length,
261
468
  },
262
469
  );
263
470
  this.metrics.recordCheckpointSuccess();
264
- return checkpoint;
471
+ // Return a broadcast result with a dummy proposal — fisherman mode skips attestation collection
472
+ return { checkpoint, proposal: undefined!, blockProposedAt: this.dateProvider.now() };
265
473
  }
266
474
 
267
- // Include the block pending broadcast in the checkpoint proposal if any
268
- const lastBlock = blockPendingBroadcast && {
269
- blockHeader: blockPendingBroadcast.block.header,
270
- indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
271
- txs: blockPendingBroadcast.txs,
272
- };
273
-
274
475
  // Create the checkpoint proposal and broadcast it
275
476
  const proposal = await this.validatorClient.createCheckpointProposal(
276
477
  checkpoint.header,
277
478
  checkpoint.archive.root,
278
- lastBlock,
479
+ feeAssetPriceModifier,
480
+ blockPendingBroadcast,
279
481
  this.proposer,
280
482
  checkpointProposalOptions,
281
483
  );
@@ -283,61 +485,15 @@ export class CheckpointProposalJob implements Traceable {
283
485
  const blockProposedAt = this.dateProvider.now();
284
486
  await this.p2pClient.broadcastCheckpointProposal(proposal);
285
487
 
286
- this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.slot);
287
- const attestations = await this.waitForAttestations(proposal);
288
- const blockAttestedAt = this.dateProvider.now();
289
-
290
- this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
291
-
292
- // Proposer must sign over the attestations before pushing them to L1
293
- const signer = this.proposer ?? this.publisher.getSenderAddress();
294
- let attestationsSignature: Signature;
295
- try {
296
- attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
297
- attestations,
298
- signer,
299
- this.slot,
300
- this.checkpointNumber,
301
- );
302
- } catch (err) {
303
- // We shouldn't really get here since we yield to another HA node
304
- // as soon as we see these errors when creating block proposals.
305
- if (err instanceof DutyAlreadySignedError) {
306
- this.log.info(`Attestations signature for slot ${this.slot} already signed by another HA node, yielding`, {
307
- slot: this.slot,
308
- signedByNode: err.signedByNode,
309
- });
310
- return undefined;
311
- }
312
- if (err instanceof SlashingProtectionError) {
313
- this.log.info(`Attestations signature for slot ${this.slot} blocked by slashing protection, yielding`, {
314
- slot: this.slot,
315
- existingMessageHash: err.existingMessageHash,
316
- attemptedMessageHash: err.attemptedMessageHash,
317
- });
318
- return undefined;
319
- }
320
- throw err;
321
- }
322
-
323
- // Enqueue publishing the checkpoint to L1
324
- this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.slot);
325
- const aztecSlotDuration = this.l1Constants.slotDuration;
326
- const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
327
- const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
328
- await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
329
- txTimeoutAt,
330
- forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
331
- });
332
-
333
- return checkpoint;
488
+ // Return immediately after broadcast — attestation collection happens in the background
489
+ return { checkpoint, proposal, blockProposedAt };
334
490
  } catch (err) {
335
491
  if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
336
492
  // swallow this error. It's already been logged by a function deeper in the stack
337
493
  return undefined;
338
494
  }
339
495
 
340
- this.log.error(`Error building checkpoint at slot ${this.slot}`, err);
496
+ this.log.error(`Error building checkpoint at slot ${this.targetSlot}`, err);
341
497
  return undefined;
342
498
  }
343
499
  }
@@ -353,21 +509,18 @@ export class CheckpointProposalJob implements Traceable {
353
509
  blockProposalOptions: BlockProposalOptions,
354
510
  ): Promise<{
355
511
  blocksInCheckpoint: L2Block[];
356
- blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined;
512
+ blockPendingBroadcast: BlockProposal | undefined;
357
513
  }> {
358
514
  const blocksInCheckpoint: L2Block[] = [];
359
515
  const txHashesAlreadyIncluded = new Set<string>();
360
516
  const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
361
517
 
362
- // Remaining blob fields available for blocks (checkpoint end marker already subtracted)
363
- let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
364
-
365
518
  // Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
366
- let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
519
+ let blockPendingBroadcast: BlockProposal | undefined = undefined;
367
520
 
368
521
  while (true) {
369
522
  const blocksBuilt = blocksInCheckpoint.length;
370
- const indexWithinCheckpoint = blocksBuilt;
523
+ const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
371
524
  const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
372
525
 
373
526
  const secondsIntoSlot = this.getSecondsIntoSlot();
@@ -375,7 +528,7 @@ export class CheckpointProposalJob implements Traceable {
375
528
 
376
529
  if (!timingInfo.canStart) {
377
530
  this.log.debug(`Not enough time left in slot to start another block`, {
378
- slot: this.slot,
531
+ slot: this.targetSlot,
379
532
  blocksBuilt,
380
533
  secondsIntoSlot,
381
534
  });
@@ -394,24 +547,25 @@ export class CheckpointProposalJob implements Traceable {
394
547
  blockNumber,
395
548
  indexWithinCheckpoint,
396
549
  txHashesAlreadyIncluded,
397
- remainingBlobFields,
398
550
  });
399
551
 
400
- if (!buildResult && timingInfo.isLastBlock) {
401
- // If no block was produced due to not enough txs and this was the last subslot, exit
402
- break;
403
- } else if (!buildResult && timingInfo.deadline !== undefined) {
404
- // But if there is still time for more blocks, wait until the next subslot and try again
552
+ // If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
553
+ if ('failure' in buildResult) {
554
+ // If this was the last subslot, or we're running with a single block per slot, we're done
555
+ if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
556
+ break;
557
+ }
558
+ // Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
405
559
  await this.waitUntilNextSubslot(timingInfo.deadline);
406
560
  continue;
407
- } else if (!buildResult) {
408
- // Exit if there is no possibility of building more blocks
409
- break;
410
- } else if ('error' in buildResult) {
411
- // If there was an error building the block, just exit the loop and give up the rest of the slot
561
+ }
562
+
563
+ // If there was an error building the block, we just exit the loop and give up the rest of the slot.
564
+ // We don't want to risk building more blocks if something went wrong.
565
+ if ('error' in buildResult) {
412
566
  if (!(buildResult.error instanceof SequencerInterruptedError)) {
413
- this.log.warn(`Halting block building for slot ${this.slot}`, {
414
- slot: this.slot,
567
+ this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
568
+ slot: this.targetSlot,
415
569
  blocksBuilt,
416
570
  error: buildResult.error,
417
571
  });
@@ -419,210 +573,256 @@ export class CheckpointProposalJob implements Traceable {
419
573
  break;
420
574
  }
421
575
 
422
- const { block, usedTxs, remainingBlobFields: newRemainingBlobFields } = buildResult;
576
+ const { block, usedTxs } = buildResult;
423
577
  blocksInCheckpoint.push(block);
578
+ usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
424
579
 
425
- // Update remaining blob fields for the next block
426
- remainingBlobFields = newRemainingBlobFields;
580
+ // Sign the block proposal. This will throw if HA signing fails.
581
+ const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
427
582
 
428
- // Sync the proposed block to the archiver to make it available
429
- // Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
430
- // Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
431
- // Fire and forget - don't block the critical path, but log errors
432
- this.syncProposedBlockToArchiver(block).catch(err => {
433
- this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
434
- });
583
+ // Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal,
584
+ // so we avoid polluting our archive with a block that would fail.
585
+ // We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
586
+ // If this throws, we abort the entire checkpoint.
587
+ await this.syncProposedBlockToArchiver(block);
435
588
 
436
- // If this is the last block, exit the loop now so we start collecting attestations
589
+ // If this is the last block, do not broadcast it, since it will be included in the checkpoint proposal.
437
590
  if (timingInfo.isLastBlock) {
438
- this.log.verbose(`Completed final block ${blockNumber} for slot ${this.slot}`, {
439
- slot: this.slot,
591
+ this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
592
+ slot: this.targetSlot,
440
593
  blockNumber,
441
594
  blocksBuilt,
442
595
  });
443
- blockPendingBroadcast = { block, txs: usedTxs };
596
+
597
+ blockPendingBroadcast = proposal;
444
598
  break;
445
599
  }
446
600
 
447
- // For non-last blocks, broadcast the block proposal (unless we're in fisherman mode)
448
- // If the block is the last one, we'll broadcast it along with the checkpoint at the end of the loop
449
- if (!this.config.fishermanMode) {
450
- const proposal = await this.validatorClient.createBlockProposal(
451
- block.header,
452
- block.indexWithinCheckpoint,
453
- inHash,
454
- block.archive.root,
455
- usedTxs,
456
- this.proposer,
457
- blockProposalOptions,
458
- );
459
- await this.p2pClient.broadcastProposal(proposal);
460
- }
601
+ // Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
602
+ proposal && (await this.p2pClient.broadcastProposal(proposal));
461
603
 
462
604
  // Wait until the next block's start time
463
605
  await this.waitUntilNextSubslot(timingInfo.deadline);
464
606
  }
465
607
 
466
- this.log.verbose(`Block building loop completed for slot ${this.slot}`, {
467
- slot: this.slot,
608
+ this.log.verbose(`Block building loop completed for slot ${this.targetSlot}`, {
609
+ slot: this.targetSlot,
468
610
  blocksBuilt: blocksInCheckpoint.length,
469
611
  });
470
612
 
471
613
  return { blocksInCheckpoint, blockPendingBroadcast };
472
614
  }
473
615
 
616
+ /** Creates a block proposal for a given block via the validator client (unless in fisherman mode) */
617
+ private createBlockProposal(
618
+ block: L2Block,
619
+ inHash: Fr,
620
+ usedTxs: Tx[],
621
+ blockProposalOptions: BlockProposalOptions,
622
+ ): Promise<BlockProposal | undefined> {
623
+ if (this.config.fishermanMode) {
624
+ this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`);
625
+ return Promise.resolve(undefined);
626
+ }
627
+ return this.validatorClient.createBlockProposal(
628
+ block.header,
629
+ block.indexWithinCheckpoint,
630
+ inHash,
631
+ block.archive.root,
632
+ usedTxs,
633
+ this.proposer,
634
+ blockProposalOptions,
635
+ );
636
+ }
637
+
474
638
  /** Sleeps until it is time to produce the next block in the slot */
475
639
  @trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
476
640
  private async waitUntilNextSubslot(nextSubslotStart: number) {
477
- this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.slot);
478
- this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, { slot: this.slot });
641
+ this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot);
642
+ this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, {
643
+ slot: this.targetSlot,
644
+ });
479
645
  await this.waitUntilTimeInSlot(nextSubslotStart);
480
646
  }
481
647
 
482
648
  /** Builds a single block. Called from the main block building loop. */
483
649
  @trackSpan('CheckpointProposalJob.buildSingleBlock')
484
- private async buildSingleBlock(
650
+ protected async buildSingleBlock(
485
651
  checkpointBuilder: CheckpointBuilder,
486
652
  opts: {
487
653
  forceCreate?: boolean;
488
654
  blockTimestamp: bigint;
489
655
  blockNumber: BlockNumber;
490
- indexWithinCheckpoint: number;
656
+ indexWithinCheckpoint: IndexWithinCheckpoint;
491
657
  buildDeadline: Date | undefined;
492
658
  txHashesAlreadyIncluded: Set<string>;
493
- remainingBlobFields: number;
494
659
  },
495
- ): Promise<{ block: L2Block; usedTxs: Tx[]; remainingBlobFields: number } | { error: Error } | undefined> {
496
- const {
497
- blockTimestamp,
498
- forceCreate,
499
- blockNumber,
500
- indexWithinCheckpoint,
501
- buildDeadline,
502
- txHashesAlreadyIncluded,
503
- remainingBlobFields,
504
- } = opts;
660
+ ): Promise<
661
+ { block: L2Block; usedTxs: Tx[] } | { failure: 'insufficient-txs' | 'insufficient-valid-txs' } | { error: Error }
662
+ > {
663
+ const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
664
+ opts;
505
665
 
506
666
  this.log.verbose(
507
- `Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
667
+ `Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot}`,
508
668
  { ...checkpointBuilder.getConstantData(), ...opts },
509
669
  );
510
670
 
511
671
  try {
512
672
  // Wait until we have enough txs to build the block
513
- const minTxs = this.config.minTxsPerBlock;
514
- const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
673
+ const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
515
674
  if (!canStartBuilding) {
516
675
  this.log.warn(
517
- `Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`,
518
- { blockNumber, slot: this.slot, indexWithinCheckpoint },
676
+ `Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (got ${availableTxs} txs but needs ${minTxs})`,
677
+ { blockNumber, slot: this.targetSlot, indexWithinCheckpoint },
519
678
  );
520
- this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.slot });
679
+ this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.targetSlot });
521
680
  this.metrics.recordBlockProposalFailed('insufficient_txs');
522
- return undefined;
681
+ return { failure: 'insufficient-txs' };
523
682
  }
524
683
 
525
684
  // Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
526
685
  // just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
527
686
  const pendingTxs = filter(
528
- this.p2pClient.iteratePendingTxs(),
687
+ this.p2pClient.iterateEligiblePendingTxs(),
529
688
  tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
530
689
  );
531
690
 
532
691
  this.log.debug(
533
- `Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.slot} with ${availableTxs} available txs`,
534
- { slot: this.slot, blockNumber, indexWithinCheckpoint },
692
+ `Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot} with ${availableTxs} available txs`,
693
+ { slot: this.targetSlot, blockNumber, indexWithinCheckpoint },
535
694
  );
536
- this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
537
-
538
- // Calculate blob fields limit for txs (remaining capacity - this block's end overhead)
539
- const blockEndOverhead = getNumBlockEndBlobFields(indexWithinCheckpoint === 0);
540
- const maxBlobFieldsForTxs = remainingBlobFields - blockEndOverhead;
695
+ this.setStateFn(SequencerState.CREATING_BLOCK, this.targetSlot);
541
696
 
542
- const blockBuilderOptions: PublicProcessorLimits = {
697
+ // Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
698
+ // by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
699
+ // minValidTxs is passed into the builder so it can reject the block *before* updating state.
700
+ const minValidTxs = forceCreate ? 0 : (this.config.minValidTxsPerBlock ?? minTxs);
701
+ const blockBuilderOptions: BlockBuilderOptions = {
543
702
  maxTransactions: this.config.maxTxsPerBlock,
544
- maxBlockSize: this.config.maxBlockSizeInBytes,
545
- maxBlockGas: new Gas(this.config.maxDABlockGas, this.config.maxL2BlockGas),
546
- maxBlobFields: maxBlobFieldsForTxs,
703
+ maxBlockGas:
704
+ this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
705
+ ? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
706
+ : undefined,
547
707
  deadline: buildDeadline,
708
+ isBuildingProposal: true,
709
+ minValidTxs,
710
+ maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
711
+ perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
548
712
  };
549
713
 
550
- // Actually build the block by executing txs
551
- const workTimer = new Timer();
552
- const {
553
- publicGas,
554
- block,
555
- publicProcessorDuration,
556
- numTxs,
557
- blockBuildingTimer,
558
- usedTxs,
559
- failedTxs,
560
- usedTxBlobFields,
561
- } = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
562
- const blockBuildDuration = workTimer.ms();
714
+ // Actually build the block by executing txs. The builder throws InsufficientValidTxsError
715
+ // if the number of successfully processed txs is below minValidTxs, ensuring state is not
716
+ // updated for blocks that will be discarded.
717
+ const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
718
+ checkpointBuilder,
719
+ pendingTxs,
720
+ blockNumber,
721
+ blockTimestamp,
722
+ blockBuilderOptions,
723
+ );
563
724
 
564
725
  // If any txs failed during execution, drop them from the mempool so we don't pick them up again
565
- await this.dropFailedTxsFromP2P(failedTxs);
726
+ await this.dropFailedTxsFromP2P(buildResult.failedTxs);
566
727
 
567
- // Check if we have created a block with enough txs. If there were invalid txs in the pool, or if execution took
568
- // too long, then we may not get to minTxsPerBlock after executing public functions.
569
- const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
570
- if (!forceCreate && numTxs < minValidTxs) {
728
+ if (buildResult.status === 'insufficient-valid-txs') {
571
729
  this.log.warn(
572
- `Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed (got ${numTxs} but required ${minValidTxs})`,
573
- { slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint },
730
+ `Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.targetSlot} has too few valid txs to be proposed`,
731
+ {
732
+ slot: this.targetSlot,
733
+ blockNumber,
734
+ numTxs: buildResult.processedCount,
735
+ indexWithinCheckpoint,
736
+ minValidTxs,
737
+ },
574
738
  );
575
- this.eventEmitter.emit('block-tx-count-check-failed', {
576
- minTxs: minValidTxs,
577
- availableTxs: numTxs,
578
- slot: this.slot,
739
+ this.eventEmitter.emit('block-build-failed', {
740
+ reason: `Insufficient valid txs`,
741
+ slot: this.targetSlot,
579
742
  });
580
743
  this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
581
- return undefined;
744
+ return { failure: 'insufficient-valid-txs' };
582
745
  }
583
746
 
584
747
  // Block creation succeeded, emit stats and metrics
748
+ const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
749
+
585
750
  const blockStats = {
586
751
  eventName: 'l2-block-built',
587
752
  duration: blockBuildDuration,
588
753
  publicProcessDuration: publicProcessorDuration,
589
- rollupCircuitsDuration: blockBuildingTimer.ms(),
590
754
  ...block.getStats(),
591
755
  } satisfies L2BlockBuiltStats;
592
756
 
593
757
  const blockHash = await block.hash();
594
758
  const txHashes = block.body.txEffects.map(tx => tx.txHash);
595
- const manaPerSec = publicGas.l2Gas / (blockBuildDuration / 1000);
759
+ const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
596
760
 
597
761
  this.log.info(
598
- `Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
762
+ `Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.targetSlot} with ${numTxs} txs`,
599
763
  { blockHash, txHashes, manaPerSec, ...blockStats },
600
764
  );
601
765
 
602
- this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
603
- this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
766
+ // `slot` is the target/submission slot (may be one ahead when pipelining),
767
+ // `buildSlot` is the wall-clock slot during which the block was actually built.
768
+ this.eventEmitter.emit('block-proposed', {
769
+ blockNumber: block.number,
770
+ slot: this.targetSlot,
771
+ buildSlot: this.slotNow,
772
+ });
773
+ this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe(), this.targetSlot);
604
774
 
605
- return { block, usedTxs, remainingBlobFields: maxBlobFieldsForTxs - usedTxBlobFields };
775
+ return { block, usedTxs };
606
776
  } catch (err: any) {
607
- this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
608
- this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
777
+ this.eventEmitter.emit('block-build-failed', {
778
+ reason: err.message,
779
+ slot: this.targetSlot,
780
+ });
781
+ this.log.error(`Error building block`, err, { blockNumber, slot: this.targetSlot });
609
782
  this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
610
783
  this.metrics.recordFailedBlock();
611
784
  return { error: err };
612
785
  }
613
786
  }
614
787
 
788
+ /** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
789
+ private async buildSingleBlockWithCheckpointBuilder(
790
+ checkpointBuilder: CheckpointBuilder,
791
+ pendingTxs: AsyncIterable<Tx>,
792
+ blockNumber: BlockNumber,
793
+ blockTimestamp: bigint,
794
+ blockBuilderOptions: BlockBuilderOptions,
795
+ ) {
796
+ try {
797
+ const workTimer = new Timer();
798
+ const result = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
799
+ const blockBuildDuration = workTimer.ms();
800
+ return { ...result, blockBuildDuration, status: 'success' as const };
801
+ } catch (err: unknown) {
802
+ if (isErrorClass(err, InsufficientValidTxsError)) {
803
+ return {
804
+ failedTxs: err.failedTxs,
805
+ processedCount: err.processedCount,
806
+ status: 'insufficient-valid-txs' as const,
807
+ };
808
+ }
809
+ throw err;
810
+ }
811
+ }
812
+
615
813
  /** Waits until minTxs are available on the pool for building a block. */
616
814
  @trackSpan('CheckpointProposalJob.waitForMinTxs')
617
815
  private async waitForMinTxs(opts: {
618
816
  forceCreate?: boolean;
619
817
  blockNumber: BlockNumber;
620
- indexWithinCheckpoint: number;
818
+ indexWithinCheckpoint: IndexWithinCheckpoint;
621
819
  buildDeadline: Date | undefined;
622
- }): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
623
- const minTxs = this.config.minTxsPerBlock;
820
+ }): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
624
821
  const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
625
822
 
823
+ // We only allow a block with 0 txs in the first block of the checkpoint
824
+ const minTxs = indexWithinCheckpoint > 0 && this.config.minTxsPerBlock === 0 ? 1 : this.config.minTxsPerBlock;
825
+
626
826
  // Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
627
827
  const startBuildingDeadline = buildDeadline
628
828
  ? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000)
@@ -634,20 +834,20 @@ export class CheckpointProposalJob implements Traceable {
634
834
  // If we're past deadline, or we have no deadline, give up
635
835
  const now = this.dateProvider.nowAsDate();
636
836
  if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
637
- return { canStartBuilding: false, availableTxs: availableTxs };
837
+ return { canStartBuilding: false, availableTxs, minTxs };
638
838
  }
639
839
 
640
840
  // Wait a bit before checking again
641
- this.setStateFn(SequencerState.WAITING_FOR_TXS, this.slot);
841
+ this.setStateFn(SequencerState.WAITING_FOR_TXS, this.targetSlot);
642
842
  this.log.verbose(
643
- `Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (have ${availableTxs} but need ${minTxs})`,
644
- { blockNumber, slot: this.slot, indexWithinCheckpoint },
843
+ `Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (have ${availableTxs} but need ${minTxs})`,
844
+ { blockNumber, slot: this.targetSlot, indexWithinCheckpoint },
645
845
  );
646
- await sleep(TXS_POLLING_MS);
846
+ await this.waitForTxsPollingInterval();
647
847
  availableTxs = await this.p2pClient.getPendingTxCount();
648
848
  }
649
849
 
650
- return { canStartBuilding: true, availableTxs };
850
+ return { canStartBuilding: true, availableTxs, minTxs };
651
851
  }
652
852
 
653
853
  /**
@@ -673,7 +873,7 @@ export class CheckpointProposalJob implements Traceable {
673
873
  this.log.debug(`Attesting committee length is ${committee.length}`, { committee });
674
874
  }
675
875
 
676
- const numberOfRequiredAttestations = Math.floor((committee.length * 2) / 3) + 1;
876
+ const numberOfRequiredAttestations = computeQuorum(committee.length);
677
877
 
678
878
  if (this.config.skipCollectingAttestations) {
679
879
  this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
@@ -682,9 +882,9 @@ export class CheckpointProposalJob implements Traceable {
682
882
  }
683
883
 
684
884
  const attestationTimeAllowed = this.config.enforceTimeTable
685
- ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_CHECKPOINT)!
885
+ ? this.timetable.getCheckpointAttestationDeadline()
686
886
  : this.l1Constants.slotDuration;
687
- const attestationDeadline = new Date(this.dateProvider.now() + attestationTimeAllowed * 1000);
887
+ const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
688
888
 
689
889
  this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
690
890
 
@@ -699,11 +899,28 @@ export class CheckpointProposalJob implements Traceable {
699
899
 
700
900
  collectedAttestationsCount = attestations.length;
701
901
 
902
+ // Trim attestations to minimum required to save L1 calldata gas
903
+ const localAddresses = this.validatorClient.getValidatorAddresses();
904
+ const trimmed = trimAttestations(
905
+ attestations,
906
+ numberOfRequiredAttestations,
907
+ this.attestorAddress,
908
+ localAddresses,
909
+ );
910
+ if (trimmed.length < attestations.length) {
911
+ this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
912
+ }
913
+
702
914
  // Rollup contract requires that the signatures are provided in the order of the committee
703
- const sorted = orderAttestations(attestations, committee);
915
+ const sorted = orderAttestations(trimmed, committee);
704
916
 
705
917
  // Manipulate the attestations if we've been configured to do so
706
- if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
918
+ if (
919
+ this.config.injectFakeAttestation ||
920
+ this.config.injectHighSValueAttestation ||
921
+ this.config.injectUnrecoverableSignatureAttestation ||
922
+ this.config.shuffleAttestationOrdering
923
+ ) {
707
924
  return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
708
925
  }
709
926
 
@@ -732,7 +949,11 @@ export class CheckpointProposalJob implements Traceable {
732
949
  this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
733
950
  );
734
951
 
735
- if (this.config.injectFakeAttestation) {
952
+ if (
953
+ this.config.injectFakeAttestation ||
954
+ this.config.injectHighSValueAttestation ||
955
+ this.config.injectUnrecoverableSignatureAttestation
956
+ ) {
736
957
  // Find non-empty attestations that are not from the proposer
737
958
  const nonProposerIndices: number[] = [];
738
959
  for (let i = 0; i < attestations.length; i++) {
@@ -742,8 +963,20 @@ export class CheckpointProposalJob implements Traceable {
742
963
  }
743
964
  if (nonProposerIndices.length > 0) {
744
965
  const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
745
- this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
746
- unfreeze(attestations[targetIndex]).signature = Signature.random();
966
+ if (this.config.injectHighSValueAttestation) {
967
+ this.log.warn(
968
+ `Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
969
+ );
970
+ unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
971
+ } else if (this.config.injectUnrecoverableSignatureAttestation) {
972
+ this.log.warn(
973
+ `Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
974
+ );
975
+ unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
976
+ } else {
977
+ this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
978
+ unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
979
+ }
747
980
  }
748
981
  return new CommitteeAttestationsAndSigners(attestations);
749
982
  }
@@ -752,11 +985,20 @@ export class CheckpointProposalJob implements Traceable {
752
985
  this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
753
986
 
754
987
  const shuffled = [...attestations];
755
- const [i, j] = [(proposerIndex + 1) % shuffled.length, (proposerIndex + 2) % shuffled.length];
756
- const valueI = shuffled[i];
757
- const valueJ = shuffled[j];
758
- shuffled[i] = valueJ;
759
- shuffled[j] = valueI;
988
+
989
+ // Find two non-proposer positions that both have non-empty signatures to swap.
990
+ // This ensures the bitmap doesn't change, so the MaliciousCommitteeAttestationsAndSigners
991
+ // signers array stays correctly aligned with L1's committee reconstruction.
992
+ const swappable: number[] = [];
993
+ for (let k = 0; k < shuffled.length; k++) {
994
+ if (!shuffled[k].signature.isEmpty() && k !== proposerIndex) {
995
+ swappable.push(k);
996
+ }
997
+ }
998
+ if (swappable.length >= 2) {
999
+ const [i, j] = [swappable[0], swappable[1]];
1000
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
1001
+ }
760
1002
 
761
1003
  const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
762
1004
  return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
@@ -772,16 +1014,20 @@ export class CheckpointProposalJob implements Traceable {
772
1014
  const failedTxData = failedTxs.map(fail => fail.tx);
773
1015
  const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
774
1016
  this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
775
- await this.p2pClient.deleteTxs(failedTxHashes);
1017
+ await this.p2pClient.handleFailedExecution(failedTxHashes);
776
1018
  }
777
1019
 
778
1020
  /**
779
1021
  * Adds the proposed block to the archiver so it's available via P2P.
780
1022
  * Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
781
1023
  * would never receive its own block without this explicit sync.
1024
+ *
1025
+ * In fisherman mode we skip this push: the fisherman builds blocks locally for validation
1026
+ * and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
1027
+ * whenever the real proposer's block arrives from L1.
782
1028
  */
783
1029
  private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
784
- if (this.config.skipPushProposedBlocksToArchiver !== false) {
1030
+ if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
785
1031
  this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
786
1032
  blockNumber: block.number,
787
1033
  slot: block.header.globalVariables.slotNumber,
@@ -799,27 +1045,49 @@ export class CheckpointProposalJob implements Traceable {
799
1045
  private async handleCheckpointEndAsFisherman(checkpoint: Checkpoint | undefined) {
800
1046
  // Perform L1 fee analysis before clearing requests
801
1047
  // The callback is invoked asynchronously after the next block is mined
802
- const feeAnalysis = await this.publisher.analyzeL1Fees(this.slot, analysis =>
1048
+ const feeAnalysis = await this.publisher.analyzeL1Fees(this.targetSlot, analysis =>
803
1049
  this.metrics.recordFishermanFeeAnalysis(analysis),
804
1050
  );
805
1051
 
806
1052
  if (checkpoint) {
807
- this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.slot}`, {
1053
+ this.log.info(`Validation checkpoint building SUCCEEDED for slot ${this.targetSlot}`, {
808
1054
  ...checkpoint.toCheckpointInfo(),
809
1055
  ...checkpoint.getStats(),
810
1056
  feeAnalysisId: feeAnalysis?.id,
811
1057
  });
812
1058
  } else {
813
- this.log.warn(`Validation block building FAILED for slot ${this.slot}`, {
814
- slot: this.slot,
1059
+ this.log.warn(`Validation block building FAILED for slot ${this.targetSlot}`, {
1060
+ slot: this.targetSlot,
815
1061
  feeAnalysisId: feeAnalysis?.id,
816
1062
  });
817
- this.metrics.recordBlockProposalFailed('block_build_failed');
1063
+ this.metrics.recordCheckpointProposalFailed('block_build_failed');
818
1064
  }
819
1065
 
820
1066
  this.publisher.clearPendingRequests();
821
1067
  }
822
1068
 
1069
+ /**
1070
+ * Helper to handle HA double-signing errors. Returns true if the error was handled (caller should yield).
1071
+ */
1072
+ private handleHASigningError(err: any, errorContext: string): boolean {
1073
+ if (err instanceof DutyAlreadySignedError) {
1074
+ this.log.info(`${errorContext} for slot ${this.targetSlot} already signed by another HA node, yielding`, {
1075
+ slot: this.targetSlot,
1076
+ signedByNode: err.signedByNode,
1077
+ });
1078
+ return true;
1079
+ }
1080
+ if (err instanceof SlashingProtectionError) {
1081
+ this.log.info(`${errorContext} for slot ${this.targetSlot} blocked by slashing protection, yielding`, {
1082
+ slot: this.targetSlot,
1083
+ existingMessageHash: err.existingMessageHash,
1084
+ attemptedMessageHash: err.attemptedMessageHash,
1085
+ });
1086
+ return true;
1087
+ }
1088
+ return false;
1089
+ }
1090
+
823
1091
  /** Waits until a specific time within the current slot */
824
1092
  @trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
825
1093
  protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
@@ -828,8 +1096,13 @@ export class CheckpointProposalJob implements Traceable {
828
1096
  await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
829
1097
  }
830
1098
 
1099
+ /** Waits the polling interval for transactions. Extracted for test overriding. */
1100
+ protected async waitForTxsPollingInterval(): Promise<void> {
1101
+ await sleep(TXS_POLLING_MS);
1102
+ }
1103
+
831
1104
  private getSlotStartBuildTimestamp(): number {
832
- return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
1105
+ return getSlotStartBuildTimestamp(this.slotNow, this.l1Constants);
833
1106
  }
834
1107
 
835
1108
  private getSecondsIntoSlot(): number {