@aztec/sequencer-client 0.70.0 → 0.72.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33,11 +33,9 @@ import { type DateProvider, Timer, elapsed } from '@aztec/foundation/timer';
33
33
  import { type P2P } from '@aztec/p2p';
34
34
  import { type BlockBuilderFactory } from '@aztec/prover-client/block-builder';
35
35
  import { type PublicProcessorFactory } from '@aztec/simulator/server';
36
- import { Attributes, type TelemetryClient, type Tracer, trackSpan } from '@aztec/telemetry-client';
36
+ import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
37
37
  import { type ValidatorClient } from '@aztec/validator-client';
38
38
 
39
- import assert from 'assert';
40
-
41
39
  import { type GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
42
40
  import { type L1Publisher, VoteType } from '../publisher/l1-publisher.js';
43
41
  import { type SlasherClient } from '../slasher/slasher_client.js';
@@ -45,24 +43,11 @@ import { createValidatorsForBlockBuilding } from '../tx_validator/tx_validator_f
45
43
  import { getDefaultAllowedSetupFunctions } from './allowed.js';
46
44
  import { type SequencerConfig } from './config.js';
47
45
  import { SequencerMetrics } from './metrics.js';
46
+ import { SequencerTimetable, SequencerTooSlowError } from './timetable.js';
48
47
  import { SequencerState, orderAttestations } from './utils.js';
49
48
 
50
49
  export { SequencerState };
51
50
 
52
- export class SequencerTooSlowError extends Error {
53
- constructor(
54
- public readonly currentState: SequencerState,
55
- public readonly proposedState: SequencerState,
56
- public readonly maxAllowedTime: number,
57
- public readonly currentTime: number,
58
- ) {
59
- super(
60
- `Too far into slot to transition to ${proposedState} (max allowed: ${maxAllowedTime}s, time into slot: ${currentTime}s)`,
61
- );
62
- this.name = 'SequencerTooSlowError';
63
- }
64
- }
65
-
66
51
  type SequencerRollupConstants = Pick<L1RollupConstants, 'ethereumSlotDuration' | 'l1GenesisTime' | 'slotDuration'>;
67
52
 
68
53
  /**
@@ -78,7 +63,7 @@ export class Sequencer {
78
63
  private runningPromise?: RunningPromise;
79
64
  private pollingIntervalMs: number = 1000;
80
65
  private maxTxsPerBlock = 32;
81
- private minTxsPerBLock = 1;
66
+ private minTxsPerBlock = 1;
82
67
  private maxL1TxInclusionTimeIntoSlot = 0;
83
68
  // TODO: zero values should not be allowed for the following 2 values in PROD
84
69
  private _coinbase = EthAddress.ZERO;
@@ -86,17 +71,13 @@ export class Sequencer {
86
71
  private state = SequencerState.STOPPED;
87
72
  private allowedInSetup: AllowedElement[] = getDefaultAllowedSetupFunctions();
88
73
  private maxBlockSizeInBytes: number = 1024 * 1024;
89
- private maxBlockGas: Gas = new Gas(10e9, 10e9);
90
- protected processTxTime: number = 12;
91
- private attestationPropagationTime: number = 2;
74
+ private maxBlockGas: Gas = new Gas(100e9, 100e9);
92
75
  private metrics: SequencerMetrics;
93
76
  private isFlushing: boolean = false;
94
77
 
95
- /**
96
- * The maximum number of seconds that the sequencer can be into a slot to transition to a particular state.
97
- * For example, in order to transition into WAITING_FOR_ATTESTATIONS, the sequencer can be at most 3 seconds into the slot.
98
- */
99
- protected timeTable!: Record<SequencerState, number>;
78
+ /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
79
+ protected timetable!: SequencerTimetable;
80
+
100
81
  protected enforceTimeTable: boolean = false;
101
82
 
102
83
  constructor(
@@ -113,8 +94,8 @@ export class Sequencer {
113
94
  protected contractDataSource: ContractDataSource,
114
95
  protected l1Constants: SequencerRollupConstants,
115
96
  protected dateProvider: DateProvider,
116
- telemetry: TelemetryClient,
117
97
  protected config: SequencerConfig = {},
98
+ telemetry: TelemetryClient = getTelemetryClient(),
118
99
  protected log = createLogger('sequencer'),
119
100
  ) {
120
101
  this.updateConfig(config);
@@ -145,7 +126,7 @@ export class Sequencer {
145
126
  this.maxTxsPerBlock = config.maxTxsPerBlock;
146
127
  }
147
128
  if (config.minTxsPerBlock !== undefined) {
148
- this.minTxsPerBLock = config.minTxsPerBlock;
129
+ this.minTxsPerBlock = config.minTxsPerBlock;
149
130
  }
150
131
  if (config.maxDABlockGas !== undefined) {
151
132
  this.maxBlockGas = new Gas(config.maxDABlockGas, this.maxBlockGas.l2Gas);
@@ -182,78 +163,15 @@ export class Sequencer {
182
163
  }
183
164
 
184
165
  private setTimeTable() {
185
- // How late into the slot can we be to start working
186
- const initialTime = 2;
187
-
188
- // How long it takes to get ready to start building
189
- const blockPrepareTime = 1;
190
-
191
- // How long it takes to for proposals and attestations to travel across the p2p layer (one-way)
192
- const attestationPropagationTime = 2;
193
- this.attestationPropagationTime = attestationPropagationTime;
194
-
195
- // How long it takes to get a published block into L1. L1 builders typically accept txs up to 4 seconds into their slot,
196
- // but we'll timeout sooner to give it more time to propagate (remember we also have blobs!). Still, when working in anvil,
197
- // we can just post in the very last second of the L1 slot and still expect the tx to be accepted.
198
- const l1PublishingTime = this.l1Constants.ethereumSlotDuration - this.maxL1TxInclusionTimeIntoSlot;
199
-
200
- // How much time we spend validating and processing a block after building it,
201
- // and assembling the proposal to send to attestors
202
- const blockValidationTime = 1;
203
-
204
- // How much time we have left in the slot for actually processing txs and building the block.
205
- const remainingTimeInSlot =
206
- this.aztecSlotDuration -
207
- initialTime -
208
- blockPrepareTime -
209
- blockValidationTime -
210
- 2 * attestationPropagationTime -
211
- l1PublishingTime;
212
-
213
- // Check that we actually have time left for processing txs
214
- if (this.enforceTimeTable && remainingTimeInSlot < 0) {
215
- throw new Error(`Not enough time for block building in ${this.aztecSlotDuration}s slot`);
216
- }
217
-
218
- // How much time we have for actually processing txs. Note that we need both the sequencer and the validators to execute txs.
219
- const processTxsTime = remainingTimeInSlot / 2;
220
- this.processTxTime = processTxsTime;
221
-
222
- // Sanity check
223
- const totalSlotTime =
224
- initialTime + // Archiver, world-state, and p2p sync
225
- blockPrepareTime + // Setup globals, initial checks, etc
226
- processTxsTime + // Processing public txs for building the block
227
- blockValidationTime + // Validating the block produced
228
- attestationPropagationTime + // Propagating the block proposal to validators
229
- processTxsTime + // Validators run public txs before signing
230
- attestationPropagationTime + // Attestations fly back to the proposer
231
- l1PublishingTime; // The publish tx sits on the L1 mempool waiting to be picked up
232
-
233
- assert(
234
- totalSlotTime === this.aztecSlotDuration,
235
- `Computed total slot time does not match slot duration: ${totalSlotTime}s`,
166
+ this.timetable = new SequencerTimetable(
167
+ this.l1Constants.ethereumSlotDuration,
168
+ this.aztecSlotDuration,
169
+ this.maxL1TxInclusionTimeIntoSlot,
170
+ this.enforceTimeTable,
171
+ this.metrics,
172
+ this.log,
236
173
  );
237
-
238
- const newTimeTable: Record<SequencerState, number> = {
239
- // No checks needed for any of these transitions
240
- [SequencerState.STOPPED]: this.aztecSlotDuration,
241
- [SequencerState.IDLE]: this.aztecSlotDuration,
242
- [SequencerState.SYNCHRONIZING]: this.aztecSlotDuration,
243
- // We always want to allow the full slot to check if we are the proposer
244
- [SequencerState.PROPOSER_CHECK]: this.aztecSlotDuration,
245
- // How late we can start initializing a new block proposal
246
- [SequencerState.INITIALIZING_PROPOSAL]: initialTime,
247
- // When we start building a block
248
- [SequencerState.CREATING_BLOCK]: initialTime + blockPrepareTime,
249
- // We start collecting attestations after building the block
250
- [SequencerState.COLLECTING_ATTESTATIONS]: initialTime + blockPrepareTime + processTxsTime + blockValidationTime,
251
- // We publish the block after collecting attestations
252
- [SequencerState.PUBLISHING_BLOCK]: this.aztecSlotDuration - l1PublishingTime,
253
- };
254
-
255
- this.log.verbose(`Sequencer time table updated with ${processTxsTime}s for processing txs`, newTimeTable);
256
- this.timeTable = newTimeTable;
174
+ this.log.verbose(`Sequencer timetable updated`, { enforceTimeTable: this.enforceTimeTable });
257
175
  }
258
176
 
259
177
  /**
@@ -318,20 +236,15 @@ export class Sequencer {
318
236
  this.setState(SequencerState.PROPOSER_CHECK, 0n);
319
237
 
320
238
  const chainTip = await this.l2BlockSource.getBlock(-1);
321
- const historicalHeader = chainTip?.header;
322
239
 
323
- const newBlockNumber =
324
- (historicalHeader === undefined
325
- ? await this.l2BlockSource.getBlockNumber()
326
- : Number(historicalHeader.globalVariables.blockNumber.toBigInt())) + 1;
240
+ const newBlockNumber = (chainTip?.header.globalVariables.blockNumber.toNumber() ?? 0) + 1;
327
241
 
328
242
  // If we cannot find a tip archive, assume genesis.
329
- const chainTipArchive =
330
- chainTip == undefined ? new Fr(GENESIS_ARCHIVE_ROOT).toBuffer() : chainTip?.archive.root.toBuffer();
243
+ const chainTipArchive = chainTip?.archive.root ?? new Fr(GENESIS_ARCHIVE_ROOT);
331
244
 
332
245
  let slot: bigint;
333
246
  try {
334
- slot = await this.mayProposeBlock(chainTipArchive, BigInt(newBlockNumber));
247
+ slot = await this.mayProposeBlock(chainTipArchive.toBuffer(), BigInt(newBlockNumber));
335
248
  } catch (err) {
336
249
  this.log.debug(`Cannot propose for block ${newBlockNumber}`);
337
250
  return;
@@ -349,8 +262,8 @@ export class Sequencer {
349
262
 
350
263
  // Check the pool has enough txs to build a block
351
264
  const pendingTxCount = this.p2pClient.getPendingTxCount();
352
- if (pendingTxCount < this.minTxsPerBLock && !this.isFlushing) {
353
- this.log.verbose(`Not enough txs to propose block. Got ${pendingTxCount} min ${this.minTxsPerBLock}.`, {
265
+ if (pendingTxCount < this.minTxsPerBlock && !this.isFlushing) {
266
+ this.log.verbose(`Not enough txs to propose block. Got ${pendingTxCount} min ${this.minTxsPerBlock}.`, {
354
267
  slot,
355
268
  blockNumber: newBlockNumber,
356
269
  });
@@ -360,7 +273,7 @@ export class Sequencer {
360
273
 
361
274
  this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
362
275
  this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
363
- chainTipArchive: new Fr(chainTipArchive),
276
+ chainTipArchive,
364
277
  blockNumber: newBlockNumber,
365
278
  slot,
366
279
  });
@@ -371,7 +284,7 @@ export class Sequencer {
371
284
 
372
285
  // If I created a "partial" header here that should make our job much easier.
373
286
  const proposalHeader = new BlockHeader(
374
- new AppendOnlyTreeSnapshot(Fr.fromBuffer(chainTipArchive), 1),
287
+ new AppendOnlyTreeSnapshot(chainTipArchive, 1),
375
288
  ContentCommitment.empty(),
376
289
  StateReference.empty(),
377
290
  newGlobalVariables,
@@ -384,9 +297,12 @@ export class Sequencer {
384
297
  // @note It is very important that the following function will FAIL and not just return early
385
298
  // if it have made any state changes. If not, we won't rollback the state, and you will
386
299
  // be in for a world of pain.
387
- await this.buildBlockAndAttemptToPublish(pendingTxs, proposalHeader, historicalHeader);
300
+ await this.buildBlockAndAttemptToPublish(pendingTxs, proposalHeader);
388
301
  } catch (err) {
389
302
  this.log.error(`Error assembling block`, err, { blockNumber: newBlockNumber, slot });
303
+
304
+ // If the block failed to build, we might still want to claim the proving rights
305
+ await this.claimEpochProofRightIfAvailable(slot);
390
306
  }
391
307
  this.setState(SequencerState.IDLE, 0n);
392
308
  }
@@ -427,29 +343,6 @@ export class Sequencer {
427
343
  }
428
344
  }
429
345
 
430
- doIHaveEnoughTimeLeft(proposedState: SequencerState, secondsIntoSlot: number): boolean {
431
- if (!this.enforceTimeTable) {
432
- return true;
433
- }
434
-
435
- const maxAllowedTime = this.timeTable[proposedState];
436
- if (maxAllowedTime === this.aztecSlotDuration) {
437
- return true;
438
- }
439
-
440
- const bufferSeconds = maxAllowedTime - secondsIntoSlot;
441
-
442
- if (bufferSeconds < 0) {
443
- this.log.debug(`Too far into slot to transition to ${proposedState}`, { maxAllowedTime, secondsIntoSlot });
444
- return false;
445
- }
446
-
447
- this.metrics.recordStateTransitionBufferMs(Math.floor(bufferSeconds * 1000), proposedState);
448
-
449
- this.log.trace(`Enough time to transition to ${proposedState}`, { maxAllowedTime, secondsIntoSlot });
450
- return true;
451
- }
452
-
453
346
  /**
454
347
  * Sets the sequencer state and checks if we have enough time left in the slot to transition to the new state.
455
348
  * @param proposedState - The new state to transition to.
@@ -465,9 +358,7 @@ export class Sequencer {
465
358
  return;
466
359
  }
467
360
  const secondsIntoSlot = this.getSecondsIntoSlot(currentSlotNumber);
468
- if (!this.doIHaveEnoughTimeLeft(proposedState, secondsIntoSlot)) {
469
- throw new SequencerTooSlowError(this.state, proposedState, this.timeTable[proposedState], secondsIntoSlot);
470
- }
361
+ this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
471
362
  this.log.debug(`Transitioning from ${this.state} to ${proposedState}`);
472
363
  this.state = proposedState;
473
364
  }
@@ -482,17 +373,16 @@ export class Sequencer {
482
373
  * @param historicalHeader - The historical header of the parent
483
374
  * @param opts - Whether to just validate the block as a validator, as opposed to building it as a proposal
484
375
  */
485
- private async buildBlock(
376
+ protected async buildBlock(
486
377
  pendingTxs: Iterable<Tx>,
487
378
  newGlobalVariables: GlobalVariables,
488
- historicalHeader?: BlockHeader,
489
379
  opts: { validateOnly?: boolean } = {},
490
380
  ) {
491
- const blockNumber = newGlobalVariables.blockNumber.toBigInt();
381
+ const blockNumber = newGlobalVariables.blockNumber.toNumber();
492
382
  const slot = newGlobalVariables.slotNumber.toBigInt();
493
383
 
494
384
  this.log.debug(`Requesting L1 to L2 messages from contract for block ${blockNumber}`);
495
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
385
+ const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(BigInt(blockNumber));
496
386
  const msgCount = l1ToL2Messages.length;
497
387
 
498
388
  this.log.verbose(`Building block ${blockNumber} for slot ${slot}`, {
@@ -503,31 +393,27 @@ export class Sequencer {
503
393
  });
504
394
 
505
395
  // Sync to the previous block at least
506
- await this.worldState.syncImmediate(newGlobalVariables.blockNumber.toNumber() - 1);
507
- this.log.debug(`Synced to previous block ${newGlobalVariables.blockNumber.toNumber() - 1}`);
396
+ await this.worldState.syncImmediate(blockNumber - 1);
397
+ this.log.debug(`Synced to previous block ${blockNumber - 1}`);
508
398
 
509
399
  // NB: separating the dbs because both should update the state
510
400
  const publicProcessorFork = await this.worldState.fork();
511
401
  const orchestratorFork = await this.worldState.fork();
512
402
 
403
+ const previousBlockHeader =
404
+ (await this.l2BlockSource.getBlock(blockNumber - 1))?.header ?? orchestratorFork.getInitialHeader();
405
+
513
406
  try {
514
- const processor = this.publicProcessorFactory.create(
515
- publicProcessorFork,
516
- historicalHeader,
517
- newGlobalVariables,
518
- true,
519
- );
407
+ const processor = this.publicProcessorFactory.create(publicProcessorFork, newGlobalVariables, true);
520
408
  const blockBuildingTimer = new Timer();
521
409
  const blockBuilder = this.blockBuilderFactory.create(orchestratorFork);
522
- await blockBuilder.startNewBlock(newGlobalVariables, l1ToL2Messages);
410
+ await blockBuilder.startNewBlock(newGlobalVariables, l1ToL2Messages, previousBlockHeader);
523
411
 
524
- // When building a block as a proposer, we set the deadline for tx processing to the start of the
525
- // CREATING_BLOCK phase, plus the expected time for tx processing. When validating, we start counting
526
- // the time for tx processing from the start of the COLLECTING_ATTESTATIONS phase plus the attestation
527
- // propagation time. See the comments in setTimeTable for more details.
412
+ // Deadline for processing depends on whether we're proposing a block
413
+ const secondsIntoSlot = this.getSecondsIntoSlot(slot);
528
414
  const processingEndTimeWithinSlot = opts.validateOnly
529
- ? this.timeTable[SequencerState.COLLECTING_ATTESTATIONS] + this.attestationPropagationTime + this.processTxTime
530
- : this.timeTable[SequencerState.CREATING_BLOCK] + this.processTxTime;
415
+ ? this.timetable.getValidatorReexecTimeEnd(secondsIntoSlot)
416
+ : this.timetable.getBlockProposalExecTimeEnd(secondsIntoSlot);
531
417
 
532
418
  // Deadline is only set if enforceTimeTable is enabled.
533
419
  const deadline = this.enforceTimeTable
@@ -552,7 +438,12 @@ export class Sequencer {
552
438
  // TODO(#11000): Public processor should just handle processing, one tx at a time. It should be responsibility
553
439
  // of the sequencer to update world state and iterate over txs. We should refactor this along with unifying the
554
440
  // publicProcessorFork and orchestratorFork, to avoid doing tree insertions twice when building the block.
555
- const limits = { deadline, maxTransactions: this.maxTxsPerBlock, maxBlockSize: this.maxBlockSizeInBytes };
441
+ const proposerLimits = {
442
+ maxTransactions: this.maxTxsPerBlock,
443
+ maxBlockSize: this.maxBlockSizeInBytes,
444
+ maxBlockGas: this.maxBlockGas,
445
+ };
446
+ const limits = opts.validateOnly ? { deadline } : { deadline, ...proposerLimits };
556
447
  const [publicProcessorDuration, [processedTxs, failedTxs]] = await elapsed(() =>
557
448
  processor.process(pendingTxs, limits, validators),
558
449
  );
@@ -566,11 +457,11 @@ export class Sequencer {
566
457
  if (
567
458
  !opts.validateOnly && // We check for minTxCount only if we are proposing a block, not if we are validating it
568
459
  !this.isFlushing && // And we skip the check when flushing, since we want all pending txs to go out, no matter if too few
569
- this.minTxsPerBLock !== undefined &&
570
- processedTxs.length < this.minTxsPerBLock
460
+ this.minTxsPerBlock !== undefined &&
461
+ processedTxs.length < this.minTxsPerBlock
571
462
  ) {
572
463
  this.log.warn(
573
- `Block ${blockNumber} has too few txs to be proposed (got ${processedTxs.length} but required ${this.minTxsPerBLock})`,
464
+ `Block ${blockNumber} has too few txs to be proposed (got ${processedTxs.length} but required ${this.minTxsPerBlock})`,
574
465
  { slot, blockNumber, processedTxCount: processedTxs.length },
575
466
  );
576
467
  throw new Error(`Block has too few successful txs to be proposed`);
@@ -606,7 +497,8 @@ export class Sequencer {
606
497
  await publicProcessorFork.close();
607
498
  await orchestratorFork.close();
608
499
  } catch (err) {
609
- this.log.error(`Error closing forks`, err);
500
+ // This can happen if the sequencer is stopped before we hit this timeout.
501
+ this.log.warn(`Error closing forks for block processing`, err);
610
502
  }
611
503
  }, 5000);
612
504
  }
@@ -620,16 +512,11 @@ export class Sequencer {
620
512
  *
621
513
  * @param pendingTxs - Iterable of pending transactions to construct the block from
622
514
  * @param proposalHeader - The partial header constructed for the proposal
623
- * @param historicalHeader - The historical header of the parent
624
515
  */
625
- @trackSpan('Sequencer.buildBlockAndAttemptToPublish', (_validTxs, proposalHeader, _historicalHeader) => ({
516
+ @trackSpan('Sequencer.buildBlockAndAttemptToPublish', (_validTxs, proposalHeader) => ({
626
517
  [Attributes.BLOCK_NUMBER]: proposalHeader.globalVariables.blockNumber.toNumber(),
627
518
  }))
628
- private async buildBlockAndAttemptToPublish(
629
- pendingTxs: Iterable<Tx>,
630
- proposalHeader: BlockHeader,
631
- historicalHeader: BlockHeader | undefined,
632
- ): Promise<void> {
519
+ private async buildBlockAndAttemptToPublish(pendingTxs: Iterable<Tx>, proposalHeader: BlockHeader): Promise<void> {
633
520
  await this.publisher.validateBlockForSubmission(proposalHeader);
634
521
 
635
522
  const newGlobalVariables = proposalHeader.globalVariables;
@@ -644,7 +531,7 @@ export class Sequencer {
644
531
  const proofQuotePromise = this.createProofClaimForPreviousEpoch(slot);
645
532
 
646
533
  try {
647
- const buildBlockRes = await this.buildBlock(pendingTxs, newGlobalVariables, historicalHeader);
534
+ const buildBlockRes = await this.buildBlock(pendingTxs, newGlobalVariables);
648
535
  const { publicGas, block, publicProcessorDuration, numTxs, numMsgs, blockBuildingTimer } = buildBlockRes;
649
536
 
650
537
  // TODO(@PhilWindle) We should probably periodically check for things like another
@@ -743,14 +630,22 @@ export class Sequencer {
743
630
  this.log.debug('Creating block proposal for validators');
744
631
  const proposal = await this.validatorClient.createBlockProposal(block.header, block.archive.root, txHashes);
745
632
  if (!proposal) {
746
- this.log.warn(`Failed to create block proposal, skipping collecting attestations`);
747
- return undefined;
633
+ const msg = `Failed to create block proposal`;
634
+ throw new Error(msg);
748
635
  }
749
636
 
750
637
  this.log.debug('Broadcasting block proposal to validators');
751
638
  this.validatorClient.broadcastBlockProposal(proposal);
752
639
 
753
- const attestations = await this.validatorClient.collectAttestations(proposal, numberOfRequiredAttestations);
640
+ const attestationTimeAllowed = this.enforceTimeTable
641
+ ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_BLOCK)!
642
+ : this.aztecSlotDuration;
643
+ const attestationDeadline = new Date(this.dateProvider.now() + attestationTimeAllowed * 1000);
644
+ const attestations = await this.validatorClient.collectAttestations(
645
+ proposal,
646
+ numberOfRequiredAttestations,
647
+ attestationDeadline,
648
+ );
754
649
 
755
650
  // note: the smart contract requires that the signatures are provided in the order of the committee
756
651
  return orderAttestations(attestations, committee);
@@ -0,0 +1,123 @@
1
+ import { createLogger } from '@aztec/aztec.js';
2
+
3
+ import { type SequencerMetrics } from './metrics.js';
4
+ import { SequencerState } from './utils.js';
5
+
6
+ export class SequencerTimetable {
7
+ /** How late into the slot can we be to start working */
8
+ public readonly initialTime = 3;
9
+
10
+ /** How long it takes to get ready to start building */
11
+ public readonly blockPrepareTime = 1;
12
+
13
+ /** How long it takes to for proposals and attestations to travel across the p2p layer (one-way) */
14
+ public readonly attestationPropagationTime = 2;
15
+
16
+ /** How much time we spend validating and processing a block after building it, and assembling the proposal to send to attestors */
17
+ public readonly blockValidationTime = 1;
18
+
19
+ /**
20
+ * How long it takes to get a published block into L1. L1 builders typically accept txs up to 4 seconds into their slot,
21
+ * but we'll timeout sooner to give it more time to propagate (remember we also have blobs!). Still, when working in anvil,
22
+ * we can just post in the very last second of the L1 slot and still expect the tx to be accepted.
23
+ */
24
+ public readonly l1PublishingTime;
25
+
26
+ constructor(
27
+ private readonly ethereumSlotDuration: number,
28
+ private readonly aztecSlotDuration: number,
29
+ private readonly maxL1TxInclusionTimeIntoSlot: number,
30
+ private readonly enforce: boolean = true,
31
+ private readonly metrics?: SequencerMetrics,
32
+ private readonly log = createLogger('sequencer:timetable'),
33
+ ) {
34
+ this.l1PublishingTime = this.ethereumSlotDuration - this.maxL1TxInclusionTimeIntoSlot;
35
+ }
36
+
37
+ private get afterBlockBuildingTimeNeededWithoutReexec() {
38
+ return this.blockValidationTime + this.attestationPropagationTime * 2 + this.l1PublishingTime;
39
+ }
40
+
41
+ public getBlockProposalExecTimeEnd(secondsIntoSlot: number): number {
42
+ // We are N seconds into the slot. We need to account for `afterBlockBuildingTimeNeededWithoutReexec` seconds,
43
+ // send then split the remaining time between the re-execution and the block building.
44
+ const maxAllowed = this.aztecSlotDuration - this.afterBlockBuildingTimeNeededWithoutReexec;
45
+ const available = maxAllowed - secondsIntoSlot;
46
+ const executionTimeEnd = secondsIntoSlot + available / 2;
47
+ this.log.debug(`Block proposal execution time deadline is ${executionTimeEnd}`, {
48
+ secondsIntoSlot,
49
+ maxAllowed,
50
+ available,
51
+ executionTimeEnd,
52
+ });
53
+ return executionTimeEnd;
54
+ }
55
+
56
+ private get afterBlockReexecTimeNeeded() {
57
+ return this.attestationPropagationTime + this.l1PublishingTime;
58
+ }
59
+
60
+ public getValidatorReexecTimeEnd(secondsIntoSlot: number): number {
61
+ // We need to leave for `afterBlockReexecTimeNeeded` seconds available.
62
+ const validationTimeEnd = this.aztecSlotDuration - this.afterBlockReexecTimeNeeded;
63
+ this.log.debug(`Validator re-execution time deadline is ${validationTimeEnd}`, {
64
+ secondsIntoSlot,
65
+ validationTimeEnd,
66
+ });
67
+ return validationTimeEnd;
68
+ }
69
+
70
+ public getMaxAllowedTime(state: SequencerState): number | undefined {
71
+ switch (state) {
72
+ case SequencerState.STOPPED:
73
+ case SequencerState.IDLE:
74
+ case SequencerState.SYNCHRONIZING:
75
+ case SequencerState.PROPOSER_CHECK:
76
+ return; // We don't really care about times for this states
77
+ case SequencerState.INITIALIZING_PROPOSAL:
78
+ return this.initialTime;
79
+ case SequencerState.CREATING_BLOCK:
80
+ return this.initialTime + this.blockPrepareTime;
81
+ case SequencerState.COLLECTING_ATTESTATIONS:
82
+ return this.aztecSlotDuration - this.l1PublishingTime - 2 * this.attestationPropagationTime;
83
+ case SequencerState.PUBLISHING_BLOCK:
84
+ return this.aztecSlotDuration - this.l1PublishingTime;
85
+ default: {
86
+ const _exhaustiveCheck: never = state;
87
+ throw new Error(`Unexpected state: ${state}`);
88
+ }
89
+ }
90
+ }
91
+
92
+ public assertTimeLeft(newState: SequencerState, secondsIntoSlot: number) {
93
+ if (!this.enforce) {
94
+ return;
95
+ }
96
+
97
+ const maxAllowedTime = this.getMaxAllowedTime(newState);
98
+ if (maxAllowedTime === undefined) {
99
+ return;
100
+ }
101
+
102
+ const bufferSeconds = maxAllowedTime - secondsIntoSlot;
103
+ if (bufferSeconds < 0) {
104
+ throw new SequencerTooSlowError(newState, maxAllowedTime, secondsIntoSlot);
105
+ }
106
+
107
+ this.metrics?.recordStateTransitionBufferMs(Math.floor(bufferSeconds * 1000), newState);
108
+ this.log.trace(`Enough time to transition to ${newState}`, { maxAllowedTime, secondsIntoSlot });
109
+ }
110
+ }
111
+
112
+ export class SequencerTooSlowError extends Error {
113
+ constructor(
114
+ public readonly proposedState: SequencerState,
115
+ public readonly maxAllowedTime: number,
116
+ public readonly currentTime: number,
117
+ ) {
118
+ super(
119
+ `Too far into slot for ${proposedState} (time into slot ${currentTime}s greater than ${maxAllowedTime}s allowance)`,
120
+ );
121
+ this.name = 'SequencerTooSlowError';
122
+ }
123
+ }
@@ -4,8 +4,7 @@ import { createLogger } from '@aztec/foundation/log';
4
4
  import { type AztecKVStore } from '@aztec/kv-store';
5
5
  import { type DataStoreConfig } from '@aztec/kv-store/config';
6
6
  import { createStore } from '@aztec/kv-store/lmdb';
7
- import { type TelemetryClient } from '@aztec/telemetry-client';
8
- import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
7
+ import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
9
8
 
10
9
  import { SlasherClient } from './slasher_client.js';
11
10
  import { type SlasherConfig } from './slasher_client.js';
@@ -13,7 +12,7 @@ import { type SlasherConfig } from './slasher_client.js';
13
12
  export const createSlasherClient = async (
14
13
  _config: SlasherConfig & DataStoreConfig & L1ContractsConfig & L1ReaderConfig,
15
14
  l2BlockSource: L2BlockSource,
16
- telemetry: TelemetryClient = new NoopTelemetryClient(),
15
+ telemetry: TelemetryClient = getTelemetryClient(),
17
16
  deps: { store?: AztecKVStore } = {},
18
17
  ) => {
19
18
  const config = { ..._config };
@@ -12,8 +12,7 @@ import { EthAddress } from '@aztec/foundation/eth-address';
12
12
  import { createLogger } from '@aztec/foundation/log';
13
13
  import { type AztecKVStore, type AztecMap, type AztecSingleton } from '@aztec/kv-store';
14
14
  import { SlashFactoryAbi } from '@aztec/l1-artifacts';
15
- import { type TelemetryClient, WithTracer } from '@aztec/telemetry-client';
16
- import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
15
+ import { type TelemetryClient, WithTracer, getTelemetryClient } from '@aztec/telemetry-client';
17
16
 
18
17
  import {
19
18
  type Chain,
@@ -113,7 +112,7 @@ export class SlasherClient extends WithTracer {
113
112
  private config: SlasherConfig & L1ContractsConfig & L1ReaderConfig,
114
113
  private store: AztecKVStore,
115
114
  private l2BlockSource: L2BlockSource,
116
- telemetry: TelemetryClient = new NoopTelemetryClient(),
115
+ telemetry: TelemetryClient = getTelemetryClient(),
117
116
  private log = createLogger('slasher'),
118
117
  ) {
119
118
  super(telemetry, 'slasher');
package/src/test/index.ts CHANGED
@@ -3,12 +3,11 @@ import { type PublicProcessorFactory } from '@aztec/simulator/server';
3
3
  import { SequencerClient } from '../client/sequencer-client.js';
4
4
  import { type L1Publisher } from '../publisher/l1-publisher.js';
5
5
  import { Sequencer } from '../sequencer/sequencer.js';
6
- import { type SequencerState } from '../sequencer/utils.js';
6
+ import { type SequencerTimetable } from '../sequencer/timetable.js';
7
7
 
8
8
  class TestSequencer_ extends Sequencer {
9
9
  public override publicProcessorFactory!: PublicProcessorFactory;
10
- public override timeTable!: Record<SequencerState, number>;
11
- public override processTxTime!: number;
10
+ public override timetable!: SequencerTimetable;
12
11
  public override publisher!: L1Publisher;
13
12
  }
14
13
 
@@ -1,5 +1,6 @@
1
1
  import { type Tx, TxExecutionPhase, type TxValidationResult, type TxValidator } from '@aztec/circuit-types';
2
- import { type AztecAddress, type Fr, FunctionSelector, type GasFees } from '@aztec/circuits.js';
2
+ import { type AztecAddress, Fr, FunctionSelector, type GasFees } from '@aztec/circuits.js';
3
+ import { U128 } from '@aztec/foundation/abi';
3
4
  import { createLogger } from '@aztec/foundation/log';
4
5
  import { computeFeePayerBalanceStorageSlot, getExecutionRequestsByPhase } from '@aztec/simulator/server';
5
6
 
@@ -34,6 +35,12 @@ export class GasTxValidator implements TxValidator<Tx> {
34
35
  return this.#validateTxFee(tx);
35
36
  }
36
37
 
38
+ /**
39
+ * Check whether the tx's max fees are valid for the current block, and skip if not.
40
+ * We skip instead of invalidating since the tx may become elligible later.
41
+ * Note that circuits check max fees even if fee payer is unset, so we
42
+ * keep this validation even if the tx does not pay fees.
43
+ */
37
44
  #shouldSkip(tx: Tx): boolean {
38
45
  const gasSettings = tx.data.constants.txContext.gasSettings;
39
46
 
@@ -78,12 +85,17 @@ export class GasTxValidator implements TxValidator<Tx> {
78
85
  fn.callContext.msgSender.equals(this.#feeJuiceAddress) &&
79
86
  fn.args.length > 2 &&
80
87
  // Public functions get routed through the dispatch function, whose first argument is the target function selector.
81
- fn.args[0].equals(FunctionSelector.fromSignature('_increase_public_balance((Field),Field)').toField()) &&
88
+ fn.args[0].equals(
89
+ FunctionSelector.fromSignature('_increase_public_balance((Field),(Field,Field))').toField(),
90
+ ) &&
82
91
  fn.args[1].equals(feePayer.toField()) &&
83
92
  !fn.callContext.isStaticCall,
84
93
  );
85
94
 
86
- const balance = claimFunctionCall ? initialBalance.add(claimFunctionCall.args[2]) : initialBalance;
95
+ // `amount` in the claim function call arguments occupies 2 fields as it is represented as U128.
96
+ const balance = claimFunctionCall
97
+ ? initialBalance.add(new Fr(U128.fromFields(claimFunctionCall.args.slice(2, 4)).toInteger()))
98
+ : initialBalance;
87
99
  if (balance.lt(feeLimit)) {
88
100
  this.#log.warn(`Rejecting transaction due to not enough fee payer balance`, {
89
101
  feePayer,