@aztec/sequencer-client 3.0.0-devnet.6 → 3.0.0-devnet.6-patch.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dest/client/index.d.ts +1 -1
  2. package/dest/client/sequencer-client.d.ts +2 -2
  3. package/dest/client/sequencer-client.d.ts.map +1 -1
  4. package/dest/client/sequencer-client.js +6 -2
  5. package/dest/config.d.ts +3 -2
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +11 -1
  8. package/dest/global_variable_builder/global_builder.d.ts +5 -7
  9. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  10. package/dest/global_variable_builder/global_builder.js +12 -8
  11. package/dest/global_variable_builder/index.d.ts +1 -1
  12. package/dest/index.d.ts +1 -1
  13. package/dest/publisher/config.d.ts +7 -2
  14. package/dest/publisher/config.d.ts.map +1 -1
  15. package/dest/publisher/config.js +12 -1
  16. package/dest/publisher/index.d.ts +1 -1
  17. package/dest/publisher/sequencer-publisher-factory.d.ts +3 -2
  18. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  19. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  20. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  21. package/dest/publisher/sequencer-publisher.d.ts +40 -31
  22. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  23. package/dest/publisher/sequencer-publisher.js +117 -62
  24. package/dest/sequencer/block_builder.d.ts +4 -3
  25. package/dest/sequencer/block_builder.d.ts.map +1 -1
  26. package/dest/sequencer/block_builder.js +5 -8
  27. package/dest/sequencer/config.d.ts +2 -2
  28. package/dest/sequencer/config.d.ts.map +1 -1
  29. package/dest/sequencer/errors.d.ts +1 -1
  30. package/dest/sequencer/errors.d.ts.map +1 -1
  31. package/dest/sequencer/index.d.ts +1 -1
  32. package/dest/sequencer/metrics.d.ts +12 -3
  33. package/dest/sequencer/metrics.d.ts.map +1 -1
  34. package/dest/sequencer/metrics.js +38 -0
  35. package/dest/sequencer/sequencer.d.ts +48 -27
  36. package/dest/sequencer/sequencer.d.ts.map +1 -1
  37. package/dest/sequencer/sequencer.js +418 -166
  38. package/dest/sequencer/timetable.d.ts +3 -1
  39. package/dest/sequencer/timetable.d.ts.map +1 -1
  40. package/dest/sequencer/utils.d.ts +1 -1
  41. package/dest/test/index.d.ts +2 -2
  42. package/dest/test/index.d.ts.map +1 -1
  43. package/dest/tx_validator/nullifier_cache.d.ts +1 -1
  44. package/dest/tx_validator/nullifier_cache.d.ts.map +1 -1
  45. package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
  46. package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
  47. package/dest/tx_validator/tx_validator_factory.js +1 -1
  48. package/package.json +31 -30
  49. package/src/client/sequencer-client.ts +6 -9
  50. package/src/config.ts +12 -6
  51. package/src/global_variable_builder/global_builder.ts +19 -17
  52. package/src/publisher/config.ts +17 -6
  53. package/src/publisher/sequencer-publisher-factory.ts +4 -2
  54. package/src/publisher/sequencer-publisher.ts +165 -94
  55. package/src/sequencer/block_builder.ts +8 -12
  56. package/src/sequencer/config.ts +1 -1
  57. package/src/sequencer/metrics.ts +52 -3
  58. package/src/sequencer/sequencer.ts +480 -198
  59. package/src/sequencer/timetable.ts +7 -0
  60. package/src/test/index.ts +1 -1
  61. package/src/tx_validator/tx_validator_factory.ts +3 -2
@@ -1,12 +1,15 @@
1
1
  import { L2Block } from '@aztec/aztec.js/block';
2
- import { BLOBS_PER_BLOCK, FIELDS_PER_BLOB, INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
2
+ import { getKzg } from '@aztec/blob-lib';
3
+ import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB, INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
3
4
  import type { EpochCache } from '@aztec/epoch-cache';
4
- import { FormattedViemError, NoCommitteeError, type RollupContract } from '@aztec/ethereum';
5
+ import { NoCommitteeError, type RollupContract } from '@aztec/ethereum/contracts';
6
+ import { FormattedViemError } from '@aztec/ethereum/utils';
7
+ import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
5
8
  import { omit, pick } from '@aztec/foundation/collection';
6
- import { randomInt } from '@aztec/foundation/crypto';
9
+ import { randomInt } from '@aztec/foundation/crypto/random';
10
+ import { Fr } from '@aztec/foundation/curves/bn254';
7
11
  import { EthAddress } from '@aztec/foundation/eth-address';
8
12
  import { Signature } from '@aztec/foundation/eth-signature';
9
- import { Fr } from '@aztec/foundation/fields';
10
13
  import { createLogger } from '@aztec/foundation/log';
11
14
  import { RunningPromise } from '@aztec/foundation/running-promise';
12
15
  import { type DateProvider, Timer } from '@aztec/foundation/timer';
@@ -17,6 +20,7 @@ import {
17
20
  CommitteeAttestation,
18
21
  CommitteeAttestationsAndSigners,
19
22
  type L2BlockSource,
23
+ MaliciousCommitteeAttestationsAndSigners,
20
24
  type ValidateBlockResult,
21
25
  } from '@aztec/stdlib/block';
22
26
  import { type L1RollupConstants, getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
@@ -60,7 +64,7 @@ export type SequencerEvents = {
60
64
  oldState: SequencerState;
61
65
  newState: SequencerState;
62
66
  secondsIntoSlot?: number;
63
- slotNumber?: bigint;
67
+ slotNumber?: SlotNumber;
64
68
  }) => void;
65
69
  ['proposer-rollup-check-failed']: (args: { reason: string }) => void;
66
70
  ['tx-count-check-failed']: (args: { minTxs: number; availableTxs: number }) => void;
@@ -71,7 +75,7 @@ export type SequencerEvents = {
71
75
  sentActions?: Action[];
72
76
  expiredActions?: Action[];
73
77
  }) => void;
