@aztec/sequencer-client 3.0.0-canary.a9708bd → 3.0.0-devnet.20251212

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 (73) hide show
  1. package/dest/client/index.d.ts +1 -1
  2. package/dest/client/sequencer-client.d.ts +7 -6
  3. package/dest/client/sequencer-client.d.ts.map +1 -1
  4. package/dest/client/sequencer-client.js +23 -14
  5. package/dest/config.d.ts +5 -3
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +21 -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 +9 -10
  14. package/dest/publisher/config.d.ts.map +1 -1
  15. package/dest/publisher/config.js +19 -17
  16. package/dest/publisher/index.d.ts +2 -2
  17. package/dest/publisher/index.d.ts.map +1 -1
  18. package/dest/publisher/index.js +1 -1
  19. package/dest/publisher/sequencer-publisher-factory.d.ts +9 -3
  20. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  21. package/dest/publisher/sequencer-publisher-factory.js +8 -1
  22. package/dest/publisher/sequencer-publisher-metrics.d.ts +2 -2
  23. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  24. package/dest/publisher/sequencer-publisher-metrics.js +1 -1
  25. package/dest/publisher/sequencer-publisher.d.ts +53 -50
  26. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  27. package/dest/publisher/sequencer-publisher.js +187 -132
  28. package/dest/sequencer/block_builder.d.ts +6 -8
  29. package/dest/sequencer/block_builder.d.ts.map +1 -1
  30. package/dest/sequencer/block_builder.js +18 -9
  31. package/dest/sequencer/config.d.ts +2 -2
  32. package/dest/sequencer/config.d.ts.map +1 -1
  33. package/dest/sequencer/errors.d.ts +11 -0
  34. package/dest/sequencer/errors.d.ts.map +1 -0
  35. package/dest/sequencer/errors.js +15 -0
  36. package/dest/sequencer/index.d.ts +1 -1
  37. package/dest/sequencer/metrics.d.ts +17 -20
  38. package/dest/sequencer/metrics.d.ts.map +1 -1
  39. package/dest/sequencer/metrics.js +59 -87
  40. package/dest/sequencer/sequencer.d.ts +54 -32
  41. package/dest/sequencer/sequencer.d.ts.map +1 -1
  42. package/dest/sequencer/sequencer.js +458 -184
  43. package/dest/sequencer/timetable.d.ts +4 -8
  44. package/dest/sequencer/timetable.d.ts.map +1 -1
  45. package/dest/sequencer/timetable.js +3 -10
  46. package/dest/sequencer/utils.d.ts +11 -25
  47. package/dest/sequencer/utils.d.ts.map +1 -1
  48. package/dest/sequencer/utils.js +9 -24
  49. package/dest/test/index.d.ts +2 -2
  50. package/dest/test/index.d.ts.map +1 -1
  51. package/dest/tx_validator/nullifier_cache.d.ts +1 -1
  52. package/dest/tx_validator/nullifier_cache.d.ts.map +1 -1
  53. package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
  54. package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
  55. package/dest/tx_validator/tx_validator_factory.js +12 -9
  56. package/package.json +32 -31
  57. package/src/client/sequencer-client.ts +24 -18
  58. package/src/config.ts +23 -6
  59. package/src/global_variable_builder/global_builder.ts +19 -17
  60. package/src/publisher/config.ts +29 -27
  61. package/src/publisher/index.ts +1 -1
  62. package/src/publisher/sequencer-publisher-factory.ts +16 -3
  63. package/src/publisher/sequencer-publisher-metrics.ts +1 -1
  64. package/src/publisher/sequencer-publisher.ts +257 -183
  65. package/src/sequencer/block_builder.ts +23 -28
  66. package/src/sequencer/config.ts +1 -1
  67. package/src/sequencer/errors.ts +21 -0
  68. package/src/sequencer/metrics.ts +71 -98
  69. package/src/sequencer/sequencer.ts +546 -237
  70. package/src/sequencer/timetable.ts +10 -14
  71. package/src/sequencer/utils.ts +10 -24
  72. package/src/test/index.ts +1 -1
  73. package/src/tx_validator/tx_validator_factory.ts +13 -7
@@ -1,18 +1,29 @@
1
- import type { L2Block } from '@aztec/aztec.js';
2
- import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
1
+ import { L2Block } from '@aztec/aztec.js/block';
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';
9
+ import { randomInt } from '@aztec/foundation/crypto/random';
10
+ import { Fr } from '@aztec/foundation/curves/bn254';
6
11
  import { EthAddress } from '@aztec/foundation/eth-address';
7
- import { Fr } from '@aztec/foundation/fields';
12
+ import { Signature } from '@aztec/foundation/eth-signature';
8
13
  import { createLogger } from '@aztec/foundation/log';
9
14
  import { RunningPromise } from '@aztec/foundation/running-promise';
10
15
  import { type DateProvider, Timer } from '@aztec/foundation/timer';
11
- import type { TypedEventEmitter } from '@aztec/foundation/types';
16
+ import { type TypedEventEmitter, unfreeze } from '@aztec/foundation/types';
12
17
  import type { P2P } from '@aztec/p2p';
13
18
  import type { SlasherClientInterface } from '@aztec/slasher';
14
- import type { CommitteeAttestation, L2BlockSource, ValidateBlockResult } from '@aztec/stdlib/block';
15
- import { type L1RollupConstants, getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
19
+ import {
20
+ CommitteeAttestation,
21
+ CommitteeAttestationsAndSigners,
22
+ type L2BlockSource,
23
+ MaliciousCommitteeAttestationsAndSigners,
24
+ type ValidateBlockResult,
25
+ } from '@aztec/stdlib/block';
26
+ import { type L1RollupConstants, getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
16
27
  import { Gas } from '@aztec/stdlib/gas';
17
28
  import {
18
29
  type IFullNodeBlockBuilder,
@@ -23,17 +34,11 @@ import {
23
34
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
24
35
  import type { BlockProposalOptions } from '@aztec/stdlib/p2p';
25
36
  import { orderAttestations } from '@aztec/stdlib/p2p';
37
+ import { CheckpointHeader } from '@aztec/stdlib/rollup';
26
38
  import { pickFromSchema } from '@aztec/stdlib/schemas';
27
39
  import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
28
40
  import { MerkleTreeId } from '@aztec/stdlib/trees';
29
- import {
30
- ContentCommitment,
31
- type FailedTx,
32
- GlobalVariables,
33
- ProposedBlockHeader,
34
- Tx,
35
- type TxHash,
36
- } from '@aztec/stdlib/tx';
41
+ import { ContentCommitment, type FailedTx, GlobalVariables, Tx } from '@aztec/stdlib/tx';
37
42
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
38
43
  import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
39
44
  import type { ValidatorClient } from '@aztec/validator-client';
@@ -45,8 +50,9 @@ import type { GlobalVariableBuilder } from '../global_variable_builder/global_bu
45
50
  import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
46
51
  import type { Action, InvalidateBlockRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
47
52
  import type { SequencerConfig } from './config.js';
53
+ import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
48
54
  import { SequencerMetrics } from './metrics.js';
49
- import { SequencerTimetable, SequencerTooSlowError } from './timetable.js';
55
+ import { SequencerTimetable } from './timetable.js';
50
56
  import { SequencerState, type SequencerStateWithSlot } from './utils.js';
51
57
 
52
58
  export { SequencerState };
@@ -58,7 +64,7 @@ export type SequencerEvents = {
58
64
  oldState: SequencerState;
59
65
  newState: SequencerState;
60
66
  secondsIntoSlot?: number;
61
- slotNumber?: bigint;
67
+ slotNumber?: SlotNumber;
62
68
  }) => void;
63
69
  ['proposer-rollup-check-failed']: (args: { reason: string }) => void;
64
70
  ['tx-count-check-failed']: (args: { minTxs: number; availableTxs: number }) => void;
@@ -69,7 +75,7 @@ export type SequencerEvents = {
69
75
  sentActions?: Action[];
70
76
  expiredActions?: Action[];
71
77
  }) => void;
72
- ['block-published']: (args: { blockNumber: number; slot: number }) => void;
78
+ ['block-published']: (args: { blockNumber: BlockNumber; slot: number }) => void;
73
79
  };
74
80
 
75
81
  /**
@@ -96,6 +102,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
96
102
 
97
103
  private governanceProposerPayload: EthAddress | undefined;
98
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
+
99
111
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
100
112
  protected timetable!: SequencerTimetable;
101
113
  protected enforceTimeTable: boolean = false;
@@ -127,15 +139,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
127
139
  ) {
128
140
  super();
129
141
 
130
- // Set an initial coinbase for metrics purposes, but this will potentially change with each block.
131
- const validatorAddresses = this.validatorClient?.getValidatorAddresses() ?? [];
132
- const coinbase =
133
- validatorAddresses.length === 0
134
- ? EthAddress.ZERO
135
- : (this.validatorClient?.getCoinbaseForAttestor(validatorAddresses[0]) ?? EthAddress.ZERO);
136
-
137
- this.metrics = new SequencerMetrics(telemetry, () => this.state, coinbase, this.rollupContract, 'Sequencer');
142
+ // Add [FISHERMAN] prefix to logger if in fisherman mode
143
+ if (this.config.fishermanMode) {
144
+ this.log = log.createChild('[FISHERMAN]');
145
+ }
138
146
 
147
+ this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
139
148
  // Initialize config
140
149
  this.updateConfig(this.config);
141
150
  }
@@ -213,6 +222,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
213
222
  }
214
223
 
215
224
  public async init() {
225
+ // Takes ~3s to precompute some tables.
226
+ getKzg();
216
227
  this.publisher = (await this.publisherFactory.create(undefined)).publisher;
217
228
  }
218
229
 
@@ -220,8 +231,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
220
231
  * Starts the sequencer and moves to IDLE state.
221
232
  */
222
233
  public start() {
223
- this.metrics.start();
224
- 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);
225
235
  this.setState(SequencerState.IDLE, undefined, { force: true });
226
236
  this.runningPromise.start();
227
237
  this.log.info('Started sequencer');
@@ -232,9 +242,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
232
242
  */
233
243
  public async stop(): Promise<void> {
234
244
  this.log.info(`Stopping sequencer`);
235
- this.metrics.stop();
245
+ this.setState(SequencerState.STOPPING, undefined, { force: true });
236
246
  this.publisher?.interrupt();
237
- await this.validatorClient?.stop();
238
247
  await this.runningPromise?.stop();
239
248
  this.setState(SequencerState.STOPPED, undefined, { force: true });
240
249
  this.log.info('Stopped sequencer');
@@ -256,27 +265,28 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
256
265
  * - Submit block
257
266
  * - If our block for some reason is not included, revert the state
258
267
  */
259
- protected async doRealWork() {
268
+ protected async work() {
260
269
  this.setState(SequencerState.SYNCHRONIZING, undefined);
270
+ const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
261
271
 
262
- // Check all components are synced to latest as seen by the archiver
263
- 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
+ }
264
279
 
265
- // 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 });
266
282
  if (!syncedTo) {
283
+ await this.tryVoteWhenSyncFails({ slot, ts });
267
284
  return;
268
285
  }
269
286
 
270
287
  const chainTipArchive = syncedTo.archive;
271
- const newBlockNumber = syncedTo.blockNumber + 1;
288
+ const newBlockNumber = BlockNumber(syncedTo.blockNumber + 1);
272
289
 
273
- const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
274
-
275
- this.setState(SequencerState.PROPOSER_CHECK, slot);
276
-
277
- // Check that the archiver and dependencies have synced to the previous L1 slot at least
278
- // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
279
- // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
280
290
  const syncLogData = {
281
291
  now,
282
292
  syncedToL1Ts: syncedTo.l1Timestamp,
@@ -288,86 +298,70 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
288
298
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
289
299
  };
290
300
 
291
- if (syncedTo.l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
292
- this.log.debug(
293
- `Cannot propose block ${newBlockNumber} at next L2 slot ${slot} due to pending sync from L1`,
294
- syncLogData,
295
- );
296
- return;
297
- }
298
-
299
- // Check that the slot is not taken by a block already
300
- if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
301
- this.log.debug(
302
- `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
303
- { ...syncLogData, block: syncedTo.block.header.toInspect() },
304
- );
305
- return;
306
- }
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);
307
304
 
308
- // Or that we haven't published it ourselves
309
- if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
310
- this.log.debug(
311
- `Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`,
312
- { ...syncLogData, block: this.lastBlockPublished.header.toInspect() },
313
- );
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);
314
308
  return;
315
309
  }
316
310
 
317
- // Check that we are a proposer for the next slot
318
- let proposerInNextSlot: EthAddress | undefined;
319
- try {
320
- proposerInNextSlot = await this.epochCache.getProposerAttesterAddressInNextSlot();
321
- } catch (e) {
322
- if (e instanceof NoCommitteeError) {
323
- this.log.warn(
324
- `Cannot propose block ${newBlockNumber} at next L2 slot ${slot} since the committee does not exist on L1`,
325
- );
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 });
326
315
  return;
327
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;
328
323
  }
329
324
 
330
- // If get proposer in next slot is undefined, then the committee is empty and anyone may propose.
331
- // If the committee is defined and not empty, but none of our validators are the proposer, then stop.
332
- const validatorAddresses = this.validatorClient!.getValidatorAddresses();
333
- if (proposerInNextSlot !== undefined && !validatorAddresses.some(addr => addr.equals(proposerInNextSlot))) {
334
- this.log.debug(`Cannot propose block ${newBlockNumber} since we are not a proposer`, {
335
- us: validatorAddresses,
336
- proposer: proposerInNextSlot,
337
- ...syncLogData,
338
- });
339
- // If the pending chain is invalid, we may need to invalidate the block if no one else is doing it.
340
- if (!syncedTo.pendingChainValidationStatus.valid) {
341
- // We pass i undefined here to get any available publisher.
342
- const { publisher } = await this.publisherFactory.create(undefined);
343
- await this.considerInvalidatingBlock(syncedTo, slot, validatorAddresses, publisher);
344
- }
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');
345
332
  return;
346
333
  }
347
334
 
348
- // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
349
- // if all the previous checks are good, but we do it just in case.
350
- const proposerAddressInNextSlot = proposerInNextSlot ?? EthAddress.ZERO;
351
-
352
335
  // We now need to get ourselves a publisher.
353
336
  // The returned attestor will be the one we provided if we provided one.
354
337
  // Otherwise it will be a valid attestor for the returned publisher.
355
- const { attestorAddress, publisher } = await this.publisherFactory.create(proposerInNextSlot);
356
-
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
+ );
357
342
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
358
-
359
343
  this.publisher = publisher;
360
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
361
354
  const coinbase = this.validatorClient!.getCoinbaseForAttestor(attestorAddress);
362
355
  const feeRecipient = this.validatorClient!.getFeeRecipientForAttestor(attestorAddress);
363
356
 
364
- this.metrics.setCoinbase(coinbase);
365
-
366
357
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
367
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.
368
362
  const canProposeCheck = await publisher.canProposeAtNextEthBlock(
369
363
  chainTipArchive,
370
- proposerAddressInNextSlot,
364
+ proposer ?? EthAddress.ZERO,
371
365
  invalidateBlock,
372
366
  );
373
367
 
@@ -377,6 +371,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
377
371
  syncLogData,
378
372
  );
379
373
  this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed' });
374
+ this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
380
375
  return;
381
376
  } else if (canProposeCheck.slot !== slot) {
382
377
  this.log.warn(
@@ -384,20 +379,19 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
384
379
  { ...syncLogData, rollup: canProposeCheck, newBlockNumber, expectedSlot: slot },
385
380
  );
386
381
  this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch' });
382
+ this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
387
383
  return;
388
- } else if (canProposeCheck.blockNumber !== BigInt(newBlockNumber)) {
384
+ } else if (canProposeCheck.checkpointNumber !== CheckpointNumber.fromBlockNumber(newBlockNumber)) {
389
385
  this.log.warn(
390
- `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}.`,
391
387
  { ...syncLogData, rollup: canProposeCheck, newBlockNumber, expectedSlot: slot },
392
388
  );
393
389
  this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch' });
390
+ this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
394
391
  return;
395
392
  }
396
393
 
397
- this.log.debug(
398
- `Can propose block ${newBlockNumber} at slot ${slot}` + (proposerInNextSlot ? ` as ${proposerInNextSlot}` : ''),
399
- { ...syncLogData, validatorAddresses },
400
- );
394
+ this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot} as ${proposer}`, { ...syncLogData });
401
395
 
402
396
  const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(
403
397
  newBlockNumber,
@@ -406,51 +400,98 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
406
400
  slot,
407
401
  );
408
402
 
409
- const { timestamp } = newGlobalVariables;
410
- const signerFn = (msg: TypedDataDefinition) =>
411
- this.validatorClient!.signWithAddress(attestorAddress, msg).then(s => s.toString());
412
-
413
- const enqueueGovernanceSignalPromise =
414
- this.governanceProposerPayload && !this.governanceProposerPayload.isZero()
415
- ? publisher
416
- .enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn)
417
- .catch(err => {
418
- this.log.error(`Error enqueuing governance vote`, err, { blockNumber: newBlockNumber, slot });
419
- return false;
420
- })
421
- : Promise.resolve(false);
422
-
423
- const enqueueSlashingActionsPromise = this.slasherClient
424
- ? this.slasherClient
425
- .getProposerActions(slot)
426
- .then(actions => publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn))
427
- .catch(err => {
428
- this.log.error(`Error enqueuing slashing actions`, err, { blockNumber: newBlockNumber, slot });
429
- return false;
430
- })
431
- : Promise.resolve(false);
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
+ );
432
411
 
