@aztec/sequencer-client 3.0.0-devnet.2 → 3.0.0-manual.20251030

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":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAEhD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAG5F,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAC5D,OAAO,EAAE,EAAE,EAAE,MAAM,0BAA0B,CAAC;AAG9C,OAAO,EAAE,KAAK,YAAY,EAAS,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,KAAK,iBAAiB,EAAY,MAAM,yBAAyB,CAAC;AAC3E,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EACL,oBAAoB,EACpB,+BAA+B,EAC/B,KAAK,aAAa,EAClB,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,iBAAiB,EAAkD,MAAM,6BAA6B,CAAC;AAErH,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAE1B,KAAK,sBAAsB,EAC5B,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAOnE,OAAO,EAAqD,EAAE,EAAE,MAAM,kBAAkB,CAAC;AAEzF,OAAO,EAAc,KAAK,eAAe,EAAE,KAAK,MAAM,EAAiC,MAAM,yBAAyB,CAAC;AACvH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAK/D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8CAA8C,CAAC;AAC1F,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,6CAA6C,CAAC;AAC7F,OAAO,KAAK,EAAE,MAAM,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAC9G,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,KAAK,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzE,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B,KAAK,wBAAwB,GAAG,IAAI,CAAC,iBAAiB,EAAE,sBAAsB,GAAG,eAAe,GAAG,cAAc,CAAC,CAAC;AAEnH,MAAM,MAAM,eAAe,GAAG;IAC5B,CAAC,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE;QACxB,QAAQ,EAAE,cAAc,CAAC;QACzB,QAAQ,EAAE,cAAc,CAAC;QACzB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,KAAK,IAAI,CAAC;IACX,CAAC,8BAA8B,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACrE,CAAC,uBAAuB,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACpF,CAAC,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3D,CAAC,sBAAsB,CAAC,EAAE,CAAC,IAAI,EAAE;QAC/B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;QAC7B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;QACzB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QACvB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;KAC3B,KAAK,IAAI,CAAC;IACX,CAAC,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CAC5E,CAAC;8BAW8C,UAAU,iBAAiB,CAAC,eAAe,CAAC;AAT5F;;;;;;;;GAQG;AACH,qBAAa,SAAU,SAAQ,cAA8D;IA8BzF,SAAS,CAAC,gBAAgB,EAAE,yBAAyB;IACrD,SAAS,CAAC,eAAe,EAAE,eAAe,GAAG,SAAS;IACtD,SAAS,CAAC,cAAc,EAAE,qBAAqB;IAC/C,SAAS,CAAC,SAAS,EAAE,GAAG;IACxB,SAAS,CAAC,UAAU,EAAE,sBAAsB;IAC5C,SAAS,CAAC,aAAa,EAAE,sBAAsB,GAAG,SAAS;IAC3D,SAAS,CAAC,aAAa,EAAE,aAAa;IACtC,SAAS,CAAC,mBAAmB,EAAE,mBAAmB;IAClD,SAAS,CAAC,YAAY,EAAE,qBAAqB;IAC7C,SAAS,CAAC,WAAW,EAAE,wBAAwB;IAC/C,SAAS,CAAC,YAAY,EAAE,YAAY;IACpC,SAAS,CAAC,UAAU,EAAE,UAAU;IAChC,SAAS,CAAC,cAAc,EAAE,cAAc;IACxC,SAAS,CAAC,MAAM,EAAE,eAAe;IACjC,SAAS,CAAC,SAAS,EAAE,eAAe;IACpC,SAAS,CAAC,GAAG;IA5Cf,OAAO,CAAC,cAAc,CAAC,CAAiB;IACxC,OAAO,CAAC,iBAAiB,CAAgB;IACzC,OAAO,CAAC,cAAc,CAAM;IAC5B,OAAO,CAAC,cAAc,CAAK;IAC3B,OAAO,CAAC,4BAA4B,CAAK;IACzC,OAAO,CAAC,KAAK,CAA0B;IACvC,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,OAAO,CAAmB;IAElC,OAAO,CAAC,kBAAkB,CAAsB;IAEhD,OAAO,CAAC,yBAAyB,CAAyB;IAE1D,oGAAoG;IACpG,OAAO,CAAC,6BAA6B,CAAqB;IAE1D,+GAA+G;IAC/G,SAAS,CAAC,SAAS,EAAG,kBAAkB,CAAC;IACzC,SAAS,CAAC,gBAAgB,EAAE,OAAO,CAAS;IAO5C,SAAS,CAAC,SAAS,EAAE,kBAAkB,GAAG,SAAS,CAAC;gBAGxC,gBAAgB,EAAE,yBAAyB,EAC3C,eAAe,EAAE,eAAe,GAAG,SAAS,EAAE,wDAAwD;IACtG,cAAc,EAAE,qBAAqB,EACrC,SAAS,EAAE,GAAG,EACd,UAAU,EAAE,sBAAsB,EAClC,aAAa,EAAE,sBAAsB,GAAG,SAAS,EACjD,aAAa,EAAE,aAAa,EAC5B,mBAAmB,EAAE,mBAAmB,EACxC,YAAY,EAAE,qBAAqB,EACnC,WAAW,EAAE,wBAAwB,EACrC,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,UAAU,EACtB,cAAc,EAAE,cAAc,EAC9B,MAAM,EAAE,eAAe,EACvB,SAAS,GAAE,eAAsC,EACjD,GAAG,yCAA4B;IAS3C,IAAI,MAAM,IAAI,MAAM,CAEnB;IAEM,qBAAqB;IAIrB,SAAS;IAIhB;;;OAGG;IACI,YAAY,CAAC,MAAM,EAAE,eAAe;IA0C3C,OAAO,CAAC,YAAY;IAcP,IAAI;IAIjB;;OAEG;IACI,KAAK;IAOZ;;OAEG;IACU,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IASlC;;;OAGG;IACI,MAAM;;;IAIb;;;;;;;OAOG;cACa,IAAI;IAmJpB,sFAAsF;YACxE,8BAA8B;cA6D5B,QAAQ;IAmBxB;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,sBAAsB,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;IACrG,QAAQ,CACN,aAAa,EAAE,OAAO,CAAC,cAAc,EAAE,sBAAsB,CAAC,EAC9D,UAAU,CAAC,EAAE,SAAS,EACtB,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,GACzB,IAAI;YAgCO,oBAAoB;IAUlC,SAAS,CAAC,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,qBAAqB;IAkBrE;;;;;;;;;;OAUG;YAIW,2BAA2B;cAoGzB,mBAAmB,CACjC,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,EAAE,EAAE,EACT,eAAe,EAAE,UAAU,GAAG,SAAS,GACtC,OAAO,CAAC,oBAAoB,EAAE,GAAG,SAAS,CAAC;IA2F9C;;;OAGG;cAIa,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,sBAAsB,EAAE,+BAA+B,EACvD,+BAA+B,EAAE,SAAS,EAC1C,eAAe,EAAE,sBAAsB,GAAG,SAAS,EACnD,SAAS,EAAE,kBAAkB,GAC5B,OAAO,CAAC,IAAI,CAAC;IAuBhB;;;OAGG;cACa,SAAS,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAClE;QACE,KAAK,CAAC,EAAE,OAAO,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,EAAE,CAAC;QACZ,WAAW,EAAE,MAAM,CAAC;QACpB,4BAA4B,EAAE,mBAAmB,CAAC;KACnD,GACD,SAAS,CACZ;IAiED;;;;;;;;OAQG;IACH,SAAS,CAAC,iCAAiC,CACzC,SAAS,EAAE,kBAAkB,EAC7B,eAAe,EAAE,UAAU,EAC3B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,SAAS,CAAC;IAgC/D;;;OAGG;cACa,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,OAAO,EAAE,UAAU,GAAG,SAAS,CAAC,CAAC;IA6BzF;;;OAGG;cACa,oBAAoB,CAAC,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IA2DvF;;;;;OAKG;cACa,yBAAyB,CACvC,QAAQ,EAAE,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAClE,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC;IA+DhB,OAAO,CAAC,0BAA0B;IAIlC,OAAO,CAAC,kBAAkB;IAK1B,IAAI,iBAAiB,WAEpB;IAED,IAAI,aAAa,IAAI,MAAM,GAAG,SAAS,CAEtC;IAEM,gBAAgB,IAAI,sBAAsB,GAAG,SAAS;CAG9D"}
@@ -68,6 +68,7 @@ export { SequencerState };
68
68
  metrics;
