@aztec/sequencer-client 0.0.1-commit.e6bd8901 → 0.0.1-commit.f146247c

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,16 +1,22 @@
1
1
  import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
2
2
  import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
3
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
- import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
4
+ import {
5
+ BlockNumber,
6
+ CheckpointNumber,
7
+ EpochNumber,
8
+ IndexWithinCheckpoint,
9
+ SlotNumber,
10
+ } from '@aztec/foundation/branded-types';
5
11
  import { randomInt } from '@aztec/foundation/crypto/random';
6
12
  import { Fr } from '@aztec/foundation/curves/bn254';
7
13
  import { EthAddress } from '@aztec/foundation/eth-address';
8
14
  import { Signature } from '@aztec/foundation/eth-signature';
9
15
  import { filter } from '@aztec/foundation/iterator';
10
- import type { Logger } from '@aztec/foundation/log';
16
+ import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
11
17
  import { sleep, sleepUntil } from '@aztec/foundation/sleep';
12
18
  import { type DateProvider, Timer } from '@aztec/foundation/timer';
13
- import { type TypedEventEmitter, unfreeze } from '@aztec/foundation/types';
19
+ import { type TypedEventEmitter, isErrorClass, unfreeze } from '@aztec/foundation/types';
14
20
  import type { P2P } from '@aztec/p2p';
15
21
  import type { SlasherClientInterface } from '@aztec/slasher';