74
- ['block-published']: (args: { blockNumber: number; slot: number }) => void;
78
+ ['block-published']: (args: { blockNumber: BlockNumber; slot: number }) => void;
75
79
  };
76
80
 
77
81
  /**
@@ -98,6 +102,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
98
102
 
99
103
  private governanceProposerPayload: EthAddress | undefined;
100
104
 
105
+ /** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */
106
+ private lastSlotForVoteWhenSyncFailed: SlotNumber | undefined;
107
+
108
+ /** The last slot for which we built a validation block in fisherman mode, to prevent duplicate attempts. */
109
+ private lastSlotForValidationBlock: SlotNumber | undefined;
110
+
101
111
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
102
112
  protected timetable!: SequencerTimetable;
103
113
  protected enforceTimeTable: boolean = false;
@@ -129,6 +139,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
129
139
  ) {
130
140
  super();
131
141
 
142
+ // Add [FISHERMAN] prefix to logger if in fisherman mode
143
+ if (this.config.fishermanMode) {
144
+ this.log = log.createChild('[FISHERMAN]');
145
+ }
146
+
132
147
  this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
133
148
  // Initialize config
134
149
  this.updateConfig(this.config);
@@ -207,6 +222,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
207
222
  }
208
223
 
209
224
  public async init() {
225
+ // Takes ~3s to precompute some tables.
226
+ getKzg();
210
227
  this.publisher = (await this.publisherFactory.create(undefined)).publisher;
211
228
  }
212
229
 
@@ -214,7 +231,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
214
231
  * Starts the sequencer and moves to IDLE state.
215
232
  */
216
233
  public start() {
217
- this.runningPromise = new RunningPromise(this.work.bind(this), this.log, this.pollingIntervalMs);
234
+ this.runningPromise = new RunningPromise(this.safeWork.bind(this), this.log, this.pollingIntervalMs);
218
235
  this.setState(SequencerState.IDLE, undefined, { force: true });
219
236
  this.runningPromise.start();
220
237
  this.log.info('Started sequencer');
@@ -248,27 +265,28 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
248
265
  * - Submit block
249
266
  * - If our block for some reason is not included, revert the state
250
267
  */
251
- protected async doRealWork() {
268
+ protected async work() {
252
269
  this.setState(SequencerState.SYNCHRONIZING, undefined);
270
+ const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
253
271
 
254
- // Check all components are synced to latest as seen by the archiver
255
- const syncedTo = await this.getChainTip();
272
+ // Check we have not already published a block for this slot (cheapest check)
273
+ if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
274
+ this.log.debug(
275
+ `Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`,
276
+ );
277
+ return;
278
+ }
256
279
 
257
- // Do not go forward with new block if the previous one has not been mined and processed
280
+ // Check all components are synced to latest as seen by the archiver (queries all subsystems)
281
+ const syncedTo = await this.checkSync({ ts, slot });
258
282
  if (!syncedTo) {
283
+ await this.tryVoteWhenSyncFails({ slot, ts });
259
284
  return;
260
285
  }
261
286
 
262
287
  const chainTipArchive = syncedTo.archive;
263
- const newBlockNumber = syncedTo.blockNumber + 1;
264
-
265
- const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
288
+ const newBlockNumber = BlockNumber(syncedTo.blockNumber + 1);
266
289
 
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
290
  const syncLogData = {
273
291
  now,
274
292
  syncedToL1Ts: syncedTo.l1Timestamp,
@@ -280,84 +298,70 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
280
298
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
281
299
  };
282
300
 
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
- );
288
- return;
289
- }
290
-
291
- // Check that the slot is not taken by a block already
292
- if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
293
- this.log.debug(
294
- `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
295
- { ...syncLogData, block: syncedTo.block.header.toInspect() },
296
- );
297
- return;
298
- }
301
+ // Check that we are a proposer for the next slot
302
+ this.setState(SequencerState.PROPOSER_CHECK, slot);
303
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
299
304
 
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
- );
305
+ // If we are not a proposer check if we should invalidate a invalid block, and bail
306
+ if (!canPropose) {
307
+ await this.considerInvalidatingBlock(syncedTo, slot);
306
308
  return;
307
309
  }
308
310
 
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
- );
311
+ // In fisherman mode, check if we've already validated this slot to prevent duplicate attempts
312
+ if (this.config.fishermanMode) {
313
+ if (this.lastSlotForValidationBlock === slot) {
314
+ this.log.trace(`Already validated block building for slot ${slot} (skipping)`, { slot });
318
315
  return;
319
316
  }
317
+ this.log.debug(
318
+ `Building validation block for slot ${slot} (actual proposer: ${proposer?.toString() ?? 'none'})`,
319
+ { slot, proposer: proposer?.toString() },
320
+ );
321
+ // Mark this slot as being validated
322
+ this.lastSlotForValidationBlock = slot;
320
323
  }
321
324
 
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
- }
325
+ // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
326
+ if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
327
+ this.log.warn(
328
+ `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
329
+ { ...syncLogData, block: syncedTo.block.header.toInspect() },
330
+ );
331
+ this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
337
332
  return;
338
333
  }
339
334
 
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
335
  // We now need to get ourselves a publisher.
345
336
  // The returned attestor will be the one we provided if we provided one.
346
337
  // Otherwise it will be a valid attestor for the returned publisher.
