@aztec/sequencer-client 3.0.0-nightly.20251221 → 3.0.0-nightly.20251223

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