@aztec/sequencer-client 2.1.0-rc.9 → 2.1.2-rc.1

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.
@@ -89,6 +89,8 @@ export declare class Sequencer extends Sequencer_base {
89
89
  private metrics;
90
90
  private lastBlockPublished;
91
91
  private governanceProposerPayload;
92
+ /** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */
93
+ private lastSlotForVoteWhenSyncFailed;
92
94
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
93
95
  protected timetable: SequencerTimetable;
94
96
  protected enforceTimeTable: boolean;
@@ -128,8 +130,10 @@ export declare class Sequencer extends Sequencer_base {
128
130
  * - Submit block
129
131
  * - If our block for some reason is not included, revert the state
130
132
  */
131
- protected doRealWork(): Promise<void>;
132
133
  protected work(): Promise<void>;
134
+ /** Tries building a block proposal, and if successful, enqueues it for publishing. */
135
+ private tryBuildBlockAndEnqueuePublish;
136
+ protected safeWork(): Promise<void>;
133
137
  /**
134
138
  * Sets the sequencer state and checks if we have enough time left in the slot to transition to the new state.
135
139
  * @param proposedState - The new state to transition to.
@@ -165,22 +169,47 @@ export declare class Sequencer extends Sequencer_base {
165
169
  /**
166
170
  * Returns whether all dependencies have caught up.
167
171
  * We don't check against the previous block submitted since it may have been reorg'd out.
168
- * @returns Boolean indicating if our dependencies are synced to the latest block.
169
172
  */
170
- protected getChainTip(): Promise<{
173
+ protected checkSync(args: {
174
+ ts: bigint;
175
+ slot: bigint;
176
+ }): Promise<{
171
177
  block?: L2Block;
172
178
  blockNumber: number;
173
179
  archive: Fr;
174
180
  l1Timestamp: bigint;
175
181
  pendingChainValidationStatus: ValidateBlockResult;
176
182
  } | undefined>;
183
+ /**
184
+ * Enqueues governance and slashing votes with the publisher. Does not block.
185
+ * @param publisher - The publisher to enqueue votes with
186
+ * @param attestorAddress - The attestor address to use for signing
187
+ * @param slot - The slot number
188
+ * @param timestamp - The timestamp for the votes
189
+ * @param context - Optional context for logging (e.g., block number)
190
+ * @returns A tuple of [governanceEnqueued, slashingEnqueued]
191
+ */
192
+ protected enqueueGovernanceAndSlashingVotes(publisher: SequencerPublisher, attestorAddress: EthAddress, slot: bigint, timestamp: bigint): [Promise<boolean> | undefined, Promise<boolean> | undefined];
193
+ /**
194
+ * Checks if we are the proposer for the next slot.
195
+ * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
196
+ */
197
+ protected checkCanPropose(slot: bigint): Promise<[boolean, EthAddress | undefined]>;
198
+ /**
199
+ * Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
200
+ * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
201
+ */
202
+ protected tryVoteWhenSyncFails(args: {
203
+ slot: bigint;
204
+ ts: bigint;
205
+ }): Promise<void>;
177
206
  /**
178
207
  * Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
179
208
  * has been there without being invalidated and whether the sequencer is in the committee or not. We always
180
209
  * have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
181
210
  * and if they fail, any sequencer will try as well.
182
211
  */
183
- protected considerInvalidatingBlock(syncedTo: NonNullable<Awaited<ReturnType<Sequencer['getChainTip']>>>, currentSlot: bigint, ourValidatorAddresses: EthAddress[], publisher: SequencerPublisher): Promise<void>;
212
+ protected considerInvalidatingBlock(syncedTo: NonNullable<Awaited<ReturnType<Sequencer['checkSync']>>>, currentSlot: bigint): Promise<void>;
184
213
  private getSlotStartBuildTimestamp;
185
214
  private getSecondsIntoSlot;
186
215
  get aztecSlotDuration(): number;
@@ -1 +1 @@
1
- {"version":3,"file":"sequencer.d.ts","sourceRoot":"","sources":["../../src/sequencer/sequencer.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sequencer.d.ts","sourceRoot":"","sources":["../../src/sequencer/sequencer.ts"],"names":[],"mappings":"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"}
@@ -67,6 +67,7 @@ export { SequencerState };
67
67
  metrics;
68
68
  lastBlockPublished;
69
69
  governanceProposerPayload;
70
+ /** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */ lastSlotForVoteWhenSyncFailed;
70
71
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */ timetable;
71
72
  enforceTimeTable;
72
73
  // This shouldn't be here as this gets re-created each time we build/propose a block.
@@ -142,7 +143,7 @@ export { SequencerState };
142
143
  /**
143
144
  * Starts the sequencer and moves to IDLE state.
144
145
  */ start() {
145
- this.runningPromise = new RunningPromise(this.work.bind(this), this.log, this.pollingIntervalMs);
146
+ this.runningPromise = new RunningPromise(this.safeWork.bind(this), this.log, this.pollingIntervalMs);
146
147
  this.setState(SequencerState.IDLE, undefined, {
147
148
  force: true
148
149
  });
@@ -178,21 +179,28 @@ export { SequencerState };
178
179
  * - Collect attestations for the block
179
180
  * - Submit block
180
181
  * - If our block for some reason is not included, revert the state
181
- */ async doRealWork() {
182
+ */ async work() {
182
183
  this.setState(SequencerState.SYNCHRONIZING, undefined);
183
- // Check all components are synced to latest as seen by the archiver
184
- const syncedTo = await this.getChainTip();
185
- // Do not go forward with new block if the previous one has not been mined and processed
184
+ const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
185
+ // Check we have not already published a block for this slot (cheapest check)
186
+ if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
187
+ this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`);
188
+ return;
189
+ }
190
+ // Check all components are synced to latest as seen by the archiver (queries all subsystems)
191
+ const syncedTo = await this.checkSync({
192
+ ts,
193
+ slot
194
+ });
186
195
  if (!syncedTo) {
196
+ await this.tryVoteWhenSyncFails({
197
+ slot,
198
+ ts
199
+ });
187
200
  return;
188
201
  }
189
202
  const chainTipArchive = syncedTo.archive;
190
203
  const newBlockNumber = syncedTo.blockNumber + 1;
191
- const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
192
- this.setState(SequencerState.PROPOSER_CHECK, slot);
193
- // Check that the archiver and dependencies have synced to the previous L1 slot at least
194
- // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
195
- // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
196
204
  const syncLogData = {
197
205
  now,
198
206
  syncedToL1Ts: syncedTo.l1Timestamp,
@@ -203,67 +211,35 @@ export { SequencerState };
203
211
  newBlockNumber,
204
212
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex')
205
213
  };
206
- if (syncedTo.l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
207
- this.log.debug(`Cannot propose block ${newBlockNumber} at next L2 slot ${slot} due to pending sync from L1`, syncLogData);
214
+ // Check that we are a proposer for the next slot
215
+ this.setState(SequencerState.PROPOSER_CHECK, slot);
216
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
217
+ // If we are not a proposer, check if we should invalidate a invalid block, and bail
218
+ if (!canPropose) {
219
+ await this.considerInvalidatingBlock(syncedTo, slot);
208
220
  return;
209
221
  }
210
- // Check that the slot is not taken by a block already
222
+ // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
211
223
  if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
212
- this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, {
224
+ this.log.warn(`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, {
213
225
  ...syncLogData,
214
226
  block: syncedTo.block.header.toInspect()
215
227
  });
216
228
  return;
217
229
  }
218
- // Or that we haven't published it ourselves
219
- if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
220
- this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`, {
221
- ...syncLogData,
222
- block: this.lastBlockPublished.header.toInspect()
223
- });
224
- return;
225
- }
226
- // Check that we are a proposer for the next slot
227
- let proposerInNextSlot;
228
- try {
229
- proposerInNextSlot = await this.epochCache.getProposerAttesterAddressInNextSlot();
230
- } catch (e) {
231
- if (e instanceof NoCommitteeError) {
232
- this.log.warn(`Cannot propose block ${newBlockNumber} at next L2 slot ${slot} since the committee does not exist on L1`);
233
- return;
234
- }
235
- }
236
- // If get proposer in next slot is undefined, then the committee is empty and anyone may propose.
237
- // If the committee is defined and not empty, but none of our validators are the proposer, then stop.
238
- const validatorAddresses = this.validatorClient.getValidatorAddresses();
239
- if (proposerInNextSlot !== undefined && !validatorAddresses.some((addr)=>addr.equals(proposerInNextSlot))) {
240
- this.log.debug(`Cannot propose block ${newBlockNumber} since we are not a proposer`, {
241
- us: validatorAddresses,
242
- proposer: proposerInNextSlot,
243
- ...syncLogData
244
- });
245
- // If the pending chain is invalid, we may need to invalidate the block if no one else is doing it.
246
- if (!syncedTo.pendingChainValidationStatus.valid) {
247
- // We pass i undefined here to get any available publisher.
248
- const { publisher } = await this.publisherFactory.create(undefined);
249
- await this.considerInvalidatingBlock(syncedTo, slot, validatorAddresses, publisher);
250
- }
251
- return;
252
- }
253
- // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
254
- // if all the previous checks are good, but we do it just in case.
255
- const proposerAddressInNextSlot = proposerInNextSlot ?? EthAddress.ZERO;
256
230
  // We now need to get ourselves a publisher.
257
231
  // The returned attestor will be the one we provided if we provided one.
258
232
  // Otherwise it will be a valid attestor for the returned publisher.
259
- const { attestorAddress, publisher } = await this.publisherFactory.create(proposerInNextSlot);
233
+ const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
260
234
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
261
235
  this.publisher = publisher;
262
236
  const coinbase = this.validatorClient.getCoinbaseForAttestor(attestorAddress);
263
237
  const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(attestorAddress);
264
238
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
265
239
  const invalidateBlock = await publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
266
- const canProposeCheck = await publisher.canProposeAtNextEthBlock(chainTipArchive, proposerAddressInNextSlot, invalidateBlock);
240
+ // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
241
+ // if all the previous checks are good, but we do it just in case.
242
+ const canProposeCheck = await publisher.canProposeAtNextEthBlock(chainTipArchive, proposer ?? EthAddress.ZERO, invalidateBlock);
267
243
  if (canProposeCheck === undefined) {
268
244
  this.log.warn(`Cannot propose block ${newBlockNumber} at slot ${slot} due to failed rollup contract check`, syncLogData);
269
245
  this.emit('proposer-rollup-check-failed', {
@@ -293,43 +269,46 @@ export { SequencerState };
293
269
  });
294
270
  return;
295
271
  }
296
- this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot}` + (proposerInNextSlot ? ` as ${proposerInNextSlot}` : ''), {
297
- ...syncLogData,
298
- validatorAddresses
272
+ this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, {
273
+ ...syncLogData
299
274
  });
300
275
  const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(newBlockNumber, coinbase, feeRecipient, slot);
301
- const { timestamp } = newGlobalVariables;
302
- const signerFn = (msg)=>this.validatorClient.signWithAddress(attestorAddress, msg).then((s)=>s.toString());
303
- const enqueueGovernanceSignalPromise = this.governanceProposerPayload && !this.governanceProposerPayload.isZero() ? publisher.enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn).catch((err)=>{
304
- this.log.error(`Error enqueuing governance vote`, err, {
305
- blockNumber: newBlockNumber,
306
- slot
307
- });
308
- return false;
309
- }) : Promise.resolve(false);
310
- const enqueueSlashingActionsPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn)).catch((err)=>{
311
- this.log.error(`Error enqueuing slashing actions`, err, {
312
- blockNumber: newBlockNumber,
313
- slot
314
- });
315
- return false;
316
- }) : Promise.resolve(false);
276
+ // Enqueue governance and slashing votes (returns promises that will be awaited later)
277
+ const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, newGlobalVariables.timestamp);
278
+ // Enqueues block invalidation
317
279
  if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
318
280
  publisher.enqueueInvalidateBlock(invalidateBlock);
319
281
  }