347
- const { attestorAddress, publisher } = await this.publisherFactory.create(proposerInNextSlot);
348
-
338
+ // In fisherman mode, pass undefined to use the fisherman's own keystore instead of the actual proposer's
339
+ const { attestorAddress, publisher } = await this.publisherFactory.create(
340
+ this.config.fishermanMode ? undefined : proposer,
341
+ );
349
342
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
350
-
351
343
  this.publisher = publisher;
352
344
 
345
+ // In fisherman mode, set the actual proposer's address for simulations
346
+ if (this.config.fishermanMode) {
347
+ if (proposer) {
348
+ publisher.setProposerAddressForSimulation(proposer);
349
+ this.log.debug(`Set proposer address ${proposer} for simulation in fisherman mode`);
350
+ }
351
+ }
352
+
353
+ // Get proposer credentials
353
354
  const coinbase = this.validatorClient!.getCoinbaseForAttestor(attestorAddress);
354
355
  const feeRecipient = this.validatorClient!.getFeeRecipientForAttestor(attestorAddress);
355
356
 
356
357
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
357
358
  const invalidateBlock = await publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
359
+
360
+ // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
361
+ // if all the previous checks are good, but we do it just in case.
358
362
  const canProposeCheck = await publisher.canProposeAtNextEthBlock(
359
363
  chainTipArchive,
360
- proposerAddressInNextSlot,
364
+ proposer ?? EthAddress.ZERO,
361
365
  invalidateBlock,
362
366
  );
363
367
 
@@ -367,6 +371,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
367
371
  syncLogData,
368
372
  );
369
373
  this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed' });
374
+ this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
370
375
  return;
371
376
  } else if (canProposeCheck.slot !== slot) {
372
377
  this.log.warn(
@@ -374,20 +379,19 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
374
379
  { ...syncLogData, rollup: canProposeCheck, newBlockNumber, expectedSlot: slot },
375
380
  );
376
381
  this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch' });
382
+ this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
377
383
  return;
378
- } else if (canProposeCheck.blockNumber !== BigInt(newBlockNumber)) {
384
+ } else if (canProposeCheck.checkpointNumber !== CheckpointNumber.fromBlockNumber(newBlockNumber)) {
379
385
  this.log.warn(
380
- `Cannot propose block due to block mismatch with rollup contract (this can be caused by a pending archiver sync). Expected block ${newBlockNumber} but got ${canProposeCheck.blockNumber}.`,
386
+ `Cannot propose block due to block mismatch with rollup contract (this can be caused by a pending archiver sync). Expected block ${newBlockNumber} but got ${canProposeCheck.checkpointNumber}.`,
381
387
  { ...syncLogData, rollup: canProposeCheck, newBlockNumber, expectedSlot: slot },
382
388
  );
383
389
  this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch' });
390
+ this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
384
391
  return;
385
392
  }
386
393
 
387
- this.log.debug(
388
- `Can propose block ${newBlockNumber} at slot ${slot}` + (proposerInNextSlot ? ` as ${proposerInNextSlot}` : ''),
389
- { ...syncLogData, validatorAddresses },
390
- );
394
+ this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, { ...syncLogData });
391
395
 
392
396
  const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(
393
397
  newBlockNumber,
@@ -396,53 +400,98 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
396
400
  slot,
397
401
  );
398
402
 
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);
403
+ // Enqueue governance and slashing votes (returns promises that will be awaited later)
404
+ // In fisherman mode, we simulate slashing but don't actually publish to L1
405
+ const votesPromises = this.enqueueGovernanceAndSlashingVotes(
406
+ publisher,
407
+ attestorAddress,
408
+ slot,
409
+ newGlobalVariables.timestamp,
410
+ );
422
411
 
412
+ // Enqueues block invalidation
423
413
  if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
424
414
  publisher.enqueueInvalidateBlock(invalidateBlock);
425
415
  }
426
416
 
417
+ // Actual block building
427
418
  this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
419
+ this.metrics.incOpenSlot(slot, proposer?.toString() ?? 'unknown');
420
+ const block: L2Block | undefined = await this.tryBuildBlockAndEnqueuePublish(
421
+ slot,
422
+ proposer,
423
+ newBlockNumber,
424
+ publisher,
425
+ newGlobalVariables,
426
+ chainTipArchive,
427
+ invalidateBlock,
428
+ );
428
429
 
429
- this.metrics.incOpenSlot(slot, proposerAddressInNextSlot.toString());
430
+ // Wait until the voting promises have resolved, so all requests are enqueued
431
+ await Promise.all(votesPromises);
432
+
433
+ // In fisherman mode, we don't publish to L1
434
+ if (this.config.fishermanMode) {
435
+ // Clear pending requests
436
+ publisher.clearPendingRequests();
437
+
438
+ if (block) {
439
+ this.log.info(`Validation block building SUCCEEDED for slot ${slot}`, {
440
+ blockNumber: newBlockNumber,
441
+ slot: Number(slot),
442
+ archive: block.archive.toString(),
443
+ txCount: block.body.txEffects.length,
444
+ });
445
+ this.lastBlockPublished = block;
446
+ this.metrics.recordBlockProposalSuccess();
447
+ } else {
448
+ // Block building failed in fisherman mode
449
+ this.log.warn(`Validation block building FAILED for slot ${slot}`, {
450
+ blockNumber: newBlockNumber,
451
+ slot: Number(slot),
452
+ });
453
+ this.metrics.recordBlockProposalFailed('block_build_failed');
454
+ }
455
+ } else {
456
+ // Normal mode: send the tx to L1
457
+ const l1Response = await publisher.sendRequests();
458
+ const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
459
+ if (proposedBlock) {
460
+ this.lastBlockPublished = block;
461
+ this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
462
+ await this.metrics.incFilledSlot(publisher.getSenderAddress().toString(), coinbase);
463
+ } else if (block) {
464
+ this.emit('block-publish-failed', l1Response ?? {});
465
+ }
466
+ }
467
+
468
+ this.setState(SequencerState.IDLE, undefined);
469
+ }
470
+
471
+ /** Tries building a block proposal, and if successful, enqueues it for publishing. */
472
+ private async tryBuildBlockAndEnqueuePublish(
473
+ slot: SlotNumber,
474
+ proposer: EthAddress | undefined,
475
+ newBlockNumber: BlockNumber,
476
+ publisher: SequencerPublisher,
477
+ newGlobalVariables: GlobalVariables,
478
+ chainTipArchive: Fr,
479
+ invalidateBlock: InvalidateBlockRequest | undefined,
480
+ ) {
430
481
  this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
431
- proposer: proposerInNextSlot?.toString(),
432
- coinbase,
482
+ proposer,
433
483
  publisher: publisher.getSenderAddress(),
434
- feeRecipient,
435
484
  globalVariables: newGlobalVariables.toInspect(),
436
485
  chainTipArchive,
437
486
  blockNumber: newBlockNumber,
438
487
  slot,
439
488
  });