69
69
  lastBlockPublished;
70
70
  governanceProposerPayload;
71
+ /** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */ lastSlotForVoteWhenSyncFailed;
71
72
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */ timetable;
72
73
  enforceTimeTable;
73
74
  // This shouldn't be here as this gets re-created each time we build/propose a block.
@@ -143,7 +144,7 @@ export { SequencerState };
143
144
  /**
144
145
  * Starts the sequencer and moves to IDLE state.
145
146
  */ start() {
146
- this.runningPromise = new RunningPromise(this.work.bind(this), this.log, this.pollingIntervalMs);
147
+ this.runningPromise = new RunningPromise(this.safeWork.bind(this), this.log, this.pollingIntervalMs);
147
148
  this.setState(SequencerState.IDLE, undefined, {
148
149
  force: true
149
150
  });
@@ -179,21 +180,28 @@ export { SequencerState };
179
180
  * - Collect attestations for the block
180
181
  * - Submit block
181
182
  * - If our block for some reason is not included, revert the state
182
- */ async doRealWork() {
183
+ */ async work() {
183
184
  this.setState(SequencerState.SYNCHRONIZING, undefined);
184
- // Check all components are synced to latest as seen by the archiver
185
- const syncedTo = await this.getChainTip();
186
- // Do not go forward with new block if the previous one has not been mined and processed
185
+ const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
186
+ // Check we have not already published a block for this slot (cheapest check)
187
+ if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
188
+ this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`);
189
+ return;
190
+ }
191
+ // Check all components are synced to latest as seen by the archiver (queries all subsystems)
192
+ const syncedTo = await this.checkSync({
193
+ ts,
194
+ slot
195
+ });
187
196
  if (!syncedTo) {
197
+ await this.tryVoteWhenSyncFails({
198
+ slot,
199
+ ts
200
+ });
188
201
  return;
189
202
  }
190
203
  const chainTipArchive = syncedTo.archive;
191
204
  const newBlockNumber = syncedTo.blockNumber + 1;
192
- const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
193
- this.setState(SequencerState.PROPOSER_CHECK, slot);
194
- // Check that the archiver and dependencies have synced to the previous L1 slot at least
195
- // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
196
- // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
197
205
  const syncLogData = {
198
206
  now,
199
207
  syncedToL1Ts: syncedTo.l1Timestamp,
@@ -204,67 +212,35 @@ export { SequencerState };
204
212
  newBlockNumber,
205
213
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex')
206
214
  };
207
- if (syncedTo.l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
208
- this.log.debug(`Cannot propose block ${newBlockNumber} at next L2 slot ${slot} due to pending sync from L1`, syncLogData);
215
+ // Check that we are a proposer for the next slot
216
+ this.setState(SequencerState.PROPOSER_CHECK, slot);
217
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
218
+ // If we are not a proposer, check if we should invalidate a invalid block, and bail
219
+ if (!canPropose) {
220
+ await this.considerInvalidatingBlock(syncedTo, slot);
209
221
  return;
210
222
  }
211
- // Check that the slot is not taken by a block already
223
+ // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
212
224
  if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
213
- this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, {
225
+ this.log.warn(`Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`, {
214
226
  ...syncLogData,
215
227
  block: syncedTo.block.header.toInspect()
216
228
  });
217
229
  return;
218
230
  }
219
- // Or that we haven't published it ourselves
220
- if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
221
- this.log.debug(`Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`, {
222
- ...syncLogData,
223
- block: this.lastBlockPublished.header.toInspect()
224
- });
225
- return;
226
- }
227
- // Check that we are a proposer for the next slot
228
- let proposerInNextSlot;
229
- try {
230
- proposerInNextSlot = await this.epochCache.getProposerAttesterAddressInNextSlot();
231
- } catch (e) {
232
- if (e instanceof NoCommitteeError) {
233
- this.log.warn(`Cannot propose block ${newBlockNumber} at next L2 slot ${slot} since the committee does not exist on L1`);
234
- return;
235
- }
236
- }
237
- // If get proposer in next slot is undefined, then the committee is empty and anyone may propose.
238
- // If the committee is defined and not empty, but none of our validators are the proposer, then stop.
239
- const validatorAddresses = this.validatorClient.getValidatorAddresses();
240
- if (proposerInNextSlot !== undefined && !validatorAddresses.some((addr)=>addr.equals(proposerInNextSlot))) {
241
- this.log.debug(`Cannot propose block ${newBlockNumber} since we are not a proposer`, {
242
- us: validatorAddresses,
243
- proposer: proposerInNextSlot,
244
- ...syncLogData
245
- });
246
- // If the pending chain is invalid, we may need to invalidate the block if no one else is doing it.
247
- if (!syncedTo.pendingChainValidationStatus.valid) {
248
- // We pass i undefined here to get any available publisher.
249
- const { publisher } = await this.publisherFactory.create(undefined);
250
- await this.considerInvalidatingBlock(syncedTo, slot, validatorAddresses, publisher);
251
- }
252
- return;
253
- }
254
- // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
255
- // if all the previous checks are good, but we do it just in case.
256
- const proposerAddressInNextSlot = proposerInNextSlot ?? EthAddress.ZERO;
257
231
  // We now need to get ourselves a publisher.
258
232
  // The returned attestor will be the one we provided if we provided one.
259
233
  // Otherwise it will be a valid attestor for the returned publisher.
260
- const { attestorAddress, publisher } = await this.publisherFactory.create(proposerInNextSlot);
234
+ const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
261
235
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
262
236
  this.publisher = publisher;
263
237
  const coinbase = this.validatorClient.getCoinbaseForAttestor(attestorAddress);
264
238
  const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(attestorAddress);
265
239
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
266
240
  const invalidateBlock = await publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
267
- const canProposeCheck = await publisher.canProposeAtNextEthBlock(chainTipArchive, proposerAddressInNextSlot, invalidateBlock);
241
+ // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
242
+ // if all the previous checks are good, but we do it just in case.
243
+ const canProposeCheck = await publisher.canProposeAtNextEthBlock(chainTipArchive, proposer ?? EthAddress.ZERO, invalidateBlock);
268
244
  if (canProposeCheck === undefined) {
269
245
  this.log.warn(`Cannot propose block ${newBlockNumber} at slot ${slot} due to failed rollup contract check`, syncLogData);
270
246
  this.emit('proposer-rollup-check-failed', {
@@ -294,43 +270,45 @@ export { SequencerState };
294
270
  });
295
271
  return;
296
272
  }
297
- this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot}` + (proposerInNextSlot ? ` as ${proposerInNextSlot}` : ''), {
298
- ...syncLogData,
299
- validatorAddresses
273
+ this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, {
274
+ ...syncLogData
300
275
  });
301
276
  const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(newBlockNumber, coinbase, feeRecipient, slot);
302
- const { timestamp } = newGlobalVariables;
303
- const signerFn = (msg)=>this.validatorClient.signWithAddress(attestorAddress, msg).then((s)=>s.toString());
304
- const enqueueGovernanceSignalPromise = this.governanceProposerPayload && !this.governanceProposerPayload.isZero() ? publisher.enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn).catch((err)=>{
305
- this.log.error(`Error enqueuing governance vote`, err, {
306
- blockNumber: newBlockNumber,
307
- slot
308
- });
309
- return false;
310
- }) : Promise.resolve(false);
311
- const enqueueSlashingActionsPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn)).catch((err)=>{
312
- this.log.error(`Error enqueuing slashing actions`, err, {
313
- blockNumber: newBlockNumber,
314
- slot
315
- });
316
- return false;
317
- }) : Promise.resolve(false);
277
+ // Enqueue governance and slashing votes (returns promises that will be awaited later)
278
+ const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, newGlobalVariables.timestamp);
279
+ // Enqueues block invalidation
318
280
  if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