282
+ // Actual block building
320
283
  this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
321
- this.metrics.incOpenSlot(slot, proposerAddressInNextSlot.toString());
284
+ const block = await this.tryBuildBlockAndEnqueuePublish(slot, proposer, newBlockNumber, publisher, newGlobalVariables, chainTipArchive, invalidateBlock);
285
+ // Wait until the voting promises have resolved, so all requests are enqueued
286
+ await Promise.all(votesPromises);
287
+ // And send the tx to L1
288
+ const l1Response = await publisher.sendRequests();
289
+ const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
290
+ if (proposedBlock) {
291
+ this.lastBlockPublished = block;
292
+ this.emit('block-published', {
293
+ blockNumber: newBlockNumber,
294
+ slot: Number(slot)
295
+ });
296
+ await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
297
+ } else if (block) {
298
+ this.emit('block-publish-failed', l1Response ?? {});
299
+ }
300
+ this.setState(SequencerState.IDLE, undefined);
301
+ }
302
+ /** Tries building a block proposal, and if successful, enqueues it for publishing. */ async tryBuildBlockAndEnqueuePublish(slot, proposer, newBlockNumber, publisher, newGlobalVariables, chainTipArchive, invalidateBlock) {
303
+ this.metrics.incOpenSlot(slot, (proposer ?? EthAddress.ZERO).toString());
322
304
  this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
323
- proposer: proposerInNextSlot?.toString(),
324
- coinbase,
305
+ proposer,
325
306
  publisher: publisher.getSenderAddress(),
326
- feeRecipient,
327
307
  globalVariables: newGlobalVariables.toInspect(),
328
308
  chainTipArchive,
329
309
  blockNumber: newBlockNumber,
330
310
  slot
331
311
  });
332
- // If I created a "partial" header here that should make our job much easier.
333
312
  const proposalHeader = ProposedBlockHeader.from({
334
313
  ...newGlobalVariables,
335
314
  timestamp: newGlobalVariables.timestamp,
@@ -344,7 +323,7 @@ export { SequencerState };
344
323
  // and also we may need to fetch more if we don't have enough valid txs.
345
324
  const pendingTxs = this.p2pClient.iteratePendingTxs();
346
325
  try {
347
- block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposerInNextSlot, invalidateBlock, publisher);
326
+ block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposer, invalidateBlock, publisher);
348
327
  } catch (err) {
349
328
  this.emit('block-build-failed', {
350
329
  reason: err.message
@@ -369,27 +348,11 @@ export { SequencerState };
369
348
  availableTxs: pendingTxCount
370
349
  });
371
350
  }
