@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.
@@ -6,8 +6,7 @@ function _ts_decorate(decorators, target, key, desc) {
6
6
  }
7
7
  import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
8
8
  import { FormattedViemError, NoCommitteeError } from '@aztec/ethereum';
9
- import { Buffer32 } from '@aztec/foundation/buffer';
10
- import { omit } from '@aztec/foundation/collection';
9
+ import { omit, pick } from '@aztec/foundation/collection';
11
10
  import { EthAddress } from '@aztec/foundation/eth-address';
12
11
  import { Fr } from '@aztec/foundation/fields';
13
12
  import { createLogger } from '@aztec/foundation/log';
@@ -17,16 +16,17 @@ import { AztecAddress } from '@aztec/stdlib/aztec-address';
17
16
  import { getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
18
17
  import { Gas } from '@aztec/stdlib/gas';
19
18
  import { SequencerConfigSchema } from '@aztec/stdlib/interfaces/server';
19
+ import { orderAttestations } from '@aztec/stdlib/p2p';
20
20
  import { pickFromSchema } from '@aztec/stdlib/schemas';
21
21
  import { MerkleTreeId } from '@aztec/stdlib/trees';
22
- import { ContentCommitment, ProposedBlockHeader, Tx } from '@aztec/stdlib/tx';
22
+ import { ContentCommitment, ProposedBlockHeader } from '@aztec/stdlib/tx';
23
23
  import { AttestationTimeoutError } from '@aztec/stdlib/validators';
24
24
  import { Attributes, L1Metrics, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
25
25
  import EventEmitter from 'node:events';
26
- import { VoteType } from '../publisher/sequencer-publisher.js';
26
+ import { SignalType } from '../publisher/sequencer-publisher.js';
27
27
  import { SequencerMetrics } from './metrics.js';
28
28
  import { SequencerTimetable, SequencerTooSlowError } from './timetable.js';
29
- import { SequencerState, orderAttestations } from './utils.js';
29
+ import { SequencerState } from './utils.js';
30
30
  export { SequencerState };
31
31
  /**
32
32
  * Sequencer client
@@ -65,17 +65,18 @@ export { SequencerState };
65
65
  metrics;
66
66
  l1Metrics;
67
67
  lastBlockPublished;
68
- isFlushing;
69
68
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */ timetable;
70
69
  enforceTimeTable;
71
70
  constructor(publisher, validatorClient, globalsBuilder, p2pClient, worldState, slasherClient, l2BlockSource, l1ToL2MessageSource, blockBuilder, l1Constants, dateProvider, config = {}, telemetry = getTelemetryClient(), log = createLogger('sequencer')){
72
- super(), this.publisher = publisher, this.validatorClient = validatorClient, this.globalsBuilder = globalsBuilder, this.p2pClient = p2pClient, this.worldState = worldState, this.slasherClient = slasherClient, this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.blockBuilder = blockBuilder, this.l1Constants = l1Constants, this.dateProvider = dateProvider, this.config = config, this.telemetry = telemetry, this.log = log, this.pollingIntervalMs = 1000, this.maxTxsPerBlock = 32, this.minTxsPerBlock = 1, this.maxL1TxInclusionTimeIntoSlot = 0, this._coinbase = EthAddress.ZERO, this._feeRecipient = AztecAddress.ZERO, this.state = SequencerState.STOPPED, this.maxBlockSizeInBytes = 1024 * 1024, this.maxBlockGas = new Gas(100e9, 100e9), this.isFlushing = false, this.enforceTimeTable = false;
71
+ super(), this.publisher = publisher, this.validatorClient = validatorClient, this.globalsBuilder = globalsBuilder, this.p2pClient = p2pClient, this.worldState = worldState, this.slasherClient = slasherClient, this.l2BlockSource = l2BlockSource, this.l1ToL2MessageSource = l1ToL2MessageSource, this.blockBuilder = blockBuilder, this.l1Constants = l1Constants, this.dateProvider = dateProvider, this.config = config, this.telemetry = telemetry, this.log = log, this.pollingIntervalMs = 1000, this.maxTxsPerBlock = 32, this.minTxsPerBlock = 1, this.maxL1TxInclusionTimeIntoSlot = 0, this._coinbase = EthAddress.ZERO, this._feeRecipient = AztecAddress.ZERO, this.state = SequencerState.STOPPED, this.maxBlockSizeInBytes = 1024 * 1024, this.maxBlockGas = new Gas(100e9, 100e9), this.enforceTimeTable = false;
73
72
  this.metrics = new SequencerMetrics(telemetry, ()=>this.state, this.config.coinbase ?? this.publisher.getSenderAddress(), this.publisher.getRollupContract(), 'Sequencer');
74
73
  this.l1Metrics = new L1Metrics(telemetry.getMeter('SequencerL1Metrics'), publisher.l1TxUtils.client, [
75
74
  publisher.getSenderAddress()
76
75
  ]);
77
76
  // Register the slasher on the publisher to fetch slashing payloads
78
77
  this.publisher.registerSlashPayloadGetter(this.slasherClient.getSlashPayload.bind(this.slasherClient));
78
+ // Initialize config
79
+ this.updateConfig(this.config);
79
80
  }
80
81
  get tracer() {
81
82
  return this.metrics.tracer;
@@ -83,6 +84,9 @@ export { SequencerState };
83
84
  getValidatorAddresses() {
84
85
  return this.validatorClient?.getValidatorAddresses();
85
86
  }
87
+ getConfig() {
88
+ return this.config;
89
+ }
86
90
  /**
87
91
  * Updates sequencer config by the defined values in the config on input.
88
92
  * @param config - New parameters.
@@ -128,18 +132,22 @@ export { SequencerState };
128
132
  Object.assign(this.config, config);
129
133
  }
130
134
  setTimeTable() {
131
- this.timetable = new SequencerTimetable(this.l1Constants.ethereumSlotDuration, this.aztecSlotDuration, this.maxL1TxInclusionTimeIntoSlot, this.enforceTimeTable, this.metrics, this.log);
132
- this.log.verbose(`Sequencer timetable updated`, {
133
- enforceTimeTable: this.enforceTimeTable
134
- });
135
+ this.timetable = new SequencerTimetable({
136
+ ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
137
+ aztecSlotDuration: this.aztecSlotDuration,
138
+ maxL1TxInclusionTimeIntoSlot: this.maxL1TxInclusionTimeIntoSlot,
139
+ attestationPropagationTime: this.config.attestationPropagationTime,
140
+ enforce: this.enforceTimeTable
141
+ }, this.metrics, this.log);
135
142
  }
136
143
  /**
137
144
  * Starts the sequencer and moves to IDLE state.
138
145
  */ start() {
139
- this.updateConfig(this.config);
140
146
  this.metrics.start();
141
147
  this.runningPromise = new RunningPromise(this.work.bind(this), this.log, this.pollingIntervalMs);
142
- this.setState(SequencerState.IDLE, 0n, true);
148
+ this.setState(SequencerState.IDLE, undefined, {
149
+ force: true
150
+ });
143
151
  this.runningPromise.start();
144
152
  this.l1Metrics.start();
145
153
  this.log.info(`Sequencer started with address ${this.publisher.getSenderAddress().toString()}`);
@@ -147,12 +155,14 @@ export { SequencerState };
147
155
  /**
148
156
  * Stops the sequencer from processing txs and moves to STOPPED state.
149
157
  */ async stop() {
150
- this.log.debug(`Stopping sequencer`);
158
+ this.log.info(`Stopping sequencer`);
151
159
  this.metrics.stop();
152
160
  await this.validatorClient?.stop();
153
161
  await this.runningPromise?.stop();
154
162
  this.publisher.interrupt();
155
- this.setState(SequencerState.STOPPED, 0n, true);
163
+ this.setState(SequencerState.STOPPED, undefined, {
164
+ force: true
165
+ });
156
166
  this.l1Metrics.stop();
157
167
  this.log.info('Stopped sequencer');
158
168
  }
@@ -162,7 +172,9 @@ export { SequencerState };
162
172
  this.log.info('Restarting sequencer');
163
173
  this.publisher.restart();
164
174
  this.runningPromise.start();
165
- this.setState(SequencerState.IDLE, 0n, true);
175
+ this.setState(SequencerState.IDLE, undefined, {
176
+ force: true
177
+ });
166
178
  }
167
179
  /**
168
180
  * Returns the current state of the sequencer.
@@ -172,9 +184,6 @@ export { SequencerState };
172
184
  state: this.state
173
185
  };
174
186
  }
175
- /** Forces the sequencer to bypass all time and tx count checks for the next block and build anyway. */ flush() {
176
- this.isFlushing = true;
177
- }
178
187
  /**
179
188
  * @notice Performs most of the sequencer duties:
180
189
  * - Checks if we are up to date
@@ -183,14 +192,14 @@ export { SequencerState };
183
192
  * - Submit block
184
193
  * - If our block for some reason is not included, revert the state
185
194
  */ async doRealWork() {
186
- this.setState(SequencerState.SYNCHRONIZING, 0n);
195
+ this.setState(SequencerState.SYNCHRONIZING, undefined);
187
196
  // Check all components are synced to latest as seen by the archiver
188
197
  const syncedTo = await this.getChainTip();
189
198
  // Do not go forward with new block if the previous one has not been mined and processed
190
199
  if (!syncedTo) {
191
200
  return;
192
201
  }
193
- this.setState(SequencerState.PROPOSER_CHECK, 0n);
202
+ this.setState(SequencerState.PROPOSER_CHECK, undefined);
194
203
  const chainTipArchive = syncedTo.archive;
195
204
  const newBlockNumber = syncedTo.blockNumber + 1;
196
205
  const { slot, ts, now } = this.publisher.epochCache.getEpochAndSlotInNextL1Slot();
@@ -204,7 +213,9 @@ export { SequencerState };
204
213
  syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
205
214
  nextL2Slot: slot,
206
215
  nextL2SlotTs: ts,
207
- l1SlotDuration: this.l1Constants.ethereumSlotDuration
216
+ l1SlotDuration: this.l1Constants.ethereumSlotDuration,
217
+ newBlockNumber,
218
+ isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex')
208
219
  };
209
220
  if (syncedTo.l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
210
221
  this.log.debug(`Cannot propose block ${newBlockNumber} at next L2 slot ${slot} due to pending sync from L1`, syncLogData);
@@ -226,32 +237,37 @@ export { SequencerState };
226
237
  });
227
238
  return;
228
239
  }
240
+ // Check that we are a proposer for the next slot
229
241
  let proposerInNextSlot;
230
242
  try {
231
- // Check that we are a proposer for the next slot
232
243
  proposerInNextSlot = await this.publisher.epochCache.getProposerAttesterAddressInNextSlot();
233
244
  } catch (e) {
234
245
  if (e instanceof NoCommitteeError) {
235
- this.log.warn(`Cannot propose block ${newBlockNumber} since the committee does not exist on L1`);
246
+ this.log.warn(`Cannot propose block ${newBlockNumber} at next L2 slot ${slot} since the committee does not exist on L1`);
236
247
  return;
237
248
  }
238
249
  }
239
- const validatorAddresses = this.validatorClient.getValidatorAddresses();
240
250
  // If get proposer in next slot is undefined, then the committee is empty and anyone may propose.
241
- // If the committee is defined and not empty, but none of our validators are the proposer,
242
- // then stop.
251
+ // If the committee is defined and not empty, but none of our validators are the proposer, then stop.
252
+ const validatorAddresses = this.validatorClient.getValidatorAddresses();
243
253
  if (proposerInNextSlot !== undefined && !validatorAddresses.some((addr)=>addr.equals(proposerInNextSlot))) {
244
254
  this.log.debug(`Cannot propose block ${newBlockNumber} since we are not a proposer`, {
245
255
  us: validatorAddresses,
246
256
  proposer: proposerInNextSlot,
247
257
  ...syncLogData
248
258
  });
259
+ // If the pending chain is invalid, we may need to invalidate the block if no one else is doing it.
260
+ if (!syncedTo.pendingChainValidationStatus.valid) {
261
+ await this.considerInvalidatingBlock(syncedTo, slot, validatorAddresses);
262
+ }
249
263
  return;
250
264
  }
251
- // Double check we are good for proposing at the next block before we start operations.
252
- // We should never fail this check assuming the logic above is good.
265
+ // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
266
+ const invalidateBlock = await this.publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
267
+ // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
268
+ // if all the previous checks are good, but we do it just in case.
253
269
  const proposerAddress = proposerInNextSlot ?? EthAddress.ZERO;
254
- const canProposeCheck = await this.publisher.canProposeAtNextEthBlock(chainTipArchive.toBuffer(), proposerAddress);
270
+ const canProposeCheck = await this.publisher.canProposeAtNextEthBlock(chainTipArchive, proposerAddress, invalidateBlock);
255
271
  if (canProposeCheck === undefined) {
256
272
  this.log.warn(`Cannot propose block ${newBlockNumber} at slot ${slot} due to failed rollup contract check`, syncLogData);
257
273
  this.emit('proposer-rollup-check-failed', {
@@ -281,13 +297,16 @@ export { SequencerState };
281
297
  });
282
298
  return;
283
299
  }
284
- this.log.debug(`${proposerInNextSlot ? `Validator ${proposerInNextSlot.toString()} can` : 'Can'} propose block ${newBlockNumber} at slot ${slot}`, {
300
+ this.log.debug(`Can propose block ${newBlockNumber} at slot ${slot}` + (proposerInNextSlot ? ` as ${proposerInNextSlot}` : ''), {
285
301
  ...syncLogData,
286
302
  validatorAddresses
287
303
  });
288
304
  const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(newBlockNumber, this.coinbase, this._feeRecipient, slot);
289
- const enqueueGovernanceVotePromise = this.publisher.enqueueCastVote(slot, newGlobalVariables.timestamp, VoteType.GOVERNANCE, proposerAddress, (msg)=>this.validatorClient.signWithAddress(proposerAddress, Buffer32.fromString(msg)).then((s)=>s.toString()));
290
- const enqueueSlashingVotePromise = this.publisher.enqueueCastVote(slot, newGlobalVariables.timestamp, VoteType.SLASHING, proposerAddress, (msg)=>this.validatorClient.signWithAddress(proposerAddress, Buffer32.fromString(msg)).then((s)=>s.toString()));
305
+ const enqueueGovernanceVotePromise = this.publisher.enqueueCastSignal(slot, newGlobalVariables.timestamp, SignalType.GOVERNANCE, proposerAddress, (msg)=>this.validatorClient.signWithAddress(proposerAddress, msg).then((s)=>s.toString()));
306
+ const enqueueSlashingVotePromise = this.publisher.enqueueCastSignal(slot, newGlobalVariables.timestamp, SignalType.SLASHING, proposerAddress, (msg)=>this.validatorClient.signWithAddress(proposerAddress, msg).then((s)=>s.toString()));
307
+ if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
308
+ this.publisher.enqueueInvalidateBlock(invalidateBlock);
309
+ }
291
310
  this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
292
311
  this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
293
312
  proposer: proposerInNextSlot?.toString(),
@@ -304,15 +323,14 @@ export { SequencerState };
304
323
  contentCommitment: ContentCommitment.empty(),
305
324
  totalManaUsed: Fr.ZERO
306
325
  });
