@aztec/sequencer-client 0.0.1-commit.ef17749e1 → 0.0.1-commit.f103f88
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.
- package/dest/client/sequencer-client.d.ts +6 -12
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +30 -80
- package/dest/config.d.ts +4 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +17 -13
- package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
- package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
- package/dest/global_variable_builder/fee_predictor.js +128 -0
- package/dest/global_variable_builder/fee_provider.d.ts +21 -0
- package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
- package/dest/global_variable_builder/fee_provider.js +58 -0
- package/dest/global_variable_builder/global_builder.d.ts +15 -14
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +16 -50
- package/dest/global_variable_builder/index.d.ts +4 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/global_variable_builder/index.js +2 -0
- package/dest/publisher/config.d.ts +13 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +19 -4
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +3 -4
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.d.ts +3 -5
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +2 -3
- package/dest/publisher/sequencer-publisher.d.ts +51 -43
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +135 -111
- package/dest/sequencer/chain_state_overrides.d.ts +25 -0
- package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
- package/dest/sequencer/chain_state_overrides.js +39 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +26 -7
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +296 -185
- package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
- package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
- package/dest/sequencer/checkpoint_voter.d.ts +1 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +2 -5
- package/dest/sequencer/events.d.ts +2 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +9 -10
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +23 -20
- package/dest/sequencer/sequencer.d.ts +22 -7
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +136 -73
- package/dest/sequencer/timetable.d.ts +14 -1
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +45 -36
- package/dest/sequencer/types.d.ts +2 -5
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +4 -4
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +42 -108
- package/src/config.ts +20 -12
- package/src/global_variable_builder/README.md +44 -0
- package/src/global_variable_builder/fee_predictor.ts +172 -0
- package/src/global_variable_builder/fee_provider.ts +75 -0
- package/src/global_variable_builder/global_builder.ts +26 -62
- package/src/global_variable_builder/index.ts +3 -1
- package/src/publisher/config.ts +38 -4
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
- package/src/publisher/sequencer-publisher-factory.ts +3 -6
- package/src/publisher/sequencer-publisher.ts +202 -158
- package/src/sequencer/README.md +83 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +383 -206
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +29 -25
- package/src/sequencer/sequencer.ts +197 -80
- package/src/sequencer/timetable.ts +57 -45
- package/src/sequencer/types.ts +2 -5
- package/src/test/mock_checkpoint_builder.ts +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
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 { NoCommitteeError, type RollupContract } from '@aztec/ethereum/contracts';
|
|
4
|
+
import { NoCommitteeError, type RollupContract, SimulationOverridesBuilder } from '@aztec/ethereum/contracts';
|
|
5
5
|
import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
6
6
|
import { merge, omit, pick } from '@aztec/foundation/collection';
|
|
7
7
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
@@ -13,8 +13,8 @@ import type { TypedEventEmitter } from '@aztec/foundation/types';
|
|
|
13
13
|
import type { P2P } from '@aztec/p2p';
|
|
14
14
|
import type { SlasherClientInterface } from '@aztec/slasher';
|
|
15
15
|
import type { BlockData, L2BlockSink, L2BlockSource, ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
16
|
-
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
17
|
-
import {
|
|
16
|
+
import type { Checkpoint, ProposedCheckpointData } from '@aztec/stdlib/checkpoint';
|
|
17
|
+
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
18
18
|
import {
|
|
19
19
|
type ResolvedSequencerConfig,
|
|
20
20
|
type SequencerConfig,
|
|
@@ -34,6 +34,7 @@ import type { GlobalVariableBuilder } from '../global_variable_builder/global_bu
|
|
|
34
34
|
import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
|
|
35
35
|
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
36
36
|
import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
|
|
37
|
+
import { CheckpointProposalJobMetrics } from './checkpoint_proposal_job_metrics.js';
|
|
37
38
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
38
39
|
import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
|
|
39
40
|
import type { SequencerEvents } from './events.js';
|
|
@@ -56,6 +57,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
56
57
|
private runningPromise?: RunningPromise;
|
|
57
58
|
private state = SequencerState.STOPPED;
|
|
58
59
|
private metrics: SequencerMetrics;
|
|
60
|
+
private checkpointProposalJobMetrics: CheckpointProposalJobMetrics;
|
|
59
61
|
|
|
60
62
|
/** The last slot for which we attempted to perform our voting duties with degraded block production */
|
|
61
63
|
private lastSlotForFallbackVote: SlotNumber | undefined;
|
|
@@ -72,6 +74,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
72
74
|
/** The last epoch for which we logged strategy comparison in fisherman mode. */
|
|
73
75
|
private lastEpochForStrategyComparison: EpochNumber | undefined;
|
|
74
76
|
|
|
77
|
+
/** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */
|
|
78
|
+
protected lastCheckpointProposalJob: CheckpointProposalJob | undefined;
|
|
79
|
+
|
|
75
80
|
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
|
|
76
81
|
protected timetable!: SequencerTimetable;
|
|
77
82
|
|
|
@@ -104,6 +109,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
|
|
112
|
+
this.checkpointProposalJobMetrics = new CheckpointProposalJobMetrics(telemetry);
|
|
107
113
|
this.updateConfig(config);
|
|
108
114
|
}
|
|
109
115
|
|
|
@@ -112,14 +118,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
112
118
|
const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
|
|
113
119
|
this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
|
|
114
120
|
this.config = merge(this.config, filteredConfig);
|
|
121
|
+
const p2pPropagationTime = this.config.attestationPropagationTime;
|
|
115
122
|
this.timetable = new SequencerTimetable(
|
|
116
123
|
{
|
|
117
124
|
ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
|
|
118
125
|
aztecSlotDuration: this.aztecSlotDuration,
|
|
119
126
|
l1PublishingTime: this.l1PublishingTime,
|
|
120
|
-
p2pPropagationTime
|
|
127
|
+
p2pPropagationTime,
|
|
121
128
|
blockDurationMs: this.config.blockDurationMs,
|
|
122
129
|
enforce: this.config.enforceTimeTable,
|
|
130
|
+
pipelining: this.epochCache.isProposerPipeliningEnabled(),
|
|
123
131
|
},
|
|
124
132
|
this.metrics,
|
|
125
133
|
this.log,
|
|
@@ -143,12 +151,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
143
151
|
this.log.info('Started sequencer');
|
|
144
152
|
}
|
|
145
153
|
|
|
154
|
+
/** Triggers an immediate run of the sequencer, bypassing the polling interval. */
|
|
155
|
+
public trigger() {
|
|
156
|
+
return this.runningPromise?.trigger();
|
|
157
|
+
}
|
|
158
|
+
|
|
146
159
|
/** Stops the sequencer from building blocks and moves to STOPPED state. */
|
|
147
160
|
public async stop(): Promise<void> {
|
|
148
161
|
this.log.info(`Stopping sequencer`);
|
|
149
162
|
this.setState(SequencerState.STOPPING, undefined, { force: true });
|
|
150
|
-
this.publisherFactory.
|
|
163
|
+
await this.publisherFactory.stopAll();
|
|
151
164
|
await this.runningPromise?.stop();
|
|
165
|
+
await this.lastCheckpointProposalJob?.awaitPendingSubmission();
|
|
152
166
|
this.setState(SequencerState.STOPPED, undefined, { force: true });
|
|
153
167
|
this.log.info('Stopped sequencer');
|
|
154
168
|
}
|
|
@@ -192,14 +206,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
192
206
|
@trackSpan('Sequencer.work')
|
|
193
207
|
protected async work() {
|
|
194
208
|
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
195
|
-
const { slot, ts,
|
|
209
|
+
const { slot, ts, nowSeconds, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
210
|
+
const { slot: targetSlot, epoch: targetEpoch } = this.epochCache.getTargetEpochAndSlotInNextL1Slot();
|
|
196
211
|
|
|
197
212
|
// Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
|
|
198
|
-
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
213
|
+
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
214
|
+
slot,
|
|
215
|
+
targetSlot,
|
|
216
|
+
epoch,
|
|
217
|
+
targetEpoch,
|
|
218
|
+
ts,
|
|
219
|
+
nowSeconds,
|
|
220
|
+
);
|
|
199
221
|
if (!checkpointProposalJob) {
|
|
200
222
|
return;
|
|
201
223
|
}
|
|
202
224
|
|
|
225
|
+
// Track the job so we can await its pending L1 submission during shutdown
|
|
226
|
+
this.lastCheckpointProposalJob = checkpointProposalJob;
|
|
227
|
+
|
|
203
228
|
// Execute the checkpoint proposal job
|
|
204
229
|
const checkpoint = await checkpointProposalJob.execute();
|
|
205
230
|
|
|
@@ -208,13 +233,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
208
233
|
this.lastCheckpointProposed = checkpoint;
|
|
209
234
|
}
|
|
210
235
|
|
|
211
|
-
// Log fee strategy comparison if on fisherman
|
|
236
|
+
// Log fee strategy comparison if on fisherman (uses target epoch since we mirror the proposer's perspective)
|
|
212
237
|
if (
|
|
213
238
|
this.config.fishermanMode &&
|
|
214
|
-
(this.lastEpochForStrategyComparison === undefined ||
|
|
239
|
+
(this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
|
|
215
240
|
) {
|
|
216
|
-
this.logStrategyComparison(
|
|
217
|
-
this.lastEpochForStrategyComparison =
|
|
241
|
+
this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
|
|
242
|
+
this.lastEpochForStrategyComparison = targetEpoch;
|
|
218
243
|
}
|
|
219
244
|
|
|
220
245
|
return checkpoint;
|
|
@@ -226,44 +251,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
226
251
|
* @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
|
|
227
252
|
*/
|
|
228
253
|
@trackSpan('Sequencer.prepareCheckpointProposal')
|
|
229
|
-
|
|
230
|
-
epoch: EpochNumber,
|
|
254
|
+
protected async prepareCheckpointProposal(
|
|
231
255
|
slot: SlotNumber,
|
|
256
|
+
targetSlot: SlotNumber,
|
|
257
|
+
epoch: EpochNumber,
|
|
258
|
+
targetEpoch: EpochNumber,
|
|
232
259
|
ts: bigint,
|
|
233
|
-
|
|
260
|
+
nowSeconds: bigint,
|
|
234
261
|
): Promise<CheckpointProposalJob | undefined> {
|
|
235
|
-
// Check we have not already processed this slot (cheapest check)
|
|
262
|
+
// Check we have not already processed this target slot (cheapest check)
|
|
236
263
|
// We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
|
|
237
264
|
// running against actual time (eg when we use sandbox-style automining)
|
|
238
265
|
if (
|
|
239
266
|
this.lastSlotForCheckpointProposalJob &&
|
|
240
|
-
this.lastSlotForCheckpointProposalJob >=
|
|
267
|
+
this.lastSlotForCheckpointProposalJob >= targetSlot &&
|
|
241
268
|
this.config.enforceTimeTable
|
|
242
269
|
) {
|
|
243
|
-
this.log.trace(`
|
|
270
|
+
this.log.trace(`Target slot ${targetSlot} has already been processed`);
|
|
244
271
|
return undefined;
|
|
245
272
|
}
|
|
246
273
|
|
|
247
|
-
// But if we have already proposed for this slot,
|
|
248
|
-
if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >=
|
|
249
|
-
this.log.trace(
|
|
274
|
+
// But if we have already proposed for this slot, then we definitely have to skip it, automining or not
|
|
275
|
+
if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= targetSlot) {
|
|
276
|
+
this.log.trace(
|
|
277
|
+
`Slot ${targetSlot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`,
|
|
278
|
+
);
|
|
250
279
|
return undefined;
|
|
251
280
|
}
|
|
252
281
|
|
|
253
282
|
// Check all components are synced to latest as seen by the archiver (queries all subsystems)
|
|
254
283
|
const syncedTo = await this.checkSync({ ts, slot });
|
|
255
284
|
if (!syncedTo) {
|
|
256
|
-
await this.tryVoteWhenSyncFails({ slot, ts });
|
|
285
|
+
await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
|
|
257
286
|
return undefined;
|
|
258
287
|
}
|
|
259
288
|
|
|
260
|
-
// If escape hatch is open for
|
|
289
|
+
// If escape hatch is open for the target epoch, do not start checkpoint proposal work and do not attempt invalidations.
|
|
261
290
|
// Still perform governance/slashing voting (as proposer) once per slot.
|
|
262
|
-
|
|
291
|
+
// When pipelining, we check the target epoch (slot+1's epoch) since that's the epoch we're building for.
|
|
292
|
+
const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(targetEpoch);
|
|
263
293
|
|
|
264
294
|
if (isEscapeHatchOpen) {
|
|
265
295
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
266
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
296
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
267
297
|
if (canPropose) {
|
|
268
298
|
await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
|
|
269
299
|
} else {
|
|
@@ -280,18 +310,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
280
310
|
const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
|
|
281
311
|
|
|
282
312
|
const logCtx = {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
|
|
313
|
+
nowSeconds,
|
|
314
|
+
syncedToL2Slot: syncedTo.syncedL2Slot,
|
|
286
315
|
slot,
|
|
316
|
+
targetSlot,
|
|
287
317
|
slotTs: ts,
|
|
288
318
|
checkpointNumber,
|
|
289
319
|
isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
|
|
290
320
|
};
|
|
291
321
|
|
|
292
|
-
// Check that we are a proposer for the
|
|
322
|
+
// Check that we are a proposer for the target slot.
|
|
293
323
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
294
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
324
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
295
325
|
|
|
296
326
|
// If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
|
|
297
327
|
if (!canPropose) {
|
|
@@ -299,10 +329,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
299
329
|
return undefined;
|
|
300
330
|
}
|
|
301
331
|
|
|
302
|
-
//
|
|
303
|
-
|
|
332
|
+
// Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
|
|
333
|
+
// confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
|
|
334
|
+
const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
|
|
335
|
+
if (checkpointNumber > confirmedCkpt + 2) {
|
|
336
|
+
this.log.verbose(
|
|
337
|
+
`Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
|
|
338
|
+
);
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
|
|
343
|
+
if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
|
|
304
344
|
this.log.warn(
|
|
305
|
-
`Cannot propose block at
|
|
345
|
+
`Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`,
|
|
306
346
|
{ ...logCtx, block: syncedTo.blockData.header.toInspect() },
|
|
307
347
|
);
|
|
308
348
|
this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
|
|
@@ -324,14 +364,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
324
364
|
}
|
|
325
365
|
|
|
326
366
|
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
327
|
-
|
|
367
|
+
let invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
|
|
368
|
+
|
|
369
|
+
// Determine the correct archive and L1 state overrides for the canProposeAt check.
|
|
370
|
+
// The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
|
|
371
|
+
// When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
|
|
372
|
+
let archiveForCheck = syncedTo.archive;
|
|
373
|
+
const l1SimulationOverridesBuilder = new SimulationOverridesBuilder();
|
|
374
|
+
|
|
375
|
+
if (this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint) {
|
|
376
|
+
// Parent checkpoint hasn't landed on L1 yet. Override both the proposed checkpoint number
|
|
377
|
+
// and the archive at that checkpoint so L1 simulation sees the correct chain tip.
|
|
378
|
+
const parentCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
|
|
379
|
+
l1SimulationOverridesBuilder.forPendingCheckpoint(parentCheckpointNumber).withPendingArchive(syncedTo.archive);
|
|
380
|
+
this.metrics.recordPipelineDepth(1);
|
|
381
|
+
|
|
382
|
+
this.log.verbose(
|
|
383
|
+
`Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber})`,
|
|
384
|
+
);
|
|
385
|
+
// Clear the invalidation - the proposed checkpoint should handle it.
|
|
386
|
+
invalidateCheckpoint = undefined;
|
|
387
|
+
} else if (invalidateCheckpoint) {
|
|
388
|
+
// After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
|
|
389
|
+
// exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
|
|
390
|
+
archiveForCheck = invalidateCheckpoint.lastArchive;
|
|
391
|
+
l1SimulationOverridesBuilder.forPendingCheckpoint(invalidateCheckpoint.forcePendingCheckpointNumber);
|
|
392
|
+
this.metrics.recordPipelineDepth(0);
|
|
393
|
+
} else {
|
|
394
|
+
this.metrics.recordPipelineDepth(0);
|
|
395
|
+
}
|
|
328
396
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
syncedTo.archive,
|
|
397
|
+
const simulationOverridesPlan = l1SimulationOverridesBuilder.build();
|
|
398
|
+
const canProposeCheck = await publisher.canProposeAt(
|
|
399
|
+
archiveForCheck,
|
|
333
400
|
proposer ?? EthAddress.ZERO,
|
|
334
|
-
|
|
401
|
+
simulationOverridesPlan,
|
|
335
402
|
);
|
|
336
403
|
|
|
337
404
|
if (canProposeCheck === undefined) {
|
|
@@ -344,10 +411,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
344
411
|
return undefined;
|
|
345
412
|
}
|
|
346
413
|
|
|
347
|
-
if (canProposeCheck.slot !==
|
|
414
|
+
if (canProposeCheck.slot !== targetSlot) {
|
|
348
415
|
this.log.warn(
|
|
349
|
-
`Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${
|
|
350
|
-
{ ...logCtx, rollup: canProposeCheck, expectedSlot:
|
|
416
|
+
`Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${targetSlot} but got ${canProposeCheck.slot}.`,
|
|
417
|
+
{ ...logCtx, rollup: canProposeCheck, expectedSlot: targetSlot },
|
|
351
418
|
);
|
|
352
419
|
this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
|
|
353
420
|
this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
|
|
@@ -364,36 +431,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
364
431
|
return undefined;
|
|
365
432
|
}
|
|
366
433
|
|
|
367
|
-
this.lastSlotForCheckpointProposalJob =
|
|
368
|
-
|
|
369
|
-
this.
|
|
434
|
+
this.lastSlotForCheckpointProposalJob = targetSlot;
|
|
435
|
+
|
|
436
|
+
await this.p2pClient.prepareForSlot(targetSlot);
|
|
437
|
+
this.log.info(
|
|
438
|
+
`Preparing checkpoint proposal ${checkpointNumber} for target slot ${targetSlot} during wall-clock slot ${slot}`,
|
|
439
|
+
{
|
|
440
|
+
...logCtx,
|
|
441
|
+
proposer,
|
|
442
|
+
pipeliningEnabled: this.epochCache.isProposerPipeliningEnabled(),
|
|
443
|
+
},
|
|
444
|
+
);
|
|
370
445
|
|
|
371
446
|
// Create and return the checkpoint proposal job
|
|
372
447
|
return this.createCheckpointProposalJob(
|
|
373
|
-
epoch,
|
|
374
448
|
slot,
|
|
449
|
+
targetSlot,
|
|
450
|
+
targetEpoch,
|
|
375
451
|
checkpointNumber,
|
|
376
452
|
syncedTo.blockNumber,
|
|
377
453
|
proposer,
|
|
378
454
|
publisher,
|
|
379
455
|
attestorAddress,
|
|
380
456
|
invalidateCheckpoint,
|
|
457
|
+
syncedTo.proposedCheckpointData,
|
|
381
458
|
);
|
|
382
459
|
}
|
|
383
460
|
|
|
384
461
|
protected createCheckpointProposalJob(
|
|
385
|
-
epoch: EpochNumber,
|
|
386
462
|
slot: SlotNumber,
|
|
463
|
+
targetSlot: SlotNumber,
|
|
464
|
+
targetEpoch: EpochNumber,
|
|
387
465
|
checkpointNumber: CheckpointNumber,
|
|
388
466
|
syncedToBlockNumber: BlockNumber,
|
|
389
467
|
proposer: EthAddress | undefined,
|
|
390
468
|
publisher: SequencerPublisher,
|
|
391
469
|
attestorAddress: EthAddress,
|
|
392
470
|
invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
|
|
471
|
+
proposedCheckpointData?: ProposedCheckpointData,
|
|
393
472
|
): CheckpointProposalJob {
|
|
394
473
|
return new CheckpointProposalJob(
|
|
395
|
-
epoch,
|
|
396
474
|
slot,
|
|
475
|
+
targetSlot,
|
|
476
|
+
targetEpoch,
|
|
397
477
|
checkpointNumber,
|
|
398
478
|
syncedToBlockNumber,
|
|
399
479
|
proposer,
|
|
@@ -415,10 +495,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
415
495
|
this.epochCache,
|
|
416
496
|
this.dateProvider,
|
|
417
497
|
this.metrics,
|
|
498
|
+
this.checkpointProposalJobMetrics.createRecorder(),
|
|
418
499
|
this,
|
|
419
500
|
this.setState.bind(this),
|
|
420
501
|
this.tracer,
|
|
421
502
|
this.log.getBindings(),
|
|
503
|
+
proposedCheckpointData,
|
|
422
504
|
);
|
|
423
505
|
}
|
|
424
506
|
|
|
@@ -475,16 +557,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
475
557
|
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
476
558
|
*/
|
|
477
559
|
protected async checkSync(args: { ts: bigint; slot: SlotNumber }): Promise<SequencerSyncCheckResult | undefined> {
|
|
478
|
-
// Check that the archiver
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
const
|
|
482
|
-
const { slot
|
|
483
|
-
if (
|
|
560
|
+
// Check that the archiver has fully synced the L2 slot before the one we want to propose in.
|
|
561
|
+
// The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
|
|
562
|
+
// See getSyncedL2SlotNumber for how missed L1 blocks are handled.
|
|
563
|
+
const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
|
|
564
|
+
const { slot } = args;
|
|
565
|
+
if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
|
|
484
566
|
this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
|
|
485
567
|
slot,
|
|
486
|
-
|
|
487
|
-
l1Timestamp,
|
|
568
|
+
syncedL2Slot,
|
|
488
569
|
});
|
|
489
570
|
return undefined;
|
|
490
571
|
}
|
|
@@ -494,25 +575,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
494
575
|
number: syncSummary.latestBlockNumber,
|
|
495
576
|
hash: syncSummary.latestBlockHash,
|
|
496
577
|
})),
|
|
497
|
-
this.l2BlockSource
|
|
578
|
+
this.l2BlockSource
|
|
579
|
+
.getL2Tips()
|
|
580
|
+
.then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
|
|
498
581
|
this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
|
|
499
|
-
this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
|
|
582
|
+
this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
|
|
500
583
|
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
584
|
+
this.l2BlockSource.getProposedCheckpointOnly(),
|
|
501
585
|
] as const);
|
|
502
586
|
|
|
503
|
-
const [worldState,
|
|
587
|
+
const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
|
|
588
|
+
syncedBlocks;
|
|
504
589
|
|
|
505
590
|
// 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,
|
|
506
591
|
// as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
|
|
507
592
|
// TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
|
|
508
593
|
const result =
|
|
509
|
-
(
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
594
|
+
(l2Tips.proposed.number === 0 &&
|
|
595
|
+
l2Tips.checkpointed.block.number === 0 &&
|
|
596
|
+
l2Tips.checkpointed.checkpoint.number === 0 &&
|
|
597
|
+
worldState.number === 0 &&
|
|
598
|
+
p2p.number === 0 &&
|
|
599
|
+
l1ToL2MessageSourceTips.proposed.number === 0 &&
|
|
600
|
+
l1ToL2MessageSourceTips.checkpointed.block.number === 0 &&
|
|
601
|
+
l1ToL2MessageSourceTips.checkpointed.checkpoint.number === 0) ||
|
|
602
|
+
(worldState.hash === l2Tips.proposed.hash &&
|
|
603
|
+
p2p.hash === l2Tips.proposed.hash &&
|
|
604
|
+
l1ToL2MessageSourceTips.proposed.hash === l2Tips.proposed.hash &&
|
|
605
|
+
l1ToL2MessageSourceTips.checkpointed.block.hash === l2Tips.checkpointed.block.hash &&
|
|
606
|
+
l1ToL2MessageSourceTips.checkpointed.checkpoint.hash === l2Tips.checkpointed.checkpoint.hash);
|
|
513
607
|
|
|
514
608
|
if (!result) {
|
|
515
|
-
this.log.debug(`Sequencer sync check failed`, {
|
|
609
|
+
this.log.debug(`Sequencer sync check failed`, {
|
|
610
|
+
worldState,
|
|
611
|
+
l2BlockSource: l2Tips.proposed,
|
|
612
|
+
p2p,
|
|
613
|
+
l1ToL2MessageSourceTips,
|
|
614
|
+
});
|
|
516
615
|
return undefined;
|
|
517
616
|
}
|
|
518
617
|
|
|
@@ -522,9 +621,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
522
621
|
const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
523
622
|
return {
|
|
524
623
|
checkpointNumber: CheckpointNumber.ZERO,
|
|
624
|
+
checkpointedCheckpointNumber: CheckpointNumber.ZERO,
|
|
525
625
|
blockNumber: BlockNumber.ZERO,
|
|
526
626
|
archive,
|
|
527
|
-
|
|
627
|
+
hasProposedCheckpoint: false,
|
|
628
|
+
syncedL2Slot,
|
|
528
629
|
pendingChainValidationStatus,
|
|
529
630
|
};
|
|
530
631
|
}
|
|
@@ -536,12 +637,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
536
637
|
return undefined;
|
|
537
638
|
}
|
|
538
639
|
|
|
640
|
+
const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
|
|
641
|
+
|
|
539
642
|
return {
|
|
540
643
|
blockData,
|
|
541
644
|
blockNumber: blockData.header.getBlockNumber(),
|
|
542
645
|
checkpointNumber: blockData.checkpointNumber,
|
|
646
|
+
checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
|
|
543
647
|
archive: blockData.archive.root,
|
|
544
|
-
|
|
648
|
+
hasProposedCheckpoint,
|
|
649
|
+
proposedCheckpointData,
|
|
650
|
+
syncedL2Slot,
|
|
545
651
|
pendingChainValidationStatus,
|
|
546
652
|
};
|
|
547
653
|
}
|
|
@@ -550,20 +656,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
550
656
|
* Checks if we are the proposer for the next slot.
|
|
551
657
|
* @returns True if we can propose, and the proposer address (undefined if anyone can propose)
|
|
552
658
|
*/
|
|
553
|
-
protected async checkCanPropose(
|
|
659
|
+
protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
|
|
554
660
|
let proposer: EthAddress | undefined;
|
|
555
661
|
|
|
556
662
|
try {
|
|
557
|
-
proposer = await this.epochCache.getProposerAttesterAddressInSlot(
|
|
663
|
+
proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
|
|
558
664
|
} catch (e) {
|
|
559
665
|
if (e instanceof NoCommitteeError) {
|
|
560
|
-
if (this.lastSlotForNoCommitteeWarning !==
|
|
561
|
-
this.lastSlotForNoCommitteeWarning =
|
|
562
|
-
this.log.warn(`Cannot propose at
|
|
666
|
+
if (this.lastSlotForNoCommitteeWarning !== targetSlot) {
|
|
667
|
+
this.lastSlotForNoCommitteeWarning = targetSlot;
|
|
668
|
+
this.log.warn(`Cannot propose at target slot ${targetSlot} since the committee does not exist on L1`);
|
|
563
669
|
}
|
|
564
670
|
return [false, undefined];
|
|
565
671
|
}
|
|
566
|
-
this.log.error(`Error getting proposer for slot ${
|
|
672
|
+
this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
|
|
567
673
|
return [false, undefined];
|
|
568
674
|
}
|
|
569
675
|
|
|
@@ -580,10 +686,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
580
686
|
const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
|
|
581
687
|
|
|
582
688
|
if (!weAreProposer) {
|
|
583
|
-
this.log.debug(`Cannot propose at slot ${
|
|
689
|
+
this.log.debug(`Cannot propose at target slot ${targetSlot} since we are not a proposer`, {
|
|
690
|
+
targetSlot,
|
|
691
|
+
validatorAddresses,
|
|
692
|
+
proposer,
|
|
693
|
+
});
|
|
584
694
|
return [false, proposer];
|
|
585
695
|
}
|
|
586
696
|
|
|
697
|
+
this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
|
|
698
|
+
targetSlot,
|
|
699
|
+
proposer,
|
|
700
|
+
});
|
|
587
701
|
return [true, proposer];
|
|
588
702
|
}
|
|
589
703
|
|
|
@@ -592,8 +706,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
592
706
|
* This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
|
|
593
707
|
*/
|
|
594
708
|
@trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
|
|
595
|
-
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
|
|
596
|
-
const { slot } = args;
|
|
709
|
+
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
|
|
710
|
+
const { slot, targetSlot } = args;
|
|
597
711
|
|
|
598
712
|
// Prevent duplicate attempts in the same slot
|
|
599
713
|
if (this.lastSlotForFallbackVote === slot) {
|
|
@@ -621,7 +735,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
621
735
|
});
|
|
622
736
|
|
|
623
737
|
// Check if we're a proposer or proposal is open
|
|
624
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
738
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
625
739
|
if (!canPropose) {
|
|
626
740
|
this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
|
|
627
741
|
return;
|
|
@@ -638,9 +752,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
638
752
|
slot,
|
|
639
753
|
});
|
|
640
754
|
|
|
641
|
-
// Enqueue governance and slashing votes
|
|
755
|
+
// Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
|
|
642
756
|
const voter = new CheckpointVoter(
|
|
643
|
-
|
|
757
|
+
targetSlot,
|
|
644
758
|
publisher,
|
|
645
759
|
attestorAddress,
|
|
646
760
|
this.validatorClient,
|
|
@@ -720,7 +834,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
720
834
|
syncedTo: SequencerSyncCheckResult,
|
|
721
835
|
currentSlot: SlotNumber,
|
|
722
836
|
): Promise<void> {
|
|
723
|
-
const { pendingChainValidationStatus,
|
|
837
|
+
const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
|
|
724
838
|
if (pendingChainValidationStatus.valid) {
|
|
725
839
|
return;
|
|
726
840
|
}
|
|
@@ -735,7 +849,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
735
849
|
|
|
736
850
|
const logData = {
|
|
737
851
|
invalidL1Timestamp: invalidCheckpointTimestamp,
|
|
738
|
-
|
|
852
|
+
syncedL2Slot,
|
|
739
853
|
invalidCheckpoint: pendingChainValidationStatus.checkpoint,
|
|
740
854
|
secondsBeforeInvalidatingBlockAsCommitteeMember,
|
|
741
855
|
secondsBeforeInvalidatingBlockAsNonCommitteeMember,
|
|
@@ -880,8 +994,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
880
994
|
type SequencerSyncCheckResult = {
|
|
881
995
|
blockData?: BlockData;
|
|
882
996
|
checkpointNumber: CheckpointNumber;
|
|
997
|
+
checkpointedCheckpointNumber: CheckpointNumber;
|
|
883
998
|
blockNumber: BlockNumber;
|
|
884
999
|
archive: Fr;
|
|
885
|
-
|
|
1000
|
+
hasProposedCheckpoint: boolean;
|
|
1001
|
+
proposedCheckpointData?: ProposedCheckpointData;
|
|
1002
|
+
syncedL2Slot: SlotNumber;
|
|
886
1003
|
pendingChainValidationStatus: ValidateCheckpointResult;
|
|
887
1004
|
};
|