372
- await Promise.all([
373
- enqueueGovernanceSignalPromise,
374
- enqueueSlashingActionsPromise
375
- ]);
376
- const l1Response = await publisher.sendRequests();
377
- const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
378
- if (proposedBlock) {
379
- this.lastBlockPublished = block;
380
- this.emit('block-published', {
381
- blockNumber: newBlockNumber,
382
- slot: Number(slot)
383
- });
384
- await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
385
- } else if (block) {
386
- this.emit('block-publish-failed', l1Response ?? {});
387
- }
388
- this.setState(SequencerState.IDLE, undefined);
351
+ return block;
389
352
  }
390
- async work() {
353
+ async safeWork() {
391
354
  try {
392
- await this.doRealWork();
355
+ await this.work();
393
356
  } catch (err) {
394
357
  if (err instanceof SequencerTooSlowError) {
395
358
  // Log as warn only if we had to abort halfway through the block proposal
@@ -612,8 +575,20 @@ export { SequencerState };
612
575
  /**
613
576
  * Returns whether all dependencies have caught up.
614
577
  * We don't check against the previous block submitted since it may have been reorg'd out.
615
- * @returns Boolean indicating if our dependencies are synced to the latest block.
616
- */ async getChainTip() {
578
+ */ async checkSync(args) {
579
+ // Check that the archiver and dependencies have synced to the previous L1 slot at least
580
+ // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
581
+ // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
582
+ const l1Timestamp = await this.l2BlockSource.getL1Timestamp();
583
+ const { slot, ts } = args;
584
+ if (l1Timestamp === undefined || l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
585
+ this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
586
+ slot,
587
+ ts,
588
+ l1Timestamp
589
+ });
590
+ return undefined;
591
+ }
617
592
  const syncedBlocks = await Promise.all([
618
593
  this.worldState.status().then(({ syncSummary })=>({
619
594
  number: syncSummary.latestBlockNumber,
@@ -622,39 +597,24 @@ export { SequencerState };
622
597
  this.l2BlockSource.getL2Tips().then((t)=>t.latest),
623
598
  this.p2pClient.getStatus().then((p2p)=>p2p.syncedToL2Block),
624
599
  this.l1ToL2MessageSource.getL2Tips().then((t)=>t.latest),
625
- this.l2BlockSource.getL1Timestamp(),
626
600
  this.l2BlockSource.getPendingChainValidationStatus()
627
601
  ]);
628
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, l1Timestamp, pendingChainValidationStatus] = syncedBlocks;
602
+ const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
629
603
  // The archiver reports 'undefined' hash for the genesis block
630
604
  // because it doesn't have access to world state to compute it (facepalm)
631
605
  const result = l2BlockSource.hash === undefined ? worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0 : worldState.hash === l2BlockSource.hash && p2p.hash === l2BlockSource.hash && l1ToL2MessageSource.hash === l2BlockSource.hash;
632
- const logData = {
633
- worldState,
634
- l2BlockSource,
635
- p2p,
636
- l1ToL2MessageSource
637
- };
638
- this.log.debug(`Sequencer sync check ${result ? 'succeeded' : 'failed'}`, logData);
639
606
  if (!result) {
607
+ this.log.debug(`Sequencer sync check failed`, {
608
+ worldState,
609
+ l2BlockSource,
610
+ p2p,
611
+ l1ToL2MessageSource
612
+ });
640
613
  return undefined;
641
614
  }
615
+ // Special case for genesis state
642
616
  const blockNumber = worldState.number;
643
- if (blockNumber >= INITIAL_L2_BLOCK_NUM) {
644
- const block = await this.l2BlockSource.getBlock(blockNumber);
645
- if (!block) {
646
- // this shouldn't really happen because a moment ago we checked that all components were in sync
647
- this.log.warn(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`, logData);
648
- return undefined;
649
- }
650
- return {
651
- block,
652
- blockNumber: block.number,
653
- archive: block.archive.root,
654
- l1Timestamp,
655
- pendingChainValidationStatus
656
- };
657
- } else {
617
+ if (blockNumber < INITIAL_L2_BLOCK_NUM) {
658
618
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
659
619
  return {
660
620
  blockNumber: INITIAL_L2_BLOCK_NUM - 1,
@@ -663,20 +623,170 @@ export { SequencerState };
663
623
  pendingChainValidationStatus
664
624
  };
665
625
  }
626
+ const block = await this.l2BlockSource.getBlock(blockNumber);
627
+ if (!block) {
628
+ // this shouldn't really happen because a moment ago we checked that all components were in sync
629
+ this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
630
+ return undefined;
631
+ }
632
+ return {
633
+ block,
634
+ blockNumber: block.number,
635
+ archive: block.archive.root,
636
+ l1Timestamp,
637
+ pendingChainValidationStatus
638
+ };
639
+ }
640
+ /**
641
+ * Enqueues governance and slashing votes with the publisher. Does not block.
642
+ * @param publisher - The publisher to enqueue votes with
643
+ * @param attestorAddress - The attestor address to use for signing
644
+ * @param slot - The slot number
645
+ * @param timestamp - The timestamp for the votes
646
+ * @param context - Optional context for logging (e.g., block number)
647
+ * @returns A tuple of [governanceEnqueued, slashingEnqueued]
648
+ */ enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, timestamp) {
649
+ try {
650
+ const signerFn = (msg)=>this.validatorClient.signWithAddress(attestorAddress, msg).then((s)=>s.toString());
651
+ const enqueueGovernancePromise = this.governanceProposerPayload && !this.governanceProposerPayload.isZero() ? publisher.enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn).catch((err)=>{
652
+ this.log.error(`Error enqueuing governance vote`, err, {
653
+ slot
654
+ });
655
+ return false;
656
+ }) : undefined;
657
+ const enqueueSlashingPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn)).catch((err)=>{
658
+ this.log.error(`Error enqueuing slashing actions`, err, {
659
+ slot
660
+ });
661
+ return false;
662
+ }) : undefined;
663
+ return [
664
+ enqueueGovernancePromise,
665
+ enqueueSlashingPromise
666
+ ];
667
+ } catch (err) {
668
+ this.log.error(`Error enqueueing governance and slashing votes`, err);
669
+ return [
670
+ undefined,
671
+ undefined
672
+ ];
673
+ }
674
+ }
675
+ /**
676
+ * Checks if we are the proposer for the next slot.
677
+ * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
678
+ */ async checkCanPropose(slot) {
679
+ let proposer;
680
+ try {
681
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
682
+ } catch (e) {
683
+ if (e instanceof NoCommitteeError) {
684
+ this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
685
+ return [
686
+ false,
687
+ undefined
688
+ ];
689
+ }
690
+ this.log.error(`Error getting proposer for slot ${slot}`, e);
691
+ return [
692
+ false,
693
+ undefined
694
+ ];
695
+ }
696
+ // If proposer is undefined, then the committee is empty and anyone may propose
697
+ if (proposer === undefined) {
698
+ return [
699
+ true,
700
+ undefined
701
+ ];
702
+ }
703
+ const validatorAddresses = this.validatorClient.getValidatorAddresses();
704
+ const weAreProposer = validatorAddresses.some((addr)=>addr.equals(proposer));
705
+ if (!weAreProposer) {
706
+ this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, {
707
+ validatorAddresses,
708
+ proposer
709
+ });
710
+ return [
711
+ false,
712
+ proposer
713
+ ];
714
+ }
715
+ return [
716
+ true,
717
+ proposer
718
+ ];
719
+ }
720
+ /**
721
+ * Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
722
+ * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
723
+ */ async tryVoteWhenSyncFails(args) {
724
+ const { slot, ts } = args;
725
+ // Prevent duplicate attempts in the same slot
726
+ if (this.lastSlotForVoteWhenSyncFailed === slot) {
727
+ this.log.debug(`Already attempted to vote in slot ${slot} (skipping)`);
728
+ return;
729
+ }
730
+ // Check if we're past the max time for initializing a proposal
731
+ const secondsIntoSlot = this.getSecondsIntoSlot(slot);
732
+ const maxAllowedTime = this.timetable.getMaxAllowedTime(SequencerState.INITIALIZING_PROPOSAL);
733
+ // If we haven't exceeded the time limit for initializing a proposal, don't proceed with voting
734
+ // We use INITIALIZING_PROPOSAL time limit because if we're past that, we can't build a block anyway
735
+ if (maxAllowedTime === undefined || secondsIntoSlot <= maxAllowedTime) {
736
+ this.log.trace(`Not attempting to vote since there is still for block building`, {
737
+ secondsIntoSlot,
738
+ maxAllowedTime
739
+ });
740
+ return;
741
+ }
742
+ this.log.debug(`Sync for slot ${slot} failed, checking for voting opportunities`, {
743
+ secondsIntoSlot,
744
+ maxAllowedTime
745
+ });
746
+ // Check if we're a proposer or proposal is open
747
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
748
+ if (!canPropose) {
749
+ this.log.debug(`Cannot vote in slot ${slot} since we are not a proposer`, {
750
+ slot,
751
+ proposer
752
+ });
753
+ return;
754
+ }
755
+ // Mark this slot as attempted
756
+ this.lastSlotForVoteWhenSyncFailed = slot;
757
+ // Get a publisher for voting
758
+ const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
759
+ this.log.debug(`Attempting to vote despite sync failure at slot ${slot}`, {
760
+ attestorAddress,
761
+ slot
762
+ });
763
+ // Enqueue governance and slashing votes using the shared helper method
764
+ const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, ts);
765
+ await Promise.all(votesPromises);
766
+ if (votesPromises.every((p)=>!p)) {
767
+ this.log.debug(`No votes to enqueue for slot ${slot}`);
768
+ return;
769
+ }
770
+ this.log.info(`Voting in slot ${slot} despite sync failure`, {
771
+ slot
772
+ });
773
+ await publisher.sendRequests();
666
774
  }
667
775
  /**
668
776
  * Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
669
777
  * has been there without being invalidated and whether the sequencer is in the committee or not. We always
670
778
  * have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
671
779
  * and if they fail, any sequencer will try as well.
672
- */ async considerInvalidatingBlock(syncedTo, currentSlot, ourValidatorAddresses, publisher) {
780
+ */ async considerInvalidatingBlock(syncedTo, currentSlot) {
673
781
  const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
674
782
  if (pendingChainValidationStatus.valid) {
675
783
  return;
676
784
  }
785
+ const { publisher } = await this.publisherFactory.create(undefined);
677
786
  const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
678
787
  const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
679
788
  const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
789
+ const ourValidatorAddresses = this.validatorClient.getValidatorAddresses();
680
790
  const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } = this.config;
681
791
  const logData = {
682
792
  invalidL1Timestamp: invalidBlockTimestamp,
@@ -722,7 +832,7 @@ export { SequencerState };
722
832
  }
723
833
  _ts_decorate([
724
834
  trackSpan('Sequencer.work')
725
- ], Sequencer.prototype, "work", null);
835
+ ], Sequencer.prototype, "safeWork", null);
726
836
  _ts_decorate([
727
837
  trackSpan('Sequencer.buildBlockAndEnqueuePublish', (_validTxs, _proposalHeader, newGlobalVariables)=>({
728
838
  [Attributes.BLOCK_NUMBER]: newGlobalVariables.blockNumber
@@ -42,6 +42,8 @@ export declare class SequencerTimetable {
42
42
  getBlockProposalExecTimeEnd(secondsIntoSlot: number): number;
43
43
  private get afterBlockReexecTimeNeeded();
44
44
  getValidatorReexecTimeEnd(secondsIntoSlot?: number): number;
45
+ getMaxAllowedTime(state: Extract<SequencerState, SequencerState.STOPPED | SequencerState.STOPPING | SequencerState.IDLE | SequencerState.SYNCHRONIZING>): undefined;
46
+ getMaxAllowedTime(state: Exclude<SequencerState, SequencerState.STOPPED | SequencerState.STOPPING | SequencerState.IDLE | SequencerState.SYNCHRONIZING>): number;
45
47
  getMaxAllowedTime(state: SequencerState): number | undefined;
46
48
  assertTimeLeft(newState: SequencerState, secondsIntoSlot: number): void;
47
49
  }
@@ -1 +1 @@
1
- {"version":3,"file":"timetable.d.ts","sourceRoot":"","sources":["../../src/sequencer/timetable.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAM5C,qBAAa,kBAAkB;IA+C3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG;IA/CtB;;;;OAIG;IACH,SAAgB,kBAAkB,EAAE,MAAM,CAAC;IAE3C;;;;OAIG;IACH,SAAgB,gBAAgB,SAAC;IAEjC,sHAAsH;IACtH,SAAgB,gBAAgB,EAAE,MAAM,CAAsB;IAE9D,uDAAuD;IACvD,SAAgB,gBAAgB,EAAE,MAAM,CAAsB;IAE9D,mGAAmG;IACnG,SAAgB,0BAA0B,EAAE,MAAM,CAAC;IAEnD,mIAAmI;IACnI,SAAgB,mBAAmB,EAAE,MAAM,CAAyB;IAEpE,wCAAwC;IACxC,SAAgB,oBAAoB,EAAE,MAAM,CAAC;IAE7C,kFAAkF;IAClF,SAAgB,iBAAiB,EAAE,MAAM,CAAC;IAE1C,2IAA2I;IAC3I,SAAgB,4BAA4B,EAAE,MAAM,CAAC;IAErD,4DAA4D;IAC5D,SAAgB,OAAO,EAAE,OAAO,CAAC;gBAG/B,IAAI,EAAE;QACJ,oBAAoB,EAAE,MAAM,CAAC;QAC7B,iBAAiB,EAAE,MAAM,CAAC;QAC1B,4BAA4B,EAAE,MAAM,CAAC;QACrC,0BAA0B,CAAC,EAAE,MAAM,CAAC;QACpC,OAAO,EAAE,OAAO,CAAC;KAClB,EACgB,OAAO,CAAC,EAAE,gBAAgB,YAAA,EAC1B,GAAG,mCAAsC;IA+C5D,OAAO,KAAK,yCAAyC,GAEpD;IAEM,2BAA2B,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM;IAenE,OAAO,KAAK,0BAA0B,GAErC;IAEM,yBAAyB,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM;IAU3D,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS;IAuB5D,cAAc,CAAC,QAAQ,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM;CAkBxE"}
1
+ {"version":3,"file":"timetable.d.ts","sourceRoot":"","sources":["../../src/sequencer/timetable.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAM5C,qBAAa,kBAAkB;IA+C3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG;IA/CtB;;;;OAIG;IACH,SAAgB,kBAAkB,EAAE,MAAM,CAAC;IAE3C;;;;OAIG;IACH,SAAgB,gBAAgB,SAAC;IAEjC,sHAAsH;IACtH,SAAgB,gBAAgB,EAAE,MAAM,CAAsB;IAE9D,uDAAuD;IACvD,SAAgB,gBAAgB,EAAE,MAAM,CAAsB;IAE9D,mGAAmG;IACnG,SAAgB,0BAA0B,EAAE,MAAM,CAAC;IAEnD,mIAAmI;IACnI,SAAgB,mBAAmB,EAAE,MAAM,CAAyB;IAEpE,wCAAwC;IACxC,SAAgB,oBAAoB,EAAE,MAAM,CAAC;IAE7C,kFAAkF;IAClF,SAAgB,iBAAiB,EAAE,MAAM,CAAC;IAE1C,2IAA2I;IAC3I,SAAgB,4BAA4B,EAAE,MAAM,CAAC;IAErD,4DAA4D;IAC5D,SAAgB,OAAO,EAAE,OAAO,CAAC;gBAG/B,IAAI,EAAE;QACJ,oBAAoB,EAAE,MAAM,CAAC;QAC7B,iBAAiB,EAAE,MAAM,CAAC;QAC1B,4BAA4B,EAAE,MAAM,CAAC;QACrC,0BAA0B,CAAC,EAAE,MAAM,CAAC;QACpC,OAAO,EAAE,OAAO,CAAC;KAClB,EACgB,OAAO,CAAC,EAAE,gBAAgB,YAAA,EAC1B,GAAG,mCAAsC;IA+C5D,OAAO,KAAK,yCAAyC,GAEpD;IAEM,2BAA2B,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM;IAenE,OAAO,KAAK,0BAA0B,GAErC;IAEM,yBAAyB,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM;IAU3D,iBAAiB,CACtB,KAAK,EAAE,OAAO,CACZ,cAAc,EACd,cAAc,CAAC,OAAO,GAAG,cAAc,CAAC,QAAQ,GAAG,cAAc,CAAC,IAAI,GAAG,cAAc,CAAC,aAAa,CACtG,GACA,SAAS;IACL,iBAAiB,CACtB,KAAK,EAAE,OAAO,CACZ,cAAc,EACd,cAAc,CAAC,OAAO,GAAG,cAAc,CAAC,QAAQ,GAAG,cAAc,CAAC,IAAI,GAAG,cAAc,CAAC,aAAa,CACtG,GACA,MAAM;IACF,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS;IAwB5D,cAAc,CAAC,QAAQ,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM;CAkBxE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/sequencer-client",
3
- "version": "2.1.0-rc.9",
3
+ "version": "2.1.2-rc.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -26,37 +26,37 @@
26
26
  "test:integration:run": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --no-cache --config jest.integration.config.json"
27
27
  },
28
28
  "dependencies": {
29
- "@aztec/aztec.js": "2.1.0-rc.9",
30
- "@aztec/bb-prover": "2.1.0-rc.9",
31
- "@aztec/blob-lib": "2.1.0-rc.9",
32
- "@aztec/blob-sink": "2.1.0-rc.9",
33
- "@aztec/constants": "2.1.0-rc.9",
34
- "@aztec/epoch-cache": "2.1.0-rc.9",
35
- "@aztec/ethereum": "2.1.0-rc.9",
36
- "@aztec/foundation": "2.1.0-rc.9",
37
- "@aztec/l1-artifacts": "2.1.0-rc.9",
38
- "@aztec/merkle-tree": "2.1.0-rc.9",
39
- "@aztec/node-keystore": "2.1.0-rc.9",
40
- "@aztec/noir-acvm_js": "2.1.0-rc.9",
41
- "@aztec/noir-contracts.js": "2.1.0-rc.9",
42
- "@aztec/noir-protocol-circuits-types": "2.1.0-rc.9",
43
- "@aztec/noir-types": "2.1.0-rc.9",
44
- "@aztec/p2p": "2.1.0-rc.9",
45
- "@aztec/protocol-contracts": "2.1.0-rc.9",
46
- "@aztec/prover-client": "2.1.0-rc.9",
47
- "@aztec/simulator": "2.1.0-rc.9",
48
- "@aztec/slasher": "2.1.0-rc.9",
49
- "@aztec/stdlib": "2.1.0-rc.9",
50
- "@aztec/telemetry-client": "2.1.0-rc.9",
51
- "@aztec/validator-client": "2.1.0-rc.9",
52
- "@aztec/world-state": "2.1.0-rc.9",
29
+ "@aztec/aztec.js": "2.1.2-rc.1",
30
+ "@aztec/bb-prover": "2.1.2-rc.1",
31
+ "@aztec/blob-lib": "2.1.2-rc.1",
32
+ "@aztec/blob-sink": "2.1.2-rc.1",
33
+ "@aztec/constants": "2.1.2-rc.1",
34
+ "@aztec/epoch-cache": "2.1.2-rc.1",
35
+ "@aztec/ethereum": "2.1.2-rc.1",
36
+ "@aztec/foundation": "2.1.2-rc.1",
37
+ "@aztec/l1-artifacts": "2.1.2-rc.1",
38
+ "@aztec/merkle-tree": "2.1.2-rc.1",
39
+ "@aztec/node-keystore": "2.1.2-rc.1",
40
+ "@aztec/noir-acvm_js": "2.1.2-rc.1",
41
+ "@aztec/noir-contracts.js": "2.1.2-rc.1",
42
+ "@aztec/noir-protocol-circuits-types": "2.1.2-rc.1",
43
+ "@aztec/noir-types": "2.1.2-rc.1",
44
+ "@aztec/p2p": "2.1.2-rc.1",
45
+ "@aztec/protocol-contracts": "2.1.2-rc.1",
46
+ "@aztec/prover-client": "2.1.2-rc.1",
47
+ "@aztec/simulator": "2.1.2-rc.1",
48
+ "@aztec/slasher": "2.1.2-rc.1",
49
+ "@aztec/stdlib": "2.1.2-rc.1",
50
+ "@aztec/telemetry-client": "2.1.2-rc.1",
51
+ "@aztec/validator-client": "2.1.2-rc.1",
52
+ "@aztec/world-state": "2.1.2-rc.1",
53
53
  "lodash.chunk": "^4.2.0",
54
54
  "tslib": "^2.4.0",
55
- "viem": "2.23.7"
55
+ "viem": "npm:@spalladino/viem@2.38.2-eip7594.0"
56
56
  },
57
57
  "devDependencies": {
58
- "@aztec/archiver": "2.1.0-rc.9",
59
- "@aztec/kv-store": "2.1.0-rc.9",
58
+ "@aztec/archiver": "2.1.2-rc.1",
59
+ "@aztec/kv-store": "2.1.2-rc.1",
60
60
  "@jest/globals": "^30.0.0",
61
61
  "@types/jest": "^30.0.0",
62
62
  "@types/lodash.chunk": "^4.2.7",
@@ -96,6 +96,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
96
96
 
97
97
  private governanceProposerPayload: EthAddress | undefined;
98
98
 
99
+ /** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */
100
+ private lastSlotForVoteWhenSyncFailed: bigint | undefined;
101
+
99
102
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
100
103
  protected timetable!: SequencerTimetable;
101
104
  protected enforceTimeTable: boolean = false;
@@ -212,7 +215,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
212
215
  * Starts the sequencer and moves to IDLE state.
213
216
  */
214
217
  public start() {
215
- this.runningPromise = new RunningPromise(this.work.bind(this), this.log, this.pollingIntervalMs);
218
+ this.runningPromise = new RunningPromise(this.safeWork.bind(this), this.log, this.pollingIntervalMs);
216
219
  this.setState(SequencerState.IDLE, undefined, { force: true });
217
220
  this.runningPromise.start();
218
221
  this.log.info('Started sequencer');
@@ -246,27 +249,28 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
246
249
  * - Submit block
247
250
  * - If our block for some reason is not included, revert the state
248
251
  */
249
- protected async doRealWork() {
252
+ protected async work() {
250
253
  this.setState(SequencerState.SYNCHRONIZING, undefined);
254
+ const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
251
255
 
252
- // Check all components are synced to latest as seen by the archiver
253
- const syncedTo = await this.getChainTip();
256
+ // Check we have not already published a block for this slot (cheapest check)
257
+ if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
258
+ this.log.debug(
259
+ `Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`,
260
+ );
261
+ return;
262
+ }
254
263
 
255
- // Do not go forward with new block if the previous one has not been mined and processed
264
+ // Check all components are synced to latest as seen by the archiver (queries all subsystems)
265
+ const syncedTo = await this.checkSync({ ts, slot });
256
266
  if (!syncedTo) {
267
+ await this.tryVoteWhenSyncFails({ slot, ts });
257
268
  return;
258
269
  }
259
270
 
260
271
  const chainTipArchive = syncedTo.archive;
261
272
  const newBlockNumber = syncedTo.blockNumber + 1;
262
273
 
263
- const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
264
-
265
- this.setState(SequencerState.PROPOSER_CHECK, slot);
266
-
267
- // Check that the archiver and dependencies have synced to the previous L1 slot at least
268
- // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
269
- // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
270
274
  const syncLogData = {
271
275
  now,
272
276
  syncedToL1Ts: syncedTo.l1Timestamp,
@@ -278,74 +282,30 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
278
282
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
279
283
  };
280
284
 
281
- if (syncedTo.l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
282
- this.log.debug(
283
- `Cannot propose block ${newBlockNumber} at next L2 slot ${slot} due to pending sync from L1`,
284
- syncLogData,
285
- );
285
+ // Check that we are a proposer for the next slot
286
+ this.setState(SequencerState.PROPOSER_CHECK, slot);
287
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
288
+
289
+ // If we are not a proposer, check if we should invalidate a invalid block, and bail
290
+ if (!canPropose) {
291
+ await this.considerInvalidatingBlock(syncedTo, slot);
286
292
  return;
287
293
  }
288
294
 
289
- // Check that the slot is not taken by a block already
295
+ // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
290
296
  if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
291
- this.log.debug(
297
+ this.log.warn(
292
298
  `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
293
299
  { ...syncLogData, block: syncedTo.block.header.toInspect() },
294
300
  );
295
301
  return;
296
302
  }
297
303
 
298
- // Or that we haven't published it ourselves
299
- if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
300
- this.log.debug(
301
- `Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`,
302
- { ...syncLogData, block: this.lastBlockPublished.header.toInspect() },
303
- );
304
- return;
305
- }
306
-
307
- // Check that we are a proposer for the next slot
308
- let proposerInNextSlot: EthAddress | undefined;
309
- try {
310
- proposerInNextSlot = await this.epochCache.getProposerAttesterAddressInNextSlot();
311
- } catch (e) {
312
- if (e instanceof NoCommitteeError) {
313
- this.log.warn(
314
- `Cannot propose block ${newBlockNumber} at next L2 slot ${slot} since the committee does not exist on L1`,
315
- );
316
- return;
317
- }
318
- }
319
-
320
- // If get proposer in next slot is undefined, then the committee is empty and anyone may propose.
321
- // If the committee is defined and not empty, but none of our validators are the proposer, then stop.
322
- const validatorAddresses = this.validatorClient!.getValidatorAddresses();
323
- if (proposerInNextSlot !== undefined && !validatorAddresses.some(addr => addr.equals(proposerInNextSlot))) {
324
- this.log.debug(`Cannot propose block ${newBlockNumber} since we are not a proposer`, {
325
- us: validatorAddresses,
326
- proposer: proposerInNextSlot,
327
- ...syncLogData,
328
- });
329
- // If the pending chain is invalid, we may need to invalidate the block if no one else is doing it.
330
- if (!syncedTo.pendingChainValidationStatus.valid) {
331
- // We pass i undefined here to get any available publisher.
332
- const { publisher } = await this.publisherFactory.create(undefined);
333
- await this.considerInvalidatingBlock(syncedTo, slot, validatorAddresses, publisher);
334
- }
335
- return;
336
- }
337
-
338
- // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
339
- // if all the previous checks are good, but we do it just in case.
340
- const proposerAddressInNextSlot = proposerInNextSlot ?? EthAddress.ZERO;
341
-
342
304
  // We now need to get ourselves a publisher.
343
305
  // The returned attestor will be the one we provided if we provided one.
344
306
  // Otherwise it will be a valid attestor for the returned publisher.
345
- const { attestorAddress, publisher } = await this.publisherFactory.create(proposerInNextSlot);
346
-
307
+ const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
347
308
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
348
-
349
309
  this.publisher = publisher;
350
310
 
351
311
  const coinbase = this.validatorClient!.getCoinbaseForAttestor(attestorAddress);
@@ -353,9 +313,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
353
313
 
354
314
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
355
315
  const invalidateBlock = await publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
316
+
317
+ // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
318
+ // if all the previous checks are good, but we do it just in case.
356
319
  const canProposeCheck = await publisher.canProposeAtNextEthBlock(
357
320
  chainTipArchive,
358
- proposerAddressInNextSlot,
321
+ proposer ?? EthAddress.ZERO,
359
322
  invalidateBlock,
360
323
  );
361
324
 
@@ -382,10 +345,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
382
345
  return;
383
346
  }
384
347
 
385
- this.log.debug(
386
- `Can propose block ${newBlockNumber} at slot ${slot}` + (proposerInNextSlot ? ` as ${proposerInNextSlot}` : ''),
387
- { ...syncLogData, validatorAddresses },
388
- );
348
+ this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, { ...syncLogData });
389
349
 
390
350
  const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(
391
351
  newBlockNumber,
@@ -394,49 +354,68 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
394
354
  slot,
395
355
  );
396
356
 
397
- const { timestamp } = newGlobalVariables;
398
- const signerFn = (msg: TypedDataDefinition) =>
399
- this.validatorClient!.signWithAddress(attestorAddress, msg).then(s => s.toString());
400
-
401
- const enqueueGovernanceSignalPromise =
402
- this.governanceProposerPayload && !this.governanceProposerPayload.isZero()
403
- ? publisher
404
- .enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn)
405
- .catch(err => {
406
- this.log.error(`Error enqueuing governance vote`, err, { blockNumber: newBlockNumber, slot });
407
- return false;
408
- })
409
- : Promise.resolve(false);
410
-
411
- const enqueueSlashingActionsPromise = this.slasherClient
412
- ? this.slasherClient
413
- .getProposerActions(slot)
414
- .then(actions => publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn))
415
- .catch(err => {
416
- this.log.error(`Error enqueuing slashing actions`, err, { blockNumber: newBlockNumber, slot });
417
- return false;
418
- })
419
- : Promise.resolve(false);
357
+ // Enqueue governance and slashing votes (returns promises that will be awaited later)
358
+ const votesPromises = this.enqueueGovernanceAndSlashingVotes(
359
+ publisher,
360
+ attestorAddress,
361
+ slot,
362
+ newGlobalVariables.timestamp,
363
+ );
420
364
 
365
+ // Enqueues block invalidation
421
366
  if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
422
367
  publisher.enqueueInvalidateBlock(invalidateBlock);
423
368
  }
424
369
 
370
+ // Actual block building
425
371
  this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
372
+ const block: L2Block | undefined = await this.tryBuildBlockAndEnqueuePublish(
373
+ slot,
374
+ proposer,
375
+ newBlockNumber,
376
+ publisher,
377
+ newGlobalVariables,
378
+ chainTipArchive,
379
+ invalidateBlock,
380
+ );
381
+
382
+ // Wait until the voting promises have resolved, so all requests are enqueued
383
+ await Promise.all(votesPromises);
426
384
 
427
- this.metrics.incOpenSlot(slot, proposerAddressInNextSlot.toString());
385
+ // And send the tx to L1
386
+ const l1Response = await publisher.sendRequests();
387
+ const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
388
+ if (proposedBlock) {
389
+ this.lastBlockPublished = block;
390
+ this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
391
+ await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
392
+ } else if (block) {
393
+ this.emit('block-publish-failed', l1Response ?? {});
394
+ }
395
+
396
+ this.setState(SequencerState.IDLE, undefined);
397
+ }
398
+
399
+ /** Tries building a block proposal, and if successful, enqueues it for publishing. */
400
+ private async tryBuildBlockAndEnqueuePublish(
401
+ slot: bigint,
402
+ proposer: EthAddress | undefined,
403
+ newBlockNumber: number,
404
+ publisher: SequencerPublisher,
405
+ newGlobalVariables: GlobalVariables,
406
+ chainTipArchive: Fr,
407
+ invalidateBlock: InvalidateBlockRequest | undefined,
408
+ ) {
409
+ this.metrics.incOpenSlot(slot, (proposer ?? EthAddress.ZERO).toString());
428
410
  this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
429
- proposer: proposerInNextSlot?.toString(),
430
- coinbase,
411
+ proposer,
431
412
  publisher: publisher.getSenderAddress(),
432
- feeRecipient,
433
413
  globalVariables: newGlobalVariables.toInspect(),
434
414
  chainTipArchive,
435
415
  blockNumber: newBlockNumber,
436
416
  slot,
437
417
  });
