@aztec/sequencer-client 0.0.1-commit.f504929 → 0.0.1-commit.f650c0a5c
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 -27
- 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 -5
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +2 -3
- package/dest/publisher/sequencer-publisher.d.ts +58 -32
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +132 -89
- package/dest/sequencer/checkpoint_proposal_job.d.ts +36 -9
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +302 -180
- 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 +130 -71
- package/dest/sequencer/timetable.d.ts +7 -3
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +21 -12
- 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 -28
- 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 -6
- package/src/publisher/sequencer-publisher.ts +205 -127
- package/src/sequencer/README.md +81 -12
- package/src/sequencer/checkpoint_proposal_job.ts +385 -199
- 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 +186 -80
- package/src/sequencer/timetable.ts +26 -15
- 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
|
|
|
@@ -120,6 +123,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
120
123
|
p2pPropagationTime: this.config.attestationPropagationTime,
|
|
121
124
|
blockDurationMs: this.config.blockDurationMs,
|
|
122
125
|
enforce: this.config.enforceTimeTable,
|
|
126
|
+
pipelining: this.epochCache.isProposerPipeliningEnabled(),
|
|
123
127
|
},
|
|
124
128
|
this.metrics,
|
|
125
129
|
this.log,
|
|
@@ -147,8 +151,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
147
151
|
public async stop(): Promise<void> {
|
|
148
152
|
this.log.info(`Stopping sequencer`);
|
|
149
153
|
this.setState(SequencerState.STOPPING, undefined, { force: true });
|
|
150
|
-
this.publisherFactory.
|
|
154
|
+
await this.publisherFactory.stopAll();
|
|
151
155
|
await this.runningPromise?.stop();
|
|
156
|
+
await this.lastCheckpointProposalJob?.awaitPendingSubmission();
|
|
152
157
|
this.setState(SequencerState.STOPPED, undefined, { force: true });
|
|
153
158
|
this.log.info('Stopped sequencer');
|
|
154
159
|
}
|
|
@@ -192,14 +197,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
192
197
|
@trackSpan('Sequencer.work')
|
|
193
198
|
protected async work() {
|
|
194
199
|
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
195
|
-
const { slot, ts,
|
|
200
|
+
const { slot, ts, nowSeconds, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
201
|
+
const { slot: targetSlot, epoch: targetEpoch } = this.epochCache.getTargetEpochAndSlotInNextL1Slot();
|
|
196
202
|
|
|
197
203
|
// Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
|
|
198
|
-
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
204
|
+
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
205
|
+
slot,
|
|
206
|
+
targetSlot,
|
|
207
|
+
epoch,
|
|
208
|
+
targetEpoch,
|
|
209
|
+
ts,
|
|
210
|
+
nowSeconds,
|
|
211
|
+
);
|
|
199
212
|
if (!checkpointProposalJob) {
|
|
200
213
|
return;
|
|
201
214
|
}
|
|
202
215
|
|
|
216
|
+
// Track the job so we can await its pending L1 submission during shutdown
|
|
217
|
+
this.lastCheckpointProposalJob = checkpointProposalJob;
|
|
218
|
+
|
|
203
219
|
// Execute the checkpoint proposal job
|
|
204
220
|
const checkpoint = await checkpointProposalJob.execute();
|
|
205
221
|
|
|
@@ -208,13 +224,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
208
224
|
this.lastCheckpointProposed = checkpoint;
|
|
209
225
|
}
|
|
210
226
|
|
|
211
|
-
// Log fee strategy comparison if on fisherman
|
|
227
|
+
// Log fee strategy comparison if on fisherman (uses target epoch since we mirror the proposer's perspective)
|
|
212
228
|
if (
|
|
213
229
|
this.config.fishermanMode &&
|
|
214
|
-
(this.lastEpochForStrategyComparison === undefined ||
|
|
230
|
+
(this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
|
|
215
231
|
) {
|
|
216
|
-
this.logStrategyComparison(
|
|
217
|
-
this.lastEpochForStrategyComparison =
|
|
232
|
+
this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
|
|
233
|
+
this.lastEpochForStrategyComparison = targetEpoch;
|
|
218
234
|
}
|
|
219
235
|
|
|
220
236
|
return checkpoint;
|
|
@@ -226,44 +242,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
226
242
|
* @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
|
|
227
243
|
*/
|
|
228
244
|
@trackSpan('Sequencer.prepareCheckpointProposal')
|
|
229
|
-
|
|
230
|
-
epoch: EpochNumber,
|
|
245
|
+
protected async prepareCheckpointProposal(
|
|
231
246
|
slot: SlotNumber,
|
|
247
|
+
targetSlot: SlotNumber,
|
|
248
|
+
epoch: EpochNumber,
|
|
249
|
+
targetEpoch: EpochNumber,
|
|
232
250
|
ts: bigint,
|
|
233
|
-
|
|
251
|
+
nowSeconds: bigint,
|
|
234
252
|
): Promise<CheckpointProposalJob | undefined> {
|
|
235
|
-
// Check we have not already processed this slot (cheapest check)
|
|
253
|
+
// Check we have not already processed this target slot (cheapest check)
|
|
236
254
|
// We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
|
|
237
255
|
// running against actual time (eg when we use sandbox-style automining)
|
|
238
256
|
if (
|
|
239
257
|
this.lastSlotForCheckpointProposalJob &&
|
|
240
|
-
this.lastSlotForCheckpointProposalJob >=
|
|
258
|
+
this.lastSlotForCheckpointProposalJob >= targetSlot &&
|
|
241
259
|
this.config.enforceTimeTable
|
|
242
260
|
) {
|
|
243
|
-
this.log.trace(`
|
|
261
|
+
this.log.trace(`Target slot ${targetSlot} has already been processed`);
|
|
244
262
|
return undefined;
|
|
245
263
|
}
|
|
246
264
|
|
|
247
|
-
// But if we have already proposed for this slot,
|
|
248
|
-
if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >=
|
|
249
|
-
this.log.trace(
|
|
265
|
+
// But if we have already proposed for this slot, then we definitely have to skip it, automining or not
|
|
266
|
+
if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= targetSlot) {
|
|
267
|
+
this.log.trace(
|
|
268
|
+
`Slot ${targetSlot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`,
|
|
269
|
+
);
|
|
250
270
|
return undefined;
|
|
251
271
|
}
|
|
252
272
|
|
|
253
273
|
// Check all components are synced to latest as seen by the archiver (queries all subsystems)
|
|
254
274
|
const syncedTo = await this.checkSync({ ts, slot });
|
|
255
275
|
if (!syncedTo) {
|
|
256
|
-
await this.tryVoteWhenSyncFails({ slot, ts });
|
|
276
|
+
await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
|
|
257
277
|
return undefined;
|
|
258
278
|
}
|
|
259
279
|
|
|
260
|
-
// If escape hatch is open for
|
|
280
|
+
// If escape hatch is open for the target epoch, do not start checkpoint proposal work and do not attempt invalidations.
|
|
261
281
|
// Still perform governance/slashing voting (as proposer) once per slot.
|
|
262
|
-
|
|
282
|
+
// When pipelining, we check the target epoch (slot+1's epoch) since that's the epoch we're building for.
|
|
283
|
+
const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(targetEpoch);
|
|
263
284
|
|
|
264
285
|
if (isEscapeHatchOpen) {
|
|
265
286
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
266
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
287
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
267
288
|
if (canPropose) {
|
|
268
289
|
await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
|
|
269
290
|
} else {
|
|
@@ -280,18 +301,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
280
301
|
const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
|
|
281
302
|
|
|
282
303
|
const logCtx = {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
|
|
304
|
+
nowSeconds,
|
|
305
|
+
syncedToL2Slot: syncedTo.syncedL2Slot,
|
|
286
306
|
slot,
|
|
307
|
+
targetSlot,
|
|
287
308
|
slotTs: ts,
|
|
288
309
|
checkpointNumber,
|
|
289
310
|
isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
|
|
290
311
|
};
|
|
291
312
|
|
|
292
|
-
// Check that we are a proposer for the
|
|
313
|
+
// Check that we are a proposer for the target slot.
|
|
293
314
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
294
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
315
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
295
316
|
|
|
296
317
|
// If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
|
|
297
318
|
if (!canPropose) {
|
|
@@ -299,10 +320,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
299
320
|
return undefined;
|
|
300
321
|
}
|
|
301
322
|
|
|
302
|
-
//
|
|
303
|
-
|
|
323
|
+
// Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
|
|
324
|
+
// confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
|
|
325
|
+
const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
|
|
326
|
+
if (checkpointNumber > confirmedCkpt + 2) {
|
|
327
|
+
this.log.verbose(
|
|
328
|
+
`Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
|
|
329
|
+
);
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
|
|
334
|
+
if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
|
|
304
335
|
this.log.warn(
|
|
305
|
-
`Cannot propose block at
|
|
336
|
+
`Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`,
|
|
306
337
|
{ ...logCtx, block: syncedTo.blockData.header.toInspect() },
|
|
307
338
|
);
|
|
308
339
|
this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
|
|
@@ -324,15 +355,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
324
355
|
}
|
|
325
356
|
|
|
326
357
|
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
327
|
-
|
|
358
|
+
let invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
|
|
359
|
+
|
|
360
|
+
// Determine the correct archive and L1 state overrides for the canProposeAt check.
|
|
361
|
+
// The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
|
|
362
|
+
// When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
|
|
363
|
+
let archiveForCheck = syncedTo.archive;
|
|
364
|
+
const l1Overrides: {
|
|
365
|
+
forcePendingCheckpointNumber?: CheckpointNumber;
|
|
366
|
+
forceArchive?: { checkpointNumber: CheckpointNumber; archive: Fr };
|
|
367
|
+
} = {};
|
|
368
|
+
|
|
369
|
+
if (this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint) {
|
|
370
|
+
// Parent checkpoint hasn't landed on L1 yet. Override both the proposed checkpoint number
|
|
371
|
+
// and the archive at that checkpoint so L1 simulation sees the correct chain tip.
|
|
372
|
+
const parentCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
|
|
373
|
+
l1Overrides.forcePendingCheckpointNumber = parentCheckpointNumber;
|
|
374
|
+
l1Overrides.forceArchive = { checkpointNumber: parentCheckpointNumber, archive: syncedTo.archive };
|
|
375
|
+
this.metrics.recordPipelineDepth(1);
|
|
376
|
+
|
|
377
|
+
this.log.verbose(
|
|
378
|
+
`Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber})`,
|
|
379
|
+
);
|
|
380
|
+
// Clear the invalidation - the proposed checkpoint should handle it.
|
|
381
|
+
invalidateCheckpoint = undefined;
|
|
382
|
+
} else if (invalidateCheckpoint) {
|
|
383
|
+
// After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
|
|
384
|
+
// exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
|
|
385
|
+
archiveForCheck = invalidateCheckpoint.lastArchive;
|
|
386
|
+
l1Overrides.forcePendingCheckpointNumber = invalidateCheckpoint.forcePendingCheckpointNumber;
|
|
387
|
+
this.metrics.recordPipelineDepth(0);
|
|
388
|
+
} else {
|
|
389
|
+
this.metrics.recordPipelineDepth(0);
|
|
390
|
+
}
|
|
328
391
|
|
|
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
|
-
);
|
|
392
|
+
const canProposeCheck = await publisher.canProposeAt(archiveForCheck, proposer ?? EthAddress.ZERO, l1Overrides);
|
|
336
393
|
|
|
337
394
|
if (canProposeCheck === undefined) {
|
|
338
395
|
this.log.warn(
|
|
@@ -344,10 +401,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
344
401
|
return undefined;
|
|
345
402
|
}
|
|
346
403
|
|
|
347
|
-
if (canProposeCheck.slot !==
|
|
404
|
+
if (canProposeCheck.slot !== targetSlot) {
|
|
348
405
|
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:
|
|
406
|
+
`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}.`,
|
|
407
|
+
{ ...logCtx, rollup: canProposeCheck, expectedSlot: targetSlot },
|
|
351
408
|
);
|
|
352
409
|
this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
|
|
353
410
|
this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
|
|
@@ -364,36 +421,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
364
421
|
return undefined;
|
|
365
422
|
}
|
|
366
423
|
|
|
367
|
-
this.lastSlotForCheckpointProposalJob =
|
|
368
|
-
|
|
369
|
-
this.
|
|
424
|
+
this.lastSlotForCheckpointProposalJob = targetSlot;
|
|
425
|
+
|
|
426
|
+
await this.p2pClient.prepareForSlot(targetSlot);
|
|
427
|
+
this.log.info(
|
|
428
|
+
`Preparing checkpoint proposal ${checkpointNumber} for target slot ${targetSlot} during wall-clock slot ${slot}`,
|
|
429
|
+
{
|
|
430
|
+
...logCtx,
|
|
431
|
+
proposer,
|
|
432
|
+
pipeliningEnabled: this.epochCache.isProposerPipeliningEnabled(),
|
|
433
|
+
},
|
|
434
|
+
);
|
|
370
435
|
|
|
371
436
|
// Create and return the checkpoint proposal job
|
|
372
437
|
return this.createCheckpointProposalJob(
|
|
373
|
-
epoch,
|
|
374
438
|
slot,
|
|
439
|
+
targetSlot,
|
|
440
|
+
targetEpoch,
|
|
375
441
|
checkpointNumber,
|
|
376
442
|
syncedTo.blockNumber,
|
|
377
443
|
proposer,
|
|
378
444
|
publisher,
|
|
379
445
|
attestorAddress,
|
|
380
446
|
invalidateCheckpoint,
|
|
447
|
+
syncedTo.proposedCheckpointData,
|
|
381
448
|
);
|
|
382
449
|
}
|
|
383
450
|
|
|
384
451
|
protected createCheckpointProposalJob(
|
|
385
|
-
epoch: EpochNumber,
|
|
386
452
|
slot: SlotNumber,
|
|
453
|
+
targetSlot: SlotNumber,
|
|
454
|
+
targetEpoch: EpochNumber,
|
|
387
455
|
checkpointNumber: CheckpointNumber,
|
|
388
456
|
syncedToBlockNumber: BlockNumber,
|
|
389
457
|
proposer: EthAddress | undefined,
|
|
390
458
|
publisher: SequencerPublisher,
|
|
391
459
|
attestorAddress: EthAddress,
|
|
392
460
|
invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
|
|
461
|
+
proposedCheckpointData?: ProposedCheckpointData,
|
|
393
462
|
): CheckpointProposalJob {
|
|
394
463
|
return new CheckpointProposalJob(
|
|
395
|
-
epoch,
|
|
396
464
|
slot,
|
|
465
|
+
targetSlot,
|
|
466
|
+
targetEpoch,
|
|
397
467
|
checkpointNumber,
|
|
398
468
|
syncedToBlockNumber,
|
|
399
469
|
proposer,
|
|
@@ -419,6 +489,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
419
489
|
this.setState.bind(this),
|
|
420
490
|
this.tracer,
|
|
421
491
|
this.log.getBindings(),
|
|
492
|
+
proposedCheckpointData,
|
|
422
493
|
);
|
|
423
494
|
}
|
|
424
495
|
|
|
@@ -475,16 +546,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
475
546
|
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
476
547
|
*/
|
|
477
548
|
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 (
|
|
549
|
+
// Check that the archiver has fully synced the L2 slot before the one we want to propose in.
|
|
550
|
+
// The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
|
|
551
|
+
// See getSyncedL2SlotNumber for how missed L1 blocks are handled.
|
|
552
|
+
const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
|
|
553
|
+
const { slot } = args;
|
|
554
|
+
if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
|
|
484
555
|
this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
|
|
485
556
|
slot,
|
|
486
|
-
|
|
487
|
-
l1Timestamp,
|
|
557
|
+
syncedL2Slot,
|
|
488
558
|
});
|
|
489
559
|
return undefined;
|
|
490
560
|
}
|
|
@@ -494,25 +564,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
494
564
|
number: syncSummary.latestBlockNumber,
|
|
495
565
|
hash: syncSummary.latestBlockHash,
|
|
496
566
|
})),
|
|
497
|
-
this.l2BlockSource
|
|
567
|
+
this.l2BlockSource
|
|
568
|
+
.getL2Tips()
|
|
569
|
+
.then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
|
|
498
570
|
this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
|
|
499
|
-
this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
|
|
571
|
+
this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
|
|
500
572
|
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
573
|
+
this.l2BlockSource.getProposedCheckpointOnly(),
|
|
501
574
|
] as const);
|
|
502
575
|
|
|
503
|
-
const [worldState,
|
|
576
|
+
const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
|
|
577
|
+
syncedBlocks;
|
|
504
578
|
|
|
505
579
|
// 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
580
|
// as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
|
|
507
581
|
// TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
|
|
508
582
|
const result =
|
|
509
|
-
(
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
583
|
+
(l2Tips.proposed.number === 0 &&
|
|
584
|
+
l2Tips.checkpointed.block.number === 0 &&
|
|
585
|
+
l2Tips.checkpointed.checkpoint.number === 0 &&
|
|
586
|
+
worldState.number === 0 &&
|
|
587
|
+
p2p.number === 0 &&
|
|
588
|
+
l1ToL2MessageSourceTips.proposed.number === 0 &&
|
|
589
|
+
l1ToL2MessageSourceTips.checkpointed.block.number === 0 &&
|
|
590
|
+
l1ToL2MessageSourceTips.checkpointed.checkpoint.number === 0) ||
|
|
591
|
+
(worldState.hash === l2Tips.proposed.hash &&
|
|
592
|
+
p2p.hash === l2Tips.proposed.hash &&
|
|
593
|
+
l1ToL2MessageSourceTips.proposed.hash === l2Tips.proposed.hash &&
|
|
594
|
+
l1ToL2MessageSourceTips.checkpointed.block.hash === l2Tips.checkpointed.block.hash &&
|
|
595
|
+
l1ToL2MessageSourceTips.checkpointed.checkpoint.hash === l2Tips.checkpointed.checkpoint.hash);
|
|
513
596
|
|
|
514
597
|
if (!result) {
|
|
515
|
-
this.log.debug(`Sequencer sync check failed`, {
|
|
598
|
+
this.log.debug(`Sequencer sync check failed`, {
|
|
599
|
+
worldState,
|
|
600
|
+
l2BlockSource: l2Tips.proposed,
|
|
601
|
+
p2p,
|
|
602
|
+
l1ToL2MessageSourceTips,
|
|
603
|
+
});
|
|
516
604
|
return undefined;
|
|
517
605
|
}
|
|
518
606
|
|
|
@@ -522,9 +610,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
522
610
|
const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
523
611
|
return {
|
|
524
612
|
checkpointNumber: CheckpointNumber.ZERO,
|
|
613
|
+
checkpointedCheckpointNumber: CheckpointNumber.ZERO,
|
|
525
614
|
blockNumber: BlockNumber.ZERO,
|
|
526
615
|
archive,
|
|
527
|
-
|
|
616
|
+
hasProposedCheckpoint: false,
|
|
617
|
+
syncedL2Slot,
|
|
528
618
|
pendingChainValidationStatus,
|
|
529
619
|
};
|
|
530
620
|
}
|
|
@@ -536,12 +626,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
536
626
|
return undefined;
|
|
537
627
|
}
|
|
538
628
|
|
|
629
|
+
const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
|
|
630
|
+
|
|
539
631
|
return {
|
|
540
632
|
blockData,
|
|
541
633
|
blockNumber: blockData.header.getBlockNumber(),
|
|
542
634
|
checkpointNumber: blockData.checkpointNumber,
|
|
635
|
+
checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
|
|
543
636
|
archive: blockData.archive.root,
|
|
544
|
-
|
|
637
|
+
hasProposedCheckpoint,
|
|
638
|
+
proposedCheckpointData,
|
|
639
|
+
syncedL2Slot,
|
|
545
640
|
pendingChainValidationStatus,
|
|
546
641
|
};
|
|
547
642
|
}
|
|
@@ -550,20 +645,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
550
645
|
* Checks if we are the proposer for the next slot.
|
|
551
646
|
* @returns True if we can propose, and the proposer address (undefined if anyone can propose)
|
|
552
647
|
*/
|
|
553
|
-
protected async checkCanPropose(
|
|
648
|
+
protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
|
|
554
649
|
let proposer: EthAddress | undefined;
|
|
555
650
|
|
|
556
651
|
try {
|
|
557
|
-
proposer = await this.epochCache.getProposerAttesterAddressInSlot(
|
|
652
|
+
proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
|
|
558
653
|
} catch (e) {
|
|
559
654
|
if (e instanceof NoCommitteeError) {
|
|
560
|
-
if (this.lastSlotForNoCommitteeWarning !==
|
|
561
|
-
this.lastSlotForNoCommitteeWarning =
|
|
562
|
-
this.log.warn(`Cannot propose at
|
|
655
|
+
if (this.lastSlotForNoCommitteeWarning !== targetSlot) {
|
|
656
|
+
this.lastSlotForNoCommitteeWarning = targetSlot;
|
|
657
|
+
this.log.warn(`Cannot propose at target slot ${targetSlot} since the committee does not exist on L1`);
|
|
563
658
|
}
|
|
564
659
|
return [false, undefined];
|
|
565
660
|
}
|
|
566
|
-
this.log.error(`Error getting proposer for slot ${
|
|
661
|
+
this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
|
|
567
662
|
return [false, undefined];
|
|
568
663
|
}
|
|
569
664
|
|
|
@@ -580,10 +675,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
580
675
|
const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
|
|
581
676
|
|
|
582
677
|
if (!weAreProposer) {
|
|
583
|
-
this.log.debug(`Cannot propose at slot ${
|
|
678
|
+
this.log.debug(`Cannot propose at target slot ${targetSlot} since we are not a proposer`, {
|
|
679
|
+
targetSlot,
|
|
680
|
+
validatorAddresses,
|
|
681
|
+
proposer,
|
|
682
|
+
});
|
|
584
683
|
return [false, proposer];
|
|
585
684
|
}
|
|
586
685
|
|
|
686
|
+
this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
|
|
687
|
+
targetSlot,
|
|
688
|
+
proposer,
|
|
689
|
+
});
|
|
587
690
|
return [true, proposer];
|
|
588
691
|
}
|
|
589
692
|
|
|
@@ -592,8 +695,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
592
695
|
* This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
|
|
593
696
|
*/
|
|
594
697
|
@trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
|
|
595
|
-
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
|
|
596
|
-
const { slot } = args;
|
|
698
|
+
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
|
|
699
|
+
const { slot, targetSlot } = args;
|
|
597
700
|
|
|
598
701
|
// Prevent duplicate attempts in the same slot
|
|
599
702
|
if (this.lastSlotForFallbackVote === slot) {
|
|
@@ -621,7 +724,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
621
724
|
});
|
|
622
725
|
|
|
623
726
|
// Check if we're a proposer or proposal is open
|
|
624
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
727
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
625
728
|
if (!canPropose) {
|
|
626
729
|
this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
|
|
627
730
|
return;
|
|
@@ -638,9 +741,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
638
741
|
slot,
|
|
639
742
|
});
|
|
640
743
|
|
|
641
|
-
// Enqueue governance and slashing votes
|
|
744
|
+
// Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
|
|
642
745
|
const voter = new CheckpointVoter(
|
|
643
|
-
|
|
746
|
+
targetSlot,
|
|
644
747
|
publisher,
|
|
645
748
|
attestorAddress,
|
|
646
749
|
this.validatorClient,
|
|
@@ -720,7 +823,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
720
823
|
syncedTo: SequencerSyncCheckResult,
|
|
721
824
|
currentSlot: SlotNumber,
|
|
722
825
|
): Promise<void> {
|
|
723
|
-
const { pendingChainValidationStatus,
|
|
826
|
+
const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
|
|
724
827
|
if (pendingChainValidationStatus.valid) {
|
|
725
828
|
return;
|
|
726
829
|
}
|
|
@@ -735,7 +838,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
735
838
|
|
|
736
839
|
const logData = {
|
|
737
840
|
invalidL1Timestamp: invalidCheckpointTimestamp,
|
|
738
|
-
|
|
841
|
+
syncedL2Slot,
|
|
739
842
|
invalidCheckpoint: pendingChainValidationStatus.checkpoint,
|
|
740
843
|
secondsBeforeInvalidatingBlockAsCommitteeMember,
|
|
741
844
|
secondsBeforeInvalidatingBlockAsNonCommitteeMember,
|
|
@@ -880,8 +983,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
880
983
|
type SequencerSyncCheckResult = {
|
|
881
984
|
blockData?: BlockData;
|
|
882
985
|
checkpointNumber: CheckpointNumber;
|
|
986
|
+
checkpointedCheckpointNumber: CheckpointNumber;
|
|
883
987
|
blockNumber: BlockNumber;
|
|
884
988
|
archive: Fr;
|
|
885
|
-
|
|
989
|
+
hasProposedCheckpoint: boolean;
|
|
990
|
+
proposedCheckpointData?: ProposedCheckpointData;
|
|
991
|
+
syncedL2Slot: SlotNumber;
|
|
886
992
|
pendingChainValidationStatus: ValidateCheckpointResult;
|
|
887
993
|
};
|