@aztec/sequencer-client 4.0.0-nightly.20250907 → 4.0.0-nightly.20260107

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 (108) hide show
  1. package/dest/client/index.d.ts +1 -1
  2. package/dest/client/sequencer-client.d.ts +10 -8
  3. package/dest/client/sequencer-client.d.ts.map +1 -1
  4. package/dest/client/sequencer-client.js +40 -28
  5. package/dest/config.d.ts +13 -5
  6. package/dest/config.d.ts.map +1 -1
  7. package/dest/config.js +82 -25
  8. package/dest/global_variable_builder/global_builder.d.ts +19 -13
  9. package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
  10. package/dest/global_variable_builder/global_builder.js +41 -28
  11. package/dest/global_variable_builder/index.d.ts +1 -1
  12. package/dest/index.d.ts +2 -2
  13. package/dest/index.d.ts.map +1 -1
  14. package/dest/index.js +1 -1
  15. package/dest/publisher/config.d.ts +11 -8
  16. package/dest/publisher/config.d.ts.map +1 -1
  17. package/dest/publisher/config.js +21 -13
  18. package/dest/publisher/index.d.ts +2 -2
  19. package/dest/publisher/index.d.ts.map +1 -1
  20. package/dest/publisher/index.js +1 -1
  21. package/dest/publisher/sequencer-publisher-factory.d.ts +11 -5
  22. package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
  23. package/dest/publisher/sequencer-publisher-factory.js +9 -2
  24. package/dest/publisher/sequencer-publisher-metrics.d.ts +4 -4
  25. package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
  26. package/dest/publisher/sequencer-publisher-metrics.js +1 -1
  27. package/dest/publisher/sequencer-publisher.d.ts +76 -69
  28. package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
  29. package/dest/publisher/sequencer-publisher.js +290 -180
  30. package/dest/sequencer/block_builder.d.ts +6 -10
  31. package/dest/sequencer/block_builder.d.ts.map +1 -1
  32. package/dest/sequencer/block_builder.js +21 -10
  33. package/dest/sequencer/checkpoint_builder.d.ts +63 -0
  34. package/dest/sequencer/checkpoint_builder.d.ts.map +1 -0
  35. package/dest/sequencer/checkpoint_builder.js +131 -0
  36. package/dest/sequencer/checkpoint_proposal_job.d.ts +74 -0
  37. package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -0
  38. package/dest/sequencer/checkpoint_proposal_job.js +642 -0
  39. package/dest/sequencer/checkpoint_voter.d.ts +34 -0
  40. package/dest/sequencer/checkpoint_voter.d.ts.map +1 -0
  41. package/dest/sequencer/checkpoint_voter.js +85 -0
  42. package/dest/sequencer/config.d.ts +3 -2
  43. package/dest/sequencer/config.d.ts.map +1 -1
  44. package/dest/sequencer/errors.d.ts +11 -0
  45. package/dest/sequencer/errors.d.ts.map +1 -0
  46. package/dest/sequencer/errors.js +15 -0
  47. package/dest/sequencer/events.d.ts +46 -0
  48. package/dest/sequencer/events.d.ts.map +1 -0
  49. package/dest/sequencer/events.js +1 -0
  50. package/dest/sequencer/index.d.ts +5 -1
  51. package/dest/sequencer/index.d.ts.map +1 -1
  52. package/dest/sequencer/index.js +4 -0
  53. package/dest/sequencer/metrics.d.ts +37 -20
  54. package/dest/sequencer/metrics.d.ts.map +1 -1
  55. package/dest/sequencer/metrics.js +211 -85
  56. package/dest/sequencer/sequencer.d.ts +109 -121
  57. package/dest/sequencer/sequencer.d.ts.map +1 -1
  58. package/dest/sequencer/sequencer.js +798 -525
  59. package/dest/sequencer/timetable.d.ts +57 -21
  60. package/dest/sequencer/timetable.d.ts.map +1 -1
  61. package/dest/sequencer/timetable.js +150 -68
  62. package/dest/sequencer/types.d.ts +3 -0
  63. package/dest/sequencer/types.d.ts.map +1 -0
  64. package/dest/sequencer/types.js +1 -0
  65. package/dest/sequencer/utils.d.ts +20 -28
  66. package/dest/sequencer/utils.d.ts.map +1 -1
  67. package/dest/sequencer/utils.js +12 -24
  68. package/dest/test/index.d.ts +4 -2
  69. package/dest/test/index.d.ts.map +1 -1
  70. package/dest/test/mock_checkpoint_builder.d.ts +83 -0
  71. package/dest/test/mock_checkpoint_builder.d.ts.map +1 -0
  72. package/dest/test/mock_checkpoint_builder.js +179 -0
  73. package/dest/test/utils.d.ts +49 -0
  74. package/dest/test/utils.d.ts.map +1 -0
  75. package/dest/test/utils.js +94 -0
  76. package/dest/tx_validator/nullifier_cache.d.ts +1 -1
  77. package/dest/tx_validator/nullifier_cache.d.ts.map +1 -1
  78. package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
  79. package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
  80. package/dest/tx_validator/tx_validator_factory.js +12 -9
  81. package/package.json +32 -31
  82. package/src/client/sequencer-client.ts +34 -40
  83. package/src/config.ts +89 -29
  84. package/src/global_variable_builder/global_builder.ts +56 -48
  85. package/src/index.ts +2 -0
  86. package/src/publisher/config.ts +32 -19
  87. package/src/publisher/index.ts +1 -1
  88. package/src/publisher/sequencer-publisher-factory.ts +19 -6
  89. package/src/publisher/sequencer-publisher-metrics.ts +3 -3
  90. package/src/publisher/sequencer-publisher.ts +410 -240
  91. package/src/sequencer/README.md +531 -0
  92. package/src/sequencer/block_builder.ts +28 -30
  93. package/src/sequencer/checkpoint_builder.ts +217 -0
  94. package/src/sequencer/checkpoint_proposal_job.ts +706 -0
  95. package/src/sequencer/checkpoint_voter.ts +105 -0
  96. package/src/sequencer/config.ts +2 -1
  97. package/src/sequencer/errors.ts +21 -0
  98. package/src/sequencer/events.ts +27 -0
  99. package/src/sequencer/index.ts +4 -0
  100. package/src/sequencer/metrics.ts +269 -94
  101. package/src/sequencer/sequencer.ts +506 -676
  102. package/src/sequencer/timetable.ts +181 -91
  103. package/src/sequencer/types.ts +6 -0
  104. package/src/sequencer/utils.ts +24 -29
  105. package/src/test/index.ts +3 -1
  106. package/src/test/mock_checkpoint_builder.ts +247 -0
  107. package/src/test/utils.ts +137 -0
  108. package/src/tx_validator/tx_validator_factory.ts +13 -7
@@ -1,540 +1,424 @@
1
- import type { L2Block } from '@aztec/aztec.js';
1
+ import { getKzg } from '@aztec/blob-lib';
2
2
  import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
3
3
  import type { EpochCache } from '@aztec/epoch-cache';
4
- import { FormattedViemError, NoCommitteeError, type RollupContract } from '@aztec/ethereum';
5
- import { omit, pick } from '@aztec/foundation/collection';
4
+ import { NoCommitteeError, type RollupContract } from '@aztec/ethereum/contracts';
5
+ import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
6
+ import { merge, omit, pick } from '@aztec/foundation/collection';
7
+ import { Fr } from '@aztec/foundation/curves/bn254';
6
8
  import { EthAddress } from '@aztec/foundation/eth-address';
7
- import { Fr } from '@aztec/foundation/fields';
8
9
  import { createLogger } from '@aztec/foundation/log';
9
10
  import { RunningPromise } from '@aztec/foundation/running-promise';
10
- import { type DateProvider, Timer } from '@aztec/foundation/timer';
11
+ import type { DateProvider } from '@aztec/foundation/timer';
11
12
  import type { TypedEventEmitter } from '@aztec/foundation/types';
12
13
  import type { P2P } from '@aztec/p2p';
13
14
  import type { SlasherClientInterface } from '@aztec/slasher';
14
- import type { CommitteeAttestation, L2BlockSource, ValidateBlockResult } from '@aztec/stdlib/block';
15
- import { type L1RollupConstants, getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
16
- import { Gas } from '@aztec/stdlib/gas';
15
+ import type { L2BlockNew, L2BlockSource, ValidateBlockResult } from '@aztec/stdlib/block';
16
+ import type { Checkpoint } from '@aztec/stdlib/checkpoint';
17
+ import { getSlotAtTimestamp, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
17
18
  import {
18
- type IFullNodeBlockBuilder,
19
- type PublicProcessorLimits,
19
+ type ResolvedSequencerConfig,
20
+ type SequencerConfig,
20
21
  SequencerConfigSchema,
21
22
  type WorldStateSynchronizer,
22
23
  } from '@aztec/stdlib/interfaces/server';
23
24
  import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
24
- import type { BlockProposalOptions } from '@aztec/stdlib/p2p';
25
- import { orderAttestations } from '@aztec/stdlib/p2p';
26
25
  import { pickFromSchema } from '@aztec/stdlib/schemas';
27
- import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
28
26
  import { MerkleTreeId } from '@aztec/stdlib/trees';
29
- import {
30
- ContentCommitment,
31
- type FailedTx,
32
- GlobalVariables,
33
- ProposedBlockHeader,
34
- Tx,
35
- type TxHash,
36
- } from '@aztec/stdlib/tx';
37
- import { AttestationTimeoutError } from '@aztec/stdlib/validators';
38
- import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
27
+ import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
39
28
  import type { ValidatorClient } from '@aztec/validator-client';
40
29
 
41
30
  import EventEmitter from 'node:events';
42
- import type { TypedDataDefinition } from 'viem';
43
31
 
32
+ import { DefaultSequencerConfig } from '../config.js';
44
33
  import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
45
34
  import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
46
- import type { Action, InvalidateBlockRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
47
- import type { SequencerConfig } from './config.js';
35
+ import type { InvalidateBlockRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
36
+ import { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
37
+ import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
38
+ import { CheckpointVoter } from './checkpoint_voter.js';
39
+ import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
40
+ import type { SequencerEvents } from './events.js';
48
41
  import { SequencerMetrics } from './metrics.js';
49
- import { SequencerTimetable, SequencerTooSlowError } from './timetable.js';
50
- import { SequencerState, type SequencerStateWithSlot } from './utils.js';
42
+ import { SequencerTimetable } from './timetable.js';
43
+ import type { SequencerRollupConstants } from './types.js';
44
+ import { SequencerState } from './utils.js';
51
45
 
52
46
  export { SequencerState };
53
47
 
54
- type SequencerRollupConstants = Pick<L1RollupConstants, 'ethereumSlotDuration' | 'l1GenesisTime' | 'slotDuration'>;
55
-
56
- export type SequencerEvents = {
57
- ['state-changed']: (args: {
58
- oldState: SequencerState;
59
- newState: SequencerState;
60
- secondsIntoSlot?: number;
61
- slotNumber?: bigint;
62
- }) => void;
63
- ['proposer-rollup-check-failed']: (args: { reason: string }) => void;
64
- ['tx-count-check-failed']: (args: { minTxs: number; availableTxs: number }) => void;
65
- ['block-build-failed']: (args: { reason: string }) => void;
66
- ['block-publish-failed']: (args: {
67
- successfulActions?: Action[];
68
- failedActions?: Action[];
69
- sentActions?: Action[];
70
- expiredActions?: Action[];
71
- }) => void;
72
- ['block-published']: (args: { blockNumber: number; slot: number }) => void;
73
- };
74
-
75
48
  /**
76
49
  * Sequencer client
77
- * - Wins a period of time to become the sequencer (depending on finalized protocol).
78
- * - Chooses a set of txs from the tx pool to be in the rollup.
79
- * - Simulate the rollup of txs.
80
- * - Adds proof requests to the request pool (not for this milestone).
81
- * - Receives results to those proofs from the network (repeats as necessary) (not for this milestone).
82
- * - Publishes L1 tx(s) to the rollup contract via RollupPublisher.
50
+ * - Checks whether it is elected as proposer for the next slot
51
+ * - Builds multiple blocks and broadcasts them
52
+ * - Collects attestations for the checkpoint
53
+ * - Publishes the checkpoint to L1
54
+ * - Votes for proposals and slashes on L1
83
55
  */
84
56
  export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<SequencerEvents>) {
85
57
  private runningPromise?: RunningPromise;
86
- private pollingIntervalMs: number = 1000;
87
- private maxTxsPerBlock = 32;
88
- private minTxsPerBlock = 1;
89
- private maxL1TxInclusionTimeIntoSlot = 0;
90
58
  private state = SequencerState.STOPPED;
91
- private maxBlockSizeInBytes: number = 1024 * 1024;
92
- private maxBlockGas: Gas = new Gas(100e9, 100e9);
93
59
  private metrics: SequencerMetrics;
94
60
 
95
- private lastBlockPublished: L2Block | undefined;
61
+ /** The last slot for which we attempted to vote when sync failed, to prevent duplicate attempts. */
62
+ private lastSlotForVoteWhenSyncFailed: SlotNumber | undefined;
96
63
 
97
- private governanceProposerPayload: EthAddress | undefined;
64
+ /** The last slot for which we triggered a checkpoint proposal job, to prevent duplicate attempts. */
65
+ private lastSlotForCheckpointProposalJob: SlotNumber | undefined;
66
+
67
+ /** Last successful checkpoint proposed */
68
+ private lastCheckpointProposed: Checkpoint | undefined;
69
+
70
+ /** The last epoch for which we logged strategy comparison in fisherman mode. */
71
+ private lastEpochForStrategyComparison: EpochNumber | undefined;
98
72
 
99
73
  /** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
100
74
  protected timetable!: SequencerTimetable;
101
- protected enforceTimeTable: boolean = false;
102
75
 
103
76
  // This shouldn't be here as this gets re-created each time we build/propose a block.
104
77
  // But we have a number of tests that abuse/rely on this class having a permanent publisher.
105
78
  // As long as those tests only configure a single publisher they will continue to work.
106
79
  // This will get re-assigned every time the sequencer goes to build a new block to a publisher that is valid
107
80
  // for the block proposer.
81
+ // TODO(palla/mbps): Remove this field and fix tests
108
82
  protected publisher: SequencerPublisher | undefined;
109
83
 
84
+ /** Config for the sequencer */
85
+ protected config: ResolvedSequencerConfig = DefaultSequencerConfig;
86
+
110
87
  constructor(
111
88
  protected publisherFactory: SequencerPublisherFactory,
112
- protected validatorClient: ValidatorClient | undefined, // During migration the validator client can be inactive
89
+ protected validatorClient: ValidatorClient,
113
90
  protected globalsBuilder: GlobalVariableBuilder,
114
91
  protected p2pClient: P2P,
115
92
  protected worldState: WorldStateSynchronizer,
116
93
  protected slasherClient: SlasherClientInterface | undefined,
117
94
  protected l2BlockSource: L2BlockSource,
118
95
  protected l1ToL2MessageSource: L1ToL2MessageSource,
119
- protected blockBuilder: IFullNodeBlockBuilder,
96
+ protected checkpointsBuilder: FullNodeCheckpointsBuilder,
120
97
  protected l1Constants: SequencerRollupConstants,
121
98
  protected dateProvider: DateProvider,
122
99
  protected epochCache: EpochCache,
123
100
  protected rollupContract: RollupContract,
124
- protected config: SequencerConfig,
101
+ config: SequencerConfig,
125
102
  protected telemetry: TelemetryClient = getTelemetryClient(),
126
103
  protected log = createLogger('sequencer'),
127
104
  ) {
128
105
  super();
129
106
 
130
- // Set an initial coinbase for metrics purposes, but this will potentially change with each block.
131
- const validatorAddresses = this.validatorClient?.getValidatorAddresses() ?? [];
132
- const coinbase =
133
- validatorAddresses.length === 0
134
- ? EthAddress.ZERO
135
- : (this.validatorClient?.getCoinbaseForAttestor(validatorAddresses[0]) ?? EthAddress.ZERO);
136
-
137
- this.metrics = new SequencerMetrics(telemetry, () => this.state, coinbase, this.rollupContract, 'Sequencer');
138
-
139
- // Initialize config
140
- this.updateConfig(this.config);
141
- }
142
-
143
- get tracer(): Tracer {
144
- return this.metrics.tracer;
145
- }
146
-
147
- public getValidatorAddresses() {
148
- return this.validatorClient?.getValidatorAddresses();
149
- }
150
-
151
- public getConfig() {
152
- return this.config;
153
- }
154
-
155
- /**
156
- * Updates sequencer config by the defined values in the config on input.
157
- * @param config - New parameters.
158
- */
159
- public updateConfig(config: SequencerConfig) {
160
- this.log.info(
161
- `Sequencer config set`,
162
- omit(pickFromSchema(config, SequencerConfigSchema), 'txPublicSetupAllowList'),
163
- );
164
-
165
- if (config.transactionPollingIntervalMS !== undefined) {
166
- this.pollingIntervalMs = config.transactionPollingIntervalMS;
167
- }
168
- if (config.maxTxsPerBlock !== undefined) {
169
- this.maxTxsPerBlock = config.maxTxsPerBlock;
170
- }
171
- if (config.minTxsPerBlock !== undefined) {
172
- this.minTxsPerBlock = config.minTxsPerBlock;
173
- }
174
- if (config.maxDABlockGas !== undefined) {
175
- this.maxBlockGas = new Gas(config.maxDABlockGas, this.maxBlockGas.l2Gas);
176
- }
177
- if (config.maxL2BlockGas !== undefined) {
178
- this.maxBlockGas = new Gas(this.maxBlockGas.daGas, config.maxL2BlockGas);
179
- }
180
- if (config.maxBlockSizeInBytes !== undefined) {
181
- this.maxBlockSizeInBytes = config.maxBlockSizeInBytes;
107
+ // Add [FISHERMAN] prefix to logger if in fisherman mode
108
+ if (config.fishermanMode) {
109
+ this.log = log.createChild('[FISHERMAN]');
182
110
  }
183
- if (config.governanceProposerPayload) {
184
- this.governanceProposerPayload = config.governanceProposerPayload;
185
- }
186
- if (config.maxL1TxInclusionTimeIntoSlot !== undefined) {
187
- this.maxL1TxInclusionTimeIntoSlot = config.maxL1TxInclusionTimeIntoSlot;
188
- }
189
- if (config.enforceTimeTable !== undefined) {
190
- this.enforceTimeTable = config.enforceTimeTable;
191
- }
192
-
193
- this.setTimeTable();
194
-
195
- // TODO: Just read everything from the config object as needed instead of copying everything into local vars.
196
111
 
197
- // Update all values on this.config that are populated in the config object.
198
- Object.assign(this.config, config);
112
+ this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
113
+ this.updateConfig(config);
199
114
  }
200
115
 
201
- private setTimeTable() {
116
+ /** Updates sequencer config by the defined values and updates the timetable */
117
+ public updateConfig(config: Partial<SequencerConfig>) {
118
+ const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
119
+ this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowList'));
120
+ this.config = merge(this.config, filteredConfig);
202
121
  this.timetable = new SequencerTimetable(
203
122
  {
204
123
  ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
205
124
  aztecSlotDuration: this.aztecSlotDuration,
206
- maxL1TxInclusionTimeIntoSlot: this.maxL1TxInclusionTimeIntoSlot,
207
- attestationPropagationTime: this.config.attestationPropagationTime,
208
- enforce: this.enforceTimeTable,
125
+ l1PublishingTime: this.l1PublishingTime,
126
+ p2pPropagationTime: this.config.attestationPropagationTime,
127
+ blockDurationMs: this.config.blockDurationMs,
128
+ enforce: this.config.enforceTimeTable,
209
129
  },
210
130
  this.metrics,
211
131
  this.log,
212
132
  );
213
133
  }
214
134
 
135
+ /** Initializes the sequencer (precomputes tables and creates a publisher). Takes about 3s. */
215
136
  public async init() {
137
+ getKzg();
216
138
  this.publisher = (await this.publisherFactory.create(undefined)).publisher;
217
139
  }
218
140
 
219
- /**
220
- * Starts the sequencer and moves to IDLE state.
221
- */
141
+ /** Starts the sequencer and moves to IDLE state. */
222
142
  public start() {
223
- this.metrics.start();
224
- this.runningPromise = new RunningPromise(this.work.bind(this), this.log, this.pollingIntervalMs);
143
+ this.runningPromise = new RunningPromise(
144
+ this.safeWork.bind(this),
145
+ this.log,
146
+ this.config.sequencerPollingIntervalMS,
147
+ );
225
148
  this.setState(SequencerState.IDLE, undefined, { force: true });
226
149
  this.runningPromise.start();
227
150
  this.log.info('Started sequencer');
228
151
  }
229
152
 
230
- /**
231
- * Stops the sequencer from processing txs and moves to STOPPED state.
232
- */
153
+ /** Stops the sequencer from building blocks and moves to STOPPED state. */
233
154
  public async stop(): Promise<void> {
234
155
  this.log.info(`Stopping sequencer`);
235
- this.metrics.stop();
156
+ this.setState(SequencerState.STOPPING, undefined, { force: true });
236
157
  this.publisher?.interrupt();
237
- await this.validatorClient?.stop();
238
158
  await this.runningPromise?.stop();
239
159
  this.setState(SequencerState.STOPPED, undefined, { force: true });
240
160
  this.log.info('Stopped sequencer');
241
161
  }
242
162
 
243
- /**
244
- * Returns the current state of the sequencer.
245
- * @returns An object with a state entry with one of SequencerState.
246
- */
163
+ @trackSpan('Sequencer.work')
164
+ /** Main sequencer loop with a try/catch */
165
+ protected async safeWork() {
166
+ try {
167
+ await this.work();
168
+ } catch (err) {
169
+ this.emit('checkpoint-error', { error: err as Error });
170
+ if (err instanceof SequencerTooSlowError) {
171
+ // TODO(palla/mbps): Add missing states
172
+ // Log as warn only if we had to abort halfway through the block proposal
173
+ const logLvl = [SequencerState.INITIALIZING_CHECKPOINT, SequencerState.PROPOSER_CHECK].includes(
174
+ err.proposedState,
175
+ )
176
+ ? ('debug' as const)
177
+ : ('warn' as const);
178
+ this.log[logLvl](err.message, { now: this.dateProvider.nowInSeconds() });
179
+ } else {
180
+ // Re-throw other errors
181
+ throw err;
182
+ }
183
+ } finally {
184
+ this.setState(SequencerState.IDLE, undefined);
185
+ }
186
+ }
187
+
188
+ /** Returns the current state of the sequencer. */
247
189
  public status() {
248
190
  return { state: this.state };
249
191
  }
250
192
 
251
193
  /**
252
- * @notice Performs most of the sequencer duties:
253
- * - Checks if we are up to date
254
- * - If we are and we are the sequencer, collect txs and build a block
255
- * - Collect attestations for the block
256
- * - Submit block
257
- * - If our block for some reason is not included, revert the state
194
+ * Main sequencer loop:
195
+ * - Checks if we are up to date
196
+ * - If we are and we are the sequencer, collect txs and build blocks
197
+ * - Build multiple blocks per slot when configured
198
+ * - Collect attestations for the final block
199
+ * - Submit checkpoint
258
200
  */
259
- protected async doRealWork() {
201
+ protected async work() {
260
202
  this.setState(SequencerState.SYNCHRONIZING, undefined);
203
+ const { slot, ts, now, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
261
204
 
262
- // Check all components are synced to latest as seen by the archiver
263
- const syncedTo = await this.getChainTip();
264
-
265
- // Do not go forward with new block if the previous one has not been mined and processed
266
- if (!syncedTo) {
205
+ // Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
206
+ const checkpointProposalJob = await this.prepareCheckpointProposal(slot, ts, now);
207
+ if (!checkpointProposalJob) {
267
208
  return;
268
209
  }
269
210
 
270
- const chainTipArchive = syncedTo.archive;
271
- const newBlockNumber = syncedTo.blockNumber + 1;
211
+ // Execute the checkpoint proposal job
212
+ const checkpoint = await checkpointProposalJob.execute();
272
213
 
273
- const { slot, ts, now } = this.epochCache.getEpochAndSlotInNextL1Slot();
214
+ // Update last checkpoint proposed (currently unused)
215
+ if (checkpoint) {
216
+ this.lastCheckpointProposed = checkpoint;
217
+ }
274
218
 
275
- this.setState(SequencerState.PROPOSER_CHECK, slot);
219
+ // Log fee strategy comparison if on fisherman
220
+ if (
221
+ this.config.fishermanMode &&
222
+ (this.lastEpochForStrategyComparison === undefined || epoch > this.lastEpochForStrategyComparison)
223
+ ) {
224
+ this.logStrategyComparison(epoch, checkpointProposalJob.getPublisher());
225
+ this.lastEpochForStrategyComparison = epoch;
226
+ }
276
227
 
277
- // Check that the archiver and dependencies have synced to the previous L1 slot at least
278
- // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
279
- // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
280
- const syncLogData = {
228
+ return checkpoint;
229
+ }
230
+
231
+ /**
232
+ * Prepares the checkpoint proposal by performing all necessary checks and setup.
233
+ * This is the initial step in the main loop.
234
+ * @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
235
+ */
236
+ private async prepareCheckpointProposal(
237
+ slot: SlotNumber,
238
+ ts: bigint,
239
+ now: bigint,
240
+ ): Promise<CheckpointProposalJob | undefined> {
241
+ // Check we have not already processed this slot (cheapest check)
242
+ // We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
243
+ // running against actual time (eg when we use sandbox-style automining)
244
+ if (
245
+ this.lastSlotForCheckpointProposalJob &&
246
+ this.lastSlotForCheckpointProposalJob >= slot &&
247
+ this.config.enforceTimeTable
248
+ ) {
249
+ this.log.trace(`Slot ${slot} has already been processed`);
250
+ return undefined;
251
+ }
252
+
253
+ // But if we have already proposed for this slot, the we definitely have to skip it, automining or not
254
+ if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= slot) {
255
+ this.log.trace(`Slot ${slot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`);
256
+ return undefined;
257
+ }
258
+
259
+ // Check all components are synced to latest as seen by the archiver (queries all subsystems)
260
+ const syncedTo = await this.checkSync({ ts, slot });
261
+ if (!syncedTo) {
262
+ await this.tryVoteWhenSyncFails({ slot, ts });
263
+ return undefined;
264
+ }
265
+
266
+ // TODO(palla/mbps): Compute proper checkpoint number
267
+ const checkpointNumber = CheckpointNumber.fromBlockNumber(BlockNumber(syncedTo.blockNumber + 1));
268
+
269
+ const logCtx = {
281
270
  now,
282
271
  syncedToL1Ts: syncedTo.l1Timestamp,
283
272
  syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
284
- nextL2Slot: slot,
285
- nextL2SlotTs: ts,
286
- l1SlotDuration: this.l1Constants.ethereumSlotDuration,
287
- newBlockNumber,
273
+ slot,
274
+ slotTs: ts,
275
+ checkpointNumber,
288
276
  isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
289
277
  };
290
278
 
291
- if (syncedTo.l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
292
- this.log.debug(
293
- `Cannot propose block ${newBlockNumber} at next L2 slot ${slot} due to pending sync from L1`,
294
- syncLogData,
295
- );
296
- return;
279
+ // Check that we are a proposer for the next slot
280
+ this.setState(SequencerState.PROPOSER_CHECK, slot);
281
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
282
+
283
+ // If we are not a proposer check if we should invalidate a invalid block, and bail
284
+ if (!canPropose) {
285
+ await this.considerInvalidatingBlock(syncedTo, slot);
286
+ return undefined;
297
287
  }
298
288
 
299
- // Check that the slot is not taken by a block already
289
+ // Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
300
290
  if (syncedTo.block && syncedTo.block.header.getSlot() >= slot) {
301
- this.log.debug(
291
+ this.log.warn(
302
292
  `Cannot propose block at next L2 slot ${slot} since that slot was taken by block ${syncedTo.blockNumber}`,
303
- { ...syncLogData, block: syncedTo.block.header.toInspect() },
304
- );
305
- return;
306
- }
307
-
308
- // Or that we haven't published it ourselves
309
- if (this.lastBlockPublished && this.lastBlockPublished.header.getSlot() >= slot) {
310
- this.log.debug(
311
- `Cannot propose block at next L2 slot ${slot} since that slot was taken by our own block ${this.lastBlockPublished.number}`,
312
- { ...syncLogData, block: this.lastBlockPublished.header.toInspect() },
293
+ { ...logCtx, block: syncedTo.block.header.toInspect() },
313
294
  );
314
- return;
315
- }
316
-
317
- // Check that we are a proposer for the next slot
318
- let proposerInNextSlot: EthAddress | undefined;
319
- try {
320
- proposerInNextSlot = await this.epochCache.getProposerAttesterAddressInNextSlot();
321
- } catch (e) {
322
- if (e instanceof NoCommitteeError) {
323
- this.log.warn(
324
- `Cannot propose block ${newBlockNumber} at next L2 slot ${slot} since the committee does not exist on L1`,
325
- );
326
- return;
327
- }
328
- }
329
-
330
- // If get proposer in next slot is undefined, then the committee is empty and anyone may propose.
331
- // If the committee is defined and not empty, but none of our validators are the proposer, then stop.
332
- const validatorAddresses = this.validatorClient!.getValidatorAddresses();
333
- if (proposerInNextSlot !== undefined && !validatorAddresses.some(addr => addr.equals(proposerInNextSlot))) {
334
- this.log.debug(`Cannot propose block ${newBlockNumber} since we are not a proposer`, {
335
- us: validatorAddresses,
336
- proposer: proposerInNextSlot,
337
- ...syncLogData,
338
- });
339
- // If the pending chain is invalid, we may need to invalidate the block if no one else is doing it.
340
- if (!syncedTo.pendingChainValidationStatus.valid) {
341
- // We pass i undefined here to get any available publisher.
342
- const { publisher } = await this.publisherFactory.create(undefined);
343
- await this.considerInvalidatingBlock(syncedTo, slot, validatorAddresses, publisher);
344
- }
345
- return;
295
+ this.metrics.recordBlockProposalPrecheckFailed('slot_already_taken');
296
+ return undefined;
346
297
  }
347
298
 
348
- // Check with the rollup if we can indeed propose at the next L2 slot. This check should not fail
349
- // if all the previous checks are good, but we do it just in case.
350
- const proposerAddressInNextSlot = proposerInNextSlot ?? EthAddress.ZERO;
351
-
352
299
  // We now need to get ourselves a publisher.
353
300
  // The returned attestor will be the one we provided if we provided one.
354
301
  // Otherwise it will be a valid attestor for the returned publisher.
355
- const { attestorAddress, publisher } = await this.publisherFactory.create(proposerInNextSlot);
356
-
302
+ // In fisherman mode, pass undefined to use the fisherman's own keystore instead of the actual proposer's
303
+ const proposerForPublisher = this.config.fishermanMode ? undefined : proposer;
304
+ const { attestorAddress, publisher } = await this.publisherFactory.create(proposerForPublisher);
357
305
  this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
358
-
359
306
  this.publisher = publisher;
360
307
 
361
- const coinbase = this.validatorClient!.getCoinbaseForAttestor(attestorAddress);
362
- const feeRecipient = this.validatorClient!.getFeeRecipientForAttestor(attestorAddress);
363
-
364
- this.metrics.setCoinbase(coinbase);
308
+ // In fisherman mode, set the actual proposer's address for simulations
309
+ if (this.config.fishermanMode && proposer) {
310
+ publisher.setProposerAddressForSimulation(proposer);
311
+ this.log.debug(`Set proposer address ${proposer} for simulation in fisherman mode`);
312
+ }
365
313
 
366
314
  // Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
315
+ // TODO(palla/mbps): We need to invalidate checkpoints, not blocks
367
316
  const invalidateBlock = await publisher.simulateInvalidateBlock(syncedTo.pendingChainValidationStatus);
317
+
318
+ // Check with the rollup contract if we can indeed propose at the next L2 slot. This check should not fail
319
+ // if all the previous checks are good, but we do it just in case.
368
320
  const canProposeCheck = await publisher.canProposeAtNextEthBlock(
369
- chainTipArchive,
370
- proposerAddressInNextSlot,
321
+ syncedTo.archive,
322
+ proposer ?? EthAddress.ZERO,
371
323
  invalidateBlock,
372
324
  );
373
325
 
374
326
  if (canProposeCheck === undefined) {
375
327
  this.log.warn(
376
- `Cannot propose block ${newBlockNumber} at slot ${slot} due to failed rollup contract check`,
377
- syncLogData,
328
+ `Cannot propose checkpoint ${checkpointNumber} at slot ${slot} due to failed rollup contract check`,
329
+ logCtx,
378
330
  );
379
- this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed' });
380
- return;
381
- } else if (canProposeCheck.slot !== slot) {
331
+ this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed', slot });
332
+ this.metrics.recordBlockProposalPrecheckFailed('rollup_contract_check_failed');
333
+ return undefined;
334
+ }
335
+
336
+ if (canProposeCheck.slot !== slot) {
382
337
  this.log.warn(
383
338
  `Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${slot} but got ${canProposeCheck.slot}.`,
384
- { ...syncLogData, rollup: canProposeCheck, newBlockNumber, expectedSlot: slot },
339
+ { ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
385
340
  );
386
- this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch' });
387
- return;
388
- } else if (canProposeCheck.blockNumber !== BigInt(newBlockNumber)) {
341
+ this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
342
+ this.metrics.recordBlockProposalPrecheckFailed('slot_mismatch');
343
+ return undefined;
344
+ }
345
+
346
+ if (canProposeCheck.checkpointNumber !== checkpointNumber) {
389
347
  this.log.warn(
390
- `Cannot propose block due to block mismatch with rollup contract (this can be caused by a pending archiver sync). Expected block ${newBlockNumber} but got ${canProposeCheck.blockNumber}.`,
391
- { ...syncLogData, rollup: canProposeCheck, newBlockNumber, expectedSlot: slot },
348
+ `Cannot propose due to block mismatch with rollup contract (this can be caused by a pending archiver sync). Expected checkpoint ${checkpointNumber} but got ${canProposeCheck.checkpointNumber}.`,
349
+ { ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
392
350
  );
393
- this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch' });
394
- return;
351
+ this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch', slot });
352
+ this.metrics.recordBlockProposalPrecheckFailed('block_number_mismatch');
353
+ return undefined;
395
354
  }
396
355
 
397
- this.log.debug(
398
- `Can propose block ${newBlockNumber} at slot ${slot}` + (proposerInNextSlot ? ` as ${proposerInNextSlot}` : ''),
399
- { ...syncLogData, validatorAddresses },
400
- );
356
+ this.lastSlotForCheckpointProposalJob = slot;
357
+ this.log.info(`Preparing checkpoint proposal ${checkpointNumber} at slot ${slot}`, { ...logCtx, proposer });
401
358
 
402
- const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(
403
- newBlockNumber,
404
- coinbase,
405
- feeRecipient,
359
+ // Create and return the checkpoint proposal job
360
+ return this.createCheckpointProposalJob(
406
361
  slot,
362
+ checkpointNumber,
363
+ syncedTo.blockNumber,
364
+ proposer,
365
+ publisher,
366
+ attestorAddress,
367
+ invalidateBlock,
407
368
  );
408
-
409
- const { timestamp } = newGlobalVariables;
410
- const signerFn = (msg: TypedDataDefinition) =>
411
- this.validatorClient!.signWithAddress(attestorAddress, msg).then(s => s.toString());
412
-
413
- const enqueueGovernanceSignalPromise =
414
- this.governanceProposerPayload && !this.governanceProposerPayload.isZero()
415
- ? publisher
416
- .enqueueGovernanceCastSignal(this.governanceProposerPayload, slot, timestamp, attestorAddress, signerFn)
417
- .catch(err => {
418
- this.log.error(`Error enqueuing governance vote`, err, { blockNumber: newBlockNumber, slot });
419
- return false;
420
- })
421
- : Promise.resolve(false);
422
-
423
- const enqueueSlashingActionsPromise = this.slasherClient
424
- ? this.slasherClient
425
- .getProposerActions(slot)
426
- .then(actions => publisher.enqueueSlashingActions(actions, slot, timestamp, attestorAddress, signerFn))
427
- .catch(err => {
428
- this.log.error(`Error enqueuing slashing actions`, err, { blockNumber: newBlockNumber, slot });
429
- return false;
430
- })
431
- : Promise.resolve(false);
432
-
433
- if (invalidateBlock && !this.config.skipInvalidateBlockAsProposer) {
434
- publisher.enqueueInvalidateBlock(invalidateBlock);
435
- }
436
-
437
- this.setState(SequencerState.INITIALIZING_PROPOSAL, slot);
438
- this.log.verbose(`Preparing proposal for block ${newBlockNumber} at slot ${slot}`, {
439
- proposer: proposerInNextSlot?.toString(),
440
- coinbase,
441
- publisher: publisher.getSenderAddress(),
442
- feeRecipient,
443
- globalVariables: newGlobalVariables.toInspect(),
444
- chainTipArchive,
445
- blockNumber: newBlockNumber,
446
- slot,
447
- });
448
-
449
- // If I created a "partial" header here that should make our job much easier.
450
- const proposalHeader = ProposedBlockHeader.from({
451
- ...newGlobalVariables,
452
- timestamp: newGlobalVariables.timestamp,
453
- lastArchiveRoot: chainTipArchive,
454
- contentCommitment: ContentCommitment.empty(),
455
- totalManaUsed: Fr.ZERO,
456
- });
457
-
458
- let block: L2Block | undefined;
459
-
460
- const pendingTxCount = await this.p2pClient.getPendingTxCount();
461
- if (pendingTxCount >= this.minTxsPerBlock) {
462
- // We don't fetch exactly maxTxsPerBlock txs here because we may not need all of them if we hit a limit before,
463
- // and also we may need to fetch more if we don't have enough valid txs.
464
- const pendingTxs = this.p2pClient.iteratePendingTxs();
465
- try {
466
- block = await this.buildBlockAndEnqueuePublish(
467
- pendingTxs,
468
- proposalHeader,
469
- newGlobalVariables,
470
- proposerInNextSlot,
471
- invalidateBlock,
472
- publisher,
473
- );
474
- } catch (err: any) {
475
- this.emit('block-build-failed', { reason: err.message });
476
- if (err instanceof FormattedViemError) {
477
- this.log.verbose(`Unable to build/enqueue block ${err.message}`);
478
- } else {
479
- this.log.error(`Error building/enqueuing block`, err, { blockNumber: newBlockNumber, slot });
480
- }
481
- }
482
- } else {
483
- this.log.verbose(
484
- `Not enough txs to build block ${newBlockNumber} at slot ${slot} (got ${pendingTxCount} txs, need ${this.minTxsPerBlock})`,
485
- { chainTipArchive, blockNumber: newBlockNumber, slot },
486
- );
487
- this.emit('tx-count-check-failed', { minTxs: this.minTxsPerBlock, availableTxs: pendingTxCount });
488
- }
489
-
490
- await Promise.all([enqueueGovernanceSignalPromise, enqueueSlashingActionsPromise]);
491
-
492
- const l1Response = await publisher.sendRequests();
493
- const proposedBlock = l1Response?.successfulActions.find(a => a === 'propose');
494
- if (proposedBlock) {
495
- this.lastBlockPublished = block;
496
- this.emit('block-published', { blockNumber: newBlockNumber, slot: Number(slot) });
497
- this.metrics.incFilledSlot(publisher.getSenderAddress().toString());
498
- } else if (block) {
499
- this.emit('block-publish-failed', l1Response ?? {});
500
- }
501
-
502
- this.setState(SequencerState.IDLE, undefined);
503
369
  }
504
370
 
505
- @trackSpan('Sequencer.work')
506
- protected async work() {
507
- try {
508
- await this.doRealWork();
509
- } catch (err) {
510
- if (err instanceof SequencerTooSlowError) {
511
- // Log as warn only if we had to abort halfway through the block proposal
512
- const logLvl = [SequencerState.INITIALIZING_PROPOSAL, SequencerState.PROPOSER_CHECK].includes(err.proposedState)
513
- ? ('debug' as const)
514
- : ('warn' as const);
515
- this.log[logLvl](err.message, { now: this.dateProvider.nowInSeconds() });
516
- } else {
517
- // Re-throw other errors
518
- throw err;
519
- }
520
- } finally {
521
- this.setState(SequencerState.IDLE, undefined);
522
- }
371
+ protected createCheckpointProposalJob(
372
+ slot: SlotNumber,
373
+ checkpointNumber: CheckpointNumber,
374
+ syncedToBlockNumber: BlockNumber,
375
+ proposer: EthAddress | undefined,
376
+ publisher: SequencerPublisher,
377
+ attestorAddress: EthAddress,
378
+ invalidateBlock: InvalidateBlockRequest | undefined,
379
+ ): CheckpointProposalJob {
380
+ return new CheckpointProposalJob(
381
+ slot,
382
+ checkpointNumber,
383
+ syncedToBlockNumber,
384
+ proposer,
385
+ publisher,
386
+ attestorAddress,
387
+ invalidateBlock,
388
+ this.validatorClient,
389
+ this.globalsBuilder,
390
+ this.p2pClient,
391
+ this.worldState,
392
+ this.l1ToL2MessageSource,
393
+ this.checkpointsBuilder,
394
+ this.l1Constants,
395
+ this.config,
396
+ this.timetable,
397
+ this.slasherClient,
398
+ this.epochCache,
399
+ this.dateProvider,
400
+ this.metrics,
401
+ this,
402
+ this.setState.bind(this),
403
+ this.log,
404
+ );
523
405
  }
524
406
 
525
407
  /**
526
- * Sets the sequencer state and checks if we have enough time left in the slot to transition to the new state.
408
+ * Internal helper for setting the sequencer state and checks if we have enough time left in the slot to transition to the new state.
527
409
  * @param proposedState - The new state to transition to.
528
410
  * @param slotNumber - The current slot number.
529
411
  * @param force - Whether to force the transition even if the sequencer is stopped.
530
412
  */
531
- setState(proposedState: SequencerStateWithSlot, slotNumber: bigint, opts?: { force?: boolean }): void;
532
- setState(
533
- proposedState: Exclude<SequencerState, SequencerStateWithSlot>,
534
- slotNumber?: undefined,
535
- opts?: { force?: boolean },
536
- ): void;
537
- setState(proposedState: SequencerState, slotNumber: bigint | undefined, opts: { force?: boolean } = {}): void {
413
+ protected setState(
414
+ proposedState: SequencerState,
415
+ slotNumber: SlotNumber | undefined,
416
+ opts: { force?: boolean } = {},
417
+ ): void {
418
+ if (this.state === SequencerState.STOPPING && proposedState !== SequencerState.STOPPED && !opts.force) {
419
+ this.log.warn(`Cannot set sequencer to ${proposedState} as it is stopping.`);
420
+ throw new SequencerInterruptedError();
421
+ }
538
422
  if (this.state === SequencerState.STOPPED && !opts.force) {
539
423
  this.log.warn(`Cannot set sequencer from ${this.state} to ${proposedState} as it is stopped.`);
540
424
  return;
@@ -545,326 +429,200 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
545
429
  this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
546
430
  }
547
431
 
548
- this.log.debug(`Transitioning from ${this.state} to ${proposedState}`, { slotNumber, secondsIntoSlot });
432
+ const boringStates = [SequencerState.IDLE, SequencerState.SYNCHRONIZING];
433
+ const logLevel =
434
+ boringStates.includes(proposedState) && boringStates.includes(this.state)
435
+ ? ('trace' as const)
436
+ : ('debug' as const);
437
+ this.log[logLevel](`Transitioning from ${this.state} to ${proposedState}`, { slotNumber, secondsIntoSlot });
438
+
549
439
  this.emit('state-changed', {
550
440
  oldState: this.state,
551
441
  newState: proposedState,
552
442
  secondsIntoSlot,
553
- slotNumber,
443
+ slot: slotNumber,
554
444
  });
555
445
  this.state = proposedState;
556
446
  }
557
447
 
558
- private async dropFailedTxsFromP2P(failedTxs: FailedTx[]) {
559
- if (failedTxs.length === 0) {
560
- return;
561
- }
562
- const failedTxData = failedTxs.map(fail => fail.tx);
563
- const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
564
- this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
565
- await this.p2pClient.deleteTxs(failedTxHashes);
566
- }
567
-
568
- protected getBlockBuilderOptions(slot: number): PublicProcessorLimits {
569
- // Deadline for processing depends on whether we're proposing a block
570
- const secondsIntoSlot = this.getSecondsIntoSlot(slot);
571
- const processingEndTimeWithinSlot = this.timetable.getBlockProposalExecTimeEnd(secondsIntoSlot);
572
-
573
- // Deadline is only set if enforceTimeTable is enabled.
574
- const deadline = this.enforceTimeTable
575
- ? new Date((this.getSlotStartBuildTimestamp(slot) + processingEndTimeWithinSlot) * 1000)
576
- : undefined;
577
- return {
578
- maxTransactions: this.maxTxsPerBlock,
579
- maxBlockSize: this.maxBlockSizeInBytes,
580
- maxBlockGas: this.maxBlockGas,
581
- deadline,
582
- };
583
- }
584
-
585
448
  /**
586
- * @notice Build and propose a block to the chain
587
- *
588
- * @dev MUST throw instead of exiting early to ensure that world-state
589
- * is being rolled back if the block is dropped.
590
- *
591
- * @param pendingTxs - Iterable of pending transactions to construct the block from
592
- * @param proposalHeader - The partial header constructed for the proposal
593
- * @param newGlobalVariables - The global variables for the new block
594
- * @param proposerAddress - The address of the proposer
449
+ * Returns whether all dependencies have caught up.
450
+ * We don't check against the previous block submitted since it may have been reorg'd out.
595
451
  */
596
- @trackSpan('Sequencer.buildBlockAndEnqueuePublish', (_validTxs, _proposalHeader, newGlobalVariables) => ({
597
- [Attributes.BLOCK_NUMBER]: newGlobalVariables.blockNumber,
598
- }))
599
- private async buildBlockAndEnqueuePublish(
600
- pendingTxs: Iterable<Tx> | AsyncIterable<Tx>,
601
- proposalHeader: ProposedBlockHeader,
602
- newGlobalVariables: GlobalVariables,
603
- proposerAddress: EthAddress | undefined,
604
- invalidateBlock: InvalidateBlockRequest | undefined,
605
- publisher: SequencerPublisher,
606
- ): Promise<L2Block> {
607
- await publisher.validateBlockHeader(proposalHeader, invalidateBlock);
608
-
609
- const blockNumber = newGlobalVariables.blockNumber;
610
- const slot = proposalHeader.slotNumber.toBigInt();
611
- const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(blockNumber);
612
-
613
- // this.metrics.recordNewBlock(blockNumber, validTxs.length);
614
- const workTimer = new Timer();
615
- this.setState(SequencerState.CREATING_BLOCK, slot);
616
-
617
- try {
618
- const blockBuilderOptions = this.getBlockBuilderOptions(Number(slot));
619
- const buildBlockRes = await this.blockBuilder.buildBlock(
620
- pendingTxs,
621
- l1ToL2Messages,
622
- newGlobalVariables,
623
- blockBuilderOptions,
624
- );
625
- const { publicGas, block, publicProcessorDuration, numTxs, numMsgs, blockBuildingTimer, usedTxs, failedTxs } =
626
- buildBlockRes;
627
- const blockBuildDuration = workTimer.ms();
628
- await this.dropFailedTxsFromP2P(failedTxs);
629
-
630
- const minTxsPerBlock = this.minTxsPerBlock;
631
- if (numTxs < minTxsPerBlock) {
632
- this.log.warn(
633
- `Block ${blockNumber} has too few txs to be proposed (got ${numTxs} but required ${minTxsPerBlock})`,
634
- { slot, blockNumber, numTxs },
635
- );
636
- throw new Error(`Block has too few successful txs to be proposed`);
637
- }
452
+ protected async checkSync(args: { ts: bigint; slot: SlotNumber }): Promise<SequencerSyncCheckResult | undefined> {
453
+ // Check that the archiver and dependencies have synced to the previous L1 slot at least
454
+ // TODO(#14766): Archiver reports L1 timestamp based on L1 blocks seen, which means that a missed L1 block will
455
+ // cause the archiver L1 timestamp to fall behind, and cause this sequencer to start processing one L1 slot later.
456
+ const l1Timestamp = await this.l2BlockSource.getL1Timestamp();
457
+ const { slot, ts } = args;
458
+ if (l1Timestamp === undefined || l1Timestamp + BigInt(this.l1Constants.ethereumSlotDuration) < ts) {
459
+ this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
460
+ slot,
461
+ ts,
462
+ l1Timestamp,
463
+ });
464
+ return undefined;
465
+ }
638
466
 
639
- // TODO(@PhilWindle) We should probably periodically check for things like another
640
- // block being published before ours instead of just waiting on our block
641
- await publisher.validateBlockHeader(block.header.toPropose(), invalidateBlock);
642
-
643
- const blockStats: L2BlockBuiltStats = {
644
- eventName: 'l2-block-built',
645
- creator: proposerAddress?.toString() ?? publisher.getSenderAddress().toString(),
646
- duration: workTimer.ms(),
647
- publicProcessDuration: publicProcessorDuration,
648
- rollupCircuitsDuration: blockBuildingTimer.ms(),
649
- ...block.getStats(),
650
- };
651
-
652
- const blockHash = await block.hash();
653
- const txHashes = block.body.txEffects.map(tx => tx.txHash);
654
- this.log.info(
655
- `Built block ${block.number} for slot ${slot} with ${numTxs} txs and ${numMsgs} messages. ${
656
- publicGas.l2Gas / workTimer.s()
657
- } mana/s`,
658
- {
659
- blockHash,
660
- globalVariables: block.header.globalVariables.toInspect(),
661
- txHashes,
662
- ...blockStats,
663
- },
664
- );
467
+ const syncedBlocks = await Promise.all([
468
+ this.worldState.status().then(({ syncSummary }) => ({
469
+ number: syncSummary.latestBlockNumber,
470
+ hash: syncSummary.latestBlockHash,
471
+ })),
472
+ this.l2BlockSource.getL2Tips().then(t => t.latest),
473
+ this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
474
+ this.l1ToL2MessageSource.getL2Tips().then(t => t.latest),
475
+ this.l2BlockSource.getPendingChainValidationStatus(),
476
+ ] as const);
665
477
 
666
- this.log.debug('Collecting attestations');
667
- const attestations = await this.collectAttestations(block, usedTxs, proposerAddress);
668
- if (attestations !== undefined) {
669
- this.log.verbose(`Collected ${attestations.length} attestations`, { blockHash, blockNumber });
670
- }
478
+ const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, pendingChainValidationStatus] = syncedBlocks;
671
479
 
672
- await this.enqueuePublishL2Block(block, attestations, txHashes, invalidateBlock, publisher);
673
- this.metrics.recordBuiltBlock(blockBuildDuration, publicGas.l2Gas);
674
- return block;
675
- } catch (err) {
676
- this.metrics.recordFailedBlock();
677
- throw err;
678
- }
679
- }
480
+ // Handle zero as a special case, since the block hash won't match across services if we're changing the prefilled data for the genesis block,
481
+ // as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
482
+ const result =
483
+ (l2BlockSource.number === 0 && worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0) ||
484
+ (worldState.hash === l2BlockSource.hash &&
485
+ p2p.hash === l2BlockSource.hash &&
486
+ l1ToL2MessageSource.hash === l2BlockSource.hash);
680
487
 
681
- @trackSpan('Sequencer.collectAttestations', (block, txHashes) => ({
682
- [Attributes.BLOCK_NUMBER]: block.number,
683
- [Attributes.BLOCK_ARCHIVE]: block.archive.toString(),
684
- [Attributes.BLOCK_TXS_COUNT]: txHashes.length,
685
- }))
686
- protected async collectAttestations(
687
- block: L2Block,
688
- txs: Tx[],
689
- proposerAddress: EthAddress | undefined,
690
- ): Promise<CommitteeAttestation[] | undefined> {
691
- const { committee } = await this.epochCache.getCommittee(block.header.getSlot());
692
-
693
- // We checked above that the committee is defined, so this should never happen.
694
- if (!committee) {
695
- throw new Error('No committee when collecting attestations');
696
- }
697
-
698
- if (committee.length === 0) {
699
- this.log.verbose(`Attesting committee is empty`);
488
+ if (!result) {
489
+ this.log.debug(`Sequencer sync check failed`, { worldState, l2BlockSource, p2p, l1ToL2MessageSource });
700
490
  return undefined;
701
- } else {
702
- this.log.debug(`Attesting committee length is ${committee.length}`);
703
491
  }
704
492
 
705
- if (!this.validatorClient) {
706
- const msg = 'Missing validator client: Cannot collect attestations';
707
- this.log.error(msg);
708
- throw new Error(msg);
493
+ // Special case for genesis state
494
+ const blockNumber = worldState.number;
495
+ if (blockNumber < INITIAL_L2_BLOCK_NUM) {
496
+ const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
497
+ return { blockNumber: BlockNumber(INITIAL_L2_BLOCK_NUM - 1), archive, l1Timestamp, pendingChainValidationStatus };
709
498
  }
710
499
 
711
- const numberOfRequiredAttestations = Math.floor((committee.length * 2) / 3) + 1;
500
+ const block = await this.l2BlockSource.getL2BlockNew(blockNumber);
501
+ if (!block) {
502
+ // this shouldn't really happen because a moment ago we checked that all components were in sync
503
+ this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
504
+ return undefined;
505
+ }
712
506
 
713
- const slotNumber = block.header.globalVariables.slotNumber.toBigInt();
714
- this.setState(SequencerState.COLLECTING_ATTESTATIONS, slotNumber);
507
+ return {
508
+ block,
509
+ blockNumber: block.number,
510
+ archive: block.archive.root,
511
+ l1Timestamp,
512
+ pendingChainValidationStatus,
513
+ };
514
+ }
715
515
 
716
- this.log.debug('Creating block proposal for validators');
717
- const blockProposalOptions: BlockProposalOptions = { publishFullTxs: !!this.config.publishTxsWithProposals };
718
- const proposal = await this.validatorClient.createBlockProposal(
719
- block.header.globalVariables.blockNumber,
720
- block.header.toPropose(),
721
- block.archive.root,
722
- block.header.state,
723
- txs,
724
- proposerAddress,
725
- blockProposalOptions,
726
- );
516
+ /**
517
+ * Checks if we are the proposer for the next slot.
518
+ * @returns True if we can propose, and the proposer address (undefined if anyone can propose)
519
+ */
520
+ protected async checkCanPropose(slot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
521
+ let proposer: EthAddress | undefined;
727
522
 
728
- if (!proposal) {
729
- throw new Error(`Failed to create block proposal`);
523
+ try {
524
+ proposer = await this.epochCache.getProposerAttesterAddressInSlot(slot);
525
+ } catch (e) {
526
+ if (e instanceof NoCommitteeError) {
527
+ this.log.warn(`Cannot propose at next L2 slot ${slot} since the committee does not exist on L1`);
528
+ return [false, undefined];
529
+ }
530
+ this.log.error(`Error getting proposer for slot ${slot}`, e);
531
+ return [false, undefined];
730
532
  }
731
533
 
732
- if (this.config.skipCollectingAttestations) {
733
- this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
734
- const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
735
- return orderAttestations(attestations ?? [], committee);
534
+ // If proposer is undefined, then the committee is empty and anyone may propose
535
+ if (proposer === undefined) {
536
+ return [true, undefined];
537
+ }
538
+ // In fisherman mode, just return the current proposer
539
+ if (this.config.fishermanMode) {
540
+ return [true, proposer];
736
541
  }
737
542
 
738
- this.log.debug('Broadcasting block proposal to validators');
739
- await this.validatorClient.broadcastBlockProposal(proposal);
740
-
741
- const attestationTimeAllowed = this.enforceTimeTable
742
- ? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_BLOCK)!
743
- : this.aztecSlotDuration;
744
-
745
- this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
746
-
747
- const timer = new Timer();
748
- let collectedAttestionsCount: number = 0;
749
- try {
750
- const attestationDeadline = new Date(this.dateProvider.now() + attestationTimeAllowed * 1000);
751
- const attestations = await this.validatorClient.collectAttestations(
752
- proposal,
753
- numberOfRequiredAttestations,
754
- attestationDeadline,
755
- );
756
-
757
- collectedAttestionsCount = attestations.length;
543
+ const validatorAddresses = this.validatorClient.getValidatorAddresses();
544
+ const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
758
545
 
759
- // note: the smart contract requires that the signatures are provided in the order of the committee
760
- return orderAttestations(attestations, committee);
761
- } catch (err) {
762
- if (err && err instanceof AttestationTimeoutError) {
763
- collectedAttestionsCount = err.collectedCount;
764
- }
765
- throw err;
766
- } finally {
767
- this.metrics.recordCollectedAttestations(collectedAttestionsCount, timer.ms());
546
+ if (!weAreProposer) {
547
+ this.log.debug(`Cannot propose at slot ${slot} since we are not a proposer`, { validatorAddresses, proposer });
548
+ return [false, proposer];
768
549
  }
550
+
551
+ return [true, proposer];
769
552
  }
770
553
 
771
554
  /**
772
- * Publishes the L2Block to the rollup contract.
773
- * @param block - The L2Block to be published.
555
+ * Tries to vote on slashing actions and governance when the sync check fails but we're past the max time for initializing a proposal.
556
+ * This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
774
557
  */
775
- @trackSpan('Sequencer.enqueuePublishL2Block', block => ({
776
- [Attributes.BLOCK_NUMBER]: block.number,
777
- }))
778
- protected async enqueuePublishL2Block(
779
- block: L2Block,
780
- attestations: CommitteeAttestation[] | undefined,
781
- txHashes: TxHash[],
782
- invalidateBlock: InvalidateBlockRequest | undefined,
783
- publisher: SequencerPublisher,
784
- ): Promise<void> {
785
- // Publishes new block to the network and awaits the tx to be mined
786
- this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber.toBigInt());
558
+ protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
559
+ const { slot } = args;
787
560
 
788
- // Time out tx at the end of the slot
789
- const slot = block.header.globalVariables.slotNumber.toNumber();
790
- const txTimeoutAt = new Date((this.getSlotStartBuildTimestamp(slot) + this.aztecSlotDuration) * 1000);
561
+ // Prevent duplicate attempts in the same slot
562
+ if (this.lastSlotForVoteWhenSyncFailed === slot) {
563
+ this.log.trace(`Already attempted to vote in slot ${slot} (skipping)`);
564
+ return;
565
+ }
791
566
 
792
- const enqueued = await publisher.enqueueProposeL2Block(block, attestations, txHashes, {
793
- txTimeoutAt,
794
- forcePendingBlockNumber: invalidateBlock?.forcePendingBlockNumber,
567
+ // Check if we're past the max time for initializing a proposal
568
+ const secondsIntoSlot = this.getSecondsIntoSlot(slot);
569
+ const maxAllowedTime = this.timetable.getMaxAllowedTime(SequencerState.INITIALIZING_CHECKPOINT);
570
+
571
+ // If we haven't exceeded the time limit for initializing a proposal, don't proceed with voting
572
+ // We use INITIALIZING_PROPOSAL time limit because if we're past that, we can't build a block anyway
573
+ if (maxAllowedTime === undefined || secondsIntoSlot <= maxAllowedTime) {
574
+ this.log.trace(`Not attempting to vote since there is still time for block building`, {
575
+ secondsIntoSlot,
576
+ maxAllowedTime,
577
+ });
578
+ return;
579
+ }
580
+
581
+ this.log.trace(`Sync for slot ${slot} failed, checking for voting opportunities`, {
582
+ secondsIntoSlot,
583
+ maxAllowedTime,
795
584
  });
796
585
 
797
- if (!enqueued) {
798
- throw new Error(`Failed to enqueue publish of block ${block.number}`);
586
+ // Check if we're a proposer or proposal is open
587
+ const [canPropose, proposer] = await this.checkCanPropose(slot);
588
+ if (!canPropose) {
589
+ this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
590
+ return;
799
591
  }
800
- }
801
592
 
802
- /**
803
- * Returns whether all dependencies have caught up.
804
- * We don't check against the previous block submitted since it may have been reorg'd out.
805
- * @returns Boolean indicating if our dependencies are synced to the latest block.
806
- */
807
- protected async getChainTip(): Promise<
808
- | {
809
- block?: L2Block;
810
- blockNumber: number;
811
- archive: Fr;
812
- l1Timestamp: bigint;
813
- pendingChainValidationStatus: ValidateBlockResult;
814
- }
815
- | undefined
816
- > {
817
- const syncedBlocks = await Promise.all([
818
- this.worldState.status().then(({ syncSummary }) => ({
819
- number: syncSummary.latestBlockNumber,
820
- hash: syncSummary.latestBlockHash,
821
- })),
822
- this.l2BlockSource.getL2Tips().then(t => t.latest),
823
- this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
824
- this.l1ToL2MessageSource.getL2Tips().then(t => t.latest),
825
- this.l2BlockSource.getL1Timestamp(),
826
- this.l2BlockSource.getPendingChainValidationStatus(),
827
- ] as const);
593
+ // Mark this slot as attempted
594
+ this.lastSlotForVoteWhenSyncFailed = slot;
828
595
 
829
- const [worldState, l2BlockSource, p2p, l1ToL2MessageSource, l1Timestamp, pendingChainValidationStatus] =
830
- syncedBlocks;
596
+ // Get a publisher for voting
597
+ const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
831
598
 
832
- // The archiver reports 'undefined' hash for the genesis block
833
- // because it doesn't have access to world state to compute it (facepalm)
834
- const result =
835
- l2BlockSource.hash === undefined
836
- ? worldState.number === 0 && p2p.number === 0 && l1ToL2MessageSource.number === 0
837
- : worldState.hash === l2BlockSource.hash &&
838
- p2p.hash === l2BlockSource.hash &&
839
- l1ToL2MessageSource.hash === l2BlockSource.hash;
599
+ this.log.debug(`Attempting to vote despite sync failure at slot ${slot}`, {
600
+ attestorAddress,
601
+ slot,
602
+ });
840
603
 
841
- const logData = { worldState, l2BlockSource, p2p, l1ToL2MessageSource };
842
- this.log.debug(`Sequencer sync check ${result ? 'succeeded' : 'failed'}`, logData);
604
+ // Enqueue governance and slashing votes
605
+ const voter = new CheckpointVoter(
606
+ slot,
607
+ publisher,
608
+ attestorAddress,
609
+ this.validatorClient,
610
+ this.slasherClient,
611
+ this.l1Constants,
612
+ this.config,
613
+ this.metrics,
614
+ this.log,
615
+ );
616
+ const votesPromises = voter.enqueueVotes();
617
+ const votes = await Promise.all(votesPromises);
843
618
 
844
- if (!result) {
845
- return undefined;
619
+ if (votes.every(p => !p)) {
620
+ this.log.debug(`No votes to enqueue for slot ${slot}`);
621
+ return;
846
622
  }
847
623
 
848
- const blockNumber = worldState.number;
849
- if (blockNumber >= INITIAL_L2_BLOCK_NUM) {
850
- const block = await this.l2BlockSource.getBlock(blockNumber);
851
- if (!block) {
852
- // this shouldn't really happen because a moment ago we checked that all components were in sync
853
- this.log.warn(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`, logData);
854
- return undefined;
855
- }
856
-
857
- return {
858
- block,
859
- blockNumber: block.number,
860
- archive: block.archive.root,
861
- l1Timestamp,
862
- pendingChainValidationStatus,
863
- };
864
- } else {
865
- const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
866
- return { blockNumber: INITIAL_L2_BLOCK_NUM - 1, archive, l1Timestamp, pendingChainValidationStatus };
867
- }
624
+ this.log.info(`Voting in slot ${slot} despite sync failure`, { slot });
625
+ await publisher.sendRequests();
868
626
  }
869
627
 
870
628
  /**
@@ -874,27 +632,26 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
874
632
  * and if they fail, any sequencer will try as well.
875
633
  */
876
634
  protected async considerInvalidatingBlock(
877
- syncedTo: NonNullable<Awaited<ReturnType<Sequencer['getChainTip']>>>,
878
- currentSlot: bigint,
879
- ourValidatorAddresses: EthAddress[],
880
- publisher: SequencerPublisher,
635
+ syncedTo: SequencerSyncCheckResult,
636
+ currentSlot: SlotNumber,
881
637
  ): Promise<void> {
882
638
  const { pendingChainValidationStatus, l1Timestamp } = syncedTo;
883
639
  if (pendingChainValidationStatus.valid) {
884
640
  return;
885
641
  }
886
642
 
887
- const invalidL1Timestamp = pendingChainValidationStatus.block.l1.timestamp;
888
- const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidL1Timestamp);
889
- const invalidBlockNumber = pendingChainValidationStatus.block.block.number;
643
+ const invalidBlockNumber = pendingChainValidationStatus.block.blockNumber;
644
+ const invalidBlockTimestamp = pendingChainValidationStatus.block.timestamp;
645
+ const timeSinceChainInvalid = this.dateProvider.nowInSeconds() - Number(invalidBlockTimestamp);
646
+ const ourValidatorAddresses = this.validatorClient.getValidatorAddresses();
890
647
 
891
648
  const { secondsBeforeInvalidatingBlockAsCommitteeMember, secondsBeforeInvalidatingBlockAsNonCommitteeMember } =
892
649
  this.config;
893
650
 
894
651
  const logData = {
895
- invalidL1Timestamp,
652
+ invalidL1Timestamp: invalidBlockTimestamp,
896
653
  l1Timestamp,
897
- invalidBlock: pendingChainValidationStatus.block.block.toBlockInfo(),
654
+ invalidBlock: pendingChainValidationStatus.block,
898
655
  secondsBeforeInvalidatingBlockAsCommitteeMember,
899
656
  secondsBeforeInvalidatingBlockAsNonCommitteeMember,
900
657
  ourValidatorAddresses,
@@ -922,6 +679,24 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
922
679
  return;
923
680
  }
924
681
 
682
+ let validatorToUse: EthAddress;
683
+ if (invalidateAsCommitteeMember) {
684
+ // When invalidating as a committee member, use first validator that's actually in the committee
685
+ const { committee } = await this.epochCache.getCommittee(currentSlot);
686
+ if (committee) {
687
+ const committeeSet = new Set(committee.map(addr => addr.toString()));
688
+ validatorToUse =
689
+ ourValidatorAddresses.find(addr => committeeSet.has(addr.toString())) ?? ourValidatorAddresses[0];
690
+ } else {
691
+ validatorToUse = ourValidatorAddresses[0];
692
+ }
693
+ } else {
694
+ // When invalidating as a non-committee member, use the first validator
695
+ validatorToUse = ourValidatorAddresses[0];
696
+ }
697
+
698
+ const { publisher } = await this.publisherFactory.create(validatorToUse);
699
+
925
700
  const invalidateBlock = await publisher.simulateInvalidateBlock(pendingChainValidationStatus);
926
701
  if (!invalidateBlock) {
927
702
  this.log.warn(`Failed to simulate invalidate block`, logData);
@@ -936,31 +711,86 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
936
711
  );
937
712
 
938
713
  publisher.enqueueInvalidateBlock(invalidateBlock);
939
- await publisher.sendRequests();
714
+
715
+ if (!this.config.fishermanMode) {
716
+ await publisher.sendRequests();
717
+ } else {
718
+ this.log.info('Invalidating block in fisherman mode, clearing pending requests');
719
+ publisher.clearPendingRequests();
720
+ }
940
721
  }
941
722
 
942
- private getSlotStartBuildTimestamp(slotNumber: number | bigint): number {
943
- return (
944
- Number(this.l1Constants.l1GenesisTime) +
945
- Number(slotNumber) * this.l1Constants.slotDuration -
946
- this.l1Constants.ethereumSlotDuration
947
- );
723
+ private logStrategyComparison(epoch: EpochNumber, publisher: SequencerPublisher): void {
724
+ const feeAnalyzer = publisher.getL1FeeAnalyzer();
725
+ if (!feeAnalyzer) {
726
+ return;
727
+ }
728
+
729
+ const comparison = feeAnalyzer.getStrategyComparison();
730
+ if (comparison.length === 0) {
731
+ this.log.debug(`No strategy data available yet for epoch ${epoch}`);
732
+ return;
733
+ }
734
+
735
+ this.log.info(`L1 Fee Strategy Performance Report - End of Epoch ${epoch}`, {
736
+ epoch: Number(epoch),
737
+ totalAnalyses: comparison[0]?.totalAnalyses,
738
+ strategies: comparison.map(s => ({
739
+ id: s.strategyId,
740
+ name: s.strategyName,
741
+ inclusionRate: `${(s.inclusionRate * 100).toFixed(1)}%`,
742
+ inclusionCount: `${s.inclusionCount}/${s.totalAnalyses}`,
743
+ avgCostEth: s.avgEstimatedCostEth.toFixed(6),
744
+ totalCostEth: s.totalEstimatedCostEth.toFixed(6),
745
+ avgOverpaymentEth: s.avgOverpaymentEth.toFixed(6),
746
+ totalOverpaymentEth: s.totalOverpaymentEth.toFixed(6),
747
+ avgPriorityFeeDeltaGwei: s.avgPriorityFeeDeltaGwei.toFixed(2),
748
+ })),
749
+ });
750
+ }
751
+
752
+ private getSlotStartBuildTimestamp(slotNumber: SlotNumber): number {
753
+ return getSlotStartBuildTimestamp(slotNumber, this.l1Constants);
948
754
  }
949
755
 
950
- private getSecondsIntoSlot(slotNumber: number | bigint): number {
756
+ private getSecondsIntoSlot(slotNumber: SlotNumber): number {
951
757
  const slotStartTimestamp = this.getSlotStartBuildTimestamp(slotNumber);
952
758
  return Number((this.dateProvider.now() / 1000 - slotStartTimestamp).toFixed(3));
953
759
  }
954
760
 
955
- get aztecSlotDuration() {
761
+ public get aztecSlotDuration() {
956
762
  return this.l1Constants.slotDuration;
957
763
  }
958
764
 
959
- get maxL2BlockGas(): number | undefined {
765
+ public get maxL2BlockGas(): number | undefined {
960
766
  return this.config.maxL2BlockGas;
961
767
  }
962
768
 
963
769
  public getSlasherClient(): SlasherClientInterface | undefined {
964
770
  return this.slasherClient;
965
771
  }
772
+
773
+ public get tracer(): Tracer {
774
+ return this.metrics.tracer;
775
+ }
776
+
777
+ public getValidatorAddresses() {
778
+ return this.validatorClient?.getValidatorAddresses();
779
+ }
780
+
781
+ public getConfig() {
782
+ return this.config;
783
+ }
784
+
785
+ private get l1PublishingTime(): number {
786
+ return this.config.l1PublishingTime ?? this.l1Constants.ethereumSlotDuration;
787
+ }
966
788
  }
789
+
790
+ type SequencerSyncCheckResult = {
791
+ block?: L2BlockNew;
792
+ blockNumber: BlockNumber;
793
+ archive: Fr;
794
+ l1Timestamp: bigint;
795
+ pendingChainValidationStatus: ValidateBlockResult;
796
+ };