438
418
 
439
- // If I created a "partial" header here that should make our job much easier.
440
419
  const proposalHeader = ProposedBlockHeader.from({
441
420
  ...newGlobalVariables,
442
421
  timestamp: newGlobalVariables.timestamp,
@@ -457,7 +436,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
457
436
  pendingTxs,
458
437
  proposalHeader,
459
438
  newGlobalVariables,
460
- proposerInNextSlot,
439
+ proposer,
461
440
  invalidateBlock,
462
441
  publisher,
463
442
  );
@@ -476,26 +455,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
476
455
  );
477
456
  this.emit('tx-count-check-failed', { minTxs: this.minTxsPerBlock, availableTxs: pendingTxCount });
478
457
  }
479
-
480
- await Promise.all([enqueueGovernanceSignalPromise, enqueueSlashingActionsPromise]);
481
-
482
- const l1Response = await publisher.sendRequests();
483
- const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
484
- if (proposedBlock) {
485
- this.lastBlockPublished = block;
486
- this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
487
- await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
488
- } else if (block) {
489
- this.emit('block-publish-failed', l1Response ?? {});
490
- }
491
-
492
- this.setState(SequencerState.IDLE, undefined);
458
+ return block;
493
459
  }
494
460
 
495
461
  @trackSpan('Sequencer.work')