319
281
  publisher.enqueueInvalidateBlock(invalidateBlock);
320
282
  }
283
+ // Actual block building
321
284
  this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
322
- this.metrics.incOpenSlot(slot, proposerAddressInNextSlot.toString());
285
+ const block = await this.tryBuildBlockAndEnqueuePublish(slot, proposer, newBlockNumber, publisher, newGlobalVariables, chainTipArchive, invalidateBlock);
286
+ // Wait until the voting promises have resolved, so all requests are enqueued
287
+ await Promise.all(votesPromises);
288
+ // And send the tx to L1
289
+ const l1Response = await publisher.sendRequests();
290
+ const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
291
+ if (proposedBlock) {
292
+ this.lastBlockPublished = block;
293
+ this.emit('block-published', {
294
+ blockNumber: newBlockNumber,
295
+ slot: Number(slot)
296
+ });
297
+ await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
298
+ } else if (block) {
299
+ this.emit('block-publish-failed', l1Response ?? {});
300
+ }
301
+ this.setState(SequencerState.IDLE, undefined);
302
+ }
303
+ /** Tries building a block proposal, and if successful, enqueues it for publishing. */ async tryBuildBlockAndEnqueuePublish(slot, proposer, newBlockNumber, publisher, newGlobalVariables, chainTipArchive, invalidateBlock) {
323
304
  this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
324
- proposer: proposerInNextSlot?.toString(),
325
- coinbase,
305
+ proposer,
326
306
  publisher: publisher.getSenderAddress(),
327
- feeRecipient,
328
307
  globalVariables: newGlobalVariables.toInspect(),
329
308
  chainTipArchive,
330
309
  blockNumber: newBlockNumber,
331
310
  slot
332
311
  });
333
- // If I created a "partial" header here that should make our job much easier.
334
312
  const proposalHeader = CheckpointHeader.from({
335
313
  ...newGlobalVariables,
336
314
  timestamp: newGlobalVariables.timestamp,
@@ -345,7 +323,7 @@ export { SequencerState };
345
323
  // and also we may need to fetch more if we don't have enough valid txs.
346
324
  const pendingTxs = this.p2pClient.iteratePendingTxs();
347
325
  try {
348
- block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposerInNextSlot, invalidateBlock, publisher);
326
+ block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposer, invalidateBlock, publisher);
349
327
  } catch (err) {
350
328
  this.emit('block-build-failed', {
351
329
  reason: err.message
@@ -370,27 +348,11 @@ export { SequencerState };
370
348
  availableTxs: pendingTxCount
371
349
  });
372
350
  }
373
- await Promise.all([
374
- enqueueGovernanceSignalPromise,
375
- enqueueSlashingActionsPromise
376
- ]);
377
- const l1Response = await publisher.sendRequests();
378
- const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
379
- if (proposedBlock) {
380
- this.lastBlockPublished = block;
381
- this.emit('block-published', {
382
- blockNumber: newBlockNumber,
383
- slot: Number(slot)
384
- });
385
- await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
386
- } else if (block) {
387
- this.emit('block-publish-failed', l1Response ?? {});
388
- }
389
- this.setState(SequencerState.IDLE, undefined);
351
+ return block;
390
352
  }
391
- async work() {
353
+ async safeWork() {
392
354
  try {
393
- await this.doRealWork();
355
+ await this.work();
394
356
  } catch (err) {
395
357
  if (err instanceof SequencerTooSlowError) {
396
358
  // Log as warn only if we had to abort halfway through the block proposal
@@ -614,8 +576,20 @@ export { SequencerState };
614
576
  /**
615
577
  * Returns whether all dependencies have caught up.
616
578
  * We don't check against the previous block submitted since it may have been reorg'd out.
617
- * @returns Boolean indicating if our dependencies are synced to the latest block.
618
- */ async getChainTip() {
579
+ */ async checkSync(args) {
580
+ // Check that the archiver and dependencies have synced to the previous L1 slot at least
581
+ // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
582
+ // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
583
+ const l1Timestamp = await this.l2BlockSource.getL1Timestamp();
584
+ const { slot, ts } = args;
585
+ if (l1Timestamp === undefined || l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
586
+ this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
587
+ slot,
588
+ ts,
589
+ l1Timestamp
590
+ });
591
+ return undefined;
592
+ }
619
593
  const syncedBlocks = await Promise.all([
620
594
  this.worldState.status().then(({ syncSummary })=>({
621
595
  number: syncSummary.latestBlockNumber,
@@ -624,39 +598,24 @@ export { SequencerState };
624
598
  this.l2BlockSource.getL2Tips().then((t)=>t.latest),
625
599
  this.p2pClient.getStatus().then((p2p)=>p2p.syncedToL2Block),
626
600
  this.l1ToL2MessageSource.getL2Tips().then((t)=>t.latest),
627
- this.l2BlockSource.getL1Timestamp(),
628
601
  this.l2BlockSource.getPendingChainValidationStatus()
629
602
  ]);
630
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, l1Timestamp, pendingChainValidationStatus] = syncedBlocks;
603
+ const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
631
604
  // The archiver reports 'undefined' hash for the genesis block
632
605
  // because it doesn't have access to world state to compute it (facepalm)
633
606
  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;
634
- const logData = {
635
- worldState,
636
- l2BlockSource,
637
- p2p,
638
- l1ToL2MessageSource
639
- };
640
- this.log.debug(`Sequencer sync check ${result ? 'succeeded' : 'failed'}`, logData);
641
607
  if (!result) {
608
+ this.log.debug(`Sequencer sync check failed`, {
609
+ worldState,
610
+ l2BlockSource,
611
+ p2p,
612
+ l1ToL2MessageSource
613
+ });
642
614
  return undefined;
643
615
  }
616
+ // Special case for genesis state
644
617
  const blockNumber = worldState.number;
645
- if (blockNumber >= INITIAL_L2_BLOCK_NUM) {
646
- const block = await this.l2BlockSource.getBlock(blockNumber);
647
- if (!block) {
648
- // this shouldn't really happen because a moment ago we checked that all components were in sync
649
- this.log.warn(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`, logData);
650
- return undefined;
651
- }
652
- return {
653
- block,
654
- blockNumber: block.number,
655
- archive: block.archive.root,
656
- l1Timestamp,
657
- pendingChainValidationStatus
658
- };
659
- } else {
618
+ if (blockNumber < INITIAL_L2_BLOCK_NUM) {
660
619
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
661
620
  return {
662
621
  blockNumber: INITIAL_L2_BLOCK_NUM - 1,
@@ -665,20 +624,170 @@ export { SequencerState };
665
624
  pendingChainValidationStatus
666
625
  };
667
626
  }
627
+ const block = await this.l2BlockSource.getBlock(blockNumber);
628
+ if (!block) {
629
+ // this shouldn't really happen because a moment ago we checked that all components were in sync
630
+ this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
631
+ return undefined;
632
+ }
633
+ return {
634
+ block,
635
+ blockNumber: block.number,
636
+ archive: block.archive.root,
637
+ l1Timestamp,
638
+ pendingChainValidationStatus
639
+ };
640
+ }
641
+ /**
642
+ * Enqueues governance and slashing votes with the publisher. Does not block.
643
+ * @param publisher - The publisher to enqueue votes with
644
+ * @param attestorAddress - The attestor address to use for signing
645
+ * @param slot - The slot number
646
+ * @param timestamp - The timestamp for the votes
647
+ * @param context - Optional context for logging (e.g., block number)
648
+ * @returns A tuple of [governanceEnqueued, slashingEnqueued]
649
+ */ enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, timestamp) {
650
+ try {
651
+ const signerFn = (msg)=>this.validatorClient.signWithAddress(attestorAddress, msg).then((s)=>s.toString());
652
+ const enqueueGovernancePromise = this.governanceProposerPayload && !this.governanceProposerPayload.isZero() ? publisher.enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn).catch((err)=>{
653
+ this.log.error(`Error enqueuing governance vote`, err, {
654
+ slot
655
+ });
656
+ return false;
657
+ }) : undefined;
658
+ const enqueueSlashingPromise = this.slasherClient ? this.slasherClient.getProposerActions(slot).then((actions)=>publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn)).catch((err)=>{
659
+ this.log.error(`Error enqueuing slashing actions`, err, {
660
+ slot
661
+ });
662
+ return false;
663
+ }) : undefined;
664
+ return [
665
+ enqueueGovernancePromise,
666
+ enqueueSlashingPromise
667
+ ];
668
+ } catch (err) {
669
+ this.log.error(`Error enqueueing governance and slashing votes`, err);
670
+ return [
671
+ undefined,
672
+ undefined
673
+ ];
674
+ }
675
+ }
676
+ /**
677
+ * Checks if we are the proposer for the next slot.
678
+ * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
679
+ */ async checkCanPropose(slot) {
680
+ let proposer;
681
+ try {
682
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
683
+ } catch (e) {
684
+ if (e instanceof NoCommitteeError) {
685
+ this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
686
+ return [
687
+ false,
688
+ undefined
689
+ ];
690
+ }
691
+ this.log.error(`Error getting proposer for slot ${slot}`, e);
692
+ return [
693
+ false,
694
+ undefined
695
+ ];
696
+ }
697
+ // If proposer is undefined, then the committee is empty and anyone may propose
698
+ if (proposer === undefined) {
699
+ return [
700
+ true,
701
+ undefined
702
+ ];
703
+ }
704
+ const validatorAddresses = this.validatorClient.getValidatorAddresses();
705
+ const weAreProposer = validatorAddresses.some((addr)=>addr.equals(proposer));
706
+ if (!weAreProposer) {
707
+ this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, {
708
+ validatorAddresses,
709
+ proposer
710
+ });
711
+ return [
712
+ false,
713
+ proposer
714
+ ];
715
+ }
716
+ return [
717
+ true,
718
+ proposer
719
+ ];
720
+ }
721
+ /**
722
+ * Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
723
+ * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
724
+ */ async tryVoteWhenSyncFails(args) {
725
+ const { slot, ts } = args;
726
+ // Prevent duplicate attempts in the same slot
727
+ if (this.lastSlotForVoteWhenSyncFailed === slot) {
728
+ this.log.debug(`Already attempted to vote in slot ${slot} (skipping)`);
729
+ return;
730
+ }
731
+ // Check if we're past the max time for initializing a proposal
732
+ const secondsIntoSlot = this.getSecondsIntoSlot(slot);
733
+ const maxAllowedTime = this.timetable.getMaxAllowedTime(SequencerState.INITIALIZING_PROPOSAL);
734
+ // If we haven't exceeded the time limit for initializing a proposal, don't proceed with voting
735
+ // We use INITIALIZING_PROPOSAL time limit because if we're past that, we can't build a block anyway
736
+ if (maxAllowedTime === undefined || secondsIntoSlot <= maxAllowedTime) {
737
+ this.log.trace(`Not attempting to vote since there is still for block building`, {
738
+ secondsIntoSlot,
739
+ maxAllowedTime
740
+ });
741
+ return;
742
+ }
743
+ this.log.debug(`Sync for slot ${slot} failed, checking for voting opportunities`, {
744
+ secondsIntoSlot,
745
+ maxAllowedTime
746
+ });
747
+ // Check if we're a proposer or proposal is open
748
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
749
+ if (!canPropose) {
750
+ this.log.debug(`Cannot vote in slot ${slot} since we are not a proposer`, {
751
+ slot,
752
+ proposer
753
+ });
754
+ return;
755
+ }
756
+ // Mark this slot as attempted
757
+ this.lastSlotForVoteWhenSyncFailed = slot;
758
+ // Get a publisher for voting
759
+ const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
760
+ this.log.debug(`Attempting to vote despite sync failure at slot ${slot}`, {
761
+ attestorAddress,
762
+ slot
763
+ });
764
+ // Enqueue governance and slashing votes using the shared helper method
765
+ const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, ts);
766
+ await Promise.all(votesPromises);
767
+ if (votesPromises.every((p)=>!p)) {
768
+ this.log.debug(`No votes to enqueue for slot ${slot}`);
769
+ return;
770
+ }
771
+ this.log.info(`Voting in slot ${slot} despite sync failure`, {
772
+ slot
773
+ });
774
+ await publisher.sendRequests();
668
775
  }
669
776
  /**
670
777
  * Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
671
778
  * has been there without being invalidated and whether the sequencer is in the committee or not. We always
672
779
  * have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
673
780
  * and if they fail, any sequencer will try as well.
674
- */ async considerInvalidatingBlock(syncedTo, currentSlot, ourValidatorAddresses, publisher) {
781
+ */ async considerInvalidatingBlock(syncedTo, currentSlot) {
675
782
  const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
676
783
  if (pendingChainValidationStatus.valid) {
677
784
  return;
678
785
  }
786
+ const { publisher } = await this.publisherFactory.create(undefined);
679
787
  const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
680
788
  const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
681
789
  const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
790
+ const ourValidatorAddresses = this.validatorClient.getValidatorAddresses();
682
791
  const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } = this.config;
683
792
  const logData = {
684
793
  invalidL1Timestamp: invalidBlockTimestamp,
@@ -724,7 +833,7 @@ export { SequencerState };
724
833
  }
725
834
  _ts_decorate([
726
835
  trackSpan('Sequencer.work')
727
- ], Sequencer.prototype, "work", null);
836
+ ], Sequencer.prototype, "safeWork", null);
728
837
  _ts_decorate([
729
838
  trackSpan('Sequencer.buildBlockAndEnqueuePublish', (_validTxs, _proposalHeader, newGlobalVariables)=>({
730
839
  [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":"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,uCAAsC;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,uCAAsC;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;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": "3.0.0-devnet.2",
3
+ "version": "3.0.0-manual.20251030",
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": "3.0.0-devnet.2",
30
- "@aztec/bb-prover": "3.0.0-devnet.2",
31
- "@aztec/blob-lib": "3.0.0-devnet.2",
32
- "@aztec/blob-sink": "3.0.0-devnet.2",
33
- "@aztec/constants": "3.0.0-devnet.2",
34
- "@aztec/epoch-cache": "3.0.0-devnet.2",
35
- "@aztec/ethereum": "3.0.0-devnet.2",
36
- "@aztec/foundation": "3.0.0-devnet.2",
37
- "@aztec/l1-artifacts": "3.0.0-devnet.2",
38
- "@aztec/merkle-tree": "3.0.0-devnet.2",
39
- "@aztec/node-keystore": "3.0.0-devnet.2",
40
- "@aztec/noir-acvm_js": "3.0.0-devnet.2",
41
- "@aztec/noir-contracts.js": "3.0.0-devnet.2",
42
- "@aztec/noir-protocol-circuits-types": "3.0.0-devnet.2",
43
- "@aztec/noir-types": "3.0.0-devnet.2",
44
- "@aztec/p2p": "3.0.0-devnet.2",
45
- "@aztec/protocol-contracts": "3.0.0-devnet.2",
46
- "@aztec/prover-client": "3.0.0-devnet.2",
47
- "@aztec/simulator": "3.0.0-devnet.2",
48
- "@aztec/slasher": "3.0.0-devnet.2",
49
- "@aztec/stdlib": "3.0.0-devnet.2",
50
- "@aztec/telemetry-client": "3.0.0-devnet.2",
51
- "@aztec/validator-client": "3.0.0-devnet.2",
52
- "@aztec/world-state": "3.0.0-devnet.2",
29
+ "@aztec/aztec.js": "3.0.0-manual.20251030",
30
+ "@aztec/bb-prover": "3.0.0-manual.20251030",
31
+ "@aztec/blob-lib": "3.0.0-manual.20251030",
32
+ "@aztec/blob-sink": "3.0.0-manual.20251030",
33
+ "@aztec/constants": "3.0.0-manual.20251030",
34
+ "@aztec/epoch-cache": "3.0.0-manual.20251030",
35
+ "@aztec/ethereum": "3.0.0-manual.20251030",
36
+ "@aztec/foundation": "3.0.0-manual.20251030",
37
+ "@aztec/l1-artifacts": "3.0.0-manual.20251030",
38
+ "@aztec/merkle-tree": "3.0.0-manual.20251030",
39
+ "@aztec/node-keystore": "3.0.0-manual.20251030",
40
+ "@aztec/noir-acvm_js": "3.0.0-manual.20251030",
41
+ "@aztec/noir-contracts.js": "3.0.0-manual.20251030",
42
+ "@aztec/noir-protocol-circuits-types": "3.0.0-manual.20251030",
43
+ "@aztec/noir-types": "3.0.0-manual.20251030",
44
+ "@aztec/p2p": "3.0.0-manual.20251030",
45
+ "@aztec/protocol-contracts": "3.0.0-manual.20251030",
46
+ "@aztec/prover-client": "3.0.0-manual.20251030",
47
+ "@aztec/simulator": "3.0.0-manual.20251030",
48
+ "@aztec/slasher": "3.0.0-manual.20251030",
49
+ "@aztec/stdlib": "3.0.0-manual.20251030",
50
+ "@aztec/telemetry-client": "3.0.0-manual.20251030",
51
+ "@aztec/validator-client": "3.0.0-manual.20251030",
52
+ "@aztec/world-state": "3.0.0-manual.20251030",
53
53
  "lodash.chunk": "^4.2.0",
54
54
  "tslib": "^2.4.0",
55
55
  "viem": "npm:@spalladino/viem@2.38.2-eip7594.0"
56
56
  },
57
57
  "devDependencies": {
58
- "@aztec/archiver": "3.0.0-devnet.2",
59
- "@aztec/kv-store": "3.0.0-devnet.2",
58
+ "@aztec/archiver": "3.0.0-manual.20251030",
59
+ "@aztec/kv-store": "3.0.0-manual.20251030",
60
60
  "@jest/globals": "^30.0.0",
61
61
  "@types/jest": "^30.0.0",
62
62
  "@types/lodash.chunk": "^4.2.7",
@@ -98,6 +98,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
98
98
 
99
99
  private governanceProposerPayload: EthAddress | undefined;
100
100
 
101
+ /** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */
102
+ private lastSlotForVoteWhenSyncFailed: bigint | undefined;
103
+
101
104
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
102
105
  protected timetable!: SequencerTimetable;
103
106
  protected enforceTimeTable: boolean = false;
@@ -214,7 +217,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
214
217
  * Starts the sequencer and moves to IDLE state.
215
218
  */
216
219
  public start() {
217
- this.runningPromise = new RunningPromise(this.work.bind(this), this.log, this.pollingIntervalMs);
220
+ this.runningPromise = new RunningPromise(this.safeWork.bind(this), this.log, this.pollingIntervalMs);
218
221
  this.setState(SequencerState.IDLE, undefined, { force: true });
219
222
  this.runningPromise.start();
220
223
  this.log.info('Started sequencer');
@@ -248,27 +251,28 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
248
251
  * - Submit block
249
252
  * - If our block for some reason is not included, revert the state
250
253
  */
251
- protected async doRealWork() {
254
+ protected async work() {
252
255
  this.setState(SequencerState.SYNCHRONIZING, undefined);
256
+ const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
253
257
 
254
- // Check all components are synced to latest as seen by the archiver
255
- const syncedTo = await this.getChainTip();
258
+ // Check we have not already published a block for this slot (cheapest check)
259
+ if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
260
+ this.log.debug(
261
+ `Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`,
262
+ );
263
+ return;
264
+ }
256
265
 
257
- // Do not go forward with new block if the previous one has not been mined and processed
266
+ // Check all components are synced to latest as seen by the archiver (queries all subsystems)
267
+ const syncedTo = await this.checkSync({ ts, slot });
258
268
  if (!syncedTo) {
269
+ await this.tryVoteWhenSyncFails({ slot, ts });
259
270
  return;
260
271
  }
261
272
 
262
273
  const chainTipArchive = syncedTo.archive;
263
274
  const newBlockNumber = syncedTo.blockNumber + 1;
264
275
 
265
- const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
266
-
267
- this.setState(SequencerState.PROPOSER_CHECK, slot);
268
-
269
- // Check that the archiver and dependencies have synced to the previous L1 slot at least
270
- // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
271
- // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
272
276
  const syncLogData = {
273
277
  now,
274
278
  syncedToL1Ts: syncedTo.l1Timestamp,
@@ -280,74 +284,30 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
280
284
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
281
285
  };
282
286
 
283
- if (syncedTo.l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
284
- this.log.debug(
285
- `Cannot propose block ${newBlockNumber} at next L2 slot ${slot} due to pending sync from L1`,
286
- syncLogData,
287
- );
287
+ // Check that we are a proposer for the next slot
288
+ this.setState(SequencerState.PROPOSER_CHECK, slot);
289
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
290
+
291
+ // If we are not a proposer, check if we should invalidate a invalid block, and bail
292
+ if (!canPropose) {
293
+ await this.considerInvalidatingBlock(syncedTo, slot);
288
294
  return;
289
295
  }
290
296
 
291
- // Check that the slot is not taken by a block already
297
+ // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
292
298
  if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
293
- this.log.debug(
299
+ this.log.warn(
294
300
  `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
295
301
  { ...syncLogData, block: syncedTo.block.header.toInspect() },
296
302
  );
297
303
  return;
298
304
  }
299
305
 
300
- // Or that we haven't published it ourselves
301
- if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
302
- this.log.debug(
303
- `Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`,
304
- { ...syncLogData, block: this.lastBlockPublished.header.toInspect() },
305
- );
306
- return;
307
- }
308
-
309
- // Check that we are a proposer for the next slot
310
- let proposerInNextSlot: EthAddress | undefined;
311
- try {
312
- proposerInNextSlot = await this.epochCache.getProposerAttesterAddressInNextSlot();
313
- } catch (e) {
314
- if (e instanceof NoCommitteeError) {
315
- this.log.warn(
316
- `Cannot propose block ${newBlockNumber} at next L2 slot ${slot} since the committee does not exist on L1`,
317
- );
318
- return;
319
- }
320
- }
321
-
322
- // If get proposer in next slot is undefined, then the committee is empty and anyone may propose.
323
- // If the committee is defined and not empty, but none of our validators are the proposer, then stop.
324
- const validatorAddresses = this.validatorClient!.getValidatorAddresses();
325
- if (proposerInNextSlot !== undefined && !validatorAddresses.some(addr => addr.equals(proposerInNextSlot))) {
326
- this.log.debug(`Cannot propose block ${newBlockNumber} since we are not a proposer`, {
327
- us: validatorAddresses,
328
- proposer: proposerInNextSlot,
329
- ...syncLogData,
330
- });
331
- // If the pending chain is invalid, we may need to invalidate the block if no one else is doing it.
332
- if (!syncedTo.pendingChainValidationStatus.valid) {
333
- // We pass i undefined here to get any available publisher.
334
- const { publisher } = await this.publisherFactory.create(undefined);
335
- await this.considerInvalidatingBlock(syncedTo, slot, validatorAddresses, publisher);
336
- }
337
- return;
338
- }
339
-
340
- // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
341
- // if all the previous checks are good, but we do it just in case.
342
- const proposerAddressInNextSlot = proposerInNextSlot ?? EthAddress.ZERO;
343
-
344
306
  // We now need to get ourselves a publisher.
345
307
  // The returned attestor will be the one we provided if we provided one.
346
308
  // Otherwise it will be a valid attestor for the returned publisher.
347
- const { attestorAddress, publisher } = await this.publisherFactory.create(proposerInNextSlot);
348
-
309
+ const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
349
310
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
350
-
351
311
  this.publisher = publisher;
352
312
 
353
313
  const coinbase = this.validatorClient!.getCoinbaseForAttestor(attestorAddress);
@@ -355,9 +315,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
355
315
 
356
316
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
357
317
  const invalidateBlock = await publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
318
+
319
+ // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
320
+ // if all the previous checks are good, but we do it just in case.
358
321
  const canProposeCheck = await publisher.canProposeAtNextEthBlock(
359
322
  chainTipArchive,
360
- proposerAddressInNextSlot,
323
+ proposer ?? EthAddress.ZERO,
361
324
  invalidateBlock,
362
325
  );
363
326
 
@@ -384,10 +347,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
384
347
  return;
385
348
  }
386
349
 
387
- this.log.debug(
388
- `Can propose block ${newBlockNumber} at slot ${slot}` + (proposerInNextSlot ? ` as ${proposerInNextSlot}` : ''),
389
- { ...syncLogData, validatorAddresses },
390
- );
350
+ this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, { ...syncLogData });
391
351
 
392
352
  const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(
393
353
  newBlockNumber,
@@ -396,49 +356,67 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
396
356
  slot,
397
357
  );
398
358
 
399
- const { timestamp } = newGlobalVariables;
400
- const signerFn = (msg: TypedDataDefinition) =>
401
- this.validatorClient!.signWithAddress(attestorAddress, msg).then(s => s.toString());
402
-
403
- const enqueueGovernanceSignalPromise =
404
- this.governanceProposerPayload && !this.governanceProposerPayload.isZero()
405
- ? publisher
406
- .enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn)
407
- .catch(err => {
408
- this.log.error(`Error enqueuing governance vote`, err, { blockNumber: newBlockNumber, slot });
409
- return false;
410
- })
411
- : Promise.resolve(false);
412
-
413
- const enqueueSlashingActionsPromise = this.slasherClient
414
- ? this.slasherClient
415
- .getProposerActions(slot)
416
- .then(actions => publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn))
417
- .catch(err => {
418
- this.log.error(`Error enqueuing slashing actions`, err, { blockNumber: newBlockNumber, slot });
419
- return false;
420
- })
421
- : Promise.resolve(false);
359
+ // Enqueue governance and slashing votes (returns promises that will be awaited later)
360
+ const votesPromises = this.enqueueGovernanceAndSlashingVotes(
361
+ publisher,
362
+ attestorAddress,
363
+ slot,
364
+ newGlobalVariables.timestamp,
365
+ );
422
366
 
367
+ // Enqueues block invalidation
423
368
  if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
424
369
  publisher.enqueueInvalidateBlock(invalidateBlock);
425
370
  }
426
371
 
372
+ // Actual block building
427
373
  this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
374
+ const block: L2Block | undefined = await this.tryBuildBlockAndEnqueuePublish(
375
+ slot,
376
+ proposer,
377
+ newBlockNumber,
378
+ publisher,
379
+ newGlobalVariables,
380
+ chainTipArchive,
381
+ invalidateBlock,
382
+ );
383
+
384
+ // Wait until the voting promises have resolved, so all requests are enqueued
385
+ await Promise.all(votesPromises);
428
386
 
429
- this.metrics.incOpenSlot(slot, proposerAddressInNextSlot.toString());
387
+ // And send the tx to L1
388
+ const l1Response = await publisher.sendRequests();
389
+ const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
390
+ if (proposedBlock) {
391
+ this.lastBlockPublished = block;
392
+ this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
393
+ await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
394
+ } else if (block) {
395
+ this.emit('block-publish-failed', l1Response ?? {});
396
+ }
397
+
398
+ this.setState(SequencerState.IDLE, undefined);
399
+ }
400
+
401
+ /** Tries building a block proposal, and if successful, enqueues it for publishing. */
402
+ private async tryBuildBlockAndEnqueuePublish(
403
+ slot: bigint,
404
+ proposer: EthAddress | undefined,
405
+ newBlockNumber: number,
406
+ publisher: SequencerPublisher,
407
+ newGlobalVariables: GlobalVariables,
408
+ chainTipArchive: Fr,
409
+ invalidateBlock: InvalidateBlockRequest | undefined,
410
+ ) {
430
411
  this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
431
- proposer: proposerInNextSlot?.toString(),
432
- coinbase,
412
+ proposer,
433
413
  publisher: publisher.getSenderAddress(),
434
- feeRecipient,
435
414
  globalVariables: newGlobalVariables.toInspect(),
436
415
  chainTipArchive,
437
416
  blockNumber: newBlockNumber,
438
417
  slot,
439
418
  });