307
- let finishedFlushing = false;
308
326
  let block;
309
327
  const pendingTxCount = await this.p2pClient.getPendingTxCount();
310
- if (pendingTxCount >= this.minTxsPerBlock || this.isFlushing) {
328
+ if (pendingTxCount >= this.minTxsPerBlock) {
311
329
  // We don't fetch exactly maxTxsPerBlock txs here because we may not need all of them if we hit a limit before,
312
330
  // and also we may need to fetch more if we don't have enough valid txs.
313
331
  const pendingTxs = this.p2pClient.iteratePendingTxs();
314
332
  try {
315
- block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposerInNextSlot);
333
+ block = await this.buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposerInNextSlot, invalidateBlock);
316
334
  } catch (err) {
317
335
  this.emit('block-build-failed', {
318
336
  reason: err.message
@@ -325,8 +343,6 @@ export { SequencerState };
325
343
  slot
326
344
  });
327
345
  }
328
- } finally{
329
- finishedFlushing = true;
330
346
  }
331
347
  } else {
332
348
  this.log.verbose(`Not enough txs to build block ${newBlockNumber} at slot ${slot} (got ${pendingTxCount} txs, need ${this.minTxsPerBlock})`, {
@@ -352,7 +368,7 @@ export { SequencerState };
352
368
  });
353
369
  });