496
- protected async work() {
462
+ protected async safeWork() {
497
463
  try {
498
- await this.doRealWork();
464
+ await this.work();
499
465
  } catch (err) {
500
466
  if (err instanceof SequencerTooSlowError) {
501
467
  // Log as warn only if we had to abort halfway through the block proposal
@@ -825,9 +791,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
825
791
  /**
826
792
  * Returns whether all dependencies have caught up.
827
793
  * We don't check against the previous block submitted since it may have been reorg'd out.
828
- * @returns Boolean indicating if our dependencies are synced to the latest block.
829
794
  */
830
- protected async getChainTip(): Promise<
795
+ protected async checkSync(args: { ts: bigint; slot: bigint }): Promise<
831
796
  | {
832
797
  block?: L2Block;
833
798
  blockNumber: number;
@@ -837,6 +802,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
837
802
  }
838
803
  | undefined
839
804
  > {
805
+ // Check that the archiver and dependencies have synced to the previous L1 slot at least
806
+ // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
807
+ // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
808
+ const l1Timestamp = await this.l2BlockSource.getL1Timestamp();
809
+ const { slot, ts } = args;
810
+ if (l1Timestamp === undefined || l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
811
+ this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
812
+ slot,
813
+ ts,
814
+ l1Timestamp,
815
+ });
816
+ return undefined;
817
+ }
818
+
840
819
  const syncedBlocks = await Promise.all([
841
820
  this.worldState.status().then(({ syncSummary }) => ({
842
821
  number: syncSummary.latestBlockNumber,
@@ -845,12 +824,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
845
824
  this.l2BlockSource.getL2Tips().then(t => t.latest),
846
825
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
847
826
  this.l1ToL2MessageSource.getL2Tips().then(t => t.latest),
848
- this.l2BlockSource.getL1Timestamp(),
849
827
  this.l2BlockSource.getPendingChainValidationStatus(),
850
828
  ] as const);
851
829
 
852
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, l1Timestamp, pendingChainValidationStatus] =
853
- syncedBlocks;
830
+ const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
854
831
 
855
832
  // The archiver reports 'undefined' hash for the genesis block
856
833
  // because it doesn't have access to world state to compute it (facepalm)
@@ -861,33 +838,174 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
861
838
  p2p.hash === l2BlockSource.hash &&
862
839
  l1ToL2MessageSource.hash === l2BlockSource.hash;
863
840
 
864
- const logData = { worldState, l2BlockSource, p2p, l1ToL2MessageSource };
865
- this.log.debug(`Sequencer sync check ${result ? 'succeeded' : 'failed'}`, logData);
866
-
867
841
  if (!result) {
842
+ this.log.debug(`Sequencer sync check failed`, { worldState, l2BlockSource, p2p, l1ToL2MessageSource });
868
843
  return undefined;
869
844
  }
870
845
 
846
+ // Special case for genesis state
871
847
  const blockNumber = worldState.number;
872
- if (blockNumber >= INITIAL_L2_BLOCK_NUM) {
873
- const block = await this.l2BlockSource.getBlock(blockNumber);
874
- if (!block) {
875
- // this shouldn't really happen because a moment ago we checked that all components were in sync
876
- this.log.warn(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`, logData);
877
- return undefined;
878
- }
879
-
880
- return {
881
- block,
882
- blockNumber: block.number,
883
- archive: block.archive.root,
884
- l1Timestamp,
885
- pendingChainValidationStatus,
886
- };
887
- } else {
848
+ if (blockNumber < INITIAL_L2_BLOCK_NUM) {
888
849
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
889
850
  return { blockNumber: INITIAL_L2_BLOCK_NUM - 1, archive, l1Timestamp, pendingChainValidationStatus };
890
851
  }
852
+
853
+ const block = await this.l2BlockSource.getBlock(blockNumber);
854
+ if (!block) {
855
+ // this shouldn't really happen because a moment ago we checked that all components were in sync
856
+ this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
857
+ return undefined;
858
+ }
859
+
860
+ return {
861
+ block,
862
+ blockNumber: block.number,
863
+ archive: block.archive.root,
864
+ l1Timestamp,
865
+ pendingChainValidationStatus,
866
+ };
867
+ }
868
+
869
+ /**
870
+ * Enqueues governance and slashing votes with the publisher. Does not block.
871
+ * @param publisher - The publisher to enqueue votes with
872
+ * @param attestorAddress - The attestor address to use for signing
873
+ * @param slot - The slot number
874
+ * @param timestamp - The timestamp for the votes
875
+ * @param context - Optional context for logging (e.g., block number)
876
+ * @returns A tuple of [governanceEnqueued, slashingEnqueued]
877
+ */
878
+ protected enqueueGovernanceAndSlashingVotes(
879
+ publisher: SequencerPublisher,
880
+ attestorAddress: EthAddress,
881
+ slot: bigint,
882
+ timestamp: bigint,
883
+ ): [Promise<boolean> | undefined, Promise<boolean> | undefined] {
884
+ try {
885
+ const signerFn = (msg: TypedDataDefinition) =>
886
+ this.validatorClient!.signWithAddress(attestorAddress, msg).then(s => s.toString());
887
+
888
+ const enqueueGovernancePromise =
889
+ this.governanceProposerPayload && !this.governanceProposerPayload.isZero()
890
+ ? publisher
891
+ .enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn)
892
+ .catch(err => {
893
+ this.log.error(`Error enqueuing governance vote`, err, { slot });
894
+ return false;
895
+ })
896
+ : undefined;
897
+
898
+ const enqueueSlashingPromise = this.slasherClient
899
+ ? this.slasherClient
900
+ .getProposerActions(slot)
901
+ .then(actions => publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn))
902
+ .catch(err => {
903
+ this.log.error(`Error enqueuing slashing actions`, err, { slot });
904
+ return false;
905
+ })
906
+ : undefined;
907
+
908
+ return [enqueueGovernancePromise, enqueueSlashingPromise];
909
+ } catch (err) {
910
+ this.log.error(`Error enqueueing governance and slashing votes`, err);
911
+ return [undefined, undefined];
912
+ }
913
+ }
914
+
915
+ /**
916
+ * Checks if we are the proposer for the next slot.
917
+ * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
918
+ */
919
+ protected async checkCanPropose(slot: bigint): Promise<[boolean, EthAddress | undefined]> {
920
+ let proposer: EthAddress | undefined;
921
+ try {
922
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
923
+ } catch (e) {
924
+ if (e instanceof NoCommitteeError) {
925
+ this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
926
+ return [false, undefined];
927
+ }
928
+ this.log.error(`Error getting proposer for slot ${slot}`, e);
929
+ return [false, undefined];
930
+ }
931
+
932
+ // If proposer is undefined, then the committee is empty and anyone may propose
933
+ if (proposer === undefined) {
934
+ return [true, undefined];
935
+ }
936
+
937
+ const validatorAddresses = this.validatorClient!.getValidatorAddresses();
938
+ const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
939
+
940
+ if (!weAreProposer) {
941
+ this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, { validatorAddresses, proposer });
942
+ return [false, proposer];
943
+ }
944
+
945
+ return [true, proposer];
946
+ }
947
+
948
+ /**
949
+ * Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
950
+ * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
951
+ */
952
+ protected async tryVoteWhenSyncFails(args: { slot: bigint; ts: bigint }): Promise<void> {
953
+ const { slot, ts } = args;
954
+
955
+ // Prevent duplicate attempts in the same slot
956
+ if (this.lastSlotForVoteWhenSyncFailed === slot) {
957
+ this.log.debug(`Already attempted to vote in slot ${slot} (skipping)`);
958
+ return;
959
+ }
960
+
961
+ // Check if we're past the max time for initializing a proposal
962
+ const secondsIntoSlot = this.getSecondsIntoSlot(slot);
963
+ const maxAllowedTime = this.timetable.getMaxAllowedTime(SequencerState.INITIALIZING_PROPOSAL);
964
+
965
+ // If we haven't exceeded the time limit for initializing a proposal, don't proceed with voting
966
+ // We use INITIALIZING_PROPOSAL time limit because if we're past that, we can't build a block anyway
967
+ if (maxAllowedTime === undefined || secondsIntoSlot <= maxAllowedTime) {
968
+ this.log.trace(`Not attempting to vote since there is still for block building`, {
969
+ secondsIntoSlot,
970
+ maxAllowedTime,
971
+ });
972
+ return;
973
+ }
974
+
975
+ this.log.debug(`Sync for slot ${slot} failed, checking for voting opportunities`, {
976
+ secondsIntoSlot,
977
+ maxAllowedTime,
978
+ });
979
+
980
+ // Check if we're a proposer or proposal is open
981
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
982
+ if (!canPropose) {
983
+ this.log.debug(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
984
+ return;
985
+ }
986
+
987
+ // Mark this slot as attempted
988
+ this.lastSlotForVoteWhenSyncFailed = slot;
989
+
990
+ // Get a publisher for voting
991
+ const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
992
+
993
+ this.log.debug(`Attempting to vote despite sync failure at slot ${slot}`, {
994
+ attestorAddress,
995
+ slot,
996
+ });
997
+
998
+ // Enqueue governance and slashing votes using the shared helper method
999
+ const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, ts);
1000
+ await Promise.all(votesPromises);
1001
+
1002
+ if (votesPromises.every(p => !p)) {
1003
+ this.log.debug(`No votes to enqueue for slot ${slot}`);
1004
+ return;
1005
+ }
1006
+
1007
+ this.log.info(`Voting in slot ${slot} despite sync failure`, { slot });
1008
+ await publisher.sendRequests();
891
1009
  }
892
1010
 
893
1011
  /**
@@ -897,19 +1015,19 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
897
1015
  * and if they fail, any sequencer will try as well.
898
1016
  */
899
1017
  protected async considerInvalidatingBlock(
900
- syncedTo: NonNullable<Awaited<ReturnType<Sequencer['getChainTip']>>>,
1018
+ syncedTo: NonNullable<Awaited<ReturnType<Sequencer['checkSync']>>>,
901
1019
  currentSlot: bigint,
902
- ourValidatorAddresses: EthAddress[],
903
- publisher: SequencerPublisher,
904
1020
  ): Promise<void> {
905
1021
  const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
906
1022
  if (pendingChainValidationStatus.valid) {
907
1023
  return;
908
1024
  }
909
1025
 
1026
+ const { publisher } = await this.publisherFactory.create(undefined);
910
1027
  const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
911
1028
  const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
912
1029
  const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
1030
+ const ourValidatorAddresses = this.validatorClient!.getValidatorAddresses();
913
1031
 
914
1032
  const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } =
915
1033
  this.config;
@@ -137,6 +137,19 @@ export class SequencerTimetable {
137
137
  return validationTimeEnd;
138
138
  }
139
139
 
140
+ public getMaxAllowedTime(
141
+ state: Extract<
142
+ SequencerState,
143
+ SequencerState.STOPPED | SequencerState.STOPPING | SequencerState.IDLE | SequencerState.SYNCHRONIZING
144
+ >,
145
+ ): undefined;
146
+ public getMaxAllowedTime(
147
+ state: Exclude<
148
+ SequencerState,
149
+ SequencerState.STOPPED | SequencerState.STOPPING | SequencerState.IDLE | SequencerState.SYNCHRONIZING
150
+ >,
151
+ ): number;
152
+ public getMaxAllowedTime(state: SequencerState): number | undefined;
140
153
  public getMaxAllowedTime(state: SequencerState): number | undefined {
141
154
  switch (state) {
142
155
  case SequencerState.STOPPED: