@aztec/sequencer-client 1.2.1 → 2.0.0-nightly.20250814

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.
@@ -1,8 +1,7 @@
1
1
  import type { L2Block } from '@aztec/aztec.js';
2
2
  import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
3
3
  import { FormattedViemError, NoCommitteeError, type ViemPublicClient } from '@aztec/ethereum';
4
- import { Buffer32 } from '@aztec/foundation/buffer';
5
- import { omit } from '@aztec/foundation/collection';
4
+ import { omit, pick } from '@aztec/foundation/collection';
6
5
  import { EthAddress } from '@aztec/foundation/eth-address';
7
6
  import { Fr } from '@aztec/foundation/fields';
8
7
  import { createLogger } from '@aztec/foundation/log';
@@ -12,7 +11,7 @@ import type { TypedEventEmitter } from '@aztec/foundation/types';
12
11
  import type { P2P } from '@aztec/p2p';
13
12
  import type { SlasherClient } from '@aztec/slasher';
14
13
  import { AztecAddress } from '@aztec/stdlib/aztec-address';
15
- import type { CommitteeAttestation, L2BlockSource } from '@aztec/stdlib/block';
14
+ import type { CommitteeAttestation, L2BlockSource, ValidateBlockResult } from '@aztec/stdlib/block';
16
15
  import { type L1RollupConstants, getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
17
16
  import { Gas } from '@aztec/stdlib/gas';
18
17
  import {
@@ -23,6 +22,7 @@ import {
23
22
  } from '@aztec/stdlib/interfaces/server';
24
23
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
25
24
  import type { BlockProposalOptions } from '@aztec/stdlib/p2p';
25
+ import { orderAttestations } from '@aztec/stdlib/p2p';
26
26
  import { pickFromSchema } from '@aztec/stdlib/schemas';
27
27
  import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
28
28
  import { MerkleTreeId } from '@aztec/stdlib/trees';
@@ -48,22 +48,37 @@ import type { ValidatorClient } from '@aztec/validator-client';
48
48
  import EventEmitter from 'node:events';
49
49
 
50
50
  import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
51
- import { type Action, type SequencerPublisher, VoteType } from '../publisher/sequencer-publisher.js';
51
+ import {
52
+ type Action,
53
+ type InvalidateBlockRequest,
54
+ type SequencerPublisher,
55
+ SignalType,
56
+ } from '../publisher/sequencer-publisher.js';
52
57
  import type { SequencerConfig } from './config.js';
53
58
  import { SequencerMetrics } from './metrics.js';
54
59
  import { SequencerTimetable, SequencerTooSlowError } from './timetable.js';
55
- import { SequencerState, orderAttestations } from './utils.js';
60
+ import { SequencerState, type SequencerStateWithSlot } from './utils.js';
56
61
 
57
62
  export { SequencerState };
58
63
 
59
64
  type SequencerRollupConstants = Pick<L1RollupConstants, 'ethereumSlotDuration' | 'l1GenesisTime' | 'slotDuration'>;
60
65
 
61
66
  export type SequencerEvents = {
62
- ['state-changed']: (args: { oldState: SequencerState; newState: SequencerState }) => void;
67
+ ['state-changed']: (args: {
68
+ oldState: SequencerState;
69
+ newState: SequencerState;
70
+ secondsIntoSlot?: number;
71
+ slotNumber?: bigint;
72
+ }) => void;
63
73
  ['proposer-rollup-check-failed']: (args: { reason: string }) => void;
64
74
  ['tx-count-check-failed']: (args: { minTxs: number; availableTxs: number }) => void;
65
75
  ['block-build-failed']: (args: { reason: string }) => void;
66
- ['block-publish-failed']: (args: { validActions?: Action[]; expiredActions?: Action[] }) => void;
76
+ ['block-publish-failed']: (args: {
77
+ successfulActions?: Action[];
78
+ failedActions?: Action[];
79
+ sentActions?: Action[];
80
+ expiredActions?: Action[];
81
+ }) => void;
67
82
  ['block-published']: (args: { blockNumber: number; slot: number }) => void;
68
83
  };
69
84
 
@@ -91,7 +106,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
91
106
  private metrics: SequencerMetrics;
92
107
  private l1Metrics: L1Metrics;
93
108
  private lastBlockPublished: L2Block | undefined;
94
- private isFlushing: boolean = false;
95
109
 
96
110
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
97
111
  protected timetable!: SequencerTimetable;
@@ -130,6 +144,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
130
144
 
131
145
  // Register the slasher on the publisher to fetch slashing payloads
132
146
  this.publisher.registerSlashPayloadGetter(this.slasherClient.getSlashPayload.bind(this.slasherClient));
147
+
148
+ // Initialize config
149
+ this.updateConfig(this.config);
133
150
  }
134
151
 
135
152
  get tracer(): Tracer {
@@ -140,6 +157,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
140
157
  return this.validatorClient?.getValidatorAddresses();
141
158
  }
142
159
 
160
+ public getConfig() {
161
+ return this.config;
162
+ }
163
+
143
164
  /**
144
165
  * Updates sequencer config by the defined values in the config on input.
145
166
  * @param config - New parameters.
@@ -195,24 +216,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
195
216
 
196
217
  private setTimeTable() {
197
218
  this.timetable = new SequencerTimetable(
198
- this.l1Constants.ethereumSlotDuration,
199
- this.aztecSlotDuration,
200
- this.maxL1TxInclusionTimeIntoSlot,
201
- this.enforceTimeTable,
219
+ {
220
+ ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
221
+ aztecSlotDuration: this.aztecSlotDuration,
222
+ maxL1TxInclusionTimeIntoSlot: this.maxL1TxInclusionTimeIntoSlot,
223
+ attestationPropagationTime: this.config.attestationPropagationTime,
224
+ enforce: this.enforceTimeTable,
225
+ },
202
226
  this.metrics,
203
227
  this.log,
204
228
  );
205
- this.log.verbose(`Sequencer timetable updated`, { enforceTimeTable: this.enforceTimeTable });
206
229
  }
207
230
 
208
231
  /**
209
232
  * Starts the sequencer and moves to IDLE state.
210
233
  */
211
234
  public start() {
212
- this.updateConfig(this.config);
213
235
  this.metrics.start();
214
236
  this.runningPromise = new RunningPromise(this.work.bind(this), this.log, this.pollingIntervalMs);
215
- this.setState(SequencerState.IDLE, 0n, true /** force */);
237
+ this.setState(SequencerState.IDLE, undefined, { force: true });
216
238
  this.runningPromise.start();
217
239
  this.l1Metrics.start();
218
240
  this.log.info(`Sequencer started with address ${this.publisher.getSenderAddress().toString()}`);
@@ -222,12 +244,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
222
244
  * Stops the sequencer from processing txs and moves to STOPPED state.
223
245
  */
224
246
  public async stop(): Promise<void> {
225
- this.log.debug(`Stopping sequencer`);
247
+ this.log.info(`Stopping sequencer`);
226
248
  this.metrics.stop();
227
249
  await this.validatorClient?.stop();
228
250
  await this.runningPromise?.stop();
229
251
  this.publisher.interrupt();
230
- this.setState(SequencerState.STOPPED, 0n, true /** force */);
252
+ this.setState(SequencerState.STOPPED, undefined, { force: true });
231
253
  this.l1Metrics.stop();
232
254
  this.log.info('Stopped sequencer');
233
255
  }
@@ -239,7 +261,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
239
261
  this.log.info('Restarting sequencer');
240
262
  this.publisher.restart();
241
263
  this.runningPromise!.start();
242
- this.setState(SequencerState.IDLE, 0n, true /** force */);
264
+ this.setState(SequencerState.IDLE, undefined, { force: true });
243
265
  }
244
266
 
245
267
  /**
@@ -250,11 +272,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
250
272
  return { state: this.state };
251
273
  }
252
274
 
253
- /** Forces the sequencer to bypass all time and tx count checks for the next block and build anyway. */
254
- public flush() {
255
- this.isFlushing = true;
256
- }
257
-
258
275
  /**
259
276
  * @notice Performs most of the sequencer duties:
260
277
  * - Checks if we are up to date
@@ -264,7 +281,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
264
281
  * - If our block for some reason is not included, revert the state
265
282
  */
266
283
  protected async doRealWork() {
267
- this.setState(SequencerState.SYNCHRONIZING, 0n);
284
+ this.setState(SequencerState.SYNCHRONIZING, undefined);
268
285
 
269
286
  // Check all components are synced to latest as seen by the archiver
270
287
  const syncedTo = await this.getChainTip();
@@ -274,7 +291,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
274
291
  return;
275
292
  }
276
293
 
277
- this.setState(SequencerState.PROPOSER_CHECK, 0n);
294
+ this.setState(SequencerState.PROPOSER_CHECK, undefined);
278
295
 
279
296
  const chainTipArchive = syncedTo.archive;
280
297
  const newBlockNumber = syncedTo.blockNumber + 1;
@@ -292,6 +309,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
292
309
  nextL2Slot: slot,
293
310
  nextL2SlotTs: ts,
294
311
  l1SlotDuration: this.l1Constants.ethereumSlotDuration,
312
+ newBlockNumber,
313
+ isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
295
314
  };
296
315
 
297
316
  if (syncedTo.l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
@@ -320,35 +339,47 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
320
339
  return;
321
340
  }
322
341
 
342
+ // Check that we are a proposer for the next slot
323
343
  let proposerInNextSlot: EthAddress | undefined;
324
344
  try {
325
- // Check that we are a proposer for the next slot
326
345
  proposerInNextSlot = await this.publisher.epochCache.getProposerAttesterAddressInNextSlot();
327
346
  } catch (e) {
328
347
  if (e instanceof NoCommitteeError) {
329
- this.log.warn(`Cannot propose block ${newBlockNumber} since the committee does not exist on L1`);
348
+ this.log.warn(
349
+ `Cannot propose block ${newBlockNumber} at next L2 slot ${slot} since the committee does not exist on L1`,
350
+ );
330
351
  return;
331
352
  }
332
353
  }
333
- const validatorAddresses = this.validatorClient!.getValidatorAddresses();
334
354
 
335
355
  // If get proposer in next slot is undefined, then the committee is empty and anyone may propose.
336
- // If the committee is defined and not empty, but none of our validators are the proposer,
337
- // then stop.
356
+ // If the committee is defined and not empty, but none of our validators are the proposer, then stop.
357
+ const validatorAddresses = this.validatorClient!.getValidatorAddresses();
338
358
  if (proposerInNextSlot !== undefined && !validatorAddresses.some(addr => addr.equals(proposerInNextSlot))) {
339
359
  this.log.debug(`Cannot propose block ${newBlockNumber} since we are not a proposer`, {
340
360
  us: validatorAddresses,
341
361
  proposer: proposerInNextSlot,
342
362
  ...syncLogData,
343
363
  });
364
+ // If the pending chain is invalid, we may need to invalidate the block if no one else is doing it.
365
+ if (!syncedTo.pendingChainValidationStatus.valid) {
366
+ await this.considerInvalidatingBlock(syncedTo, slot, validatorAddresses);
367
+ }
344
368
  return;
345
369
  }
346
370
 
347
- // Double check we are good for proposing at the next block before we start operations.
348
- // We should never fail this check assuming the logic above is good.
371
+ // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
372
+ const invalidateBlock = await this.publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
373
+
374
+ // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
375
+ // if all the previous checks are good, but we do it just in case.
349
376
  const proposerAddress = proposerInNextSlot ?? EthAddress.ZERO;
377
+ const canProposeCheck = await this.publisher.canProposeAtNextEthBlock(
378
+ chainTipArchive,
379
+ proposerAddress,
380
+ invalidateBlock,
381
+ );
350
382
 
351
- const canProposeCheck = await this.publisher.canProposeAtNextEthBlock(chainTipArchive.toBuffer(), proposerAddress);
352
383
  if (canProposeCheck === undefined) {
353
384
  this.log.warn(
354
385
  `Cannot propose block ${newBlockNumber} at slot ${slot} due to failed rollup contract check`,
@@ -373,7 +404,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
373
404
  }
374
405
 
375
406
  this.log.debug(
376
- `${proposerInNextSlot ? `Validator ${proposerInNextSlot.toString()} can` : 'Can'} propose block ${newBlockNumber} at slot ${slot}`,
407
+ `Can propose block ${newBlockNumber} at slot ${slot}` + (proposerInNextSlot ? ` as ${proposerInNextSlot}` : ''),
377
408
  { ...syncLogData, validatorAddresses },
378
409
  );
379
410
 
@@ -384,22 +415,26 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
384
415
  slot,
385
416
  );