354
370
  const l1Response = await this.publisher.sendRequests();
355
- const proposedBlock = l1Response?.validActions.find((a)=>a === 'propose');
371
+ const proposedBlock = l1Response?.successfulActions.find((a)=>a === 'propose');
356
372
  if (proposedBlock) {
357
373
  this.lastBlockPublished = block;
358
374
  this.emit('block-published', {
@@ -360,16 +376,10 @@ export { SequencerState };
360
376
  slot: Number(slot)
361
377
  });
362
378
  this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString());
363
- if (finishedFlushing) {
364
- this.isFlushing = false;
365
- }
366
379
  } else if (block) {
367
- this.emit('block-publish-failed', {
368
- validActions: l1Response?.validActions,
369
- expiredActions: l1Response?.expiredActions
370
- });
380
+ this.emit('block-publish-failed', l1Response ?? {});
371
381
  }
372
- this.setState(SequencerState.IDLE, 0n);
382
+ this.setState(SequencerState.IDLE, undefined);
373
383
  }
374
384
  async work() {
375
385
  try {
@@ -382,28 +392,28 @@ export { SequencerState };
382
392
  throw err;
383
393
  }
384
394
  } finally{
385
- this.setState(SequencerState.IDLE, 0n);
395
+ this.setState(SequencerState.IDLE, undefined);
386
396
  }