412
+ // Enqueues block invalidation
433
413
  if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
434
414
  publisher.enqueueInvalidateBlock(invalidateBlock);
435
415
  }
436
416
 
417
+ // Actual block building
437
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
+ );
429
+
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
+ ) {
438
481
  this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
439
- proposer: proposerInNextSlot?.toString(),
440
- coinbase,
482
+ proposer,
441
483
  publisher: publisher.getSenderAddress(),
442
- feeRecipient,
443
484
  globalVariables: newGlobalVariables.toInspect(),
444
485
  chainTipArchive,
445
486
  blockNumber: newBlockNumber,
446
487
  slot,
447
488
  });
448
489
 
449
- // If I created a "partial" header here that should make our job much easier.
450
- const proposalHeader = ProposedBlockHeader.from({
490
+ const proposalHeader = CheckpointHeader.from({
451
491
  ...newGlobalVariables,
452
492
  timestamp: newGlobalVariables.timestamp,
453
493
  lastArchiveRoot: chainTipArchive,
494
+ blockHeadersHash: Fr.ZERO,
454
495
  contentCommitment: ContentCommitment.empty(),
455
496
  totalManaUsed: Fr.ZERO,
456
497
  });
@@ -467,7 +508,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
467
508
  pendingTxs,
468
509
  proposalHeader,
469
510
  newGlobalVariables,
470
- proposerInNextSlot,
511
+ proposer,
471
512
  invalidateBlock,
472
513
  publisher,
473
514
  );
@@ -478,6 +519,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
478
519
  } else {
479
520
  this.log.error(`Error building/enqueuing block`, err, { blockNumber: newBlockNumber, slot });
480
521
  }