386
417
 
387
- const enqueueGovernanceVotePromise = this.publisher.enqueueCastVote(
418
+ const enqueueGovernanceVotePromise = this.publisher.enqueueCastSignal(
388
419
  slot,
389
420
  newGlobalVariables.timestamp,
390
- VoteType.GOVERNANCE,
421
+ SignalType.GOVERNANCE,
391
422
  proposerAddress,
392
- msg => this.validatorClient!.signWithAddress(proposerAddress, Buffer32.fromString(msg)).then(s => s.toString()),
423
+ msg => this.validatorClient!.signWithAddress(proposerAddress, msg).then(s => s.toString()),
393
424
  );
394
425
 
395
- const enqueueSlashingVotePromise = this.publisher.enqueueCastVote(
426
+ const enqueueSlashingVotePromise = this.publisher.enqueueCastSignal(
396
427
  slot,
397
428
  newGlobalVariables.timestamp,
398
- VoteType.SLASHING,
429
+ SignalType.SLASHING,
399
430
  proposerAddress,
400
- msg => this.validatorClient!.signWithAddress(proposerAddress, Buffer32.fromString(msg)).then(s => s.toString()),
431
+ msg => this.validatorClient!.signWithAddress(proposerAddress, msg).then(s => s.toString()),
401
432
  );
402
433
 
434
+ if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
435
+ this.publisher.enqueueInvalidateBlock(invalidateBlock);
436
+ }
437
+
403
438
  this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
404
439
  this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
405
440
  proposer: proposerInNextSlot?.toString(),
@@ -418,11 +453,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
418
453
  totalManaUsed: Fr.ZERO,
419
454
  });
420
455
 
421
- let finishedFlushing = false;
422
456
  let block: L2Block | undefined;
423
457
 
424
458
  const pendingTxCount = await this.p2pClient.getPendingTxCount();
425
- if (pendingTxCount >= this.minTxsPerBlock || this.isFlushing) {
459
+ if (pendingTxCount >= this.minTxsPerBlock) {
426
460
  // We don't fetch exactly maxTxsPerBlock txs here because we may not need all of them if we hit a limit before,
427
461
  // and also we may need to fetch more if we don't have enough valid txs.
428
462
  const pendingTxs = this.p2pClient.iteratePendingTxs();
@@ -432,6 +466,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
432
466
  proposalHeader,
433
467
  newGlobalVariables,
434
468
  proposerInNextSlot,
469
+ invalidateBlock,
435
470
  );
436
471
  } catch (err: any) {
437
472
  this.emit('block-build-failed', { reason: err.message });
@@ -440,8 +475,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
440
475
  } else {
441
476
  this.log.error(`Error building/enqueuing block`, err, { blockNumber: newBlockNumber, slot });
442
477
  }
443
- } finally {
444
- finishedFlushing = true;
445
478
  }
446
479
  } else {
447
480
  this.log.verbose(
@@ -459,22 +492,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
459
492
  });
460
493
 
461
494
  const l1Response = await this.publisher.sendRequests();
462
- const proposedBlock = l1Response?.validActions.find(a => a === 'propose');
495
+ const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
463
496
  if (proposedBlock) {
464
497
  this.lastBlockPublished = block;
465
498
  this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
466
499
  this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString());