440
489
 
441
- // If I created a "partial" header here that should make our job much easier.
442
490
  const proposalHeader = CheckpointHeader.from({
443
491
  ...newGlobalVariables,
444
492
  timestamp: newGlobalVariables.timestamp,
445
493
  lastArchiveRoot: chainTipArchive,
494
+ blockHeadersHash: Fr.ZERO,
446
495
  contentCommitment: ContentCommitment.empty(),
447
496
  totalManaUsed: Fr.ZERO,
448
497
  });
@@ -459,7 +508,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
459
508
  pendingTxs,
460
509
  proposalHeader,
461
510
  newGlobalVariables,
462
- proposerInNextSlot,
511
+ proposer,
463
512
  invalidateBlock,
464
513
  publisher,
465
514
  );
@@ -470,6 +519,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
470
519
  } else {
471
520
  this.log.error(`Error building/enqueuing block`, err, { blockNumber: newBlockNumber, slot });
472
521
  }
522
+ this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
473
523
  }
474
524
  } else {
475
525
  this.log.verbose(
@@ -477,27 +527,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
477
527
  { chainTipArchive, blockNumber: newBlockNumber, slot },
478
528
  );
479
529
  this.emit('tx-count-check-failed', { minTxs: this.minTxsPerBlock, availableTxs: pendingTxCount });
530
+ this.metrics.recordBlockProposalFailed('insufficient_txs');
480
531
  }
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);
532
+ return block;
495
533
  }
496
534
 
497
535
  @trackSpan('Sequencer.work')