16
22
  import {
@@ -24,10 +30,11 @@ import {
24
30
  import type { Checkpoint } from '@aztec/stdlib/checkpoint';
25
31
  import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
26
32
  import { Gas } from '@aztec/stdlib/gas';
27
- import type {
28
- PublicProcessorLimits,
29
- ResolvedSequencerConfig,
30
- WorldStateSynchronizer,
33
+ import {
34
+ NoValidTxsError,
35
+ type PublicProcessorLimits,
36
+ type ResolvedSequencerConfig,
37
+ type WorldStateSynchronizer,
31
38
  } from '@aztec/stdlib/interfaces/server';
32
39
  import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
33
40
  import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
@@ -59,6 +66,8 @@ const TXS_POLLING_MS = 500;
59
66
  * the Sequencer once the check for being the proposer for the slot has succeeded.
60
67
  */
61
68
  export class CheckpointProposalJob implements Traceable {
69
+ protected readonly log: Logger;
70
+
62
71
  constructor(
63
72
  private readonly epoch: EpochNumber,
64
73
  private readonly slot: SlotNumber,
@@ -86,9 +95,11 @@ export class CheckpointProposalJob implements Traceable {
86
95
  private readonly metrics: SequencerMetrics,
87
96
  private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
88
97
  private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
89
- protected readonly log: Logger,
90
98
  public readonly tracer: Tracer,
91
- ) {}
99
+ bindings?: LoggerBindings,
100
+ ) {
101
+ this.log = createLogger('sequencer:checkpoint-proposal', { ...bindings, instanceId: `slot-${slot}` });
102
+ }
92
103
 
93
104
  /**
94
105
  * Executes the checkpoint proposal job.
@@ -190,6 +201,7 @@ export class CheckpointProposalJob implements Traceable {
190
201
  l1ToL2Messages,
191
202
  previousCheckpointOutHashes,
192
203
  fork,
204
+ this.log.getBindings(),
193
205
  );
194
206
 
195
207
  // Options for the validator client when creating block and checkpoint proposals
@@ -367,7 +379,7 @@ export class CheckpointProposalJob implements Traceable {
367
379
 
368
380
  while (true) {
369
381
  const blocksBuilt = blocksInCheckpoint.length;
370
- const indexWithinCheckpoint = blocksBuilt;
382
+ const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
371
383
  const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
372
384
 
373
385
  const secondsIntoSlot = this.getSecondsIntoSlot();
@@ -397,6 +409,7 @@ export class CheckpointProposalJob implements Traceable {
397
409
  remainingBlobFields,
398
410
  });
399
411
 
412
+ // TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
400
413
  if (!buildResult && timingInfo.isLastBlock) {
401
414
  // If no block was produced due to not enough txs and this was the last subslot, exit
402
415
  break;
@@ -433,6 +446,8 @@ export class CheckpointProposalJob implements Traceable {
433
446
  this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
434
447
  });
435
448
 
449
+ usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
450
+
436
451
  // If this is the last block, exit the loop now so we start collecting attestations
437
452
  if (timingInfo.isLastBlock) {
438
453
  this.log.verbose(`Completed final block ${blockNumber} for slot ${this.slot}`, {
@@ -481,13 +496,13 @@ export class CheckpointProposalJob implements Traceable {
481
496
 
482
497
  /** Builds a single block. Called from the main block building loop. */
483
498
  @trackSpan('CheckpointProposalJob.buildSingleBlock')
484
- private async buildSingleBlock(
499
+ protected async buildSingleBlock(
485
500
  checkpointBuilder: CheckpointBuilder,
486
501
  opts: {
487
502
  forceCreate?: boolean;
488
503
  blockTimestamp: bigint;
489
504
  blockNumber: BlockNumber;
490
- indexWithinCheckpoint: number;
505
+ indexWithinCheckpoint: IndexWithinCheckpoint;
491
506
  buildDeadline: Date | undefined;
492
507
  txHashesAlreadyIncluded: Set<string>;
493
508
  remainingBlobFields: number;
@@ -548,45 +563,38 @@ export class CheckpointProposalJob implements Traceable {
548
563
  };
549
564
 
550
565
  // Actually build the block by executing txs
551
- const workTimer = new Timer();
552
- const {
553
- publicGas,
554
- block,
555
- publicProcessorDuration,
556
- numTxs,
557
- blockBuildingTimer,
558
- usedTxs,
559
- failedTxs,
560
- usedTxBlobFields,
561
- } = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
562
- const blockBuildDuration = workTimer.ms();
566
+ const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
567
+ checkpointBuilder,
568
+ pendingTxs,
569
+ blockNumber,
570
+ blockTimestamp,
571
+ blockBuilderOptions,
572
+ );
563
573
 
564
574
  // If any txs failed during execution, drop them from the mempool so we don't pick them up again
565
- await this.dropFailedTxsFromP2P(failedTxs);
575
+ await this.dropFailedTxsFromP2P(buildResult.failedTxs);
566
576
 
567
577
  // Check if we have created a block with enough txs. If there were invalid txs in the pool, or if execution took
568
578
  // too long, then we may not get to minTxsPerBlock after executing public functions.
569
579
  const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
570
- if (!forceCreate && numTxs < minValidTxs) {
580
+ const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
581
+ if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
571
582
  this.log.warn(
572
- `Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed (got ${numTxs} but required ${minValidTxs})`,
573
- { slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint },
583
+ `Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
584
+ { slot: this.slot, blockNumber, numTxs, indexWithinCheckpoint, minValidTxs, buildResult: buildResult.status },
574
585
  );
575
- this.eventEmitter.emit('block-tx-count-check-failed', {
576
- minTxs: minValidTxs,
577
- availableTxs: numTxs,
578
- slot: this.slot,
579
- });
586
+ this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
580
587
  this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
581
588
  return undefined;
582
589
  }
583
590
 
584
591
  // Block creation succeeded, emit stats and metrics
592
+ const { publicGas, block, publicProcessorDuration, usedTxs, usedTxBlobFields, blockBuildDuration } = buildResult;
593
+
585
594
  const blockStats = {
586
595
  eventName: 'l2-block-built',
587
596
  duration: blockBuildDuration,
588
597
  publicProcessDuration: publicProcessorDuration,
589
- rollupCircuitsDuration: blockBuildingTimer.ms(),
590
598
  ...block.getStats(),
591
599
  } satisfies L2BlockBuiltStats;
592
600
 
@@ -612,17 +620,40 @@ export class CheckpointProposalJob implements Traceable {
612
620
  }
613
621
  }
614
622
 
623
+ /** Uses the checkpoint builder to build a block, catching specific txs */
624
+ private async buildSingleBlockWithCheckpointBuilder(
625
+ checkpointBuilder: CheckpointBuilder,
626
+ pendingTxs: AsyncIterable<Tx>,
627
+ blockNumber: BlockNumber,
628
+ blockTimestamp: bigint,
629
+ blockBuilderOptions: PublicProcessorLimits,
630
+ ) {
631
+ try {
632
+ const workTimer = new Timer();
633
+ const result = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
634
+ const blockBuildDuration = workTimer.ms();
635
+ return { ...result, blockBuildDuration, status: 'success' as const };
636
+ } catch (err: unknown) {
637
+ if (isErrorClass(err, NoValidTxsError)) {
638
+ return { failedTxs: err.failedTxs, status: 'no-valid-txs' as const };
639
+ }
640
+ throw err;
641
+ }
642
+ }
643
+
615
644
  /** Waits until minTxs are available on the pool for building a block. */
616
645
  @trackSpan('CheckpointProposalJob.waitForMinTxs')
617
646
  private async waitForMinTxs(opts: {
618
647
  forceCreate?: boolean;
619
648
  blockNumber: BlockNumber;
620
- indexWithinCheckpoint: number;
649
+ indexWithinCheckpoint: IndexWithinCheckpoint;
621
650
  buildDeadline: Date | undefined;
622
651
  }): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
623
- const minTxs = this.config.minTxsPerBlock;
624
652
  const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
625
653
 
654
+ // We only allow a block with 0 txs in the first block of the checkpoint
655
+ const minTxs = indexWithinCheckpoint > 0 && this.config.minTxsPerBlock === 0 ? 1 : this.config.minTxsPerBlock;
656
+
626
657
  // Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
627
658
  const startBuildingDeadline = buildDeadline
628
659
  ? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000)
@@ -684,7 +715,7 @@ export class CheckpointProposalJob implements Traceable {
684
715
  const attestationTimeAllowed = this.config.enforceTimeTable
685
716
  ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_CHECKPOINT)!
686
717
  : this.l1Constants.slotDuration;
687
- const attestationDeadline = new Date(this.dateProvider.now() + attestationTimeAllowed * 1000);
718
+ const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
688
719
 
689
720
  this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
690
721
 
@@ -11,6 +11,7 @@ import {
11
11
  type TelemetryClient,
12
12
  type Tracer,
13
13
  type UpDownCounter,
14
+ createUpDownCounterWithDefault,
14
15
  } from '@aztec/telemetry-client';
15
16
 
16
17
  import { type Hex, formatUnits } from 'viem';
@@ -67,7 +68,9 @@ export class SequencerMetrics {
67
68
  this.meter = client.getMeter(name);
68
69
  this.tracer = client.getTracer(name);
69
70
 
70
- this.blockCounter = this.meter.createUpDownCounter(Metrics.SEQUENCER_BLOCK_COUNT);
71
+ this.blockCounter = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_BLOCK_COUNT, {
72
+ [Attributes.STATUS]: ['failed', 'built'],
73
+ });
71
74
 
72
75
  this.blockBuildDuration = this.meter.createHistogram(Metrics.SEQUENCER_BLOCK_BUILD_DURATION);
73
76
 
@@ -77,23 +80,15 @@ export class SequencerMetrics {
77
80
 
78
81
  this.checkpointAttestationDelay = this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_ATTESTATION_DELAY);
79
82
 
80
- // Init gauges and counters
81
- this.blockCounter.add(0, {
82
- [Attributes.STATUS]: 'failed',
83
- });
84
- this.blockCounter.add(0, {
85
- [Attributes.STATUS]: 'built',
86
- });
87
-
88
83
  this.rewards = this.meter.createGauge(Metrics.SEQUENCER_CURRENT_BLOCK_REWARDS);
89
84
 
90
- this.slots = this.meter.createUpDownCounter(Metrics.SEQUENCER_SLOT_COUNT);
85
+ this.slots = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLOT_COUNT);
91
86
 
92
87
  /**
93
88
  * NOTE: we do not track missed slots as a separate metric. That would be difficult to determine
94
89
  * Instead, use a computed metric, `slots - filledSlots` to get the number of slots a sequencer has missed.
95
90
  */
96
- this.filledSlots = this.meter.createUpDownCounter(Metrics.SEQUENCER_FILLED_SLOT_COUNT);
91
+ this.filledSlots = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_FILLED_SLOT_COUNT);
97
92
 
98
93
  this.timeToCollectAttestations = this.meter.createGauge(Metrics.SEQUENCER_COLLECT_ATTESTATIONS_DURATION);
99
94
 
@@ -103,20 +98,41 @@ export class SequencerMetrics {
103
98
 
104
99
  this.collectedAttestions = this.meter.createGauge(Metrics.SEQUENCER_COLLECTED_ATTESTATIONS_COUNT);
105
100
 
106
- this.blockProposalFailed = this.meter.createUpDownCounter(Metrics.SEQUENCER_BLOCK_PROPOSAL_FAILED_COUNT);
101
+ this.blockProposalFailed = createUpDownCounterWithDefault(
102
+ this.meter,
103
+ Metrics.SEQUENCER_BLOCK_PROPOSAL_FAILED_COUNT,
104
+ );
107
105
 
108
- this.blockProposalSuccess = this.meter.createUpDownCounter(Metrics.SEQUENCER_BLOCK_PROPOSAL_SUCCESS_COUNT);
106
+ this.blockProposalSuccess = createUpDownCounterWithDefault(
107
+ this.meter,
108
+ Metrics.SEQUENCER_BLOCK_PROPOSAL_SUCCESS_COUNT,
109
+ );
109
110
 
110
- this.checkpointSuccess = this.meter.createUpDownCounter(Metrics.SEQUENCER_CHECKPOINT_SUCCESS_COUNT);
111
+ this.checkpointSuccess = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_CHECKPOINT_SUCCESS_COUNT);
111
112
 
112
- this.blockProposalPrecheckFailed = this.meter.createUpDownCounter(
113
+ this.blockProposalPrecheckFailed = createUpDownCounterWithDefault(
114
+ this.meter,
113
115
  Metrics.SEQUENCER_BLOCK_PROPOSAL_PRECHECK_FAILED_COUNT,
116
+ {
117
+ [Attributes.ERROR_TYPE]: [
118
+ 'slot_already_taken',
119
+ 'rollup_contract_check_failed',
120
+ 'slot_mismatch',
121
+ 'block_number_mismatch',
122
+ ],
123
+ },
114
124
  );
115
125
 
116
- this.slashingAttempts = this.meter.createUpDownCounter(Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
126
+ this.slashingAttempts = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
117
127
 
118
128
  // Fisherman fee analysis metrics
119
- this.fishermanWouldBeIncluded = this.meter.createUpDownCounter(Metrics.FISHERMAN_FEE_ANALYSIS_WOULD_BE_INCLUDED);
129
+ this.fishermanWouldBeIncluded = createUpDownCounterWithDefault(
130
+ this.meter,
131
+ Metrics.FISHERMAN_FEE_ANALYSIS_WOULD_BE_INCLUDED,
132
+ {
133
+ [Attributes.OK]: [true, false],
134
+ },
135
+ );
120
136
 
121
137
  this.fishermanTimeBeforeBlock = this.meter.createHistogram(Metrics.FISHERMAN_FEE_ANALYSIS_TIME_BEFORE_BLOCK);
122
138
 
@@ -231,7 +247,9 @@ export class SequencerMetrics {
231
247
  this.blockProposalSuccess.add(1);
232
248
  }
233
249
 
234
- recordBlockProposalPrecheckFailed(checkType: string) {
250
+ recordBlockProposalPrecheckFailed(
251
+ checkType: 'slot_already_taken' | 'rollup_contract_check_failed' | 'slot_mismatch' | 'block_number_mismatch',
252
+ ) {
235
253
  this.blockProposalPrecheckFailed.add(1, {
236
254
  [Attributes.ERROR_TYPE]: checkType,
237
255
  });
@@ -424,8 +424,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
424
424
  this.metrics,
425
425
  this,
426
426
  this.setState.bind(this),
427
- this.log,
428
427
  this.tracer,
428
+ this.log.getBindings(),
429
429
  );
430
430
  }
431
431
 
@@ -1,11 +1,9 @@
1
1
  import { type BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
2
2
  import { Fr } from '@aztec/foundation/curves/bn254';
3
- import { Timer } from '@aztec/foundation/timer';
4
3
  import { L2Block } from '@aztec/stdlib/block';
5
4
  import { Checkpoint } from '@aztec/stdlib/checkpoint';
6
5
  import { Gas } from '@aztec/stdlib/gas';
7
6
  import type {
8
- BuildBlockInCheckpointResult,
9
7
  FullNodeBlockBuilderConfig,
10
8
  ICheckpointBlockBuilder,
11
9
  ICheckpointsBuilder,
@@ -15,6 +13,7 @@ import type {
15
13
  import { CheckpointHeader } from '@aztec/stdlib/rollup';
16
14
  import { makeAppendOnlyTreeSnapshot } from '@aztec/stdlib/testing';
17
15
  import type { CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
16
+ import type { BuildBlockInCheckpointResult } from '@aztec/validator-client';
18
17
 
19
18
  /**
20
19
  * A fake CheckpointBuilder for testing that implements the same interface as the real one.
@@ -35,6 +34,8 @@ export class MockCheckpointBuilder implements ICheckpointBlockBuilder {
35
34
  timestamp: bigint;
36
35
  opts: PublicProcessorLimits;
37
36
  }> = [];
37
+ /** Track all consumed transaction hashes across buildBlock calls */
38
+ public consumedTxHashes: Set<string> = new Set();
38
39
  public completeCheckpointCalled = false;
39
40
  public getCheckpointCalled = false;
40
41
 
@@ -69,8 +70,8 @@ export class MockCheckpointBuilder implements ICheckpointBlockBuilder {
69
70
  return this.constants;
70
71
  }
71
72
 
72
- buildBlock(
73
- _pendingTxs: Iterable<Tx> | AsyncIterable<Tx>,
73
+ async buildBlock(
74
+ pendingTxs: Iterable<Tx> | AsyncIterable<Tx>,
74
75
  blockNumber: BlockNumber,
75
76
  timestamp: bigint,
76
77
  opts: PublicProcessorLimits,
@@ -78,7 +79,7 @@ export class MockCheckpointBuilder implements ICheckpointBlockBuilder {
78
79
  this.buildBlockCalls.push({ blockNumber, timestamp, opts });
79
80
 
80
81
  if (this.errorOnBuild) {
81
- return Promise.reject(this.errorOnBuild);
82
+ throw this.errorOnBuild;
82
83
  }
83
84
 
84
85
  let block: L2Block;
@@ -97,16 +98,28 @@ export class MockCheckpointBuilder implements ICheckpointBlockBuilder {
97
98
  this.builtBlocks.push(block);
98
99
  }
99
100
 
100
- return Promise.resolve({
101
+ // Check that no pending tx has already been consumed
102
+ for await (const tx of pendingTxs) {
103
+ const hash = tx.getTxHash().toString();
104
+ if (this.consumedTxHashes.has(hash)) {
105
+ throw new Error(`Transaction ${hash} was already consumed in a previous block`);
106
+ }
107
+ }
108
+
109
+ // Add used txs to consumed set
110
+ for (const tx of usedTxs) {
111
+ this.consumedTxHashes.add(tx.getTxHash().toString());
112
+ }
113
+
114
+ return {
101
115
  block,
102
116
  publicGas: Gas.empty(),
103
117
  publicProcessorDuration: 0,
104
118
  numTxs: block?.body?.txEffects?.length ?? usedTxs.length,
105
- blockBuildingTimer: new Timer(),
106
119
  usedTxs,
107
120
  failedTxs: [],
108
121
  usedTxBlobFields: block?.body?.txEffects?.reduce((sum, tx) => sum + tx.getNumBlobFields(), 0) ?? 0,
109
- });
122
+ };
110
123
  }
111
124
 
112
125
  completeCheckpoint(): Promise<Checkpoint> {
@@ -170,6 +183,7 @@ export class MockCheckpointBuilder implements ICheckpointBlockBuilder {
170
183
  this.usedTxsPerBlock = [];
171
184
  this.blockIndex = 0;
172
185
  this.buildBlockCalls = [];
186
+ this.consumedTxHashes.clear();
173
187
  this.completeCheckpointCalled = false;
174
188
  this.getCheckpointCalled = false;
175
189
  this.errorOnBuild = undefined;