467
- if (finishedFlushing) {
468
- this.isFlushing = false;
469
- }
470
500
  } else if (block) {
471
- this.emit('block-publish-failed', {
472
- validActions: l1Response?.validActions,
473
- expiredActions: l1Response?.expiredActions,
474
- });
501
+ this.emit('block-publish-failed', l1Response ?? {});
475
502
  }
476
503
 
477
- this.setState(SequencerState.IDLE, 0n);
504
+ this.setState(SequencerState.IDLE, undefined);
478
505
  }
479
506
 
480
507
  @trackSpan('Sequencer.work')
@@ -489,28 +516,40 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
489
516
  throw err;
490
517
  }
491
518
  } finally {
492
- this.setState(SequencerState.IDLE, 0n);
519
+ this.setState(SequencerState.IDLE, undefined);
493
520
  }
494
521
  }
495
522
 
496
523
  /**
497
524
  * Sets the sequencer state and checks if we have enough time left in the slot to transition to the new state.
498
525
  * @param proposedState - The new state to transition to.
499
- * @param currentSlotNumber - The current slot number.
526
+ * @param slotNumber - The current slot number.
500
527
  * @param force - Whether to force the transition even if the sequencer is stopped.
501
- *
502
- * @dev If the `currentSlotNumber` doesn't matter (e.g. transitioning to IDLE), pass in `0n`;
503
- * it is only used to check if we have enough time left in the slot to transition to the new state.
504
528
  */
505
- setState(proposedState: SequencerState, currentSlotNumber: bigint, force: boolean = false) {
506
- if (this.state === SequencerState.STOPPED && force !== true) {
529
+ setState(proposedState: SequencerStateWithSlot, slotNumber: bigint, opts?: { force?: boolean }): void;
530
+ setState(
531
+ proposedState: Exclude<SequencerState, SequencerStateWithSlot>,
532
+ slotNumber?: undefined,
533
+ opts?: { force?: boolean },
534
+ ): void;
535
+ setState(proposedState: SequencerState, slotNumber: bigint | undefined, opts: { force?: boolean } = {}): void {
536
+ if (this.state === SequencerState.STOPPED && !opts.force) {
507
537
  this.log.warn(`Cannot set sequencer from ${this.state} to ${proposedState} as it is stopped.`);
508
538
  return;
509
539
  }
510
- const secondsIntoSlot = this.getSecondsIntoSlot(currentSlotNumber);
511
- this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
512
- this.log.debug(`Transitioning from ${this.state} to ${proposedState}`);
513
- this.emit('state-changed', { oldState: this.state, newState: proposedState });
540
+ let secondsIntoSlot = undefined;
541
+ if (slotNumber !== undefined) {
542
+ secondsIntoSlot = this.getSecondsIntoSlot(slotNumber);
543
+ this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
544
+ }
545
+
546
+ this.log.debug(`Transitioning from ${this.state} to ${proposedState}`, { slotNumber, secondsIntoSlot });
547
+ this.emit('state-changed', {
548
+ oldState: this.state,
549
+ newState: proposedState,
550
+ secondsIntoSlot,
551
+ slotNumber,
552
+ });
514
553
  this.state = proposedState;
515
554
  }
516
555
 
@@ -519,19 +558,19 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
519
558
  return;
520
559
  }
521
560
  const failedTxData = failedTxs.map(fail => fail.tx);
522
- const failedTxHashes = await Tx.getHashes(failedTxData);
561
+ const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
523
562
  this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
524
563
  await this.p2pClient.deleteTxs(failedTxHashes);
525
564
  }
526
565
 
