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