387
397
  }
388
- /**
389
- * Sets the sequencer state and checks if we have enough time left in the slot to transition to the new state.
390
- * @param proposedState - The new state to transition to.
391
- * @param currentSlotNumber - The current slot number.
392
- * @param force - Whether to force the transition even if the sequencer is stopped.
393
- *
394
- * @dev If the `currentSlotNumber` doesn't matter (e.g. transitioning to IDLE), pass in `0n`;
395
- * it is only used to check if we have enough time left in the slot to transition to the new state.
396
- */ setState(proposedState, currentSlotNumber, force = false) {
397
- if (this.state === SequencerState.STOPPED && force !== true) {
398
+ setState(proposedState, slotNumber, opts = {}) {
399
+ if (this.state === SequencerState.STOPPED && !opts.force) {
398
400
  this.log.warn(`Cannot set sequencer from ${this.state} to ${proposedState} as it is stopped.`);
399
401
  return;
400
402
  }
401
- const secondsIntoSlot = this.getSecondsIntoSlot(currentSlotNumber);
402
- this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
403
- this.log.debug(`Transitioning from ${this.state} to ${proposedState}`);
403
+ let secondsIntoSlot = undefined;
404
+ if (slotNumber !== undefined) {
405
+ secondsIntoSlot = this.getSecondsIntoSlot(slotNumber);
406
+ this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
407
+ }
408
+ this.log.debug(`Transitioning from ${this.state} to ${proposedState}`, {
409
+ slotNumber,
410
+ secondsIntoSlot
411
+ });
404
412
  this.emit('state-changed', {
405
413
  oldState: this.state,
406
- newState: proposedState
414
+ newState: proposedState,
415
+ secondsIntoSlot,
416
+ slotNumber
407
417
  });
408
418
  this.state = proposedState;
409
419
  }
@@ -412,16 +422,16 @@ export { SequencerState };
412
422
  return;
413
423
  }
414
424
  const failedTxData = failedTxs.map((fail)=>fail.tx);
415
- const failedTxHashes = await Tx.getHashes(failedTxData);
425
+ const failedTxHashes = failedTxData.map((tx)=>tx.getTxHash());
416
426
  this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
417
427
  await this.p2pClient.deleteTxs(failedTxHashes);
418
428
  }
419
- getDefaultBlockBuilderOptions(slot) {
429
+ getBlockBuilderOptions(slot) {
420
430
  // Deadline for processing depends on whether we're proposing a block
421
431
  const secondsIntoSlot = this.getSecondsIntoSlot(slot);
422
432
  const processingEndTimeWithinSlot = this.timetable.getBlockProposalExecTimeEnd(secondsIntoSlot);
423
433
  // Deadline is only set if enforceTimeTable is enabled.
424
- const deadline = this.enforceTimeTable ? new Date((this.getSlotStartTimestamp(slot) + processingEndTimeWithinSlot) * 1000) : undefined;
434
+ const deadline = this.enforceTimeTable ? new Date((this.getSlotStartBuildTimestamp(slot) + processingEndTimeWithinSlot) * 1000) : undefined;
425
435
  return {
426
436
  maxTransactions: this.maxTxsPerBlock,
427
437
  maxBlockSize: this.maxBlockSizeInBytes,
@@ -439,8 +449,8 @@ export { SequencerState };
439
449
  * @param proposalHeader - The partial header constructed for the proposal
440
450
  * @param newGlobalVariables - The global variables for the new block
441
451
  * @param proposerAddress - The address of the proposer
442
- */ async buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposerAddress) {
443
- await this.publisher.validateBlockHeader(proposalHeader);
452
+ */ async buildBlockAndEnqueuePublish(pendingTxs, proposalHeader, newGlobalVariables, proposerAddress, invalidateBlock) {
453
+ await this.publisher.validateBlockHeader(proposalHeader, invalidateBlock);
444
454
  const blockNumber = newGlobalVariables.blockNumber;
445
455
  const slot = proposalHeader.slotNumber.toBigInt();
446
456
  const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
@@ -448,12 +458,12 @@ export { SequencerState };
448
458
  const workTimer = new Timer();
449
459
  this.setState(SequencerState.CREATING_BLOCK, slot);
450
460
  try {
451
- const blockBuilderOptions = this.getDefaultBlockBuilderOptions(Number(slot));
461
+ const blockBuilderOptions = this.getBlockBuilderOptions(Number(slot));
452
462
  const buildBlockRes = await this.blockBuilder.buildBlock(pendingTxs, l1ToL2Messages, newGlobalVariables, blockBuilderOptions);
453
463
  const { publicGas, block, publicProcessorDuration, numTxs, numMsgs, blockBuildingTimer, usedTxs, failedTxs } = buildBlockRes;
454
464
  const blockBuildDuration = workTimer.ms();
455
465
  await this.dropFailedTxsFromP2P(failedTxs);
456
- const minTxsPerBlock = this.isFlushing ? 0 : this.minTxsPerBlock;
466
+ const minTxsPerBlock = this.minTxsPerBlock;
457
467
  if (numTxs < minTxsPerBlock) {
458
468
  this.log.warn(`Block ${blockNumber} has too few txs to be proposed (got ${numTxs} but required ${minTxsPerBlock})`, {
459
469
  slot,
@@ -464,7 +474,7 @@ export { SequencerState };
464
474
  }
465
475
  // TODO(@PhilWindle) We should probably periodically check for things like another
466
476
  // block being published before ours instead of just waiting on our block
467
- await this.publisher.validateBlockHeader(block.header.toPropose());
477
+ await this.publisher.validateBlockHeader(block.header.toPropose(), invalidateBlock);
468
478
  const blockStats = {
469
479
  eventName: 'l2-block-built',
470
480
  creator: this.publisher.getSenderAddress().toString(),
@@ -489,7 +499,7 @@ export { SequencerState };
489
499
  blockNumber
490
500
  });
491
501
  }
492
- await this.enqueuePublishL2Block(block, attestations, txHashes);
502
+ await this.enqueuePublishL2Block(block, attestations, txHashes, invalidateBlock);
493
503
  this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
494
504
  return block;
495
505
  } catch (err) {
@@ -498,8 +508,7 @@ export { SequencerState };
498
508
  }
499
509
  }
500
510
  async collectAttestations(block, txs, proposerAddress) {
501
- // TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962): inefficient to have a round trip in here - this should be cached
502
- const committee = await this.publisher.getCurrentEpochCommittee();
511
+ const { committee } = await this.publisher.epochCache.getCommittee(block.header.getSlot());
503
512
  // We checked above that the committee is defined, so this should never happen.
504
513
  if (!committee) {
505
514
  throw new Error('No committee when collecting attestations');
@@ -524,8 +533,12 @@ export { SequencerState };
524
533
  };
525
534
  const proposal = await this.validatorClient.createBlockProposal(block.header.globalVariables.blockNumber, block.header.toPropose(), block.archive.root, block.header.state, txs, proposerAddress, blockProposalOptions);
526
535
  if (!proposal) {
527
- const msg = `Failed to create block proposal`;
528
- throw new Error(msg);
536
+ throw new Error(`Failed to create block proposal`);
537
+ }
538
+ if (this.config.skipCollectingAttestations) {
539
+ this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
540
+ const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
541
+ return orderAttestations(attestations ?? [], committee);
529
542
  }
530
543
  this.log.debug('Broadcasting block proposal to validators');
531
544
  await this.validatorClient.broadcastBlockProposal(proposal);
@@ -551,14 +564,15 @@ export { SequencerState };
551
564
  /**
552
565
  * Publishes the L2Block to the rollup contract.
553
566
  * @param block - The L2Block to be published.
554
- */ async enqueuePublishL2Block(block, attestations, txHashes) {
567
+ */ async enqueuePublishL2Block(block, attestations, txHashes, invalidateBlock) {
555
568
  // Publishes new block to the network and awaits the tx to be mined
556
569
  this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber.toBigInt());
557
570
  // Time out tx at the end of the slot
558
571
  const slot = block.header.globalVariables.slotNumber.toNumber();
559
- const txTimeoutAt = new Date((this.getSlotStartTimestamp(slot) + this.aztecSlotDuration) * 1000);
572
+ const txTimeoutAt = new Date((this.getSlotStartBuildTimestamp(slot) + this.aztecSlotDuration) * 1000);
560
573
  const enqueued = await this.publisher.enqueueProposeL2Block(block, attestations, txHashes, {
561
- txTimeoutAt
574
+ txTimeoutAt,
575
+ forcePendingBlockNumber: invalidateBlock?.forcePendingBlockNumber
562
576
  });
563
577
  if (!enqueued) {
564
578
  throw new Error(`Failed to enqueue publish of block ${block.number}`);
@@ -577,9 +591,10 @@ export { SequencerState };
577
591
  this.l2BlockSource.getL2Tips().then((t)=>t.latest),
578
592
  this.p2pClient.getStatus().then((p2p)=>p2p.syncedToL2Block),
579
593
  this.l1ToL2MessageSource.getL2Tips().then((t)=>t.latest),
580
- this.l2BlockSource.getL1Timestamp()
594
+ this.l2BlockSource.getL1Timestamp(),
595
+ this.l2BlockSource.getPendingChainValidationStatus()
581
596
  ]);
582
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, l1Timestamp] = syncedBlocks;
597
+ const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, l1Timestamp, pendingChainValidationStatus] = syncedBlocks;
583
598
  // The archiver reports 'undefined' hash for the genesis block
584
599
  // because it doesn't have access to world state to compute it (facepalm)
585
600
  const result = l2BlockSource.hash === undefined ? worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0 : worldState.hash === l2BlockSource.hash && p2p.hash === l2BlockSource.hash && l1ToL2MessageSource.hash === l2BlockSource.hash;
@@ -605,22 +620,63 @@ export { SequencerState };
605
620
  block,
606
621
  blockNumber: block.number,
607
622
  archive: block.archive.root,
608
- l1Timestamp
623
+ l1Timestamp,
624
+ pendingChainValidationStatus
609
625
  };
610
626
  } else {
611
627
  const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
612
628
  return {
613
629
  blockNumber: INITIAL_L2_BLOCK_NUM - 1,
614
630
  archive,
615
- l1Timestamp
631
+ l1Timestamp,
632
+ pendingChainValidationStatus
616
633
  };
617
634
  }
618
635
  }
619
- getSlotStartTimestamp(slotNumber) {
620
- return Number(this.l1Constants.l1GenesisTime) + Number(slotNumber) * this.l1Constants.slotDuration;
636
+ /**
637
+ * Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
638
+ * has been there without being invalidated and whether the sequencer is in the committee or not. We always
639
+ * have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
640
+ * and if they fail, any sequencer will try as well.
641
+ */ async considerInvalidatingBlock(syncedTo, currentSlot, ourValidatorAddresses) {
642
+ const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
643
+ if (pendingChainValidationStatus.valid) {
644
+ return;
645
+ }
646
+ const invalidL1Timestamp = pendingChainValidationStatus.block.l1.timestamp;
647
+ const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidL1Timestamp);
648
+ const invalidBlockNumber = pendingChainValidationStatus.block.block.number;
649
+ const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } = this.config;
650
+ const logData = {
651
+ invalidL1Timestamp,
652
+ l1Timestamp,
653
+ invalidBlock: pendingChainValidationStatus.block.block.toBlockInfo(),
654
+ secondsBeforeInvalidatingBlockAsCommitteeMember,
655
+ secondsBeforeInvalidatingBlockAsNonCommitteeMember,
656
+ ourValidatorAddresses,
657
+ currentSlot
658
+ };
659
+ const inCurrentCommittee = ()=>this.publisher.epochCache.getCommittee(currentSlot).then((c)=>c?.committee?.some((member)=>ourValidatorAddresses.some((addr)=>addr.equals(member))));
660
+ const invalidateAsCommitteeMember = secondsBeforeInvalidatingBlockAsCommitteeMember !== undefined && secondsBeforeInvalidatingBlockAsCommitteeMember > 0 && timeSinceChainInvalid > secondsBeforeInvalidatingBlockAsCommitteeMember && await inCurrentCommittee();
661
+ const invalidateAsNonCommitteeMember = secondsBeforeInvalidatingBlockAsNonCommitteeMember !== undefined && secondsBeforeInvalidatingBlockAsNonCommitteeMember > 0 && timeSinceChainInvalid > secondsBeforeInvalidatingBlockAsNonCommitteeMember;
662
+ if (!invalidateAsCommitteeMember && !invalidateAsNonCommitteeMember) {
663
+ this.log.debug(`Not invalidating pending chain`, logData);
664
+ return;
665
+ }
666
+ const invalidateBlock = await this.publisher.simulateInvalidateBlock(pendingChainValidationStatus);
667
+ if (!invalidateBlock) {
668
+ this.log.warn(`Failed to simulate invalidate block`, logData);
669
+ return;
670
+ }
671
+ this.log.info(invalidateAsCommitteeMember ? `Invalidating block ${invalidBlockNumber} as committee member` : `Invalidating block ${invalidBlockNumber} as non-committee member`, logData);
672
+ this.publisher.enqueueInvalidateBlock(invalidateBlock);
673
+ await this.publisher.sendRequests();
674
+ }
675
+ getSlotStartBuildTimestamp(slotNumber) {
676
+ return Number(this.l1Constants.l1GenesisTime) + Number(slotNumber) * this.l1Constants.slotDuration - this.l1Constants.ethereumSlotDuration;
621
677
  }
622
678
  getSecondsIntoSlot(slotNumber) {
623
- const slotStartTimestamp = this.getSlotStartTimestamp(slotNumber);
679
+ const slotStartTimestamp = this.getSlotStartBuildTimestamp(slotNumber);
624
680
  return Number((this.dateProvider.now() / 1000 - slotStartTimestamp).toFixed(3));
625
681
  }
