@aztec/sequencer-client 0.0.1-commit.d431d1c → 0.0.1-commit.d58ff9d0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +281 -21
  2. package/dest/client/sequencer-client.d.ts +34 -11
  3. package/dest/client/sequencer-client.d.ts.map +1 -1
  4. package/dest/client/sequencer-client.js +77 -39
  5. package/dest/config.d.ts +32 -6
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +94 -48
  8. package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
  9. package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
  10. package/dest/global_variable_builder/fee_predictor.js +138 -0
  11. package/dest/global_variable_builder/fee_provider.d.ts +21 -0
  12. package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
  13. package/dest/global_variable_builder/fee_provider.js +80 -0
  14. package/dest/global_variable_builder/global_builder.d.ts +13 -28
  15. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  16. package/dest/global_variable_builder/global_builder.js +9 -66
  17. package/dest/global_variable_builder/index.d.ts +4 -2
  18. package/dest/global_variable_builder/index.d.ts.map +1 -1
  19. package/dest/global_variable_builder/index.js +2 -0
  20. package/dest/publisher/config.d.ts +51 -17
  21. package/dest/publisher/config.d.ts.map +1 -1
  22. package/dest/publisher/config.js +131 -42
  23. package/dest/publisher/index.d.ts +2 -1
  24. package/dest/publisher/index.d.ts.map +1 -1
  25. package/dest/publisher/l1_to_l2_messaging.d.ts +21 -0
  26. package/dest/publisher/l1_to_l2_messaging.d.ts.map +1 -0
  27. package/dest/publisher/l1_to_l2_messaging.js +70 -0
  28. package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
  29. package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
  30. package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
  31. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +98 -0
  32. package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
  33. package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +68 -0
  34. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
  35. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
  36. package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +36 -0
  37. package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
  38. package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
  39. package/dest/publisher/l1_tx_failed_store/index.js +3 -0
  40. package/dest/publisher/sequencer-bundle-simulator.d.ts +96 -0
  41. package/dest/publisher/sequencer-bundle-simulator.d.ts.map +1 -0
  42. package/dest/publisher/sequencer-bundle-simulator.js +198 -0
  43. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -5
  44. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  45. package/dest/publisher/sequencer-publisher-factory.js +27 -3
  46. package/dest/publisher/sequencer-publisher-metrics.d.ts +1 -1
  47. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  48. package/dest/publisher/sequencer-publisher-metrics.js +12 -4
  49. package/dest/publisher/sequencer-publisher.d.ts +122 -68
  50. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  51. package/dest/publisher/sequencer-publisher.js +750 -364
  52. package/dest/publisher/write_json.d.ts +11 -0
  53. package/dest/publisher/write_json.d.ts.map +1 -0
  54. package/dest/publisher/write_json.js +57 -0
  55. package/dest/sequencer/automine/automine_factory.d.ts +56 -0
  56. package/dest/sequencer/automine/automine_factory.d.ts.map +1 -0
  57. package/dest/sequencer/automine/automine_factory.js +85 -0
  58. package/dest/sequencer/automine/automine_sequencer.d.ts +189 -0
  59. package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -0
  60. package/dest/sequencer/automine/automine_sequencer.js +690 -0
  61. package/dest/sequencer/automine/index.d.ts +3 -0
  62. package/dest/sequencer/automine/index.d.ts.map +1 -0
  63. package/dest/sequencer/automine/index.js +2 -0
  64. package/dest/sequencer/checkpoint_proposal_job.d.ts +101 -22
  65. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
  66. package/dest/sequencer/checkpoint_proposal_job.js +922 -280
  67. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
  68. package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
  69. package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
  70. package/dest/sequencer/checkpoint_voter.d.ts +1 -2
  71. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
  72. package/dest/sequencer/checkpoint_voter.js +2 -5
  73. package/dest/sequencer/events.d.ts +62 -5
  74. package/dest/sequencer/events.d.ts.map +1 -1
  75. package/dest/sequencer/metrics.d.ts +23 -8
  76. package/dest/sequencer/metrics.d.ts.map +1 -1
  77. package/dest/sequencer/metrics.js +142 -36
  78. package/dest/sequencer/requests_tracker.d.ts +22 -0
  79. package/dest/sequencer/requests_tracker.d.ts.map +1 -0
  80. package/dest/sequencer/requests_tracker.js +33 -0
  81. package/dest/sequencer/sequencer.d.ts +181 -44
  82. package/dest/sequencer/sequencer.d.ts.map +1 -1
  83. package/dest/sequencer/sequencer.js +614 -216
  84. package/dest/sequencer/types.d.ts +2 -2
  85. package/dest/sequencer/types.d.ts.map +1 -1
  86. package/dest/test/index.d.ts +5 -7
  87. package/dest/test/index.d.ts.map +1 -1
  88. package/dest/test/mock_checkpoint_builder.d.ts +18 -15
  89. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
  90. package/dest/test/mock_checkpoint_builder.js +63 -40
  91. package/dest/test/utils.d.ts +22 -8
  92. package/dest/test/utils.d.ts.map +1 -1
  93. package/dest/test/utils.js +33 -14
  94. package/package.json +30 -29
  95. package/src/client/sequencer-client.ts +110 -47
  96. package/src/config.ts +121 -58
  97. package/src/global_variable_builder/README.md +44 -0
  98. package/src/global_variable_builder/fee_predictor.ts +182 -0
  99. package/src/global_variable_builder/fee_provider.ts +97 -0
  100. package/src/global_variable_builder/global_builder.ts +20 -89
  101. package/src/global_variable_builder/index.ts +3 -1
  102. package/src/publisher/config.ts +173 -40
  103. package/src/publisher/index.ts +3 -0
  104. package/src/publisher/l1_to_l2_messaging.ts +85 -0
  105. package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
  106. package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +164 -0
  107. package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +47 -0
  108. package/src/publisher/l1_tx_failed_store/index.ts +3 -0
  109. package/src/publisher/sequencer-bundle-simulator.ts +254 -0
  110. package/src/publisher/sequencer-publisher-factory.ts +38 -9
  111. package/src/publisher/sequencer-publisher-metrics.ts +7 -3
  112. package/src/publisher/sequencer-publisher.ts +854 -461
  113. package/src/publisher/write_json.ts +78 -0
  114. package/src/sequencer/README.md +162 -450
  115. package/src/sequencer/automine/README.md +60 -0
  116. package/src/sequencer/automine/automine_factory.ts +152 -0
  117. package/src/sequencer/automine/automine_sequencer.ts +796 -0
  118. package/src/sequencer/automine/index.ts +6 -0
  119. package/src/sequencer/checkpoint_proposal_job.ts +1139 -328
  120. package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
  121. package/src/sequencer/checkpoint_voter.ts +1 -12
  122. package/src/sequencer/events.ts +66 -5
  123. package/src/sequencer/metrics.ts +164 -39
  124. package/src/sequencer/requests_tracker.ts +43 -0
  125. package/src/sequencer/sequencer.ts +729 -242
  126. package/src/sequencer/types.ts +1 -1
  127. package/src/test/index.ts +4 -6
  128. package/src/test/mock_checkpoint_builder.ts +93 -65
  129. package/src/test/utils.ts +81 -21
  130. package/dest/sequencer/timetable.d.ts +0 -90
  131. package/dest/sequencer/timetable.d.ts.map +0 -1
  132. package/dest/sequencer/timetable.js +0 -226
  133. package/src/sequencer/timetable.ts +0 -282