440
419
 
441
- // If I created a "partial" header here that should make our job much easier.
442
420
  const proposalHeader = CheckpointHeader.from({
443
421
  ...newGlobalVariables,
444
422
  timestamp: newGlobalVariables.timestamp,
@@ -459,7 +437,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
459
437
  pendingTxs,
460
438
  proposalHeader,
461
439
  newGlobalVariables,
462
- proposerInNextSlot,
440
+ proposer,
463
441
  invalidateBlock,
464
442
  publisher,
465
443
  );
@@ -478,26 +456,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
478
456
  );
479
457
  this.emit('tx-count-check-failed', { minTxs: this.minTxsPerBlock, availableTxs: pendingTxCount });
480
458
  }
481
-
482
- await Promise.all([enqueueGovernanceSignalPromise, enqueueSlashingActionsPromise]);
483
-
484
- const l1Response = await publisher.sendRequests();
485
- const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
486
- if (proposedBlock) {
487
- this.lastBlockPublished = block;
488
- this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
489
- await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
490
- } else if (block) {
491
- this.emit('block-publish-failed', l1Response ?? {});
492
- }
493
-
494
- this.setState(SequencerState.IDLE, undefined);
459
+ return block;
495
460
  }
496
461
 
497
462
  @trackSpan('Sequencer.work')