626
682
  get aztecSlotDuration() {
@@ -1,10 +1,6 @@
1
1
  import type { SequencerMetrics } from './metrics.js';
2
2
  import { SequencerState } from './utils.js';
3
3
  export declare class SequencerTimetable {
4
- private readonly ethereumSlotDuration;
5
- private readonly aztecSlotDuration;
6
- private readonly maxL1TxInclusionTimeIntoSlot;
7
- private readonly enforce;
8
4
  private readonly metrics?;
9
5
  private readonly log;
10
6
  /**
@@ -27,7 +23,21 @@ export declare class SequencerTimetable {
27
23
  readonly attestationPropagationTime: number;
28
24
  /** How much time we spend validating and processing a block after building it, and assembling the proposal to send to attestors */
29
25
  readonly blockValidationTime: number;
30
- constructor(ethereumSlotDuration: number, aztecSlotDuration: number, maxL1TxInclusionTimeIntoSlot: number, enforce?: boolean, metrics?: SequencerMetrics | undefined, log?: import("@aztec/aztec.js").Logger);
26
+ /** Ethereum slot duration in seconds */
27
+ readonly ethereumSlotDuration: number;
28
+ /** Aztec slot duration in seconds (must be multiple of ethereum slot duration) */
29
+ readonly aztecSlotDuration: number;
30
+ /** How late into an L1 slot we can send a tx to make sure it gets included in the immediate next block. Complement of l1PublishingTime. */
31
+ readonly maxL1TxInclusionTimeIntoSlot: number;
32
+ /** Whether assertTimeLeft will throw if not enough time. */
33
+ readonly enforce: boolean;
34
+ constructor(opts: {
35
+ ethereumSlotDuration: number;
36
+ aztecSlotDuration: number;
37
+ maxL1TxInclusionTimeIntoSlot: number;
38
+ attestationPropagationTime?: number;
39
+ enforce: boolean;
40
+ }, metrics?: SequencerMetrics | undefined, log?: import("@aztec/aztec.js").Logger);
31
41
  private get afterBlockBuildingTimeNeededWithoutReexec();
32
42
  getBlockProposalExecTimeEnd(secondsIntoSlot: number): number;
33
43
  private get afterBlockReexecTimeNeeded();
@@ -1 +1 @@
1
- {"version":3,"file":"timetable.d.ts","sourceRoot":"","sources":["../../src/sequencer/timetable.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAO5C,qBAAa,kBAAkB;IA4B3B,OAAO,CAAC,QAAQ,CAAC,oBAAoB;IACrC,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,4BAA4B;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG;IAhCtB;;;;OAIG;IACH,SAAgB,kBAAkB,EAAE,MAAM,CAAC;IAE3C;;;;OAIG;IACH,SAAgB,gBAAgB,SAAC;IAEjC,sHAAsH;IACtH,SAAgB,gBAAgB,EAAE,MAAM,CAAsB;IAE9D,uDAAuD;IACvD,SAAgB,gBAAgB,EAAE,MAAM,CAAsB;IAE9D,mGAAmG;IACnG,SAAgB,0BAA0B,EAAE,MAAM,CAAgC;IAElF,mIAAmI;IACnI,SAAgB,mBAAmB,EAAE,MAAM,CAAyB;gBAGjD,oBAAoB,EAAE,MAAM,EAC5B,iBAAiB,EAAE,MAAM,EACzB,4BAA4B,EAAE,MAAM,EACpC,OAAO,GAAE,OAAc,EACvB,OAAO,CAAC,EAAE,gBAAgB,YAAA,EAC1B,GAAG,mCAAsC;IA0B5D,OAAO,KAAK,yCAAyC,GAEpD;IAEM,2BAA2B,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM;IAenE,OAAO,KAAK,0BAA0B,GAErC;IAEM,yBAAyB,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM;IAU3D,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS;IAsB5D,cAAc,CAAC,QAAQ,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM;CAkBxE;AAED,qBAAa,qBAAsB,SAAQ,KAAK;aAE5B,aAAa,EAAE,cAAc;aAC7B,cAAc,EAAE,MAAM;aACtB,WAAW,EAAE,MAAM;gBAFnB,aAAa,EAAE,cAAc,EAC7B,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM;CAOtC"}
1
+ {"version":3,"file":"timetable.d.ts","sourceRoot":"","sources":["../../src/sequencer/timetable.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAM5C,qBAAa,kBAAkB;IA+C3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG;IA/CtB;;;;OAIG;IACH,SAAgB,kBAAkB,EAAE,MAAM,CAAC;IAE3C;;;;OAIG;IACH,SAAgB,gBAAgB,SAAC;IAEjC,sHAAsH;IACtH,SAAgB,gBAAgB,EAAE,MAAM,CAAsB;IAE9D,uDAAuD;IACvD,SAAgB,gBAAgB,EAAE,MAAM,CAAsB;IAE9D,mGAAmG;IACnG,SAAgB,0BAA0B,EAAE,MAAM,CAAC;IAEnD,mIAAmI;IACnI,SAAgB,mBAAmB,EAAE,MAAM,CAAyB;IAEpE,wCAAwC;IACxC,SAAgB,oBAAoB,EAAE,MAAM,CAAC;IAE7C,kFAAkF;IAClF,SAAgB,iBAAiB,EAAE,MAAM,CAAC;IAE1C,2IAA2I;IAC3I,SAAgB,4BAA4B,EAAE,MAAM,CAAC;IAErD,4DAA4D;IAC5D,SAAgB,OAAO,EAAE,OAAO,CAAC;gBAG/B,IAAI,EAAE;QACJ,oBAAoB,EAAE,MAAM,CAAC;QAC7B,iBAAiB,EAAE,MAAM,CAAC;QAC1B,4BAA4B,EAAE,MAAM,CAAC;QACrC,0BAA0B,CAAC,EAAE,MAAM,CAAC;QACpC,OAAO,EAAE,OAAO,CAAC;KAClB,EACgB,OAAO,CAAC,EAAE,gBAAgB,YAAA,EAC1B,GAAG,mCAAsC;IA+C5D,OAAO,KAAK,yCAAyC,GAEpD;IAEM,2BAA2B,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM;IAenE,OAAO,KAAK,0BAA0B,GAErC;IAEM,yBAAyB,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM;IAU3D,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,GAAG,SAAS;IAsB5D,cAAc,CAAC,QAAQ,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM;CAkBxE;AAED,qBAAa,qBAAsB,SAAQ,KAAK;aAE5B,aAAa,EAAE,cAAc;aAC7B,cAAc,EAAE,MAAM;aACtB,WAAW,EAAE,MAAM;gBAFnB,aAAa,EAAE,cAAc,EAC7B,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,MAAM;CAOtC"}
@@ -1,14 +1,10 @@
1
1
  import { createLogger } from '@aztec/aztec.js';
2
+ import { DEFAULT_ATTESTATION_PROPAGATION_TIME } from '../config.js';
2
3
  import { SequencerState } from './utils.js';
3
4
  const MIN_EXECUTION_TIME = 1;
4
5
  const BLOCK_PREPARE_TIME = 1;
5
6
  const BLOCK_VALIDATION_TIME = 1;
6
- const ATTESTATION_PROPAGATION_TIME = 2;
7
7
  export class SequencerTimetable {
8
- ethereumSlotDuration;
9
- aztecSlotDuration;
10
- maxL1TxInclusionTimeIntoSlot;
11
- enforce;
12
8
  metrics;
13
9
  log;
14
10
  /**
@@ -25,18 +21,22 @@ export class SequencerTimetable {
25
21
  /** How long it takes to get ready to start building */ blockPrepareTime;
26
22
  /** How long it takes to for proposals and attestations to travel across the p2p layer (one-way) */ attestationPropagationTime;
27
23
  /** How much time we spend validating and processing a block after building it, and assembling the proposal to send to attestors */ blockValidationTime;
28
- constructor(ethereumSlotDuration, aztecSlotDuration, maxL1TxInclusionTimeIntoSlot, enforce = true, metrics, log = createLogger('sequencer:timetable')){
29
- this.ethereumSlotDuration = ethereumSlotDuration;
30
- this.aztecSlotDuration = aztecSlotDuration;
31
- this.maxL1TxInclusionTimeIntoSlot = maxL1TxInclusionTimeIntoSlot;
32
- this.enforce = enforce;
24
+ /** Ethereum slot duration in seconds */ ethereumSlotDuration;
25
+ /** Aztec slot duration in seconds (must be multiple of ethereum slot duration) */ aztecSlotDuration;
26
+ /** How late into an L1 slot we can send a tx to make sure it gets included in the immediate next block. Complement of l1PublishingTime. */ maxL1TxInclusionTimeIntoSlot;
27
+ /** Whether assertTimeLeft will throw if not enough time. */ enforce;
28
+ constructor(opts, metrics, log = createLogger('sequencer:timetable')){
33
29
  this.metrics = metrics;
34
30
  this.log = log;
35
31
  this.minExecutionTime = MIN_EXECUTION_TIME;
36
32
  this.blockPrepareTime = BLOCK_PREPARE_TIME;
37
- this.attestationPropagationTime = ATTESTATION_PROPAGATION_TIME;
38
33
  this.blockValidationTime = BLOCK_VALIDATION_TIME;
34
+ this.ethereumSlotDuration = opts.ethereumSlotDuration;
35
+ this.aztecSlotDuration = opts.aztecSlotDuration;
36
+ this.maxL1TxInclusionTimeIntoSlot = opts.maxL1TxInclusionTimeIntoSlot;
37
+ this.attestationPropagationTime = opts.attestationPropagationTime ?? DEFAULT_ATTESTATION_PROPAGATION_TIME;
39
38
  this.l1PublishingTime = this.ethereumSlotDuration - this.maxL1TxInclusionTimeIntoSlot;
39
+ this.enforce = opts.enforce;
40
40
  // Assume zero-cost propagation time and faster runs in test environments where L1 slot duration is shortened
41
41
  if (this.ethereumSlotDuration < 8) {
42
42
  this.attestationPropagationTime = 0;
@@ -45,10 +45,23 @@ export class SequencerTimetable {
45
45
  }
46
46
  const allWorkToDo = this.blockPrepareTime + this.minExecutionTime * 2 + this.attestationPropagationTime * 2 + this.blockValidationTime + this.l1PublishingTime;
47
47
  const initializeDeadline = this.aztecSlotDuration - allWorkToDo;
48
+ this.initializeDeadline = initializeDeadline;
49
+ this.log.verbose(`Sequencer timetable initialized (${this.enforce ? 'enforced' : 'not enforced'})`, {
50
+ ethereumSlotDuration: this.ethereumSlotDuration,
51
+ aztecSlotDuration: this.aztecSlotDuration,
52
+ maxL1TxInclusionTimeIntoSlot: this.maxL1TxInclusionTimeIntoSlot,
53
+ l1PublishingTime: this.l1PublishingTime,
54
+ minExecutionTime: this.minExecutionTime,
55
+ blockPrepareTime: this.blockPrepareTime,
56
+ attestationPropagationTime: this.attestationPropagationTime,
57
+ blockValidationTime: this.blockValidationTime,
58
+ initializeDeadline: this.initializeDeadline,
59
+ enforce: this.enforce,
60
+ allWorkToDo
61
+ });
48
62
  if (initializeDeadline <= 0) {
49
63
  throw new Error(`Block proposal initialize deadline cannot be negative (got ${initializeDeadline} from total time needed ${allWorkToDo} and a slot duration of ${this.aztecSlotDuration}).`);
50
64
  }
51
- this.initializeDeadline = initializeDeadline;
52
65
  }
53
66
  get afterBlockBuildingTimeNeededWithoutReexec() {
54
67
  return this.blockValidationTime + this.attestationPropagationTime * 2 + this.l1PublishingTime;
@@ -1,6 +1,3 @@
1
- import type { EthAddress } from '@aztec/foundation/eth-address';
2
- import { CommitteeAttestation } from '@aztec/stdlib/block';
3
- import type { BlockAttestation } from '@aztec/stdlib/p2p';
4
1
  export declare enum SequencerState {
5
2
  /**
6
3
  * Sequencer is stopped and not processing any txs from the pool.
@@ -35,14 +32,7 @@ export declare enum SequencerState {
35
32
  */
36
33
  PUBLISHING_BLOCK = "PUBLISHING_BLOCK"
37
34
  }
35
+ export type SequencerStateWithSlot = SequencerState.INITIALIZING_PROPOSAL | SequencerState.CREATING_BLOCK | SequencerState.COLLECTING_ATTESTATIONS | SequencerState.PUBLISHING_BLOCK;
38
36
  export type SequencerStateCallback = () => SequencerState;
39
37
  export declare function sequencerStateToNumber(state: SequencerState): number;
40
- /** Order Attestations
41
- *
42
- * Returns attestation signatures in the order of a series of provided ethereum addresses
43
- * The rollup smart contract expects attestations to appear in the order of the committee
44
- *
45
- * @todo: perform this logic within the memory attestation store instead?
46
- */
47
- export declare function orderAttestations(attestations: BlockAttestation[], orderAddresses: EthAddress[]): CommitteeAttestation[];
48
38
  //# sourceMappingURL=utils.d.ts.map