522
+ this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
481
523
  }
482
524
  } else {
483
525
  this.log.verbose(
@@ -485,27 +527,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
485
527
  { chainTipArchive, blockNumber: newBlockNumber, slot },
486
528
  );
487
529
  this.emit('tx-count-check-failed', { minTxs: this.minTxsPerBlock, availableTxs: pendingTxCount });
530
+ this.metrics.recordBlockProposalFailed('insufficient_txs');
488
531
  }
489
-
490
- await Promise.all([enqueueGovernanceSignalPromise, enqueueSlashingActionsPromise]);
491
-
492
- const l1Response = await publisher.sendRequests();
493
- const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
494
- if (proposedBlock) {
495
- this.lastBlockPublished = block;
496
- this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
497
- this.metrics.incFilledSlot(publisher.getSenderAddress().toString());
498
- } else if (block) {
499
- this.emit('block-publish-failed', l1Response ?? {});
500
- }
501
-
502
- this.setState(SequencerState.IDLE, undefined);
532
+ return block;
503
533
  }
504
534
 
505
535
  @trackSpan('Sequencer.work')
506
- protected async work() {
536
+ protected async safeWork() {
507
537
  try {
508
- await this.doRealWork();
538
+ await this.work();
509
539
  } catch (err) {
510
540
  if (err instanceof SequencerTooSlowError) {
511
541
  // Log as warn only if we had to abort halfway through the block proposal
@@ -528,13 +558,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
528
558
  * @param slotNumber - The current slot number.
529
559
  * @param force - Whether to force the transition even if the sequencer is stopped.
530
560
  */
531
- setState(proposedState: SequencerStateWithSlot, slotNumber: bigint, opts?: { force?: boolean }): void;
561
+ setState(proposedState: SequencerStateWithSlot, slotNumber: SlotNumber, opts?: { force?: boolean }): void;
532
562
  setState(
533
563
  proposedState: Exclude<SequencerState, SequencerStateWithSlot>,
534
564
  slotNumber?: undefined,
535
565
  opts?: { force?: boolean },
536
566
  ): void;
537
- setState(proposedState: SequencerState, slotNumber: bigint | undefined, opts: { force?: boolean } = {}): void {
567
+ setState(proposedState: SequencerState, slotNumber: SlotNumber | undefined, opts: { force?: boolean } = {}): void {
568
+ if (this.state === SequencerState.STOPPING && proposedState !== SequencerState.STOPPED && !opts.force) {
569
+ this.log.warn(`Cannot set sequencer to ${proposedState} as it is stopping.`);
570
+ throw new SequencerInterruptedError();
571
+ }
538
572
  if (this.state === SequencerState.STOPPED && !opts.force) {
539
573
  this.log.warn(`Cannot set sequencer from ${this.state} to ${proposedState} as it is stopped.`);
540
574
  return;
@@ -545,7 +579,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
545
579
  this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
546
580
  }
547
581
 
548
- this.log.debug(`Transitioning from ${this.state} to ${proposedState}`, { slotNumber, secondsIntoSlot });
582
+ const boringStates = [SequencerState.IDLE, SequencerState.SYNCHRONIZING];
583
+ const logLevel =
584
+ boringStates.includes(proposedState) && boringStates.includes(this.state)
585
+ ? ('trace' as const)
586
+ : ('debug' as const);
587
+ this.log[logLevel](`Transitioning from ${this.state} to ${proposedState}`, { slotNumber, secondsIntoSlot });
588
+
549
589
  this.emit('state-changed', {
550
590
  oldState: this.state,
551
591
  newState: proposedState,
@@ -565,7 +605,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
565
605
  await this.p2pClient.deleteTxs(failedTxHashes);
566
606
  }
567
607
 
568
- protected getBlockBuilderOptions(slot: number): PublicProcessorLimits {
608
+ protected getBlockBuilderOptions(slot: SlotNumber): PublicProcessorLimits {
569
609
  // Deadline for processing depends on whether we're proposing a block
570
610
  const secondsIntoSlot = this.getSecondsIntoSlot(slot);
571
611
  const processingEndTimeWithinSlot = this.timetable.getBlockProposalExecTimeEnd(secondsIntoSlot);
@@ -578,6 +618,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
578
618
  maxTransactions: this.maxTxsPerBlock,
579
619
  maxBlockSize: this.maxBlockSizeInBytes,
580
620
  maxBlockGas: this.maxBlockGas,
621
+ maxBlobFields: BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB,
581
622
  deadline,
582
623
  };
583
624
  }
@@ -598,7 +639,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
598
639
  }))
599
640
  private async buildBlockAndEnqueuePublish(
600
641
  pendingTxs: Iterable<Tx> | AsyncIterable<Tx>,
601
- proposalHeader: ProposedBlockHeader,
642
+ proposalHeader: CheckpointHeader,
602
643
  newGlobalVariables: GlobalVariables,
603
644
  proposerAddress: EthAddress | undefined,
604
645
  invalidateBlock: InvalidateBlockRequest | undefined,
@@ -607,15 +648,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
607
648
  await publisher.validateBlockHeader(proposalHeader, invalidateBlock);
608
649
 
609
650
  const blockNumber = newGlobalVariables.blockNumber;
610
- const slot = proposalHeader.slotNumber.toBigInt();
611
- 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);
612
654
 
613
- // this.metrics.recordNewBlock(blockNumber, validTxs.length);
614
655
  const workTimer = new Timer();
615
656
  this.setState(SequencerState.CREATING_BLOCK, slot);
616
657
 
617
658
  try {
618
- const blockBuilderOptions = this.getBlockBuilderOptions(Number(slot));
659
+ const blockBuilderOptions = this.getBlockBuilderOptions(slot);
619
660
  const buildBlockRes = await this.blockBuilder.buildBlock(
620
661
  pendingTxs,
621
662
  l1ToL2Messages,
@@ -638,7 +679,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
638
679
 
639
680
  // TODO(@PhilWindle) We should probably periodically check for things like another
640
681
  // block being published before ours instead of just waiting on our block
641
- await publisher.validateBlockHeader(block.header.toPropose(), invalidateBlock);
682
+ await publisher.validateBlockHeader(block.getCheckpointHeader(), invalidateBlock);
642
683
 
643
684
  const blockStats: L2BlockBuiltStats = {
644
685
  eventName: 'l2-block-built',
@@ -663,13 +704,36 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
663
704
  },
664
705
  );
665
706
 
666
- this.log.debug('Collecting attestations');
667
- const attestations = await this.collectAttestations(block, usedTxs, proposerAddress);
668
- if (attestations !== undefined) {
669
- 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
+ );
670
719
  }
671
720
 
672
- await this.enqueuePublishL2Block(block, attestations, txHashes, invalidateBlock, publisher);
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
+ );
729
+
730
+ await this.enqueuePublishL2Block(
731
+ block,
732
+ attestationsAndSigners,
733
+ attestationsAndSignersSignature,
734
+ invalidateBlock,
735
+ publisher,
736
+ );
673
737
  this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
674
738
  return block;
675
739
  } catch (err) {
@@ -687,8 +751,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
687
751
  block: L2Block,
688
752
  txs: Tx[],
689
753
  proposerAddress: EthAddress | undefined,
690
- ): Promise<CommitteeAttestation[] | undefined> {
691
- const { committee } = await this.epochCache.getCommittee(block.header.getSlot());
754
+ ): Promise<CommitteeAttestationsAndSigners> {
755
+ const { committee, seed, epoch } = await this.epochCache.getCommittee(block.slot);
692
756
 
693
757
  // We checked above that the committee is defined, so this should never happen.
694
758
  if (!committee) {
@@ -697,29 +761,29 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
697
761
 
698
762
  if (committee.length === 0) {
699
763
  this.log.verbose(`Attesting committee is empty`);
700
- return undefined;
764
+ return CommitteeAttestationsAndSigners.empty();
701
765
  } else {
702
766
  this.log.debug(`Attesting committee length is ${committee.length}`);
703
767
  }
704
768
 
705
769
  if (!this.validatorClient) {
706
- const msg = 'Missing validator client: Cannot collect attestations';
707
- this.log.error(msg);
708
- throw new Error(msg);
770
+ throw new Error('Missing validator client: Cannot collect attestations');
709
771
  }
710
772
 
711
773
  const numberOfRequiredAttestations = Math.floor((committee.length * 2) / 3) + 1;
712
774
 
713
- const slotNumber = block.header.globalVariables.slotNumber.toBigInt();
775
+ const slotNumber = block.header.globalVariables.slotNumber;
714
776
  this.setState(SequencerState.COLLECTING_ATTESTATIONS, slotNumber);
715
777
 
716
778
  this.log.debug('Creating block proposal for validators');
717
- const blockProposalOptions: BlockProposalOptions = { publishFullTxs: !!this.config.publishTxsWithProposals };
779
+ const blockProposalOptions: BlockProposalOptions = {
780
+ publishFullTxs: !!this.config.publishTxsWithProposals,
781
+ broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
782
+ };
718
783
  const proposal = await this.validatorClient.createBlockProposal(
719
784
  block.header.globalVariables.blockNumber,
720
- block.header.toPropose(),
785
+ block.getCheckpointHeader(),
721
786
  block.archive.root,
722
- block.header.state,
723
787
  txs,
724
788
  proposerAddress,
725
789
  blockProposalOptions,
@@ -732,7 +796,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
732
796
  if (this.config.skipCollectingAttestations) {
733
797
  this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
734
798
  const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
735
- return orderAttestations(attestations ?? [], committee);
799
+ return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
736
800
  }
737
801
 
738
802
  this.log.debug('Broadcasting block proposal to validators');
@@ -745,7 +809,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
745
809
  this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
746
810
 
747
811
  const timer = new Timer();
748
- let collectedAttestionsCount: number = 0;
812
+ let collectedAttestationsCount: number = 0;
749
813
  try {
750
814
  const attestationDeadline = new Date(this.dateProvider.now() + attestationTimeAllowed * 1000);
751
815
  const attestations = await this.validatorClient.collectAttestations(
@@ -754,20 +818,74 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
754
818
  attestationDeadline,
755
819
  );
756
820
 
757
- collectedAttestionsCount = attestations.length;
821
+ collectedAttestationsCount = attestations.length;
758
822
 
759
823
  // note: the smart contract requires that the signatures are provided in the order of the committee
760
- return orderAttestations(attestations, committee);
824
+ const sorted = orderAttestations(attestations, committee);
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);
829
+ }
830
+
831
+ return new CommitteeAttestationsAndSigners(sorted);
761
832
  } catch (err) {
762
833
  if (err && err instanceof AttestationTimeoutError) {
763
- collectedAttestionsCount = err.collectedCount;
834
+ collectedAttestationsCount = err.collectedCount;
764
835
  }
765
836
  throw err;
766
837
  } finally {
767
- this.metrics.recordCollectedAttestations(collectedAttestionsCount, timer.ms());
838
+ this.metrics.recordCollectedAttestations(collectedAttestationsCount, timer.ms());
768
839
  }
769
840
  }
770
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
+
771
889
  /**
772
890
  * Publishes the L2Block to the rollup contract.
773
891
  * @param block - The L2Block to be published.
@@ -777,22 +895,27 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
777
895
  }))
778
896
  protected async enqueuePublishL2Block(
779
897
  block: L2Block,
780
- attestations: CommitteeAttestation[] | undefined,
781
- txHashes: TxHash[],
898
+ attestationsAndSigners: CommitteeAttestationsAndSigners,
899
+ attestationsAndSignersSignature: Signature,
782
900
  invalidateBlock: InvalidateBlockRequest | undefined,
783
901
  publisher: SequencerPublisher,
784
902
  ): Promise<void> {
785
903
  // Publishes new block to the network and awaits the tx to be mined
786
- this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber.toBigInt());
904
+ this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber);
787
905
 
788
906
  // Time out tx at the end of the slot
789
- const slot = block.header.globalVariables.slotNumber.toNumber();
907
+ const slot = block.header.globalVariables.slotNumber;
790
908
  const txTimeoutAt = new Date((this.getSlotStartBuildTimestamp(slot) + this.aztecSlotDuration) * 1000);
791
909
 
792
- const enqueued = await publisher.enqueueProposeL2Block(block, attestations, txHashes, {
793
- txTimeoutAt,
794
- forcePendingBlockNumber: invalidateBlock?.forcePendingBlockNumber,
795
- });
910
+ const enqueued = await publisher.enqueueProposeL2Block(
911
+ block,
912
+ attestationsAndSigners,
913
+ attestationsAndSignersSignature,
914
+ {
915
+ txTimeoutAt,
916
+ forcePendingBlockNumber: invalidateBlock?.forcePendingBlockNumber,
917
+ },
918
+ );
796
919
 
797
920
  if (!enqueued) {
798
921
  throw new Error(`Failed to enqueue publish of block ${block.number}`);
@@ -802,18 +925,31 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
802
925
  /**
803
926
  * Returns whether all dependencies have caught up.
804
927
  * We don't check against the previous block submitted since it may have been reorg'd out.
805
- * @returns Boolean indicating if our dependencies are synced to the latest block.
806
928
  */
807
- protected async getChainTip(): Promise<
929
+ protected async checkSync(args: { ts: bigint; slot: SlotNumber }): Promise<
808
930
  | {
809
931
  block?: L2Block;
810
- blockNumber: number;
932
+ blockNumber: BlockNumber;
811
933
  archive: Fr;
812
934
  l1Timestamp: bigint;
813
935
  pendingChainValidationStatus: ValidateBlockResult;
814
936
  }
815
937
  | undefined
816
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
+
817
953
  const syncedBlocks = await Promise.all([
818
954
  this.worldState.status().then(({ syncSummary }) => ({
819
955
  number: syncSummary.latestBlockNumber,
@@ -822,49 +958,203 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
822
958
  this.l2BlockSource.getL2Tips().then(t => t.latest),
823
959
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
824
960
  this.l1ToL2MessageSource.getL2Tips().then(t => t.latest),
825
- this.l2BlockSource.getL1Timestamp(),
826
961
  this.l2BlockSource.getPendingChainValidationStatus(),
827
962
  ] as const);
828
963
 
829
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, l1Timestamp, pendingChainValidationStatus] =
830
- syncedBlocks;
964
+ const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
831
965
 
832
- // The archiver reports 'undefined' hash for the genesis block
833
- // 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.
834
968
  const result =
835
- l2BlockSource.hash === undefined
836
- ? worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0
837
- : worldState.hash === l2BlockSource.hash &&
838
- p2p.hash === l2BlockSource.hash &&
839
- l1ToL2MessageSource.hash === l2BlockSource.hash;
840
-
841
- const logData = { worldState, l2BlockSource, p2p, l1ToL2MessageSource };
842
- 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);
843
973
 
844
974
  if (!result) {
975
+ this.log.debug(`Sequencer sync check failed`, { worldState, l2BlockSource, p2p, l1ToL2MessageSource });
845
976
  return undefined;
846
977
  }
847
978
 
979
+ // Special case for genesis state
848
980
  const blockNumber = worldState.number;
849
- if (blockNumber >= INITIAL_L2_BLOCK_NUM) {
850
- const block = await this.l2BlockSource.getBlock(blockNumber);
851
- if (!block) {
852
- // this shouldn't really happen because a moment ago we checked that all components were in sync
853
- this.log.warn(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`, logData);
854
- 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];
855
1072
  }
1073
+ this.log.error(`Error getting proposer for slot ${slot}`, e);
1074
+ return [false, undefined];
1075
+ }
856
1076
 
857
- return {
858
- block,
859
- blockNumber: block.number,
860
- archive: block.archive.root,
861
- l1Timestamp,
862
- pendingChainValidationStatus,
863
- };
864
- } else {
865
- const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
866
- 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;
867
1154
  }
1155
+
1156
+ this.log.info(`Voting in slot ${slot} despite sync failure`, { slot });
1157
+ await publisher.sendRequests();
868
1158
  }
869
1159
 
870
1160
  /**
@@ -874,27 +1164,26 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
874
1164
  * and if they fail, any sequencer will try as well.
875
1165
  */
876
1166
  protected async considerInvalidatingBlock(
877
- syncedTo: NonNullable<Awaited<ReturnType<Sequencer['getChainTip']>>>,
878
- currentSlot: bigint,
879
- ourValidatorAddresses: EthAddress[],
880
- publisher: SequencerPublisher,
1167
+ syncedTo: NonNullable<Awaited<ReturnType<Sequencer['checkSync']>>>,
1168
+ currentSlot: SlotNumber,
881
1169
  ): Promise<void> {
882
1170
  const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
883
1171
  if (pendingChainValidationStatus.valid) {
884
1172
  return;
885
1173
  }
886
1174
 
887
- const invalidL1Timestamp = pendingChainValidationStatus.block.l1.timestamp;
888
- const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidL1Timestamp);
889
- const invalidBlockNumber = pendingChainValidationStatus.block.block.number;
1175
+ const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
1176
+ const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
1177
+ const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
1178
+ const ourValidatorAddresses = this.validatorClient!.getValidatorAddresses();
890
1179
 
891
1180
  const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } =
892
1181
  this.config;
893
1182
 
894
1183
  const logData = {
895
- invalidL1Timestamp,
1184
+ invalidL1Timestamp: invalidBlockTimestamp,
896
1185
  l1Timestamp,
897
- invalidBlock: pendingChainValidationStatus.block.block.toBlockInfo(),
1186
+ invalidBlock: pendingChainValidationStatus.block,
898
1187
  secondsBeforeInvalidatingBlockAsCommitteeMember,
899
1188
  secondsBeforeInvalidatingBlockAsNonCommitteeMember,
900
1189
  ourValidatorAddresses,
@@ -922,6 +1211,24 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
922
1211
  return;
923
1212
  }
924
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
+
925
1232
  const invalidateBlock = await publisher.simulateInvalidateBlock(pendingChainValidationStatus);
926
1233
  if (!invalidateBlock) {
927
1234
  this.log.warn(`Failed to simulate invalidate block`, logData);
@@ -936,18 +1243,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
936
1243
  );
937
1244
 
938
1245
  publisher.enqueueInvalidateBlock(invalidateBlock);
939
- 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
+ }
940
1253
  }
941
1254
 
942
- private getSlotStartBuildTimestamp(slotNumber: number | bigint): number {
943
- return (
944
- Number(this.l1Constants.l1GenesisTime) +
945
- Number(slotNumber) * this.l1Constants.slotDuration -
946
- this.l1Constants.ethereumSlotDuration
947
- );
1255
+ private getSlotStartBuildTimestamp(slotNumber: SlotNumber): number {
1256
+ return getSlotStartBuildTimestamp(slotNumber, this.l1Constants);
948
1257
  }
949
1258
 
950
- private getSecondsIntoSlot(slotNumber: number | bigint): number {
1259
+ private getSecondsIntoSlot(slotNumber: SlotNumber): number {
951
1260
  const slotStartTimestamp = this.getSlotStartBuildTimestamp(slotNumber);
952
1261
  return Number((this.dateProvider.now() / 1000 - slotStartTimestamp).toFixed(3));
953
1262
  }