@@ -0,0 +1,128 @@
1
+ import {
2
+ Attributes,
3
+ type Gauge,
4
+ type Histogram,
5
+ type Meter,
6
+ Metrics,
7
+ type TelemetryClient,
8
+ } from '@aztec/telemetry-client';
9
+
10
+ type CheckpointProposalJobInstruments = {
11
+ checkpointAttestationDelay: Histogram;
12
+ checkpointBuildDuration: Histogram;
13
+ checkpointStartToFirstBlockDuration: Histogram;
14
+ checkpointLastBlockToBroadcastDuration: Histogram;
15
+ pipelinedCheckpointBuildStartOffsetFromSlotBoundary: Histogram;
16
+ checkpointBlockCount: Gauge;
17
+ checkpointTxCount: Gauge;
18
+ checkpointTotalMana: Gauge;
19
+ };
20
+
21
+ /**
22
+ * Per-job recording surface used by {@link CheckpointProposalJob}.
23
+ *
24
+ * Ownership split:
25
+ * - {@link CheckpointProposalJobMetrics} owns the OpenTelemetry meter and instruments and should live for the
26
+ * lifetime of the sequencer process.
27
+ * - A recorder owns only mutable timing state for one checkpoint proposal job so overlapping jobs cannot
28
+ * overwrite each other's timing markers.
29
+ */
30
+ export interface CheckpointProposalJobMetricsRecorder {
31
+ recordCheckpointAttestationDelay(durationMs: number): void;
32
+ recordCheckpointBuild(durationMs: number, blockCount: number, txCount: number, totalMana: number): void;
33
+ recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(offsetMs: number): void;
34
+ startCheckpointTiming(nowMs: number): void;
35
+ noteCheckpointBlockBuilt(nowMs: number, opts: { isFirstBlock: boolean; isLastBlock: boolean }): void;
36
+ noteCheckpointBroadcast(nowMs: number): void;
37
+ }
38
+
39
+ /**
40
+ * Concrete per-job recorder.
41
+ *
42
+ * This class should be short-lived: create one recorder per checkpoint proposal job and discard it when the job
43
+ * completes. It intentionally does not create instruments; it only holds the job-local timestamps needed to derive
44
+ * timing metrics safely.
45
+ */
46
+ class CheckpointProposalJobMetricsRecorderImpl implements CheckpointProposalJobMetricsRecorder {
47
+ private checkpointStartedAt?: number;
48
+ private checkpointLastBlockBuiltAt?: number;
49
+
50
+ constructor(private readonly instruments: CheckpointProposalJobInstruments) {}
51
+
52
+ public recordCheckpointAttestationDelay(durationMs: number) {
53
+ this.instruments.checkpointAttestationDelay.record(Math.ceil(durationMs));
54
+ }
55
+
56
+ public recordCheckpointBuild(durationMs: number, blockCount: number, txCount: number, totalMana: number) {
57
+ this.instruments.checkpointBuildDuration.record(Math.ceil(durationMs));
58
+ this.instruments.checkpointBlockCount.record(blockCount);
59
+ this.instruments.checkpointTxCount.record(txCount);
60
+ this.instruments.checkpointTotalMana.record(totalMana);
61
+ }
62
+
63
+ public recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(offsetMs: number) {
64
+ this.instruments.pipelinedCheckpointBuildStartOffsetFromSlotBoundary.record(Math.ceil(Math.abs(offsetMs)), {
65
+ [Attributes.SLOT_BOUNDARY_SIDE]: offsetMs < 0 ? 'before' : 'after',
66
+ });
67
+ }
68
+
69
+ public startCheckpointTiming(nowMs: number) {
70
+ this.checkpointStartedAt = nowMs;
71
+ this.checkpointLastBlockBuiltAt = undefined;
72
+ }
73
+
74
+ public noteCheckpointBlockBuilt(nowMs: number, opts: { isFirstBlock: boolean; isLastBlock: boolean }) {
75
+ if (opts.isFirstBlock && this.checkpointStartedAt !== undefined) {
76
+ this.instruments.checkpointStartToFirstBlockDuration.record(Math.ceil(nowMs - this.checkpointStartedAt));
77
+ }
78
+
79
+ if (opts.isLastBlock) {
80
+ this.checkpointLastBlockBuiltAt = nowMs;
81
+ }
82
+ }
83
+
84
+ public noteCheckpointBroadcast(nowMs: number) {
85
+ if (this.checkpointLastBlockBuiltAt !== undefined) {
86
+ this.instruments.checkpointLastBlockToBroadcastDuration.record(
87
+ Math.ceil(nowMs - this.checkpointLastBlockBuiltAt),
88
+ );
89
+ }
90
+
91
+ this.checkpointLastBlockBuiltAt = undefined;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Long-lived owner of checkpoint proposal job telemetry instruments.
97
+ *
98
+ * The sequencer should construct this once and reuse it across jobs. Each call to {@link createRecorder} returns a
99
+ * lightweight recorder with isolated mutable state for a single checkpoint proposal job.
100
+ */
101
+ export class CheckpointProposalJobMetrics {
102
+ private readonly meter: Meter;
103
+ private readonly instruments: CheckpointProposalJobInstruments;
104
+
105
+ constructor(client: TelemetryClient, name = 'CheckpointProposalJob') {
106
+ this.meter = client.getMeter(name);
107
+ this.instruments = {
108
+ checkpointAttestationDelay: this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_ATTESTATION_DELAY),
109
+ checkpointBuildDuration: this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_BUILD_DURATION),
110
+ checkpointStartToFirstBlockDuration: this.meter.createHistogram(
111
+ Metrics.SEQUENCER_CHECKPOINT_START_TO_FIRST_BLOCK_DURATION,
112
+ ),
113
+ checkpointLastBlockToBroadcastDuration: this.meter.createHistogram(
114
+ Metrics.SEQUENCER_CHECKPOINT_LAST_BLOCK_TO_BROADCAST_DURATION,
115
+ ),
116
+ pipelinedCheckpointBuildStartOffsetFromSlotBoundary: this.meter.createHistogram(
117
+ Metrics.SEQUENCER_PIPELINED_CHECKPOINT_BUILD_START_OFFSET_FROM_SLOT_BOUNDARY,
118
+ ),
119
+ checkpointBlockCount: this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_BLOCK_COUNT),
120
+ checkpointTxCount: this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_TX_COUNT),
121
+ checkpointTotalMana: this.meter.createGauge(Metrics.SEQUENCER_CHECKPOINT_TOTAL_MANA),
122
+ };
123
+ }
124
+
125
+ public createRecorder(): CheckpointProposalJobMetricsRecorder {
126
+ return new CheckpointProposalJobMetricsRecorderImpl(this.instruments);
127
+ }
128
+ }
@@ -2,7 +2,6 @@ import type { SlotNumber } from '@aztec/foundation/branded-types';
2
2
  import type { EthAddress } from '@aztec/foundation/eth-address';
3
3
  import type { Logger } from '@aztec/foundation/log';
4
4
  import type { SlasherClientInterface } from '@aztec/slasher';
5
- import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
6
5
  import type { ResolvedSequencerConfig } from '@aztec/stdlib/interfaces/server';
7
6
  import type { ValidatorClient } from '@aztec/validator-client';
8
7
  import { DutyAlreadySignedError } from '@aztec/validator-ha-signer/errors';
@@ -18,7 +17,6 @@ import type { SequencerRollupConstants } from './types.js';
18
17
  * Handles governance and slashing voting for a given slot.
19
18
  */
20
19
  export class CheckpointVoter {
21
- private slotTimestamp: bigint;
22
20
  private governanceSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
23
21
  private slashingSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
24
22
 
@@ -33,8 +31,6 @@ export class CheckpointVoter {
33
31
  private readonly metrics: SequencerMetrics,
34
32
  private readonly log: Logger,
35
33
  ) {
36
- this.slotTimestamp = getTimestampForSlot(this.slot, this.l1Constants);
37
-
38
34
  // Create separate signers with appropriate duty contexts for governance and slashing votes
39
35
  // These use HA protection to ensure only one node signs per slot/duty
40
36
  const governanceContext: SigningContext = { slot: this.slot, dutyType: DutyType.GOVERNANCE_VOTE };
@@ -77,7 +73,6 @@ export class CheckpointVoter {
77
73
  return await this.publisher.enqueueGovernanceCastSignal(
78
74
  governanceProposerPayload,
79
75
  this.slot,
80
- this.slotTimestamp,
81
76
  this.attestorAddress,
82
77
  this.governanceSigner,
83
78
  );
@@ -108,13 +103,7 @@ export class CheckpointVoter {
108
103
 
109
104
  this.metrics.recordSlashingAttempt(actions.length);
110
105
 
111
- return await this.publisher.enqueueSlashingActions(
112
- actions,
113
- this.slot,
114
- this.slotTimestamp,
115
- this.attestorAddress,
116
- this.slashingSigner,
117
- );
106
+ return await this.publisher.enqueueSlashingActions(actions, this.slot, this.attestorAddress, this.slashingSigner);
118
107
  } catch (err) {
119
108
  if (err instanceof DutyAlreadySignedError) {
120
109
  this.log.info(`Slashing vote already signed by another node`, {
@@ -1,20 +1,76 @@
1
- import type { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
1
+ import type { BlockNumber, CheckpointNumber, IndexWithinCheckpoint, SlotNumber } from '@aztec/foundation/branded-types';
2
+ import type { BlockHash } from '@aztec/stdlib/block';
2
3
 
3
4
  import type { Action } from '../publisher/sequencer-publisher.js';
4
5
  import type { SequencerState } from './utils.js';
5
6
 
6
7
  export type SequencerEvents = {
8
+ /**
9
+ * Emitted on every sequencer state transition (including no-op transitions to the same state). The
10
+ * timing fields are anchored to the build frame of the slot being proposed for, not to wall-clock
11
+ * slot boundaries, because the proposer builds for `targetSlot` during the previous (build) slot.
12
+ *
13
+ * - `oldState` / `newState` are the previous and new {@link SequencerState}.
14
+ * - `secondsIntoBuildFrame` is the wall-clock seconds elapsed since the build-frame start of
15
+ * `targetSlot` (`now − getBuildFrameStart(targetSlot)`). Undefined for lifecycle states with no
16
+ * associated slot (e.g. IDLE/STOPPING). It can be negative if the transition happens before the
17
+ * build frame opens.
18
+ * - `targetSlot` is the slot the checkpoint is being proposed for (the submission slot, one ahead of
19
+ * the wall-clock build slot under pipelining). Undefined for lifecycle states with no slot.
20
+ */
7
21
  ['state-changed']: (args: {
8
22
  oldState: SequencerState;
9
23
  newState: SequencerState;
10
- secondsIntoSlot?: number;
11
- slot?: SlotNumber;
24
+ secondsIntoBuildFrame?: number;
25
+ targetSlot?: SlotNumber;
26
+ }) => void;
27
+ /**
28
+ * Emitted by the sequencer once it has decided it is going to attempt to build a
29
+ * checkpoint for `targetSlot`, after computing the L1 simulation overrides used by
30
+ * `canProposeAt`. Fired BEFORE the L1 simulation is run, so consumers can observe the
31
+ * decision regardless of whether the propose ultimately lands.
32
+ *
33
+ * - `hadProposedParent` indicates whether the build saw a proposed (pipelined) parent
34
+ * checkpoint that hasn't landed on L1 yet.
35
+ * - `provenOverride` is the assumed proven checkpoint number pinned for the L1
36
+ * simulation. The plan always pins both chain tips to short-circuit `canPruneAtTime`,
37
+ * so this is populated whenever a simulation plan was built — the value either
38
+ * matches the on-chain proven snapshot (defensive pin) or the assumed-proven
39
+ * checkpoint when building optimistically across a pruning boundary.
40
+ * - `simulatedPending` is the pending checkpoint passed to L1 simulation. The plan
41
+ * always pins both chain tips to short-circuit `canPruneAtTime`, so this reflects
42
+ * either the pipelined/invalidated tip or the on-chain pending snapshot.
43
+ */
44
+ ['preparing-checkpoint']: (args: {
45
+ targetSlot: SlotNumber;
46
+ checkpointNumber: CheckpointNumber;
47
+ hadProposedParent: boolean;
48
+ provenOverride: CheckpointNumber | undefined;
49
+ simulatedPending: CheckpointNumber | undefined;
12
50
  }) => void;
13
51
  ['proposer-rollup-check-failed']: (args: { reason: string; slot: SlotNumber }) => void;
14
- ['block-tx-count-check-failed']: (args: { minTxs: number; availableTxs: number; slot: SlotNumber }) => void;
52
+ ['block-tx-count-check-failed']: (args: { minTxs: number; slot: SlotNumber }) => void;
15
53
  ['block-build-failed']: (args: { reason: string; slot: SlotNumber }) => void;
16
- ['block-proposed']: (args: { blockNumber: BlockNumber; slot: SlotNumber }) => void;
54
+ ['block-proposed']: (args: {
55
+ blockNumber: BlockNumber;
56
+ blockHash: BlockHash;
57
+ checkpointNumber: CheckpointNumber;
58
+ indexWithinCheckpoint: IndexWithinCheckpoint;
59
+ slot: SlotNumber;
60
+ buildSlot: SlotNumber;
61
+ }) => void;
17
62
  ['checkpoint-empty']: (args: { slot: SlotNumber }) => void;
63
+ /**
64
+ * Emitted when the proposer's pre-broadcast `validateBlockHeader` simulation fails. This is a
65
+ * last-chance check before we gossip a checkpoint proposal: a failure here means the header
66
+ * would not be accepted by L1 (e.g. archive mismatch, stale chain tip, or some other state
67
+ * drift between when we built the checkpoint and when we are about to broadcast it).
68
+ */
69
+ ['header-validation-failed']: (args: {
70
+ slot: SlotNumber;
71
+ checkpointNumber: CheckpointNumber;
72
+ reason: string;
73
+ }) => void;
18
74
  ['checkpoint-publish-failed']: (args: {
19
75
  slot: SlotNumber;
20
76
  successfulActions?: Action[];
@@ -24,4 +80,9 @@ export type SequencerEvents = {
24
80
  }) => void;
25
81
  ['checkpoint-published']: (args: { checkpoint: CheckpointNumber; slot: SlotNumber }) => void;
26
82
  ['checkpoint-error']: (args: { error: Error }) => void;
83
+ ['pipelined-checkpoint-discarded']: (args: {
84
+ slot: SlotNumber;
85
+ checkpointNumber: CheckpointNumber;
86
+ reason: string;
87
+ }) => void;
27
88
  };
@@ -11,13 +11,13 @@ 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';
17
18
 
18
19
  import type { SequencerState } from './utils.js';
19
20
 
20
- // TODO(palla/mbps): Review all metrics and add any missing ones per checkpoint
21
21
  export class SequencerMetrics {
22
22
  public readonly tracer: Tracer;
23
23
  private meter: Meter;
@@ -26,6 +26,7 @@ export class SequencerMetrics {
26
26
  private blockBuildDuration: Histogram;
27
27
  private blockBuildManaPerSecond: Gauge;
28
28
  private stateTransitionBufferDuration: Histogram;
29
+ private stateDuration: Histogram;
29
30
 
30
31
  // these are gauges because for individual sequencers building a block is not something that happens often enough to warrant a histogram
31
32
  private timeToCollectAttestations: Gauge;
@@ -39,17 +40,24 @@ export class SequencerMetrics {
39
40
  private filledSlots: UpDownCounter;
40
41
 
41
42
  private blockProposalFailed: UpDownCounter;
42
- private blockProposalSuccess: UpDownCounter;
43
- private blockProposalPrecheckFailed: UpDownCounter;
43
+ private checkpointProposalSuccess: UpDownCounter;
44
+ private checkpointPrecheckFailed: UpDownCounter;
45
+ private checkpointProposalFailed: UpDownCounter;
44
46
  private checkpointSuccess: UpDownCounter;
45
47
  private slashingAttempts: UpDownCounter;
46
- private checkpointAttestationDelay: Histogram;
48
+ private pipelineDepth: Gauge;
49
+ private pipelineDiscards: UpDownCounter;
50
+ private pipelineParentCheckpointMismatches: UpDownCounter;
47
51
 
48
52
  // Fisherman fee analysis metrics
49
53
  private fishermanWouldBeIncluded: UpDownCounter;
50
54
  private fishermanTimeBeforeBlock: Histogram;
51
55
  private fishermanPendingBlobTxCount: Histogram;
52
56
  private fishermanIncludedBlobTxCount: Histogram;
57
+ private fishermanPendingBlobCount: Histogram;
58
+ private fishermanIncludedBlobCount: Histogram;
59
+ private fishermanBlockBlobsFull: UpDownCounter;
60
+ private fishermanMaxBlobCapacity: Histogram;
53
61
  private fishermanCalculatedPriorityFee: Histogram;
54
62
  private fishermanPriorityFeeDelta: Histogram;
55
63
  private fishermanEstimatedCost: Histogram;
@@ -57,6 +65,10 @@ export class SequencerMetrics {
57
65
  private fishermanMinedBlobTxPriorityFee: Histogram;
58
66
  private fishermanMinedBlobTxTotalCost: Histogram;
59
67
 
68
+ private blockInterBlockTime: Histogram;
69
+ private lastBlockBuiltTimestamp?: number;
70
+ private lastBlockBuiltSlot?: SlotNumber;
71
+
60
72
  private lastSeenSlot?: SlotNumber;
61
73
 
62
74
  constructor(
@@ -67,33 +79,29 @@ export class SequencerMetrics {
67
79
  this.meter = client.getMeter(name);
68
80
  this.tracer = client.getTracer(name);
69
81
 
70
- this.blockCounter = this.meter.createUpDownCounter(Metrics.SEQUENCER_BLOCK_COUNT);
82
+ this.blockCounter = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_BLOCK_COUNT, {
83
+ [Attributes.STATUS]: ['failed', 'built'],
84
+ });
71
85
 
72
86
  this.blockBuildDuration = this.meter.createHistogram(Metrics.SEQUENCER_BLOCK_BUILD_DURATION);
73
87
 
74
88
  this.blockBuildManaPerSecond = this.meter.createGauge(Metrics.SEQUENCER_BLOCK_BUILD_MANA_PER_SECOND);
75
89
 
90
+ this.blockInterBlockTime = this.meter.createHistogram(Metrics.SEQUENCER_BLOCK_INTER_BLOCK_TIME);
91
+
76
92
  this.stateTransitionBufferDuration = this.meter.createHistogram(Metrics.SEQUENCER_STATE_TRANSITION_BUFFER_DURATION);
77
93
 
78
- this.checkpointAttestationDelay = this.meter.createHistogram(Metrics.SEQUENCER_CHECKPOINT_ATTESTATION_DELAY);
94
+ this.stateDuration = this.meter.createHistogram(Metrics.SEQUENCER_STATE_DURATION);
79
95
 
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
- this.rewards = this.meter.createGauge(Metrics.SEQUENCER_CURRENT_BLOCK_REWARDS);
96
+ this.rewards = this.meter.createGauge(Metrics.SEQUENCER_CURRENT_SLOT_REWARDS);
89
97
 
90
- this.slots = this.meter.createUpDownCounter(Metrics.SEQUENCER_SLOT_COUNT);
98
+ this.slots = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLOT_COUNT);
91
99
 
92
100
  /**
93
101
  * NOTE: we do not track missed slots as a separate metric. That would be difficult to determine
94
102
  * Instead, use a computed metric, `slots - filledSlots` to get the number of slots a sequencer has missed.
95
103
  */
96
- this.filledSlots = this.meter.createUpDownCounter(Metrics.SEQUENCER_FILLED_SLOT_COUNT);
104
+ this.filledSlots = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_FILLED_SLOT_COUNT);
97
105
 
98
106
  this.timeToCollectAttestations = this.meter.createGauge(Metrics.SEQUENCER_COLLECT_ATTESTATIONS_DURATION);
99
107
 
@@ -103,20 +111,64 @@ export class SequencerMetrics {
103
111
 
104
112
  this.collectedAttestions = this.meter.createGauge(Metrics.SEQUENCER_COLLECTED_ATTESTATIONS_COUNT);
105
113
 
106
- this.blockProposalFailed = this.meter.createUpDownCounter(Metrics.SEQUENCER_BLOCK_PROPOSAL_FAILED_COUNT);
114
+ this.blockProposalFailed = createUpDownCounterWithDefault(
115
+ this.meter,
116
+ Metrics.SEQUENCER_BLOCK_PROPOSAL_FAILED_COUNT,
117
+ );
107
118
 
108
- this.blockProposalSuccess = this.meter.createUpDownCounter(Metrics.SEQUENCER_BLOCK_PROPOSAL_SUCCESS_COUNT);
119
+ this.checkpointProposalSuccess = createUpDownCounterWithDefault(
120
+ this.meter,
121
+ Metrics.SEQUENCER_CHECKPOINT_PROPOSAL_SUCCESS_COUNT,
122
+ );
109
123
 
110
- this.checkpointSuccess = this.meter.createUpDownCounter(Metrics.SEQUENCER_CHECKPOINT_SUCCESS_COUNT);
124
+ this.checkpointSuccess = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_CHECKPOINT_SUCCESS_COUNT);
125
+
126
+ this.checkpointPrecheckFailed = createUpDownCounterWithDefault(
127
+ this.meter,
128
+ Metrics.SEQUENCER_CHECKPOINT_PRECHECK_FAILED_COUNT,
129
+ {
130
+ [Attributes.ERROR_TYPE]: [
131
+ 'slot_already_taken',
132
+ 'rollup_contract_check_failed',
133
+ 'slot_mismatch',
134
+ 'block_number_mismatch',
135
+ ],
136
+ },
137
+ );
111
138
 
112
- this.blockProposalPrecheckFailed = this.meter.createUpDownCounter(
113
- Metrics.SEQUENCER_BLOCK_PROPOSAL_PRECHECK_FAILED_COUNT,
139
+ this.checkpointProposalFailed = createUpDownCounterWithDefault(
140
+ this.meter,
141
+ Metrics.SEQUENCER_CHECKPOINT_PROPOSAL_FAILED_COUNT,
114
142
  );
115
143
 
116
- this.slashingAttempts = this.meter.createUpDownCounter(Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
144
+ this.slashingAttempts = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
145
+
146
+ this.pipelineDepth = this.meter.createGauge(Metrics.SEQUENCER_PIPELINE_DEPTH);
147
+ this.pipelineDiscards = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_PIPELINE_DISCARDS_COUNT);
148
+ this.pipelineParentCheckpointMismatches = createUpDownCounterWithDefault(
149
+ this.meter,
150
+ Metrics.SEQUENCER_PIPELINE_PARENT_CHECKPOINT_MISMATCH_COUNT,
151
+ {
152
+ [Attributes.ERROR_TYPE]: [
153
+ 'archiver-sync-timeout',
154
+ 'parent-not-on-l1',
155
+ 'parent-hash-mismatch',
156
+ 'parent-invalid-attestations',
157
+ 'unexpected-parent-appeared',
158
+ ],
159
+ },
160
+ );
161
+ this.pipelineDepth.record(0);
117
162
 
118
163
  // Fisherman fee analysis metrics
119
- this.fishermanWouldBeIncluded = this.meter.createUpDownCounter(Metrics.FISHERMAN_FEE_ANALYSIS_WOULD_BE_INCLUDED);
164
+ this.fishermanWouldBeIncluded = createUpDownCounterWithDefault(
165
+ this.meter,
166
+ Metrics.FISHERMAN_FEE_ANALYSIS_WOULD_BE_INCLUDED,
167
+ {
168
+ [Attributes.OK]: [true, false],
169
+ [Attributes.BLOCK_FULL]: ['true', 'false'],
170
+ },
171
+ );
120
172
 
121
173
  this.fishermanTimeBeforeBlock = this.meter.createHistogram(Metrics.FISHERMAN_FEE_ANALYSIS_TIME_BEFORE_BLOCK);
122
174
 
@@ -145,6 +197,20 @@ export class SequencerMetrics {
145
197
  this.fishermanMinedBlobTxTotalCost = this.meter.createHistogram(
146
198
  Metrics.FISHERMAN_FEE_ANALYSIS_MINED_BLOB_TX_TOTAL_COST,
147
199
  );
200
+
201
+ this.fishermanPendingBlobCount = this.meter.createHistogram(Metrics.FISHERMAN_FEE_ANALYSIS_PENDING_BLOB_COUNT);
202
+
203
+ this.fishermanIncludedBlobCount = this.meter.createHistogram(Metrics.FISHERMAN_FEE_ANALYSIS_INCLUDED_BLOB_COUNT);
204
+
205
+ this.fishermanBlockBlobsFull = createUpDownCounterWithDefault(
206
+ this.meter,
207
+ Metrics.FISHERMAN_FEE_ANALYSIS_BLOCK_BLOBS_FULL,
208
+ {
209
+ [Attributes.OK]: [true, false],
210
+ },
211
+ );
212
+
213
+ this.fishermanMaxBlobCapacity = this.meter.createHistogram(Metrics.FISHERMAN_FEE_ANALYSIS_MAX_BLOB_CAPACITY);
148
214
  }
149
215
 
150
216
  public recordRequiredAttestations(requiredAttestationsCount: number, allowanceMs: number) {
@@ -156,21 +222,25 @@ export class SequencerMetrics {
156
222
  this.timeToCollectAttestations.record(0);
157
223
  }
158
224
 
159
- public recordCheckpointAttestationDelay(duration: number) {
160
- this.checkpointAttestationDelay.record(duration);
161
- }
162
-
163
225
  public recordCollectedAttestations(count: number, durationMs: number) {
164
226
  this.collectedAttestions.record(count);
165
227
  this.timeToCollectAttestations.record(Math.ceil(durationMs));
166
228
  }
167
229
 
168
- recordBuiltBlock(buildDurationMs: number, totalMana: number) {
230
+ recordBuiltBlock(buildDurationMs: number, totalMana: number, slot: SlotNumber) {
169
231
  this.blockCounter.add(1, {
170
232
  [Attributes.STATUS]: 'built',
171
233
  });
172
234
  this.blockBuildDuration.record(Math.ceil(buildDurationMs));
173
235
  this.blockBuildManaPerSecond.record(Math.ceil((totalMana * 1000) / buildDurationMs));
236
+
237
+ // Only record inter-block time between blocks built within the same slot.
238
+ const now = Date.now();
239
+ if (this.lastBlockBuiltTimestamp !== undefined && this.lastBlockBuiltSlot === slot) {
240
+ this.blockInterBlockTime.record(now - this.lastBlockBuiltTimestamp);
241
+ }
242
+ this.lastBlockBuiltTimestamp = now;
243
+ this.lastBlockBuiltSlot = slot;
174
244
  }
175
245
 
176
246
  recordFailedBlock() {
@@ -185,6 +255,26 @@ export class SequencerMetrics {
185
255
  });
186
256
  }
187
257
 
258
+ recordStateDuration(durationMs: number, state: SequencerState) {
259
+ this.stateDuration.record(Math.ceil(durationMs), {
260
+ [Attributes.SEQUENCER_STATE]: state,
261
+ });
262
+ }
263
+
264
+ recordPipelineDepth(depth: number) {
265
+ this.pipelineDepth.record(depth);
266
+ }
267
+
268
+ recordPipelineDiscard(count = 1) {
269
+ this.pipelineDiscards.add(count);
270
+ }
271
+
272
+ recordPipelineParentCheckpointMismatch(reason: string) {
273
+ this.pipelineParentCheckpointMismatches.add(1, {
274
+ [Attributes.ERROR_TYPE]: reason,
275
+ });
276
+ }
277
+
188
278
  incOpenSlot(slot: SlotNumber, proposer: string) {
189
279
  // sequencer went through the loop a second time. Noop
190
280
  if (slot === this.lastSeenSlot) {
@@ -227,13 +317,19 @@ export class SequencerMetrics {
227
317
  });
228
318
  }
229
319
 
230
- recordBlockProposalSuccess() {
231
- this.blockProposalSuccess.add(1);
320
+ recordCheckpointProposalSuccess() {
321
+ this.checkpointProposalSuccess.add(1);
322
+ }
323
+
324
+ recordCheckpointPrecheckFailed(
325
+ checkType: 'slot_already_taken' | 'rollup_contract_check_failed' | 'slot_mismatch' | 'block_number_mismatch',
326
+ ) {
327
+ this.checkpointPrecheckFailed.add(1, { [Attributes.ERROR_TYPE]: checkType });
232
328
  }
233
329
 
234
- recordBlockProposalPrecheckFailed(checkType: string) {
235
- this.blockProposalPrecheckFailed.add(1, {
236
- [Attributes.ERROR_TYPE]: checkType,
330
+ recordCheckpointProposalFailed(reason?: string) {
331
+ this.checkpointProposalFailed.add(1, {
332
+ ...(reason && { [Attributes.ERROR_TYPE]: reason }),
237
333
  });
238
334
  }
239
335
 
@@ -263,10 +359,12 @@ export class SequencerMetrics {
263
359
 
264
360
  // Record pending block snapshot data (once per strategy for comparison)
265
361
  this.fishermanPendingBlobTxCount.record(analysis.pendingSnapshot.pendingBlobTxCount, strategyAttributes);
362
+ this.fishermanPendingBlobCount.record(analysis.pendingSnapshot.pendingBlobCount, strategyAttributes);
266
363
 
267
364
  // Record mined block data if available
268
365
  if (analysis.minedBlock) {
269
366
  this.fishermanIncludedBlobTxCount.record(analysis.minedBlock.includedBlobTxCount, strategyAttributes);
367
+ this.fishermanIncludedBlobCount.record(analysis.minedBlock.includedBlobCount, strategyAttributes);
270
368
 
271
369
  // Record actual fees from blob transactions in the mined block
272
370
  for (const blobTx of analysis.minedBlock.includedBlobTxs) {
@@ -300,13 +398,28 @@ export class SequencerMetrics {
300
398
  if (analysis.analysis) {
301
399
  this.fishermanTimeBeforeBlock.record(Math.ceil(analysis.analysis.timeBeforeBlockMs), strategyAttributes);
302
400
 
401
+ // Record whether the block reached 100% blob capacity
402
+ if (analysis.analysis.blockBlobsFull) {
403
+ this.fishermanBlockBlobsFull.add(1, { ...strategyAttributes, [Attributes.OK]: true });
404
+ } else {
405
+ this.fishermanBlockBlobsFull.add(1, { ...strategyAttributes, [Attributes.OK]: false });
406
+ }
407
+
408
+ // Record the max blob capacity for this block
409
+ this.fishermanMaxBlobCapacity.record(analysis.analysis.maxBlobCapacity, strategyAttributes);
410
+
303
411
  // Record strategy-specific inclusion result
304
412
  if (strategyResult.wouldBeIncluded !== undefined) {
413
+ const inclusionAttributes = {
414
+ ...strategyAttributes,
415
+ [Attributes.BLOCK_FULL]: analysis.analysis.blockBlobsFull ? 'true' : 'false',
416
+ };
417
+
305
418
  if (strategyResult.wouldBeIncluded) {
306
- this.fishermanWouldBeIncluded.add(1, { ...strategyAttributes, [Attributes.OK]: true });
419
+ this.fishermanWouldBeIncluded.add(1, { ...inclusionAttributes, [Attributes.OK]: true });
307
420
  } else {
308
421
  this.fishermanWouldBeIncluded.add(1, {
309
- ...strategyAttributes,
422
+ ...inclusionAttributes,
310
423
  [Attributes.OK]: false,
311
424
  ...(strategyResult.exclusionReason && { [Attributes.ERROR_TYPE]: strategyResult.exclusionReason }),
312
425
  });
@@ -316,17 +429,29 @@ export class SequencerMetrics {
316
429
  // Record strategy-specific priority fee delta
317
430
  if (strategyResult.priorityFeeDelta !== undefined) {
318
431
  const priorityFeeDeltaGwei = Number(strategyResult.priorityFeeDelta) / 1e9;
319
- this.fishermanPriorityFeeDelta.record(priorityFeeDeltaGwei, strategyAttributes);
432
+ const deltaAttributes = {
433
+ ...strategyAttributes,
434
+ [Attributes.BLOCK_FULL]: analysis.analysis.blockBlobsFull ? 'true' : 'false',
435
+ };
436
+ this.fishermanPriorityFeeDelta.record(priorityFeeDeltaGwei, deltaAttributes);
320
437
  }
321
438
 
322
439
  // Record estimated cost if available
323
440
  if (strategyResult.estimatedCostEth !== undefined) {
324
- this.fishermanEstimatedCost.record(strategyResult.estimatedCostEth, strategyAttributes);
441
+ const costAttributes = {
442
+ ...strategyAttributes,
443
+ [Attributes.BLOCK_FULL]: analysis.analysis.blockBlobsFull ? 'true' : 'false',
444
+ };
445
+ this.fishermanEstimatedCost.record(strategyResult.estimatedCostEth, costAttributes);
325
446
  }
326
447
 
327
448
  // Record estimated overpayment if available
328
449
  if (strategyResult.estimatedOverpaymentEth !== undefined) {
329
- this.fishermanEstimatedOverpayment.record(strategyResult.estimatedOverpaymentEth, strategyAttributes);
450
+ const overpaymentAttributes = {
451
+ ...strategyAttributes,
452
+ [Attributes.BLOCK_FULL]: analysis.analysis.blockBlobsFull ? 'true' : 'false',
453
+ };
454
+ this.fishermanEstimatedOverpayment.record(strategyResult.estimatedOverpaymentEth, overpaymentAttributes);
330
455
  }
331
456
  }
332
457
  }
@@ -0,0 +1,43 @@
1
+ /** A tracked request: the awaitable to wait on, plus an optional interrupt to cancel it early. */
2
+ type TrackedRequest = { promise: Promise<unknown>; interrupt?: () => void };
3
+
4
+ /**
5
+ * Tracks fire-and-forget requests so a shutdown path can interrupt any in-flight work and await it to
6
+ * settle. Each tracked request is a promise plus an optional interrupt callback; the entry removes itself
7
+ * from the bag once its promise settles, so the tracker only ever holds requests that are still in flight.
8
+ */
9
+ export class RequestsTracker {
10
+ private readonly requests = new Map<number, TrackedRequest>();
11
+ private nextId = 0;
12
+
13
+ /** Number of in-flight requests currently tracked. */
14
+ public get size(): number {
15
+ return this.requests.size;
16
+ }
17
+
18
+ /**
19
+ * Tracks a fire-and-forget request. `interrupt`, when provided, is invoked by {@link interruptRequests}
20
+ * to cancel the in-flight work so its promise settles promptly. The entry auto-removes once `promise`
21
+ * settles, whether it resolves or rejects.
22
+ */
23
+ public trackRequest(promise: Promise<unknown>, interrupt?: () => void): void {
24
+ const id = this.nextId++;
25
+ const settled = promise.then(
26
+ () => this.requests.delete(id),
27
+ () => this.requests.delete(id),
28
+ );
29
+ this.requests.set(id, { promise: settled, interrupt });
30
+ }
31
+
32
+ /** Interrupts every in-flight request that was tracked with an interrupt callback. */
33
+ public interruptRequests(): void {
34
+ for (const { interrupt } of this.requests.values()) {
35
+ interrupt?.();
36
+ }
37
+ }
38
+
39
+ /** Awaits every currently in-flight request to settle. */
40
+ public async awaitRequests(): Promise<void> {
41
+ await Promise.all([...this.requests.values()].map(request => request.promise));
42
+ }
43
+ }