@aztec/sequencer-client 0.0.1-commit.5914bae → 0.0.1-commit.59a0419c6
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 +2 -1
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +8 -8
- package/dest/config.d.ts +3 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +10 -12
- 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 +3 -8
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +4 -39
- package/dest/global_variable_builder/index.d.ts +3 -1
- 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 +17 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +25 -5
- package/dest/publisher/index.d.ts +2 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
- package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +58 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/index.js +2 -0
- 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 +16 -3
- package/dest/publisher/sequencer-publisher.d.ts +52 -41
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +345 -101
- 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 +38 -8
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +427 -171
- 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/events.d.ts +7 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +11 -10
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +38 -20
- package/dest/sequencer/sequencer.d.ts +17 -5
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +120 -58
- 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/test/mock_checkpoint_builder.d.ts +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +0 -2
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +8 -14
- package/src/config.ts +12 -11
- 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 +8 -43
- package/src/global_variable_builder/index.ts +2 -0
- package/src/publisher/config.ts +47 -4
- package/src/publisher/index.ts +3 -0
- package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +57 -0
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
- package/src/publisher/l1_tx_failed_store/index.ts +3 -0
- package/src/publisher/sequencer-publisher-factory.ts +18 -6
- package/src/publisher/sequencer-publisher.ts +389 -140
- package/src/sequencer/README.md +83 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +517 -180
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/events.ts +6 -1
- package/src/sequencer/metrics.ts +49 -25
- package/src/sequencer/sequencer.ts +177 -63
- package/src/sequencer/timetable.ts +57 -45
- package/src/test/mock_checkpoint_builder.ts +0 -2
|
@@ -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,7 +13,7 @@ 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';
|
|
16
|
+
import type { Checkpoint, ProposedCheckpointData } from '@aztec/stdlib/checkpoint';
|
|
17
17
|
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
18
18
|
import {
|
|
19
19
|
type ResolvedSequencerConfig,
|
|
@@ -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,
|
|
@@ -152,8 +160,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
152
160
|
public async stop(): Promise<void> {
|
|
153
161
|
this.log.info(`Stopping sequencer`);
|
|
154
162
|
this.setState(SequencerState.STOPPING, undefined, { force: true });
|
|
155
|
-
this.publisherFactory.
|
|
163
|
+
await this.publisherFactory.stopAll();
|
|
156
164
|
await this.runningPromise?.stop();
|
|
165
|
+
await this.lastCheckpointProposalJob?.awaitPendingSubmission();
|
|
157
166
|
this.setState(SequencerState.STOPPED, undefined, { force: true });
|
|
158
167
|
this.log.info('Stopped sequencer');
|
|
159
168
|
}
|
|
@@ -197,14 +206,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
197
206
|
@trackSpan('Sequencer.work')
|
|
198
207
|
protected async work() {
|
|
199
208
|
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
200
|
-
const { slot, ts,
|
|
209
|
+
const { slot, ts, nowSeconds, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
210
|
+
const { slot: targetSlot, epoch: targetEpoch } = this.epochCache.getTargetEpochAndSlotInNextL1Slot();
|
|
201
211
|
|
|
202
212
|
// Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
|
|
203
|
-
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
213
|
+
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
214
|
+
slot,
|
|
215
|
+
targetSlot,
|
|
216
|
+
epoch,
|
|
217
|
+
targetEpoch,
|
|
218
|
+
ts,
|
|
219
|
+
nowSeconds,
|
|
220
|
+
);
|
|
204
221
|
if (!checkpointProposalJob) {
|
|
205
222
|
return;
|
|
206
223
|
}
|
|
207
224
|
|
|
225
|
+
// Track the job so we can await its pending L1 submission during shutdown
|
|
226
|
+
this.lastCheckpointProposalJob = checkpointProposalJob;
|
|
227
|
+
|
|
208
228
|
// Execute the checkpoint proposal job
|
|
209
229
|
const checkpoint = await checkpointProposalJob.execute();
|
|
210
230
|
|
|
@@ -213,13 +233,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
213
233
|
this.lastCheckpointProposed = checkpoint;
|
|
214
234
|
}
|
|
215
235
|
|
|
216
|
-
// 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)
|
|
217
237
|
if (
|
|
218
238
|
this.config.fishermanMode &&
|
|
219
|
-
(this.lastEpochForStrategyComparison === undefined ||
|
|
239
|
+
(this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
|
|
220
240
|
) {
|
|
221
|
-
this.logStrategyComparison(
|
|
222
|
-
this.lastEpochForStrategyComparison =
|
|
241
|
+
this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
|
|
242
|
+
this.lastEpochForStrategyComparison = targetEpoch;
|
|
223
243
|
}
|
|
224
244
|
|
|
225
245
|
return checkpoint;
|
|
@@ -231,44 +251,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
231
251
|
* @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
|
|
232
252
|
*/
|
|
233
253
|
@trackSpan('Sequencer.prepareCheckpointProposal')
|
|
234
|
-
|
|
235
|
-
epoch: EpochNumber,
|
|
254
|
+
protected async prepareCheckpointProposal(
|
|
236
255
|
slot: SlotNumber,
|
|
256
|
+
targetSlot: SlotNumber,
|
|
257
|
+
epoch: EpochNumber,
|
|
258
|
+
targetEpoch: EpochNumber,
|
|
237
259
|
ts: bigint,
|
|
238
|
-
|
|
260
|
+
nowSeconds: bigint,
|
|
239
261
|
): Promise<CheckpointProposalJob | undefined> {
|
|
240
|
-
// Check we have not already processed this slot (cheapest check)
|
|
262
|
+
// Check we have not already processed this target slot (cheapest check)
|
|
241
263
|
// We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
|
|
242
264
|
// running against actual time (eg when we use sandbox-style automining)
|
|
243
265
|
if (
|
|
244
266
|
this.lastSlotForCheckpointProposalJob &&
|
|
245
|
-
this.lastSlotForCheckpointProposalJob >=
|
|
267
|
+
this.lastSlotForCheckpointProposalJob >= targetSlot &&
|
|
246
268
|
this.config.enforceTimeTable
|
|
247
269
|
) {
|
|
248
|
-
this.log.trace(`
|
|
270
|
+
this.log.trace(`Target slot ${targetSlot} has already been processed`);
|
|
249
271
|
return undefined;
|
|
250
272
|
}
|
|
251
273
|
|
|
252
|
-
// But if we have already proposed for this slot,
|
|
253
|
-
if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >=
|
|
254
|
-
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
|
+
);
|
|
255
279
|
return undefined;
|
|
256
280
|
}
|
|
257
281
|
|
|
258
282
|
// Check all components are synced to latest as seen by the archiver (queries all subsystems)
|
|
259
283
|
const syncedTo = await this.checkSync({ ts, slot });
|
|
260
284
|
if (!syncedTo) {
|
|
261
|
-
await this.tryVoteWhenSyncFails({ slot, ts });
|
|
285
|
+
await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
|
|
262
286
|
return undefined;
|
|
263
287
|
}
|
|
264
288
|
|
|
265
|
-
// 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.
|
|
266
290
|
// Still perform governance/slashing voting (as proposer) once per slot.
|
|
267
|
-
|
|
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);
|
|
268
293
|
|
|
269
294
|
if (isEscapeHatchOpen) {
|
|
270
295
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
271
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
296
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
272
297
|
if (canPropose) {
|
|
273
298
|
await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
|
|
274
299
|
} else {
|
|
@@ -285,17 +310,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
285
310
|
const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
|
|
286
311
|
|
|
287
312
|
const logCtx = {
|
|
288
|
-
|
|
313
|
+
nowSeconds,
|
|
289
314
|
syncedToL2Slot: syncedTo.syncedL2Slot,
|
|
290
315
|
slot,
|
|
316
|
+
targetSlot,
|
|
291
317
|
slotTs: ts,
|
|
292
318
|
checkpointNumber,
|
|
293
319
|
isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
|
|
294
320
|
};
|
|
295
321
|
|
|
296
|
-
// Check that we are a proposer for the
|
|
322
|
+
// Check that we are a proposer for the target slot.
|
|
297
323
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
298
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
324
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
299
325
|
|
|
300
326
|
// If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
|
|
301
327
|
if (!canPropose) {
|
|
@@ -303,10 +329,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
303
329
|
return undefined;
|
|
304
330
|
}
|
|
305
331
|
|
|
306
|
-
//
|
|
307
|
-
|
|
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) {
|
|
308
344
|
this.log.warn(
|
|
309
|
-
`Cannot propose block at
|
|
345
|
+
`Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`,
|
|
310
346
|
{ ...logCtx, block: syncedTo.blockData.header.toInspect() },
|
|
311
347
|
);
|
|
312
348
|
this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
|
|
@@ -328,14 +364,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
328
364
|
}
|
|
329
365
|
|
|
330
366
|
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
331
|
-
|
|
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
|
+
}
|
|
332
396
|
|
|
333
|
-
|
|
334
|
-
// if all the previous checks are good, but we do it just in case.
|
|
397
|
+
const simulationOverridesPlan = l1SimulationOverridesBuilder.build();
|
|
335
398
|
const canProposeCheck = await publisher.canProposeAt(
|
|
336
|
-
|
|
399
|
+
archiveForCheck,
|
|
337
400
|
proposer ?? EthAddress.ZERO,
|
|
338
|
-
|
|
401
|
+
simulationOverridesPlan,
|
|
339
402
|
);
|
|
340
403
|
|
|
341
404
|
if (canProposeCheck === undefined) {
|
|
@@ -348,10 +411,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
348
411
|
return undefined;
|
|
349
412
|
}
|
|
350
413
|
|
|
351
|
-
if (canProposeCheck.slot !==
|
|
414
|
+
if (canProposeCheck.slot !== targetSlot) {
|
|
352
415
|
this.log.warn(
|
|
353
|
-
`Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${
|
|
354
|
-
{ ...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 },
|
|
355
418
|
);
|
|
356
419
|
this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
|
|
357
420
|
this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
|
|
@@ -368,36 +431,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
368
431
|
return undefined;
|
|
369
432
|
}
|
|
370
433
|
|
|
371
|
-
this.lastSlotForCheckpointProposalJob =
|
|
372
|
-
|
|
373
|
-
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
|
+
);
|
|
374
445
|
|
|
375
446
|
// Create and return the checkpoint proposal job
|
|
376
447
|
return this.createCheckpointProposalJob(
|
|
377
|
-
epoch,
|
|
378
448
|
slot,
|
|
449
|
+
targetSlot,
|
|
450
|
+
targetEpoch,
|
|
379
451
|
checkpointNumber,
|
|
380
452
|
syncedTo.blockNumber,
|
|
381
453
|
proposer,
|
|
382
454
|
publisher,
|
|
383
455
|
attestorAddress,
|
|
384
456
|
invalidateCheckpoint,
|
|
457
|
+
syncedTo.proposedCheckpointData,
|
|
385
458
|
);
|
|
386
459
|
}
|
|
387
460
|
|
|
388
461
|
protected createCheckpointProposalJob(
|
|
389
|
-
epoch: EpochNumber,
|
|
390
462
|
slot: SlotNumber,
|
|
463
|
+
targetSlot: SlotNumber,
|
|
464
|
+
targetEpoch: EpochNumber,
|
|
391
465
|
checkpointNumber: CheckpointNumber,
|
|
392
466
|
syncedToBlockNumber: BlockNumber,
|
|
393
467
|
proposer: EthAddress | undefined,
|
|
394
468
|
publisher: SequencerPublisher,
|
|
395
469
|
attestorAddress: EthAddress,
|
|
396
470
|
invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
|
|
471
|
+
proposedCheckpointData?: ProposedCheckpointData,
|
|
397
472
|
): CheckpointProposalJob {
|
|
398
473
|
return new CheckpointProposalJob(
|
|
399
|
-
epoch,
|
|
400
474
|
slot,
|
|
475
|
+
targetSlot,
|
|
476
|
+
targetEpoch,
|
|
401
477
|
checkpointNumber,
|
|
402
478
|
syncedToBlockNumber,
|
|
403
479
|
proposer,
|
|
@@ -419,10 +495,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
419
495
|
this.epochCache,
|
|
420
496
|
this.dateProvider,
|
|
421
497
|
this.metrics,
|
|
498
|
+
this.checkpointProposalJobMetrics.createRecorder(),
|
|
422
499
|
this,
|
|
423
500
|
this.setState.bind(this),
|
|
424
501
|
this.tracer,
|
|
425
502
|
this.log.getBindings(),
|
|
503
|
+
proposedCheckpointData,
|
|
426
504
|
);
|
|
427
505
|
}
|
|
428
506
|
|
|
@@ -497,25 +575,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
497
575
|
number: syncSummary.latestBlockNumber,
|
|
498
576
|
hash: syncSummary.latestBlockHash,
|
|
499
577
|
})),
|
|
500
|
-
this.l2BlockSource
|
|
578
|
+
this.l2BlockSource
|
|
579
|
+
.getL2Tips()
|
|
580
|
+
.then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
|
|
501
581
|
this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
|
|
502
|
-
this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
|
|
582
|
+
this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
|
|
503
583
|
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
584
|
+
this.l2BlockSource.getProposedCheckpointOnly(),
|
|
504
585
|
] as const);
|
|
505
586
|
|
|
506
|
-
const [worldState,
|
|
587
|
+
const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
|
|
588
|
+
syncedBlocks;
|
|
507
589
|
|
|
508
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,
|
|
509
591
|
// as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
|
|
510
592
|
// TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
|
|
511
593
|
const result =
|
|
512
|
-
(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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);
|
|
516
607
|
|
|
517
608
|
if (!result) {
|
|
518
|
-
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
|
+
});
|
|
519
615
|
return undefined;
|
|
520
616
|
}
|
|
521
617
|
|
|
@@ -525,8 +621,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
525
621
|
const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
526
622
|
return {
|
|
527
623
|
checkpointNumber: CheckpointNumber.ZERO,
|
|
624
|
+
checkpointedCheckpointNumber: CheckpointNumber.ZERO,
|
|
528
625
|
blockNumber: BlockNumber.ZERO,
|
|
529
626
|
archive,
|
|
627
|
+
hasProposedCheckpoint: false,
|
|
530
628
|
syncedL2Slot,
|
|
531
629
|
pendingChainValidationStatus,
|
|
532
630
|
};
|
|
@@ -539,11 +637,16 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
539
637
|
return undefined;
|
|
540
638
|
}
|
|
541
639
|
|
|
640
|
+
const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
|
|
641
|
+
|
|
542
642
|
return {
|
|
543
643
|
blockData,
|
|
544
644
|
blockNumber: blockData.header.getBlockNumber(),
|
|
545
645
|
checkpointNumber: blockData.checkpointNumber,
|
|
646
|
+
checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
|
|
546
647
|
archive: blockData.archive.root,
|
|
648
|
+
hasProposedCheckpoint,
|
|
649
|
+
proposedCheckpointData,
|
|
547
650
|
syncedL2Slot,
|
|
548
651
|
pendingChainValidationStatus,
|
|
549
652
|
};
|
|
@@ -553,20 +656,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
553
656
|
* Checks if we are the proposer for the next slot.
|
|
554
657
|
* @returns True if we can propose, and the proposer address (undefined if anyone can propose)
|
|
555
658
|
*/
|
|
556
|
-
protected async checkCanPropose(
|
|
659
|
+
protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
|
|
557
660
|
let proposer: EthAddress | undefined;
|
|
558
661
|
|
|
559
662
|
try {
|
|
560
|
-
proposer = await this.epochCache.getProposerAttesterAddressInSlot(
|
|
663
|
+
proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
|
|
561
664
|
} catch (e) {
|
|
562
665
|
if (e instanceof NoCommitteeError) {
|
|
563
|
-
if (this.lastSlotForNoCommitteeWarning !==
|
|
564
|
-
this.lastSlotForNoCommitteeWarning =
|
|
565
|
-
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`);
|
|
566
669
|
}
|
|
567
670
|
return [false, undefined];
|
|
568
671
|
}
|
|
569
|
-
this.log.error(`Error getting proposer for slot ${
|
|
672
|
+
this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
|
|
570
673
|
return [false, undefined];
|
|
571
674
|
}
|
|
572
675
|
|
|
@@ -583,10 +686,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
583
686
|
const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
|
|
584
687
|
|
|
585
688
|
if (!weAreProposer) {
|
|
586
|
-
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
|
+
});
|
|
587
694
|
return [false, proposer];
|
|
588
695
|
}
|
|
589
696
|
|
|
697
|
+
this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
|
|
698
|
+
targetSlot,
|
|
699
|
+
proposer,
|
|
700
|
+
});
|
|
590
701
|
return [true, proposer];
|
|
591
702
|
}
|
|
592
703
|
|
|
@@ -595,8 +706,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
595
706
|
* This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
|
|
596
707
|
*/
|
|
597
708
|
@trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
|
|
598
|
-
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
|
|
599
|
-
const { slot } = args;
|
|
709
|
+
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
|
|
710
|
+
const { slot, targetSlot } = args;
|
|
600
711
|
|
|
601
712
|
// Prevent duplicate attempts in the same slot
|
|
602
713
|
if (this.lastSlotForFallbackVote === slot) {
|
|
@@ -624,7 +735,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
624
735
|
});
|
|
625
736
|
|
|
626
737
|
// Check if we're a proposer or proposal is open
|
|
627
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
738
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
628
739
|
if (!canPropose) {
|
|
629
740
|
this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
|
|
630
741
|
return;
|
|
@@ -641,9 +752,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
641
752
|
slot,
|
|
642
753
|
});
|
|
643
754
|
|
|
644
|
-
// Enqueue governance and slashing votes
|
|
755
|
+
// Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
|
|
645
756
|
const voter = new CheckpointVoter(
|
|
646
|
-
|
|
757
|
+
targetSlot,
|
|
647
758
|
publisher,
|
|
648
759
|
attestorAddress,
|
|
649
760
|
this.validatorClient,
|
|
@@ -883,8 +994,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
883
994
|
type SequencerSyncCheckResult = {
|
|
884
995
|
blockData?: BlockData;
|
|
885
996
|
checkpointNumber: CheckpointNumber;
|
|
997
|
+
checkpointedCheckpointNumber: CheckpointNumber;
|
|
886
998
|
blockNumber: BlockNumber;
|
|
887
999
|
archive: Fr;
|
|
1000
|
+
hasProposedCheckpoint: boolean;
|
|
1001
|
+
proposedCheckpointData?: ProposedCheckpointData;
|
|
888
1002
|
syncedL2Slot: SlotNumber;
|
|
889
1003
|
pendingChainValidationStatus: ValidateCheckpointResult;
|
|
890
1004
|
};
|