527
- protected getDefaultBlockBuilderOptions(slot: number): PublicProcessorLimits {
566
+ protected getBlockBuilderOptions(slot: number): PublicProcessorLimits {
528
567
  // Deadline for processing depends on whether we're proposing a block
529
568
  const secondsIntoSlot = this.getSecondsIntoSlot(slot);
530
569
  const processingEndTimeWithinSlot = this.timetable.getBlockProposalExecTimeEnd(secondsIntoSlot);
531
570
 
532
571
  // Deadline is only set if enforceTimeTable is enabled.
533
572
  const deadline = this.enforceTimeTable
534
- ? new Date((this.getSlotStartTimestamp(slot) + processingEndTimeWithinSlot) * 1000)
573
+ ? new Date((this.getSlotStartBuildTimestamp(slot) + processingEndTimeWithinSlot) * 1000)
535
574
  : undefined;
536
575
  return {
537
576
  maxTransactions: this.maxTxsPerBlock,
@@ -560,8 +599,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
560
599
  proposalHeader: ProposedBlockHeader,
561
600
  newGlobalVariables: GlobalVariables,
562
601
  proposerAddress: EthAddress | undefined,
602
+ invalidateBlock: InvalidateBlockRequest | undefined,
563
603
  ): Promise<L2Block> {
564
- await this.publisher.validateBlockHeader(proposalHeader);
604
+ await this.publisher.validateBlockHeader(proposalHeader, invalidateBlock);
565
605
 
566
606
  const blockNumber = newGlobalVariables.blockNumber;
567
607
  const slot = proposalHeader.slotNumber.toBigInt();
@@ -572,7 +612,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
572
612
  this.setState(SequencerState.CREATING_BLOCK, slot);
573
613
 
574
614
  try {
575
- const blockBuilderOptions = this.getDefaultBlockBuilderOptions(Number(slot));
615
+ const blockBuilderOptions = this.getBlockBuilderOptions(Number(slot));
576
616
  const buildBlockRes = await this.blockBuilder.buildBlock(
577
617
  pendingTxs,
578
618
  l1ToL2Messages,
@@ -584,8 +624,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
584
624
  const blockBuildDuration = workTimer.ms();
585
625
  await this.dropFailedTxsFromP2P(failedTxs);
586
626
 
587
- const minTxsPerBlock = this.isFlushing ? 0 : this.minTxsPerBlock;
588
-
627
+ const minTxsPerBlock = this.minTxsPerBlock;
589
628
  if (numTxs < minTxsPerBlock) {
590
629
  this.log.warn(
591
630
  `Block ${blockNumber} has too few txs to be proposed (got ${numTxs} but required ${minTxsPerBlock})`,
@@ -596,7 +635,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
596
635
 
597
636
  // TODO(@PhilWindle) We should probably periodically check for things like another
598
637
  // block being published before ours instead of just waiting on our block
599
- await this.publisher.validateBlockHeader(block.header.toPropose());
638
+ await this.publisher.validateBlockHeader(block.header.toPropose(), invalidateBlock);
600
639
 
601
640
  const blockStats: L2BlockBuiltStats = {
602
641
  eventName: 'l2-block-built',
@@ -627,7 +666,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
627
666
  this.log.verbose(`Collected ${attestations.length} attestations`, { blockHash, blockNumber });
628
667
  }
629
668
 
630
- await this.enqueuePublishL2Block(block, attestations, txHashes);
669
+ await this.enqueuePublishL2Block(block, attestations, txHashes, invalidateBlock);
631
670
  this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
632
671
  return block;
633
672
  } catch (err) {
@@ -646,8 +685,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
646
685
  txs: Tx[],
647
686
  proposerAddress: EthAddress | undefined,
648
687
  ): Promise<CommitteeAttestation[] | undefined> {
649
- // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962): inefficient to have a round trip in here - this should be cached
650
- const committee = await this.publisher.getCurrentEpochCommittee();
688
+ const { committee } = await this.publisher.epochCache.getCommittee(block.header.getSlot());
651
689
 
652
690
  // We checked above that the committee is defined, so this should never happen.
653
691
  if (!committee) {
@@ -683,9 +721,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
683
721
  proposerAddress,
684
722
  blockProposalOptions,
685
723
  );
724
+
686
725
  if (!proposal) {
687
- const msg = `Failed to create block proposal`;
688
- throw new Error(msg);
726
+ throw new Error(`Failed to create block proposal`);
727
+ }
728
+
729
+ if (this.config.skipCollectingAttestations) {
730
+ this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
731
+ const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
732
+ return orderAttestations(attestations ?? [], committee);
689
733
  }
690
734
 
691
735
  this.log.debug('Broadcasting block proposal to validators');
@@ -715,7 +759,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
715
759
  if (err && err instanceof AttestationTimeoutError) {
716
760
  collectedAttestionsCount = err.collectedCount;
717
761
  }
718
-
719
762
  throw err;
720
763
  } finally {
721
764
  this.metrics.recordCollectedAttestations(collectedAttestionsCount, timer.ms());
@@ -731,18 +774,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
731
774
  }))
732
775
  protected async enqueuePublishL2Block(
733
776
  block: L2Block,
734
- attestations?: CommitteeAttestation[],
735
- txHashes?: TxHash[],
777
+ attestations: CommitteeAttestation[] | undefined,
778
+ txHashes: TxHash[],
779
+ invalidateBlock: InvalidateBlockRequest | undefined,
736
780
  ): Promise<void> {
737
781
  // Publishes new block to the network and awaits the tx to be mined
738
782
  this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber.toBigInt());
739
783
 
740
784
  // Time out tx at the end of the slot
741
785
  const slot = block.header.globalVariables.slotNumber.toNumber();
742
- const txTimeoutAt = new Date((this.getSlotStartTimestamp(slot) + this.aztecSlotDuration) * 1000);
786
+ const txTimeoutAt = new Date((this.getSlotStartBuildTimestamp(slot) + this.aztecSlotDuration) * 1000);
743
787
 
744
788
  const enqueued = await this.publisher.enqueueProposeL2Block(block, attestations, txHashes, {
745
789
  txTimeoutAt,
790
+ forcePendingBlockNumber: invalidateBlock?.forcePendingBlockNumber,
746
791
  });
747
792
 
748
793
  if (!enqueued) {
@@ -756,7 +801,14 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
756
801
  * @returns Boolean indicating if our dependencies are synced to the latest block.
757
802
  */
758
803
  protected async getChainTip(): Promise<
759
- { block?: L2Block; blockNumber: number; archive: Fr; l1Timestamp: bigint } | undefined
804
+ | {
805
+ block?: L2Block;
806
+ blockNumber: number;
807
+ archive: Fr;
808
+ l1Timestamp: bigint;
809
+ pendingChainValidationStatus: ValidateBlockResult;
810
+ }
811
+ | undefined
760
812
  > {
761
813
  const syncedBlocks = await Promise.all([
762
814
  this.worldState.status().then(({ syncSummary }) => ({
@@ -767,9 +819,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
767
819
  this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
768
820
  this.l1ToL2MessageSource.getL2Tips().then(t => t.latest),
769
821
  this.l2BlockSource.getL1Timestamp(),
822
+ this.l2BlockSource.getPendingChainValidationStatus(),
770
823
  ] as const);
771
824
 
772
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, l1Timestamp] = syncedBlocks;
825
+ const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, l1Timestamp, pendingChainValidationStatus] =
826
+ syncedBlocks;
773
827
 
774
828
  // The archiver reports 'undefined' hash for the genesis block
775
829
  // because it doesn't have access to world state to compute it (facepalm)
@@ -801,19 +855,95 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
801
855
  blockNumber: block.number,
802
856
  archive: block.archive.root,
803
857
  l1Timestamp,
858
+ pendingChainValidationStatus,
804
859
  };
805
860
  } else {
806
861
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
807
- return { blockNumber: INITIAL_L2_BLOCK_NUM - 1, archive, l1Timestamp };
862
+ return { blockNumber: INITIAL_L2_BLOCK_NUM - 1, archive, l1Timestamp, pendingChainValidationStatus };
808
863
  }
809
864
  }
810
865
 
811
- private getSlotStartTimestamp(slotNumber: number | bigint): number {
812
- return Number(this.l1Constants.l1GenesisTime) + Number(slotNumber) * this.l1Constants.slotDuration;
866
+ /**
867
+ * Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
868
+ * has been there without being invalidated and whether the sequencer is in the committee or not. We always
869
+ * have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
870
+ * and if they fail, any sequencer will try as well.
871
+ */
872
+ protected async considerInvalidatingBlock(
873
+ syncedTo: NonNullable<Awaited<ReturnType<Sequencer['getChainTip']>>>,
874
+ currentSlot: bigint,
875
+ ourValidatorAddresses: EthAddress[],
876
+ ): Promise<void> {
877
+ const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
878
+ if (pendingChainValidationStatus.valid) {
879
+ return;
880
+ }
881
+
882
+ const invalidL1Timestamp = pendingChainValidationStatus.block.l1.timestamp;
883
+ const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidL1Timestamp);
884
+ const invalidBlockNumber = pendingChainValidationStatus.block.block.number;
885
+
886
+ const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } =
887
+ this.config;
888
+
889
+ const logData = {
890
+ invalidL1Timestamp,
891
+ l1Timestamp,
892
+ invalidBlock: pendingChainValidationStatus.block.block.toBlockInfo(),
893
+ secondsBeforeInvalidatingBlockAsCommitteeMember,
894
+ secondsBeforeInvalidatingBlockAsNonCommitteeMember,
895
+ ourValidatorAddresses,
896
+ currentSlot,
897
+ };
898
+
899
+ const inCurrentCommittee = () =>
900
+ this.publisher.epochCache
901
+ .getCommittee(currentSlot)
902
+ .then(c => c?.committee?.some(member => ourValidatorAddresses.some(addr => addr.equals(member))));
903
+
904
+ const invalidateAsCommitteeMember =
905
+ secondsBeforeInvalidatingBlockAsCommitteeMember !== undefined &&
906
+ secondsBeforeInvalidatingBlockAsCommitteeMember > 0 &&
907
+ timeSinceChainInvalid > secondsBeforeInvalidatingBlockAsCommitteeMember &&
908
+ (await inCurrentCommittee());
909
+
910
+ const invalidateAsNonCommitteeMember =
911
+ secondsBeforeInvalidatingBlockAsNonCommitteeMember !== undefined &&
912
+ secondsBeforeInvalidatingBlockAsNonCommitteeMember > 0 &&
913
+ timeSinceChainInvalid > secondsBeforeInvalidatingBlockAsNonCommitteeMember;
914
+
915
+ if (!invalidateAsCommitteeMember && !invalidateAsNonCommitteeMember) {
916
+ this.log.debug(`Not invalidating pending chain`, logData);
917
+ return;
918
+ }
919
+
920
+ const invalidateBlock = await this.publisher.simulateInvalidateBlock(pendingChainValidationStatus);
921
+ if (!invalidateBlock) {
922
+ this.log.warn(`Failed to simulate invalidate block`, logData);
923
+ return;
924
+ }
925
+
926
+ this.log.info(
927
+ invalidateAsCommitteeMember
928
+ ? `Invalidating block ${invalidBlockNumber} as committee member`
929
+ : `Invalidating block ${invalidBlockNumber} as non-committee member`,
930
+ logData,
931
+ );
932
+
933
+ this.publisher.enqueueInvalidateBlock(invalidateBlock);
934
+ await this.publisher.sendRequests();
935
+ }
936
+
937
+ private getSlotStartBuildTimestamp(slotNumber: number | bigint): number {
938
+ return (
939
+ Number(this.l1Constants.l1GenesisTime) +
940
+ Number(slotNumber) * this.l1Constants.slotDuration -
941
+ this.l1Constants.ethereumSlotDuration
942
+ );
813
943
  }
814
944
 
815
945
  private getSecondsIntoSlot(slotNumber: number | bigint): number {
816
- const slotStartTimestamp = this.getSlotStartTimestamp(slotNumber);
946
+ const slotStartTimestamp = this.getSlotStartBuildTimestamp(slotNumber);
817
947
  return Number((this.dateProvider.now() / 1000 - slotStartTimestamp).toFixed(3));
818
948
  }
819
949