498
- protected async work() {
536
+ protected async safeWork() {
499
537
  try {
500
- await this.doRealWork();
538
+ await this.work();
501
539
  } catch (err) {
502
540
  if (err instanceof SequencerTooSlowError) {
503
541
  // Log as warn only if we had to abort halfway through the block proposal
@@ -520,13 +558,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
520
558
  * @param slotNumber - The current slot number.
521
559
  * @param force - Whether to force the transition even if the sequencer is stopped.
522
560
  */
523
- setState(proposedState: SequencerStateWithSlot, slotNumber: bigint, opts?: { force?: boolean }): void;
561
+ setState(proposedState: SequencerStateWithSlot, slotNumber: SlotNumber, opts?: { force?: boolean }): void;
524
562
  setState(
525
563
  proposedState: Exclude<SequencerState, SequencerStateWithSlot>,
526
564
  slotNumber?: undefined,
527
565
  opts?: { force?: boolean },
528
566
  ): void;
529
- setState(proposedState: SequencerState, slotNumber: bigint | undefined, opts: { force?: boolean } = {}): void {
567
+ setState(proposedState: SequencerState, slotNumber: SlotNumber | undefined, opts: { force?: boolean } = {}): void {
530
568
  if (this.state === SequencerState.STOPPING && proposedState !== SequencerState.STOPPED && !opts.force) {
531
569
  this.log.warn(`Cannot set sequencer to ${proposedState} as it is stopping.`);
532
570
  throw new SequencerInterruptedError();
@@ -567,7 +605,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
567
605
  await this.p2pClient.deleteTxs(failedTxHashes);
568
606
  }
569
607
 
570
- protected getBlockBuilderOptions(slot: number): PublicProcessorLimits {
608
+ protected getBlockBuilderOptions(slot: SlotNumber): PublicProcessorLimits {
571
609
  // Deadline for processing depends on whether we're proposing a block
572
610
  const secondsIntoSlot = this.getSecondsIntoSlot(slot);
573
611
  const processingEndTimeWithinSlot = this.timetable.getBlockProposalExecTimeEnd(secondsIntoSlot);
@@ -580,7 +618,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
580
618
  maxTransactions: this.maxTxsPerBlock,
581
619
  maxBlockSize: this.maxBlockSizeInBytes,
582
620
  maxBlockGas: this.maxBlockGas,
583
- maxBlobFields: BLOBS_PER_BLOCK * FIELDS_PER_BLOB,
621
+ maxBlobFields: BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB,
584
622
  deadline,
585
623
  };
586
624
  }
@@ -610,14 +648,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
610
648
  await publisher.validateBlockHeader(proposalHeader, invalidateBlock);
611
649
 
612
650
  const blockNumber = newGlobalVariables.blockNumber;
613
- const slot = proposalHeader.slotNumber.toBigInt();
614
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
651
+ const checkpointNumber = CheckpointNumber.fromBlockNumber(blockNumber);
652
+ const slot = proposalHeader.slotNumber;
653
+ const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
615
654
 
616
655
  const workTimer = new Timer();
617
656
  this.setState(SequencerState.CREATING_BLOCK, slot);
618
657
 
619
658
  try {
620
- const blockBuilderOptions = this.getBlockBuilderOptions(Number(slot));
659
+ const blockBuilderOptions = this.getBlockBuilderOptions(slot);
621
660
  const buildBlockRes = await this.blockBuilder.buildBlock(
622
661
  pendingTxs,
623
662
  l1ToL2Messages,
@@ -665,19 +704,28 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
665
704
  },
666
705
  );
667
706
 
668
- this.log.debug('Collecting attestations');
669
- const attestations = await this.collectAttestations(block, usedTxs, proposerAddress);
670
- if (attestations !== undefined) {
671
- this.log.verbose(`Collected ${attestations.length} attestations`, { blockHash, blockNumber });
707
+ // In fisherman mode, skip attestation collection
708
+ let attestationsAndSigners: CommitteeAttestationsAndSigners;
709
+ if (this.config.fishermanMode) {
710
+ this.log.debug('Skipping attestation collection');
711
+ attestationsAndSigners = CommitteeAttestationsAndSigners.empty();
712
+ } else {
713
+ this.log.debug('Collecting attestations');
714
+ attestationsAndSigners = await this.collectAttestations(block, usedTxs, proposerAddress);
715
+ this.log.verbose(
716
+ `Collected ${attestationsAndSigners.attestations.length} attestations for block ${blockNumber} at slot ${slot}`,
717
+ { blockHash, blockNumber, slot },
718
+ );
672
719
  }
673
720
 
674
- const attestationsAndSigners = new CommitteeAttestationsAndSigners(attestations ?? []);
675
- const attestationsAndSignersSignature = this.validatorClient
676
- ? await this.validatorClient.signAttestationsAndSigners(
677
- attestationsAndSigners,
678
- proposerAddress ?? publisher.getSenderAddress(),
679
- )
680
- : Signature.empty();
721
+ // In fisherman mode, skip attestation signing
722
+ const attestationsAndSignersSignature =
723
+ this.config.fishermanMode || !this.validatorClient
724
+ ? Signature.empty()
725
+ : await this.validatorClient.signAttestationsAndSigners(
726
+ attestationsAndSigners,
727
+ proposerAddress ?? publisher.getSenderAddress(),
728
+ );
681
729
 
682
730
  await this.enqueuePublishL2Block(
683
731
  block,
@@ -703,8 +751,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
703
751
  block: L2Block,
704
752
  txs: Tx[],
705
753
  proposerAddress: EthAddress | undefined,
706
- ): Promise<CommitteeAttestation[] | undefined> {
707
- const { committee } = await this.epochCache.getCommittee(block.header.getSlot());
754
+ ): Promise<CommitteeAttestationsAndSigners> {
755
+ const { committee, seed, epoch } = await this.epochCache.getCommittee(block.slot);
708
756
 
709
757
  // We checked above that the committee is defined, so this should never happen.
710
758
  if (!committee) {
@@ -713,20 +761,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
713
761
 
714
762
  if (committee.length === 0) {
715
763
  this.log.verbose(`Attesting committee is empty`);
716
- return undefined;
764
+ return CommitteeAttestationsAndSigners.empty();
717
765
  } else {
718
766
  this.log.debug(`Attesting committee length is ${committee.length}`);
719
767
  }
720
768
 
721
769
  if (!this.validatorClient) {
722
- const msg = 'Missing validator client: Cannot collect attestations';
723
- this.log.error(msg);
724
- throw new Error(msg);
770
+ throw new Error('Missing validator client: Cannot collect attestations');
725
771
  }
726
772
 
727
773
  const numberOfRequiredAttestations = Math.floor((committee.length * 2) / 3) + 1;
728
774
 
729
- const slotNumber = block.header.globalVariables.slotNumber.toBigInt();
775
+ const slotNumber = block.header.globalVariables.slotNumber;
730
776
  this.setState(SequencerState.COLLECTING_ATTESTATIONS, slotNumber);
731
777
 
732
778
  this.log.debug('Creating block proposal for validators');
@@ -738,7 +784,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
738
784
  block.header.globalVariables.blockNumber,
739
785
  block.getCheckpointHeader(),
740
786
  block.archive.root,
741
- block.header.state,
742
787
  txs,
743
788
  proposerAddress,
744
789
  blockProposalOptions,
@@ -751,7 +796,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
751
796
  if (this.config.skipCollectingAttestations) {
752
797
  this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
753
798
  const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
754
- return orderAttestations(attestations ?? [], committee);
799
+ return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
755
800
  }
756
801
 
757
802
  this.log.debug('Broadcasting block proposal to validators');
@@ -777,13 +822,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
777
822
 
778
823
  // note: the smart contract requires that the signatures are provided in the order of the committee
779
824
  const sorted = orderAttestations(attestations, committee);
780
- if (this.config.injectFakeAttestation) {
781
- const nonEmpty = sorted.filter(a => !a.signature.isEmpty());
782
- const randomIndex = randomInt(nonEmpty.length);
783
- this.log.warn(`Injecting fake attestation in block ${block.number}`);
784
- unfreeze(nonEmpty[randomIndex]).signature = Signature.random();
825
+
826
+ // manipulate the attestations if we've been configured to do so
827
+ if (this.config.injectFakeAttestation || this.config.shuffleAttestationOrdering) {
828
+ return this.manipulateAttestations(block, epoch, seed, committee, sorted);
785
829
  }
786
- return sorted;
830
+
831
+ return new CommitteeAttestationsAndSigners(sorted);
787
832
  } catch (err) {
788
833
  if (err && err instanceof AttestationTimeoutError) {
789
834
  collectedAttestationsCount = err.collectedCount;
@@ -794,6 +839,53 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
794
839
  }
795
840
  }
796
841
 
842
+ /** Breaks the attestations before publishing based on attack configs */
843
+ private manipulateAttestations(
844
+ block: L2Block,
845
+ epoch: EpochNumber,
846
+ seed: bigint,
847
+ committee: EthAddress[],
848
+ attestations: CommitteeAttestation[],
849
+ ) {
850
+ // Compute the proposer index in the committee, since we dont want to tweak it.
851
+ // Otherwise, the L1 rollup contract will reject the block outright.
852
+ const proposerIndex = Number(
853
+ this.epochCache.computeProposerIndex(block.slot, epoch, seed, BigInt(committee.length)),
854
+ );
855
+
856
+ if (this.config.injectFakeAttestation) {
857
+ // Find non-empty attestations that are not from the proposer
858
+ const nonProposerIndices: number[] = [];
859
+ for (let i = 0; i < attestations.length; i++) {
860
+ if (!attestations[i].signature.isEmpty() && i !== proposerIndex) {
861
+ nonProposerIndices.push(i);
862
+ }
863
+ }
864
+ if (nonProposerIndices.length > 0) {
865
+ const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
866
+ this.log.warn(`Injecting fake attestation in block ${block.number} at index ${targetIndex}`);
867
+ unfreeze(attestations[targetIndex]).signature = Signature.random();
868
+ }
869
+ return new CommitteeAttestationsAndSigners(attestations);
870
+ }
871
+
872
+ if (this.config.shuffleAttestationOrdering) {
873
+ this.log.warn(`Shuffling attestation ordering in block ${block.number} (proposer index ${proposerIndex})`);
874
+
875
+ const shuffled = [...attestations];
876
+ const [i, j] = [(proposerIndex + 1) % shuffled.length, (proposerIndex + 2) % shuffled.length];
877
+ const valueI = shuffled[i];
878
+ const valueJ = shuffled[j];
879
+ shuffled[i] = valueJ;
880
+ shuffled[j] = valueI;
881
+
882
+ const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
883
+ return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
884
+ }
885
+
886
+ return new CommitteeAttestationsAndSigners(attestations);
887
+ }
888
+
797
889
  /**
798
890
  * Publishes the L2Block to the rollup contract.
799
891
  * @param block - The L2Block to be published.
@@ -809,10 +901,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
809
901
  publisher: SequencerPublisher,
810
902
  ): Promise<void> {
811
903
  // Publishes new block to the network and awaits the tx to be mined
812
- this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber.toBigInt());
904
+ this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber);
813
905
 
814
906
  // Time out tx at the end of the slot
815
- const slot = block.header.globalVariables.slotNumber.toNumber();
907
+ const slot = block.header.globalVariables.slotNumber;
816
908
  const txTimeoutAt = new Date((this.getSlotStartBuildTimestamp(slot) + this.aztecSlotDuration) * 1000);
817
909
 
818
910
  const enqueued = await publisher.enqueueProposeL2Block(
@@ -833,18 +925,31 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
833
925
  /**
834
926
  * Returns whether all dependencies have caught up.
835
927
  * 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
928
  */
838
- protected async getChainTip(): Promise<
929
+ protected async checkSync(args: { ts: bigint; slot: SlotNumber }): Promise<
839
930
  | {
840
931
  block?: L2Block;
841
- blockNumber: number;
932
+ blockNumber: BlockNumber;
842
933
  archive: Fr;
843
934
  l1Timestamp: bigint;
844
935
  pendingChainValidationStatus: ValidateBlockResult;
845
936
  }
846
937
  | undefined
847
938
  > {
939
+ // Check that the archiver and dependencies have synced to the previous L1 slot at least
940
+ // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
941
+ // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
942
+ const l1Timestamp = await this.l2BlockSource.getL1Timestamp();
943
+ const { slot, ts } = args;
944
+ if (l1Timestamp === undefined || l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
945
+ this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
946
+ slot,
947
+ ts,
948
+ l1Timestamp,
949
+ });
950
+ return undefined;
951
+ }
952
+
848
953
  const syncedBlocks = await Promise.all([
849
954
  this.worldState.status().then(({ syncSummary }) => ({
850
955
  number: syncSummary.latestBlockNumber,
@@ -853,49 +958,203 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
853
958
  this.l2BlockSource.getL2Tips().then(t => t.latest),
854
959
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
855
960
  this.l1ToL2MessageSource.getL2Tips().then(t => t.latest),
856
- this.l2BlockSource.getL1Timestamp(),
857
961
  this.l2BlockSource.getPendingChainValidationStatus(),
858
962
  ] as const);
859
963
 
860
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, l1Timestamp, pendingChainValidationStatus] =
861
- syncedBlocks;
964
+ const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
862
965
 
863
- // The archiver reports 'undefined' hash for the genesis block
864
- // because it doesn't have access to world state to compute it (facepalm)
966
+ // Handle zero as a special case, since the block hash won't match across services if we're changing the prefilled data for the genesis block,
967
+ // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
865
968
  const result =
866
- l2BlockSource.hash === undefined
867
- ? worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0
868
- : worldState.hash === l2BlockSource.hash &&
869
- p2p.hash === l2BlockSource.hash &&
870
- l1ToL2MessageSource.hash === l2BlockSource.hash;
871
-
872
- const logData = { worldState, l2BlockSource, p2p, l1ToL2MessageSource };
873
- this.log.debug(`Sequencer sync check ${result ? 'succeeded' : 'failed'}`, logData);
969
+ (l2BlockSource.number === 0 && worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0) ||
970
+ (worldState.hash === l2BlockSource.hash &&
971
+ p2p.hash === l2BlockSource.hash &&
972
+ l1ToL2MessageSource.hash === l2BlockSource.hash);
874
973
 
875
974
  if (!result) {
975
+ this.log.debug(`Sequencer sync check failed`, { worldState, l2BlockSource, p2p, l1ToL2MessageSource });
876
976
  return undefined;
877
977
  }
878
978
 
979
+ // Special case for genesis state
879
980
  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;
981
+ if (blockNumber < INITIAL_L2_BLOCK_NUM) {
982
+ const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
983
+ return { blockNumber: BlockNumber(INITIAL_L2_BLOCK_NUM - 1), archive, l1Timestamp, pendingChainValidationStatus };
984
+ }
985
+
986
+ const block = await this.l2BlockSource.getBlock(blockNumber);
987
+ if (!block) {
988
+ // this shouldn't really happen because a moment ago we checked that all components were in sync
989
+ this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
990
+ return undefined;
991
+ }
992
+
993
+ return {
994
+ block,
995
+ blockNumber: block.number,
996
+ archive: block.archive.root,
997
+ l1Timestamp,
998
+ pendingChainValidationStatus,
999
+ };
1000
+ }
1001
+
1002
+ /**
1003
+ * Enqueues governance and slashing votes with the publisher. Does not block.
1004
+ * @param publisher - The publisher to enqueue votes with
1005
+ * @param attestorAddress - The attestor address to use for signing
1006
+ * @param slot - The slot number
1007
+ * @param timestamp - The timestamp for the votes
1008
+ * @param context - Optional context for logging (e.g., block number)
1009
+ * @returns A tuple of [governanceEnqueued, slashingEnqueued]
1010
+ */
1011
+ protected enqueueGovernanceAndSlashingVotes(
1012
+ publisher: SequencerPublisher,
1013
+ attestorAddress: EthAddress,
1014
+ slot: SlotNumber,
1015
+ timestamp: bigint,
1016
+ ): [Promise<boolean> | undefined, Promise<boolean> | undefined] {
1017
+ try {
1018
+ const signerFn = (msg: TypedDataDefinition) =>
1019
+ this.validatorClient!.signWithAddress(attestorAddress, msg).then(s => s.toString());
1020
+
1021
+ const enqueueGovernancePromise =
1022
+ this.governanceProposerPayload && !this.governanceProposerPayload.isZero()
1023
+ ? publisher
1024
+ .enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn)
1025
+ .catch(err => {
1026
+ this.log.error(`Error enqueuing governance vote`, err, { slot });
1027
+ return false;
1028
+ })
1029
+ : undefined;
1030
+
1031
+ const enqueueSlashingPromise = this.slasherClient
1032
+ ? this.slasherClient
1033
+ .getProposerActions(slot)
1034
+ .then(actions => {
1035
+ // Record metrics for fisherman mode
1036
+ if (this.config.fishermanMode && actions.length > 0) {
1037
+ this.log.debug(`Fisherman mode: simulating ${actions.length} slashing action(s) for slot ${slot}`, {
1038
+ slot,
1039
+ actionCount: actions.length,
1040
+ });
1041
+ this.metrics.recordSlashingAttempt(actions.length);
1042
+ }
1043
+ // Enqueue the actions to fully simulate L1 tx building (they won't be sent in fisherman mode)
1044
+ return publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn);
1045
+ })
1046
+ .catch(err => {
1047
+ this.log.error(`Error enqueuing slashing actions`, err, { slot });
1048
+ return false;
1049
+ })
1050
+ : undefined;
1051
+
1052
+ return [enqueueGovernancePromise, enqueueSlashingPromise];
1053
+ } catch (err) {
1054
+ this.log.error(`Error enqueueing governance and slashing votes`, err);
1055
+ return [undefined, undefined];
1056
+ }
1057
+ }
1058
+
1059
+ /**
1060
+ * Checks if we are the proposer for the next slot.
1061
+ * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
1062
+ */
1063
+ protected async checkCanPropose(slot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
1064
+ let proposer: EthAddress | undefined;
1065
+
1066
+ try {
1067
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
1068
+ } catch (e) {
1069
+ if (e instanceof NoCommitteeError) {
1070
+ this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
1071
+ return [false, undefined];
886
1072
  }
1073
+ this.log.error(`Error getting proposer for slot ${slot}`, e);
1074
+ return [false, undefined];
1075
+ }
887
1076
 
888
- return {
889
- block,
890
- blockNumber: block.number,
891
- archive: block.archive.root,
892
- l1Timestamp,
893
- pendingChainValidationStatus,
894
- };
895
- } else {
896
- const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
897
- return { blockNumber: INITIAL_L2_BLOCK_NUM - 1, archive, l1Timestamp, pendingChainValidationStatus };
1077
+ // If proposer is undefined, then the committee is empty and anyone may propose
1078
+ if (proposer === undefined) {
1079
+ return [true, undefined];
1080
+ }
1081
+ // In fisherman mode, just return the current proposer
1082
+ if (this.config.fishermanMode) {
1083
+ return [true, proposer];
1084
+ }
1085
+
1086
+ const validatorAddresses = this.validatorClient!.getValidatorAddresses();
1087
+ const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
1088
+
1089
+ if (!weAreProposer) {
1090
+ this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, { validatorAddresses, proposer });
1091
+ return [false, proposer];
1092
+ }
1093
+
1094
+ return [true, proposer];
1095
+ }
1096
+
1097
+ /**
1098
+ * Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
1099
+ * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
1100
+ */
1101
+ protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
1102
+ const { slot, ts } = args;
1103
+
1104
+ // Prevent duplicate attempts in the same slot
1105
+ if (this.lastSlotForVoteWhenSyncFailed === slot) {
1106
+ this.log.debug(`Already attempted to vote in slot ${slot} (skipping)`);
1107
+ return;
1108
+ }
1109
+
1110
+ // Check if we're past the max time for initializing a proposal
1111
+ const secondsIntoSlot = this.getSecondsIntoSlot(slot);
1112
+ const maxAllowedTime = this.timetable.getMaxAllowedTime(SequencerState.INITIALIZING_PROPOSAL);
1113
+
1114
+ // If we haven't exceeded the time limit for initializing a proposal, don't proceed with voting
1115
+ // We use INITIALIZING_PROPOSAL time limit because if we're past that, we can't build a block anyway
1116
+ if (maxAllowedTime === undefined || secondsIntoSlot <= maxAllowedTime) {
1117
+ this.log.trace(`Not attempting to vote since there is still for block building`, {
1118
+ secondsIntoSlot,
1119
+ maxAllowedTime,
1120
+ });
1121
+ return;
1122
+ }
1123
+
1124
+ this.log.debug(`Sync for slot ${slot} failed, checking for voting opportunities`, {
1125
+ secondsIntoSlot,
1126
+ maxAllowedTime,
1127
+ });
1128
+
1129
+ // Check if we're a proposer or proposal is open
1130
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
1131
+ if (!canPropose) {
1132
+ this.log.debug(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
1133
+ return;
1134
+ }
1135
+
1136
+ // Mark this slot as attempted
1137
+ this.lastSlotForVoteWhenSyncFailed = slot;
1138
+
1139
+ // Get a publisher for voting
1140
+ const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
1141
+
1142
+ this.log.debug(`Attempting to vote despite sync failure at slot ${slot}`, {
1143
+ attestorAddress,
1144
+ slot,
1145
+ });
1146
+
1147
+ // Enqueue governance and slashing votes using the shared helper method
1148
+ const votesPromises = this.enqueueGovernanceAndSlashingVotes(publisher, attestorAddress, slot, ts);
1149
+ await Promise.all(votesPromises);
1150
+
1151
+ if (votesPromises.every(p => !p)) {
1152
+ this.log.debug(`No votes to enqueue for slot ${slot}`);
1153
+ return;
898
1154
  }
1155
+
1156
+ this.log.info(`Voting in slot ${slot} despite sync failure`, { slot });
1157
+ await publisher.sendRequests();
899
1158
  }
900
1159
 
901
1160
  /**
@@ -905,10 +1164,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
905
1164
  * and if they fail, any sequencer will try as well.
906
1165
  */
907
1166
  protected async considerInvalidatingBlock(
908
- syncedTo: NonNullable<Awaited<ReturnType<Sequencer['getChainTip']>>>,
909
- currentSlot: bigint,
910
- ourValidatorAddresses: EthAddress[],
911
- publisher: SequencerPublisher,
1167
+ syncedTo: NonNullable<Awaited<ReturnType<Sequencer['checkSync']>>>,
1168
+ currentSlot: SlotNumber,
912
1169
  ): Promise<void> {
913
1170
  const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
914
1171
  if (pendingChainValidationStatus.valid) {
@@ -918,6 +1175,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
918
1175
  const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
919
1176
  const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
920
1177
  const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
1178
+ const ourValidatorAddresses = this.validatorClient!.getValidatorAddresses();
921
1179
 
922
1180
  const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } =
923
1181
  this.config;
@@ -953,6 +1211,24 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
953
1211
  return;
954
1212
  }
955
1213
 
1214
+ let validatorToUse: EthAddress;
1215
+ if (invalidateAsCommitteeMember) {
1216
+ // When invalidating as a committee member, use first validator that's actually in the committee
1217
+ const { committee } = await this.epochCache.getCommittee(currentSlot);
1218
+ if (committee) {
1219
+ const committeeSet = new Set(committee.map(addr => addr.toString()));
1220
+ validatorToUse =
1221
+ ourValidatorAddresses.find(addr => committeeSet.has(addr.toString())) ?? ourValidatorAddresses[0];
1222
+ } else {
1223
+ validatorToUse = ourValidatorAddresses[0];
1224
+ }
1225
+ } else {
1226
+ // When invalidating as a non-committee member, use the first validator
1227
+ validatorToUse = ourValidatorAddresses[0];
1228
+ }
1229
+
1230
+ const { publisher } = await this.publisherFactory.create(validatorToUse);
1231
+
956
1232
  const invalidateBlock = await publisher.simulateInvalidateBlock(pendingChainValidationStatus);
957
1233
  if (!invalidateBlock) {
958
1234
  this.log.warn(`Failed to simulate invalidate block`, logData);
@@ -967,14 +1243,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
967
1243
  );
968
1244
 
969
1245
  publisher.enqueueInvalidateBlock(invalidateBlock);
970
- await publisher.sendRequests();
1246
+
1247
+ if (!this.config.fishermanMode) {
1248
+ await publisher.sendRequests();
1249
+ } else {
1250
+ this.log.info('Invalidating block in fisherman mode, clearing pending requests');
1251
+ publisher.clearPendingRequests();
1252
+ }
971
1253
  }
972
1254
 
973
- private getSlotStartBuildTimestamp(slotNumber: number | bigint): number {
1255
+ private getSlotStartBuildTimestamp(slotNumber: SlotNumber): number {
974
1256
  return getSlotStartBuildTimestamp(slotNumber, this.l1Constants);
975
1257
  }
976
1258
 
977
- private getSecondsIntoSlot(slotNumber: number | bigint): number {
1259
+ private getSecondsIntoSlot(slotNumber: SlotNumber): number {
978
1260
  const slotStartTimestamp = this.getSlotStartBuildTimestamp(slotNumber);
979
1261
  return Number((this.dateProvider.now() / 1000 - slotStartTimestamp).toFixed(3));
980
1262
  }