498
- protected async work() {
463
+ protected async safeWork() {
499
464
  try {
500
- await this.doRealWork();
465
+ await this.work();
501
466
  } catch (err) {
502
467
  if (err instanceof SequencerTooSlowError) {
503
468
  // Log as warn only if we had to abort halfway through the block proposal
@@ -833,9 +798,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
833
798
  /**
834
799
  * Returns whether all dependencies have caught up.
835
800
  * We don't check against the previous block submitted since it may have been reorg'd out.
836
- * @returns Boolean indicating if our dependencies are synced to the latest block.
837
801
  */
838
- protected async getChainTip(): Promise<
802
+ protected async checkSync(args: { ts: bigint; slot: bigint }): Promise<
839
803
  | {
840
804
  block?: L2Block;
841
805
  blockNumber: number;
@@ -845,6 +809,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
845
809
  }
846
810
  | undefined
847
811
  > {
812
+ // Check that the archiver and dependencies have synced to the previous L1 slot at least
813
+ // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
814
+ // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
815
+ const l1Timestamp = await this.l2BlockSource.getL1Timestamp();
816
+ const { slot, ts } = args;
817
+ if (l1Timestamp === undefined || l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
818
+ this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
819
+ slot,
820
+ ts,
821
+ l1Timestamp,
822
+ });
823
+ return undefined;
824
+ }
825
+
848
826
  const syncedBlocks = await Promise.all([
849
827
  this.worldState.status().then(({ syncSummary }) => ({
850
828
  number: syncSummary.latestBlockNumber,
@@ -853,12 +831,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
853
831
  this.l2BlockSource.getL2Tips().then(t => t.latest),
854
832
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
855
833
  this.l1ToL2MessageSource.getL2Tips().then(t => t.latest),
856
- this.l2BlockSource.getL1Timestamp(),
857
834
  this.l2BlockSource.getPendingChainValidationStatus(),
858
835
  ] as const);
859
836
 
860
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, l1Timestamp, pendingChainValidationStatus] =
861
- syncedBlocks;
837
+ const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
862
838
 
863
839
  // The archiver reports 'undefined' hash for the genesis block
864
840
  // because it doesn't have access to world state to compute it (facepalm)
@@ -869,33 +845,174 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
869
845
  p2p.hash === l2BlockSource.hash &&
870
846
  l1ToL2MessageSource.hash === l2BlockSource.hash;
871
847
 
872
- const logData = { worldState, l2BlockSource, p2p, l1ToL2MessageSource };
873
- this.log.debug(`Sequencer sync check ${result ? 'succeeded' : 'failed'}`, logData);
874
-
875
848
  if (!result) {
849
+ this.log.debug(`Sequencer sync check failed`, { worldState, l2BlockSource, p2p, l1ToL2MessageSource });
876
850
  return undefined;
877
851
  }
878
852
 
853
+ // Special case for genesis state
879
854
  const blockNumber = worldState.number;
880
- if (blockNumber >= INITIAL_L2_BLOCK_NUM) {
881
- const block = await this.l2BlockSource.getBlock(blockNumber);
882
- if (!block) {
883
- // this shouldn't really happen because a moment ago we checked that all components were in sync
884
- this.log.warn(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`, logData);
885
- return undefined;
886
- }
887
-
888
- return {
889
- block,
890
- blockNumber: block.number,
891
- archive: block.archive.root,
892
- l1Timestamp,
893
- pendingChainValidationStatus,
894
- };
895
- } else {
855
+ if (blockNumber < INITIAL_L2_BLOCK_NUM) {
896
856
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
897
857
  return { blockNumber: INITIAL_L2_BLOCK_NUM - 1, archive, l1Timestamp, pendingChainValidationStatus };
898
858
  }
859
+
860
+ const block = await this.l2BlockSource.getBlock(blockNumber);
861
+ if (!block) {
862
+ // this shouldn't really happen because a moment ago we checked that all components were in sync
863
+ this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
864
+ return undefined;
865
+ }
866
+
867
+ return {
868
+ block,
869
+ blockNumber: block.number,
870
+ archive: block.archive.root,
871
+ l1Timestamp,
872
+ pendingChainValidationStatus,
873
+ };
874
+ }
875
+
876
+ /**
877
+ * Enqueues governance and slashing votes with the publisher. Does not block.
878
+ * @param publisher - The publisher to enqueue votes with
879
+ * @param attestorAddress - The attestor address to use for signing
880
+ * @param slot - The slot number
881
+ * @param timestamp - The timestamp for the votes
882
+ * @param context - Optional context for logging (e.g., block number)
883
+ * @returns A tuple of [governanceEnqueued, slashingEnqueued]
884
+ */
885
+ protected enqueueGovernanceAndSlashingVotes(
886
+ publisher: SequencerPublisher,
887
+ attestorAddress: EthAddress,
888
+ slot: bigint,
889
+ timestamp: bigint,
890
+ ): [Promise<boolean> | undefined, Promise<boolean> | undefined] {
891
+ try {
892
+ const signerFn = (msg: TypedDataDefinition) =>
893
+ this.validatorClient!.signWithAddress(attestorAddress, msg).then(s => s.toString());
894
+
895
+ const enqueueGovernancePromise =
896
+ this.governanceProposerPayload && !this.governanceProposerPayload.isZero()
897
+ ? publisher
898
+ .enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn)
899
+ .catch(err => {
900
+ this.log.error(`Error enqueuing governance vote`, err, { slot });
901
+ return false;
902
+ })
903
+ : undefined;
904
+
905
+ const enqueueSlashingPromise = this.slasherClient
906
+ ? this.slasherClient
907
+ .getProposerActions(slot)
908
+ .then(actions => publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn))
909
+ .catch(err => {
910
+ this.log.error(`Error enqueuing slashing actions`, err, { slot });
911
+ return false;
912
+ })
913
+ : undefined;
914
+
915
+ return [enqueueGovernancePromise, enqueueSlashingPromise];
916
+ } catch (err) {
917
+ this.log.error(`Error enqueueing governance and slashing votes`, err);
918
+ return [undefined, undefined];
919
+ }
920
+ }
921
+
922
+ /**
923
+ * Checks if we are the proposer for the next slot.
924
+ * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
925
+ */
926
+ protected async checkCanPropose(slot: bigint): Promise<[boolean, EthAddress | undefined]> {
927
+ let proposer: EthAddress | undefined;
928
+ try {
929
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
930
+ } catch (e) {
931
+ if (e instanceof NoCommitteeError) {
932
+ this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
933
+ return [false, undefined];
934
+ }
935
+ this.log.error(`Error getting proposer for slot ${slot}`, e);
936
+ return [false, undefined];
937
+ }
938
+
939
+ // If proposer is undefined, then the committee is empty and anyone may propose
940
+ if (proposer === undefined) {
941
+ return [true, undefined];
942
+ }
943
+
944
+ const validatorAddresses = this.validatorClient!.getValidatorAddresses();
945
+ const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
946
+
947
+ if (!weAreProposer) {
948
+ this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, { validatorAddresses, proposer });
949
+ return [false, proposer];
950
+ }
951
+
952
+ return [true, proposer];
953
+ }
954
+
955
+ /**
956
+ * Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
957
+ * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
958
+ */
959
+ protected async tryVoteWhenSyncFails(args: { slot: bigint; ts: bigint }): Promise<void> {
960
+ const { slot, ts } = args;
961
+
962
+ // Prevent duplicate attempts in the same slot
963
+ if (this.lastSlotForVoteWhenSyncFailed === slot) {
964
+ this.log.debug(`Already attempted to vote in slot ${slot} (skipping)`);
965
+ return;
966
+ }
967
+
968
+ // Check if we're past the max time for initializing a proposal
969
+ const secondsIntoSlot = this.getSecondsIntoSlot(slot);
970
+ const maxAllowedTime = this.timetable.getMaxAllowedTime(SequencerState.INITIALIZING_PROPOSAL);
971
+
972
+ // If we haven't exceeded the time limit for initializing a proposal, don't proceed with voting
973
+ // We use INITIALIZING_PROPOSAL time limit because if we're past that, we can't build a block anyway
974
+ if (maxAllowedTime === undefined || secondsIntoSlot <= maxAllowedTime) {
975
+ this.log.trace(`Not attempting to vote since there is still for block building`, {
976
+ secondsIntoSlot,
977
+ maxAllowedTime,
978
+ });
979
+ return;
980
+ }
981
+
982
+ this.log.debug(`Sync for slot ${slot} failed, checking for voting opportunities`, {
983
+ secondsIntoSlot,
984
+ maxAllowedTime,
985
+ });
986
+
987
+ // Check if we're a proposer or proposal is open
988
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
989
+ if (!canPropose) {
990
+ this.log.debug(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
991
+ return;
992
+ }
993
+
994
+ // Mark this slot as attempted
995
+ this.lastSlotForVoteWhenSyncFailed = slot;
996
+
997
+ // Get a publisher for voting
998
+ const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
999
+
1000
+ this.log.debug(`Attempting to vote despite sync failure at slot ${slot}`, {
1001
+ attestorAddress,
1002
+ slot,
1003
+ });
1004
+
1005
+ // Enqueue governance and slashing votes using the shared helper method
1006
+ const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, ts);
1007
+ await Promise.all(votesPromises);
1008
+
1009
+ if (votesPromises.every(p => !p)) {
1010
+ this.log.debug(`No votes to enqueue for slot ${slot}`);
1011
+ return;
1012
+ }
1013
+
1014
+ this.log.info(`Voting in slot ${slot} despite sync failure`, { slot });
1015
+ await publisher.sendRequests();
899
1016
  }
900
1017
 
901
1018
  /**
@@ -905,19 +1022,19 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
905
1022
  * and if they fail, any sequencer will try as well.
906
1023
  */
907
1024
  protected async considerInvalidatingBlock(
908
- syncedTo: NonNullable<Awaited<ReturnType<Sequencer['getChainTip']>>>,
1025
+ syncedTo: NonNullable<Awaited<ReturnType<Sequencer['checkSync']>>>,
909
1026
  currentSlot: bigint,
910
- ourValidatorAddresses: EthAddress[],
911
- publisher: SequencerPublisher,
912
1027
  ): Promise<void> {
913
1028
  const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
914
1029
  if (pendingChainValidationStatus.valid) {
915
1030
  return;
916
1031
  }
917
1032
 
1033
+ const { publisher } = await this.publisherFactory.create(undefined);
918
1034
  const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
919
1035
  const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
920
1036
  const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
1037
+ const ourValidatorAddresses = this.validatorClient!.getValidatorAddresses();
921
1038
 
922
1039
  const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } =
923
1040
  this.config;
@@ -137,6 +137,13 @@ export class SequencerTimetable {
137
137
  return validationTimeEnd;
138
138
  }
139
139
 
140
+ public getMaxAllowedTime(
141
+ state: Extract<SequencerState, SequencerState.STOPPED | SequencerState.IDLE | SequencerState.SYNCHRONIZING>,
142
+ ): undefined;
143
+ public getMaxAllowedTime(
144
+ state: Exclude<SequencerState, SequencerState.STOPPED | SequencerState.IDLE | SequencerState.SYNCHRONIZING>,
145
+ ): number;
146
+ public getMaxAllowedTime(state: SequencerState): number | undefined;
140
147
  public getMaxAllowedTime(state: SequencerState): number | undefined {
141
148
  switch (state) {
142
149
  case SequencerState.STOPPED: