@aztec/sequencer-client 0.0.1-commit.f504929 → 0.0.1-commit.f5d02921e
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 +4 -1
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +46 -23
- package/dest/config.d.ts +25 -5
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +21 -12
- package/dest/global_variable_builder/global_builder.d.ts +15 -9
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +29 -25
- 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 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +2 -2
- package/dest/publisher/sequencer-publisher.d.ts +52 -25
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +98 -42
- package/dest/sequencer/checkpoint_proposal_job.d.ts +33 -8
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +284 -158
- 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 +5 -1
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +11 -0
- package/dest/sequencer/sequencer.d.ts +23 -10
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +123 -68
- package/dest/sequencer/timetable.d.ts +4 -3
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +6 -7
- 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 +56 -21
- package/src/config.ts +28 -14
- package/src/global_variable_builder/global_builder.ts +37 -26
- package/src/global_variable_builder/index.ts +1 -1
- package/src/publisher/config.ts +32 -0
- package/src/publisher/sequencer-publisher-factory.ts +3 -3
- package/src/publisher/sequencer-publisher.ts +144 -54
- package/src/sequencer/checkpoint_proposal_job.ts +367 -175
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/metrics.ts +14 -0
- package/src/sequencer/sequencer.ts +178 -79
- package/src/sequencer/timetable.ts +7 -7
- package/src/sequencer/types.ts +1 -1
- package/src/test/mock_checkpoint_builder.ts +51 -48
|
@@ -2,7 +2,6 @@ import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
|
2
2
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
3
|
import type { Logger } from '@aztec/foundation/log';
|
|
4
4
|
import type { SlasherClientInterface } from '@aztec/slasher';
|
|
5
|
-
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
6
5
|
import type { ResolvedSequencerConfig } from '@aztec/stdlib/interfaces/server';
|
|
7
6
|
import type { ValidatorClient } from '@aztec/validator-client';
|
|
8
7
|
import { DutyAlreadySignedError } from '@aztec/validator-ha-signer/errors';
|
|
@@ -18,7 +17,6 @@ import type { SequencerRollupConstants } from './types.js';
|
|
|
18
17
|
* Handles governance and slashing voting for a given slot.
|
|
19
18
|
*/
|
|
20
19
|
export class CheckpointVoter {
|
|
21
|
-
private slotTimestamp: bigint;
|
|
22
20
|
private governanceSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
|
|
23
21
|
private slashingSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
|
|
24
22
|
|
|
@@ -33,8 +31,6 @@ export class CheckpointVoter {
|
|
|
33
31
|
private readonly metrics: SequencerMetrics,
|
|
34
32
|
private readonly log: Logger,
|
|
35
33
|
) {
|
|
36
|
-
this.slotTimestamp = getTimestampForSlot(this.slot, this.l1Constants);
|
|
37
|
-
|
|
38
34
|
// Create separate signers with appropriate duty contexts for governance and slashing votes
|
|
39
35
|
// These use HA protection to ensure only one node signs per slot/duty
|
|
40
36
|
const governanceContext: SigningContext = { slot: this.slot, dutyType: DutyType.GOVERNANCE_VOTE };
|
|
@@ -77,7 +73,6 @@ export class CheckpointVoter {
|
|
|
77
73
|
return await this.publisher.enqueueGovernanceCastSignal(
|
|
78
74
|
governanceProposerPayload,
|
|
79
75
|
this.slot,
|
|
80
|
-
this.slotTimestamp,
|
|
81
76
|
this.attestorAddress,
|
|
82
77
|
this.governanceSigner,
|
|
83
78
|
);
|
|
@@ -108,13 +103,7 @@ export class CheckpointVoter {
|
|
|
108
103
|
|
|
109
104
|
this.metrics.recordSlashingAttempt(actions.length);
|
|
110
105
|
|
|
111
|
-
return await this.publisher.enqueueSlashingActions(
|
|
112
|
-
actions,
|
|
113
|
-
this.slot,
|
|
114
|
-
this.slotTimestamp,
|
|
115
|
-
this.attestorAddress,
|
|
116
|
-
this.slashingSigner,
|
|
117
|
-
);
|
|
106
|
+
return await this.publisher.enqueueSlashingActions(actions, this.slot, this.attestorAddress, this.slashingSigner);
|
|
118
107
|
} catch (err) {
|
|
119
108
|
if (err instanceof DutyAlreadySignedError) {
|
|
120
109
|
this.log.info(`Slashing vote already signed by another node`, {
|
package/src/sequencer/events.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type SequencerEvents = {
|
|
|
13
13
|
['proposer-rollup-check-failed']: (args: { reason: string; slot: SlotNumber }) => void;
|
|
14
14
|
['block-tx-count-check-failed']: (args: { minTxs: number; availableTxs: number; slot: SlotNumber }) => void;
|
|
15
15
|
['block-build-failed']: (args: { reason: string; slot: SlotNumber }) => void;
|
|
16
|
-
['block-proposed']: (args: { blockNumber: BlockNumber; slot: SlotNumber }) => void;
|
|
16
|
+
['block-proposed']: (args: { blockNumber: BlockNumber; slot: SlotNumber; buildSlot: SlotNumber }) => void;
|
|
17
17
|
['checkpoint-empty']: (args: { slot: SlotNumber }) => void;
|
|
18
18
|
['checkpoint-publish-failed']: (args: {
|
|
19
19
|
slot: SlotNumber;
|
package/src/sequencer/metrics.ts
CHANGED
|
@@ -49,6 +49,8 @@ export class SequencerMetrics {
|
|
|
49
49
|
private checkpointBlockCount: Gauge;
|
|
50
50
|
private checkpointTxCount: Gauge;
|
|
51
51
|
private checkpointTotalMana: Gauge;
|
|
52
|
+
private pipelineDepth: Gauge;
|
|
53
|
+
private pipelineDiscards: UpDownCounter;
|
|
52
54
|
|
|
53
55
|
// Fisherman fee analysis metrics
|
|
54
56
|
private fishermanWouldBeIncluded: UpDownCounter;
|
|
@@ -143,6 +145,10 @@ export class SequencerMetrics {
|
|
|
143
145
|
|
|
144
146
|
this.slashingAttempts = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_SLASHING_ATTEMPTS_COUNT);
|
|
145
147
|
|
|
148
|
+
this.pipelineDepth = this.meter.createGauge(Metrics.SEQUENCER_PIPELINE_DEPTH);
|
|
149
|
+
this.pipelineDiscards = createUpDownCounterWithDefault(this.meter, Metrics.SEQUENCER_PIPELINE_DISCARDS_COUNT);
|
|
150
|
+
this.pipelineDepth.record(0);
|
|
151
|
+
|
|
146
152
|
// Fisherman fee analysis metrics
|
|
147
153
|
this.fishermanWouldBeIncluded = createUpDownCounterWithDefault(
|
|
148
154
|
this.meter,
|
|
@@ -234,6 +240,14 @@ export class SequencerMetrics {
|
|
|
234
240
|
});
|
|
235
241
|
}
|
|
236
242
|
|
|
243
|
+
recordPipelineDepth(depth: number) {
|
|
244
|
+
this.pipelineDepth.record(depth);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
recordPipelineDiscard(count = 1) {
|
|
248
|
+
this.pipelineDiscards.add(count);
|
|
249
|
+
}
|
|
250
|
+
|
|
237
251
|
incOpenSlot(slot: SlotNumber, proposer: string) {
|
|
238
252
|
// sequencer went through the loop a second time. Noop
|
|
239
253
|
if (slot === this.lastSeenSlot) {
|
|
@@ -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
|
+
private 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
|
|
|
@@ -147,8 +150,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
147
150
|
public async stop(): Promise<void> {
|
|
148
151
|
this.log.info(`Stopping sequencer`);
|
|
149
152
|
this.setState(SequencerState.STOPPING, undefined, { force: true });
|
|
150
|
-
this.publisherFactory.
|
|
153
|
+
await this.publisherFactory.stopAll();
|
|
151
154
|
await this.runningPromise?.stop();
|
|
155
|
+
await this.lastCheckpointProposalJob?.awaitPendingSubmission();
|
|
152
156
|
this.setState(SequencerState.STOPPED, undefined, { force: true });
|
|
153
157
|
this.log.info('Stopped sequencer');
|
|
154
158
|
}
|
|
@@ -192,14 +196,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
192
196
|
@trackSpan('Sequencer.work')
|
|
193
197
|
protected async work() {
|
|
194
198
|
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
195
|
-
const { slot, ts,
|
|
199
|
+
const { slot, ts, nowSeconds, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
200
|
+
const { slot: targetSlot, epoch: targetEpoch } = this.epochCache.getTargetEpochAndSlotInNextL1Slot();
|
|
196
201
|
|
|
197
202
|
// Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
|
|
198
|
-
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
203
|
+
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
204
|
+
slot,
|
|
205
|
+
targetSlot,
|
|
206
|
+
epoch,
|
|
207
|
+
targetEpoch,
|
|
208
|
+
ts,
|
|
209
|
+
nowSeconds,
|
|
210
|
+
);
|
|
199
211
|
if (!checkpointProposalJob) {
|
|
200
212
|
return;
|
|
201
213
|
}
|
|
202
214
|
|
|
215
|
+
// Track the job so we can await its pending L1 submission during shutdown
|
|
216
|
+
this.lastCheckpointProposalJob = checkpointProposalJob;
|
|
217
|
+
|
|
203
218
|
// Execute the checkpoint proposal job
|
|
204
219
|
const checkpoint = await checkpointProposalJob.execute();
|
|
205
220
|
|
|
@@ -208,13 +223,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
208
223
|
this.lastCheckpointProposed = checkpoint;
|
|
209
224
|
}
|
|
210
225
|
|
|
211
|
-
// Log fee strategy comparison if on fisherman
|
|
226
|
+
// Log fee strategy comparison if on fisherman (uses target epoch since we mirror the proposer's perspective)
|
|
212
227
|
if (
|
|
213
228
|
this.config.fishermanMode &&
|
|
214
|
-
(this.lastEpochForStrategyComparison === undefined ||
|
|
229
|
+
(this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
|
|
215
230
|
) {
|
|
216
|
-
this.logStrategyComparison(
|
|
217
|
-
this.lastEpochForStrategyComparison =
|
|
231
|
+
this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
|
|
232
|
+
this.lastEpochForStrategyComparison = targetEpoch;
|
|
218
233
|
}
|
|
219
234
|
|
|
220
235
|
return checkpoint;
|
|
@@ -226,44 +241,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
226
241
|
* @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
|
|
227
242
|
*/
|
|
228
243
|
@trackSpan('Sequencer.prepareCheckpointProposal')
|
|
229
|
-
|
|
230
|
-
epoch: EpochNumber,
|
|
244
|
+
protected async prepareCheckpointProposal(
|
|
231
245
|
slot: SlotNumber,
|
|
246
|
+
targetSlot: SlotNumber,
|
|
247
|
+
epoch: EpochNumber,
|
|
248
|
+
targetEpoch: EpochNumber,
|
|
232
249
|
ts: bigint,
|
|
233
|
-
|
|
250
|
+
nowSeconds: bigint,
|
|
234
251
|
): Promise<CheckpointProposalJob | undefined> {
|
|
235
|
-
// Check we have not already processed this slot (cheapest check)
|
|
252
|
+
// Check we have not already processed this target slot (cheapest check)
|
|
236
253
|
// We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
|
|
237
254
|
// running against actual time (eg when we use sandbox-style automining)
|
|
238
255
|
if (
|
|
239
256
|
this.lastSlotForCheckpointProposalJob &&
|
|
240
|
-
this.lastSlotForCheckpointProposalJob >=
|
|
257
|
+
this.lastSlotForCheckpointProposalJob >= targetSlot &&
|
|
241
258
|
this.config.enforceTimeTable
|
|
242
259
|
) {
|
|
243
|
-
this.log.trace(`
|
|
260
|
+
this.log.trace(`Target slot ${targetSlot} has already been processed`);
|
|
244
261
|
return undefined;
|
|
245
262
|
}
|
|
246
263
|
|
|
247
|
-
// But if we have already proposed for this slot,
|
|
248
|
-
if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >=
|
|
249
|
-
this.log.trace(
|
|
264
|
+
// But if we have already proposed for this slot, then we definitely have to skip it, automining or not
|
|
265
|
+
if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= targetSlot) {
|
|
266
|
+
this.log.trace(
|
|
267
|
+
`Slot ${targetSlot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`,
|
|
268
|
+
);
|
|
250
269
|
return undefined;
|
|
251
270
|
}
|
|
252
271
|
|
|
253
272
|
// Check all components are synced to latest as seen by the archiver (queries all subsystems)
|
|
254
273
|
const syncedTo = await this.checkSync({ ts, slot });
|
|
255
274
|
if (!syncedTo) {
|
|
256
|
-
await this.tryVoteWhenSyncFails({ slot, ts });
|
|
275
|
+
await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
|
|
257
276
|
return undefined;
|
|
258
277
|
}
|
|
259
278
|
|
|
260
|
-
// If escape hatch is open for
|
|
279
|
+
// If escape hatch is open for the target epoch, do not start checkpoint proposal work and do not attempt invalidations.
|
|
261
280
|
// Still perform governance/slashing voting (as proposer) once per slot.
|
|
262
|
-
|
|
281
|
+
// When pipelining, we check the target epoch (slot+1's epoch) since that's the epoch we're building for.
|
|
282
|
+
const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(targetEpoch);
|
|
263
283
|
|
|
264
284
|
if (isEscapeHatchOpen) {
|
|
265
285
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
266
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
286
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
267
287
|
if (canPropose) {
|
|
268
288
|
await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
|
|
269
289
|
} else {
|
|
@@ -279,19 +299,29 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
279
299
|
// Next checkpoint follows from the last synced one
|
|
280
300
|
const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
|
|
281
301
|
|
|
302
|
+
// Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
|
|
303
|
+
// confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
|
|
304
|
+
const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
|
|
305
|
+
if (checkpointNumber > confirmedCkpt + 2) {
|
|
306
|
+
this.log.warn(
|
|
307
|
+
`Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
|
|
308
|
+
);
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
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,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
299
329
|
return undefined;
|
|
300
330
|
}
|
|
301
331
|
|
|
302
|
-
// Check that the slot is not taken by a block already (should never happen, since only us can propose for this slot)
|
|
303
|
-
if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >=
|
|
332
|
+
// Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
|
|
333
|
+
if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
|
|
304
334
|
this.log.warn(
|
|
305
|
-
`Cannot propose block at
|
|
335
|
+
`Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`,
|
|
306
336
|
{ ...logCtx, block: syncedTo.blockData.header.toInspect() },
|
|
307
337
|
);
|
|
308
338
|
this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
|
|
@@ -324,15 +354,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
324
354
|
}
|
|
325
355
|
|
|
326
356
|
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
327
|
-
|
|
357
|
+
let invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
|
|
358
|
+
|
|
359
|
+
// Determine the correct archive and L1 state overrides for the canProposeAt check.
|
|
360
|
+
// The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
|
|
361
|
+
// When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
|
|
362
|
+
let archiveForCheck = syncedTo.archive;
|
|
363
|
+
const l1Overrides: {
|
|
364
|
+
forcePendingCheckpointNumber?: CheckpointNumber;
|
|
365
|
+
forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
|
|
366
|
+
} = {};
|
|
367
|
+
|
|
368
|
+
if (this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint) {
|
|
369
|
+
// Parent checkpoint hasn't landed on L1 yet. Override both the proposed checkpoint number
|
|
370
|
+
// and the archive at that checkpoint so L1 simulation sees the correct chain tip.
|
|
371
|
+
const parentCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
|
|
372
|
+
l1Overrides.forcePendingCheckpointNumber = parentCheckpointNumber;
|
|
373
|
+
l1Overrides.forceArchive = { checkpointNumber: parentCheckpointNumber, archive: syncedTo.archive };
|
|
374
|
+
this.metrics.recordPipelineDepth(1);
|
|
375
|
+
|
|
376
|
+
this.log.verbose(
|
|
377
|
+
`Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber})`,
|
|
378
|
+
);
|
|
379
|
+
// Clear the invalidation - the proposed checkpoint should handle it.
|
|
380
|
+
invalidateCheckpoint = undefined;
|
|
381
|
+
} else if (invalidateCheckpoint) {
|
|
382
|
+
// After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
|
|
383
|
+
// exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
|
|
384
|
+
archiveForCheck = invalidateCheckpoint.lastArchive;
|
|
385
|
+
l1Overrides.forcePendingCheckpointNumber = invalidateCheckpoint.forcePendingCheckpointNumber;
|
|
386
|
+
this.metrics.recordPipelineDepth(0);
|
|
387
|
+
} else {
|
|
388
|
+
this.metrics.recordPipelineDepth(0);
|
|
389
|
+
}
|
|
328
390
|
|
|
329
|
-
|
|
330
|
-
// if all the previous checks are good, but we do it just in case.
|
|
331
|
-
const canProposeCheck = await publisher.canProposeAtNextEthBlock(
|
|
332
|
-
syncedTo.archive,
|
|
333
|
-
proposer ?? EthAddress.ZERO,
|
|
334
|
-
invalidateCheckpoint,
|
|
335
|
-
);
|
|
391
|
+
const canProposeCheck = await publisher.canProposeAt(archiveForCheck, proposer ?? EthAddress.ZERO, l1Overrides);
|
|
336
392
|
|
|
337
393
|
if (canProposeCheck === undefined) {
|
|
338
394
|
this.log.warn(
|
|
@@ -344,10 +400,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
344
400
|
return undefined;
|
|
345
401
|
}
|
|
346
402
|
|
|
347
|
-
if (canProposeCheck.slot !==
|
|
403
|
+
if (canProposeCheck.slot !== targetSlot) {
|
|
348
404
|
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:
|
|
405
|
+
`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}.`,
|
|
406
|
+
{ ...logCtx, rollup: canProposeCheck, expectedSlot: targetSlot },
|
|
351
407
|
);
|
|
352
408
|
this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
|
|
353
409
|
this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
|
|
@@ -364,36 +420,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
364
420
|
return undefined;
|
|
365
421
|
}
|
|
366
422
|
|
|
367
|
-
this.lastSlotForCheckpointProposalJob =
|
|
368
|
-
|
|
369
|
-
this.
|
|
423
|
+
this.lastSlotForCheckpointProposalJob = targetSlot;
|
|
424
|
+
|
|
425
|
+
await this.p2pClient.prepareForSlot(targetSlot);
|
|
426
|
+
this.log.info(
|
|
427
|
+
`Preparing checkpoint proposal ${checkpointNumber} for target slot ${targetSlot} during wall-clock slot ${slot}`,
|
|
428
|
+
{
|
|
429
|
+
...logCtx,
|
|
430
|
+
proposer,
|
|
431
|
+
pipeliningEnabled: this.epochCache.isProposerPipeliningEnabled(),
|
|
432
|
+
},
|
|
433
|
+
);
|
|
370
434
|
|
|
371
435
|
// Create and return the checkpoint proposal job
|
|
372
436
|
return this.createCheckpointProposalJob(
|
|
373
|
-
epoch,
|
|
374
437
|
slot,
|
|
438
|
+
targetSlot,
|
|
439
|
+
targetEpoch,
|
|
375
440
|
checkpointNumber,
|
|
376
441
|
syncedTo.blockNumber,
|
|
377
442
|
proposer,
|
|
378
443
|
publisher,
|
|
379
444
|
attestorAddress,
|
|
380
445
|
invalidateCheckpoint,
|
|
446
|
+
syncedTo.proposedCheckpointData,
|
|
381
447
|
);
|
|
382
448
|
}
|
|
383
449
|
|
|
384
450
|
protected createCheckpointProposalJob(
|
|
385
|
-
epoch: EpochNumber,
|
|
386
451
|
slot: SlotNumber,
|
|
452
|
+
targetSlot: SlotNumber,
|
|
453
|
+
targetEpoch: EpochNumber,
|
|
387
454
|
checkpointNumber: CheckpointNumber,
|
|
388
455
|
syncedToBlockNumber: BlockNumber,
|
|
389
456
|
proposer: EthAddress | undefined,
|
|
390
457
|
publisher: SequencerPublisher,
|
|
391
458
|
attestorAddress: EthAddress,
|
|
392
459
|
invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
|
|
460
|
+
proposedCheckpointData?: ProposedCheckpointData,
|
|
393
461
|
): CheckpointProposalJob {
|
|
394
462
|
return new CheckpointProposalJob(
|
|
395
|
-
epoch,
|
|
396
463
|
slot,
|
|
464
|
+
targetSlot,
|
|
465
|
+
targetEpoch,
|
|
397
466
|
checkpointNumber,
|
|
398
467
|
syncedToBlockNumber,
|
|
399
468
|
proposer,
|
|
@@ -419,6 +488,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
419
488
|
this.setState.bind(this),
|
|
420
489
|
this.tracer,
|
|
421
490
|
this.log.getBindings(),
|
|
491
|
+
proposedCheckpointData,
|
|
422
492
|
);
|
|
423
493
|
}
|
|
424
494
|
|
|
@@ -475,16 +545,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
475
545
|
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
476
546
|
*/
|
|
477
547
|
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 (
|
|
548
|
+
// Check that the archiver has fully synced the L2 slot before the one we want to propose in.
|
|
549
|
+
// The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
|
|
550
|
+
// See getSyncedL2SlotNumber for how missed L1 blocks are handled.
|
|
551
|
+
const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
|
|
552
|
+
const { slot } = args;
|
|
553
|
+
if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
|
|
484
554
|
this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
|
|
485
555
|
slot,
|
|
486
|
-
|
|
487
|
-
l1Timestamp,
|
|
556
|
+
syncedL2Slot,
|
|
488
557
|
});
|
|
489
558
|
return undefined;
|
|
490
559
|
}
|
|
@@ -494,25 +563,37 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
494
563
|
number: syncSummary.latestBlockNumber,
|
|
495
564
|
hash: syncSummary.latestBlockHash,
|
|
496
565
|
})),
|
|
497
|
-
this.l2BlockSource
|
|
566
|
+
this.l2BlockSource
|
|
567
|
+
.getL2Tips()
|
|
568
|
+
.then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
|
|
498
569
|
this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
|
|
499
570
|
this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
|
|
500
571
|
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
572
|
+
this.l2BlockSource.getProposedCheckpointOnly(),
|
|
501
573
|
] as const);
|
|
502
574
|
|
|
503
|
-
const [worldState,
|
|
575
|
+
const [worldState, l2Tips, p2p, l1ToL2MessageSource, pendingChainValidationStatus, proposedCheckpointData] =
|
|
576
|
+
syncedBlocks;
|
|
504
577
|
|
|
505
578
|
// 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
579
|
// as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
|
|
507
580
|
// TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
|
|
508
581
|
const result =
|
|
509
|
-
(
|
|
510
|
-
|
|
511
|
-
p2p.
|
|
512
|
-
l1ToL2MessageSource.
|
|
582
|
+
(l2Tips.proposed.number === 0 &&
|
|
583
|
+
worldState.number === 0 &&
|
|
584
|
+
p2p.number === 0 &&
|
|
585
|
+
l1ToL2MessageSource.number === 0) ||
|
|
586
|
+
(worldState.hash === l2Tips.proposed.hash &&
|
|
587
|
+
p2p.hash === l2Tips.proposed.hash &&
|
|
588
|
+
l1ToL2MessageSource.hash === l2Tips.proposed.hash);
|
|
513
589
|
|
|
514
590
|
if (!result) {
|
|
515
|
-
this.log.debug(`Sequencer sync check failed`, {
|
|
591
|
+
this.log.debug(`Sequencer sync check failed`, {
|
|
592
|
+
worldState,
|
|
593
|
+
l2BlockSource: l2Tips.proposed,
|
|
594
|
+
p2p,
|
|
595
|
+
l1ToL2MessageSource,
|
|
596
|
+
});
|
|
516
597
|
return undefined;
|
|
517
598
|
}
|
|
518
599
|
|
|
@@ -522,9 +603,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
522
603
|
const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
523
604
|
return {
|
|
524
605
|
checkpointNumber: CheckpointNumber.ZERO,
|
|
606
|
+
checkpointedCheckpointNumber: CheckpointNumber.ZERO,
|
|
525
607
|
blockNumber: BlockNumber.ZERO,
|
|
526
608
|
archive,
|
|
527
|
-
|
|
609
|
+
hasProposedCheckpoint: false,
|
|
610
|
+
syncedL2Slot,
|
|
528
611
|
pendingChainValidationStatus,
|
|
529
612
|
};
|
|
530
613
|
}
|
|
@@ -536,12 +619,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
536
619
|
return undefined;
|
|
537
620
|
}
|
|
538
621
|
|
|
622
|
+
const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
|
|
623
|
+
|
|
539
624
|
return {
|
|
540
625
|
blockData,
|
|
541
626
|
blockNumber: blockData.header.getBlockNumber(),
|
|
542
627
|
checkpointNumber: blockData.checkpointNumber,
|
|
628
|
+
checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
|
|
543
629
|
archive: blockData.archive.root,
|
|
544
|
-
|
|
630
|
+
hasProposedCheckpoint,
|
|
631
|
+
proposedCheckpointData,
|
|
632
|
+
syncedL2Slot,
|
|
545
633
|
pendingChainValidationStatus,
|
|
546
634
|
};
|
|
547
635
|
}
|
|
@@ -550,20 +638,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
550
638
|
* Checks if we are the proposer for the next slot.
|
|
551
639
|
* @returns True if we can propose, and the proposer address (undefined if anyone can propose)
|
|
552
640
|
*/
|
|
553
|
-
protected async checkCanPropose(
|
|
641
|
+
protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
|
|
554
642
|
let proposer: EthAddress | undefined;
|
|
555
643
|
|
|
556
644
|
try {
|
|
557
|
-
proposer = await this.epochCache.getProposerAttesterAddressInSlot(
|
|
645
|
+
proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
|
|
558
646
|
} catch (e) {
|
|
559
647
|
if (e instanceof NoCommitteeError) {
|
|
560
|
-
if (this.lastSlotForNoCommitteeWarning !==
|
|
561
|
-
this.lastSlotForNoCommitteeWarning =
|
|
562
|
-
this.log.warn(`Cannot propose at
|
|
648
|
+
if (this.lastSlotForNoCommitteeWarning !== targetSlot) {
|
|
649
|
+
this.lastSlotForNoCommitteeWarning = targetSlot;
|
|
650
|
+
this.log.warn(`Cannot propose at target slot ${targetSlot} since the committee does not exist on L1`);
|
|
563
651
|
}
|
|
564
652
|
return [false, undefined];
|
|
565
653
|
}
|
|
566
|
-
this.log.error(`Error getting proposer for slot ${
|
|
654
|
+
this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
|
|
567
655
|
return [false, undefined];
|
|
568
656
|
}
|
|
569
657
|
|
|
@@ -580,10 +668,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
580
668
|
const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
|
|
581
669
|
|
|
582
670
|
if (!weAreProposer) {
|
|
583
|
-
this.log.debug(`Cannot propose at slot ${
|
|
671
|
+
this.log.debug(`Cannot propose at target slot ${targetSlot} since we are not a proposer`, {
|
|
672
|
+
targetSlot,
|
|
673
|
+
validatorAddresses,
|
|
674
|
+
proposer,
|
|
675
|
+
});
|
|
584
676
|
return [false, proposer];
|
|
585
677
|
}
|
|
586
678
|
|
|
679
|
+
this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
|
|
680
|
+
targetSlot,
|
|
681
|
+
proposer,
|
|
682
|
+
});
|
|
587
683
|
return [true, proposer];
|
|
588
684
|
}
|
|
589
685
|
|
|
@@ -592,8 +688,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
592
688
|
* This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
|
|
593
689
|
*/
|
|
594
690
|
@trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
|
|
595
|
-
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
|
|
596
|
-
const { slot } = args;
|
|
691
|
+
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
|
|
692
|
+
const { slot, targetSlot } = args;
|
|
597
693
|
|
|
598
694
|
// Prevent duplicate attempts in the same slot
|
|
599
695
|
if (this.lastSlotForFallbackVote === slot) {
|
|
@@ -621,7 +717,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
621
717
|
});
|
|
622
718
|
|
|
623
719
|
// Check if we're a proposer or proposal is open
|
|
624
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
720
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
625
721
|
if (!canPropose) {
|
|
626
722
|
this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
|
|
627
723
|
return;
|
|
@@ -638,9 +734,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
638
734
|
slot,
|
|
639
735
|
});
|
|
640
736
|
|
|
641
|
-
// Enqueue governance and slashing votes
|
|
737
|
+
// Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
|
|
642
738
|
const voter = new CheckpointVoter(
|
|
643
|
-
|
|
739
|
+
targetSlot,
|
|
644
740
|
publisher,
|
|
645
741
|
attestorAddress,
|
|
646
742
|
this.validatorClient,
|
|
@@ -720,7 +816,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
720
816
|
syncedTo: SequencerSyncCheckResult,
|
|
721
817
|
currentSlot: SlotNumber,
|
|
722
818
|
): Promise<void> {
|
|
723
|
-
const { pendingChainValidationStatus,
|
|
819
|
+
const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
|
|
724
820
|
if (pendingChainValidationStatus.valid) {
|
|
725
821
|
return;
|
|
726
822
|
}
|
|
@@ -735,7 +831,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
735
831
|
|
|
736
832
|
const logData = {
|
|
737
833
|
invalidL1Timestamp: invalidCheckpointTimestamp,
|
|
738
|
-
|
|
834
|
+
syncedL2Slot,
|
|
739
835
|
invalidCheckpoint: pendingChainValidationStatus.checkpoint,
|
|
740
836
|
secondsBeforeInvalidatingBlockAsCommitteeMember,
|
|
741
837
|
secondsBeforeInvalidatingBlockAsNonCommitteeMember,
|
|
@@ -880,8 +976,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
880
976
|
type SequencerSyncCheckResult = {
|
|
881
977
|
blockData?: BlockData;
|
|
882
978
|
checkpointNumber: CheckpointNumber;
|
|
979
|
+
checkpointedCheckpointNumber: CheckpointNumber;
|
|
883
980
|
blockNumber: BlockNumber;
|
|
884
981
|
archive: Fr;
|
|
885
|
-
|
|
982
|
+
hasProposedCheckpoint: boolean;
|
|
983
|
+
proposedCheckpointData?: ProposedCheckpointData;
|
|
984
|
+
syncedL2Slot: SlotNumber;
|
|
886
985
|
pendingChainValidationStatus: ValidateCheckpointResult;
|
|
887
986
|
};
|