@aztec/sequencer-client 2.0.3-rc.9 → 2.0.4

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