@aztec/sequencer-client 0.0.1-commit.7b86788 → 0.0.1-commit.7cbc774
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/README.md +282 -21
- package/dest/client/sequencer-client.d.ts +8 -3
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +53 -30
- package/dest/config.d.ts +28 -5
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +62 -27
- package/dest/global_variable_builder/fee_predictor.d.ts +37 -0
- package/dest/global_variable_builder/fee_predictor.d.ts.map +1 -0
- package/dest/global_variable_builder/fee_predictor.js +128 -0
- package/dest/global_variable_builder/fee_provider.d.ts +21 -0
- package/dest/global_variable_builder/fee_provider.d.ts.map +1 -0
- package/dest/global_variable_builder/fee_provider.js +58 -0
- package/dest/global_variable_builder/global_builder.d.ts +14 -14
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +16 -50
- package/dest/global_variable_builder/index.d.ts +4 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/global_variable_builder/index.js +2 -0
- package/dest/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/publisher/config.d.ts +15 -3
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +19 -4
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +3 -4
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
- package/dest/publisher/sequencer-bundle-simulator.d.ts +96 -0
- package/dest/publisher/sequencer-bundle-simulator.d.ts.map +1 -0
- package/dest/publisher/sequencer-bundle-simulator.js +198 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +3 -5
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +16 -3
- package/dest/publisher/sequencer-publisher.d.ts +77 -65
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +345 -512
- package/dest/sequencer/automine/automine_factory.d.ts +54 -0
- package/dest/sequencer/automine/automine_factory.d.ts.map +1 -0
- package/dest/sequencer/automine/automine_factory.js +84 -0
- package/dest/sequencer/automine/automine_sequencer.d.ts +151 -0
- package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -0
- package/dest/sequencer/automine/automine_sequencer.js +506 -0
- package/dest/sequencer/chain_state_overrides.d.ts +61 -0
- package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
- package/dest/sequencer/chain_state_overrides.js +98 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +54 -11
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +776 -227
- package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts +34 -0
- package/dest/sequencer/checkpoint_proposal_job_metrics.d.ts.map +1 -0
- package/dest/sequencer/checkpoint_proposal_job_metrics.js +72 -0
- package/dest/sequencer/checkpoint_voter.d.ts +1 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +2 -5
- package/dest/sequencer/events.d.ts +47 -2
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/index.d.ts +3 -1
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +2 -0
- package/dest/sequencer/metrics.d.ts +13 -10
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +45 -20
- package/dest/sequencer/sequencer.d.ts +59 -16
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +333 -119
- 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/dest/test/utils.d.ts +1 -1
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +7 -6
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +72 -33
- package/src/config.ts +76 -27
- package/src/global_variable_builder/README.md +44 -0
- package/src/global_variable_builder/fee_predictor.ts +172 -0
- package/src/global_variable_builder/fee_provider.ts +75 -0
- package/src/global_variable_builder/global_builder.ts +25 -62
- package/src/global_variable_builder/index.ts +3 -1
- package/src/index.ts +10 -1
- package/src/publisher/config.ts +40 -6
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
- package/src/publisher/sequencer-bundle-simulator.ts +254 -0
- package/src/publisher/sequencer-publisher-factory.ts +18 -6
- package/src/publisher/sequencer-publisher.ts +426 -567
- package/src/sequencer/README.md +152 -450
- package/src/sequencer/automine/README.md +46 -0
- package/src/sequencer/automine/automine_factory.ts +145 -0
- package/src/sequencer/automine/automine_sequencer.ts +592 -0
- package/src/sequencer/chain_state_overrides.ts +162 -0
- package/src/sequencer/checkpoint_proposal_job.ts +941 -256
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +50 -2
- package/src/sequencer/index.ts +2 -0
- package/src/sequencer/metrics.ts +57 -24
- package/src/sequencer/sequencer.ts +414 -132
- package/src/sequencer/timetable.ts +64 -52
- package/src/sequencer/types.ts +1 -1
- package/src/test/mock_checkpoint_builder.ts +51 -48
- package/src/test/utils.ts +28 -10
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import { getKzg } from '@aztec/blob-lib';
|
|
2
|
-
import {
|
|
3
|
-
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import { type EpochCache, PROPOSER_PIPELINING_SLOT_OFFSET } from '@aztec/epoch-cache';
|
|
4
3
|
import { NoCommitteeError, type RollupContract } from '@aztec/ethereum/contracts';
|
|
5
4
|
import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
6
5
|
import { merge, omit, pick } from '@aztec/foundation/collection';
|
|
7
6
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
8
7
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
9
|
-
import { createLogger } from '@aztec/foundation/log';
|
|
8
|
+
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
10
9
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
11
10
|
import type { DateProvider } from '@aztec/foundation/timer';
|
|
12
11
|
import type { TypedEventEmitter } from '@aztec/foundation/types';
|
|
13
12
|
import type { P2P } from '@aztec/p2p';
|
|
14
13
|
import type { SlasherClientInterface } from '@aztec/slasher';
|
|
15
|
-
import type {
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
import type {
|
|
15
|
+
BlockData,
|
|
16
|
+
L2BlockSink,
|
|
17
|
+
L2BlockSource,
|
|
18
|
+
ProposedCheckpointSink,
|
|
19
|
+
ValidateCheckpointResult,
|
|
20
|
+
} from '@aztec/stdlib/block';
|
|
21
|
+
import type { Checkpoint, ProposedCheckpointData } from '@aztec/stdlib/checkpoint';
|
|
22
|
+
import type { ChainConfig } from '@aztec/stdlib/config';
|
|
23
|
+
import { getEpochAtSlot, getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
18
24
|
import {
|
|
19
25
|
type ResolvedSequencerConfig,
|
|
20
26
|
type SequencerConfig,
|
|
@@ -22,8 +28,8 @@ import {
|
|
|
22
28
|
type WorldStateSynchronizer,
|
|
23
29
|
} from '@aztec/stdlib/interfaces/server';
|
|
24
30
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
31
|
+
import type { CoordinationSignatureContext } from '@aztec/stdlib/p2p';
|
|
25
32
|
import { pickFromSchema } from '@aztec/stdlib/schemas';
|
|
26
|
-
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
27
33
|
import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
28
34
|
import { FullNodeCheckpointsBuilder, NodeKeystoreAdapter, type ValidatorClient } from '@aztec/validator-client';
|
|
29
35
|
|
|
@@ -33,7 +39,9 @@ import { DefaultSequencerConfig } from '../config.js';
|
|
|
33
39
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
34
40
|
import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
|
|
35
41
|
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
42
|
+
import { buildCheckpointSimulationOverridesPlan } from './chain_state_overrides.js';
|
|
36
43
|
import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
|
|
44
|
+
import { CheckpointProposalJobMetrics } from './checkpoint_proposal_job_metrics.js';
|
|
37
45
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
38
46
|
import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
|
|
39
47
|
import type { SequencerEvents } from './events.js';
|
|
@@ -44,6 +52,16 @@ import { SequencerState } from './utils.js';
|
|
|
44
52
|
|
|
45
53
|
export { SequencerState };
|
|
46
54
|
|
|
55
|
+
/** Slot snapshot used to prepare a checkpoint proposal. */
|
|
56
|
+
type SequencerSlotContext = {
|
|
57
|
+
slot: SlotNumber;
|
|
58
|
+
targetSlot: SlotNumber;
|
|
59
|
+
epoch: EpochNumber;
|
|
60
|
+
targetEpoch: EpochNumber;
|
|
61
|
+
ts: bigint;
|
|
62
|
+
nowSeconds: bigint;
|
|
63
|
+
};
|
|
64
|
+
|
|
47
65
|
/**
|
|
48
66
|
* Sequencer client
|
|
49
67
|
* - Checks whether it is elected as proposer for the next slot
|
|
@@ -55,7 +73,11 @@ export { SequencerState };
|
|
|
55
73
|
export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<SequencerEvents>) {
|
|
56
74
|
private runningPromise?: RunningPromise;
|
|
57
75
|
private state = SequencerState.STOPPED;
|
|
76
|
+
private stateSlotNumber: SlotNumber | undefined;
|
|
77
|
+
private stateEnteredAtMs = performance.now();
|
|
58
78
|
private metrics: SequencerMetrics;
|
|
79
|
+
private checkpointProposalJobMetrics: CheckpointProposalJobMetrics;
|
|
80
|
+
private readonly stateLog: Logger;
|
|
59
81
|
|
|
60
82
|
/** The last slot for which we attempted to perform our voting duties with degraded block production */
|
|
61
83
|
private lastSlotForFallbackVote: SlotNumber | undefined;
|
|
@@ -72,11 +94,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
72
94
|
/** The last epoch for which we logged strategy comparison in fisherman mode. */
|
|
73
95
|
private lastEpochForStrategyComparison: EpochNumber | undefined;
|
|
74
96
|
|
|
97
|
+
/** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */
|
|
98
|
+
protected lastCheckpointProposalJob: CheckpointProposalJob | undefined;
|
|
99
|
+
|
|
75
100
|
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
|
|
76
101
|
protected timetable!: SequencerTimetable;
|
|
77
102
|
|
|
78
103
|
/** Config for the sequencer */
|
|
79
104
|
protected config: ResolvedSequencerConfig = DefaultSequencerConfig;
|
|
105
|
+
private readonly signatureContext: CoordinationSignatureContext;
|
|
80
106
|
|
|
81
107
|
constructor(
|
|
82
108
|
protected publisherFactory: SequencerPublisherFactory,
|
|
@@ -85,41 +111,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
85
111
|
protected p2pClient: P2P,
|
|
86
112
|
protected worldState: WorldStateSynchronizer,
|
|
87
113
|
protected slasherClient: SlasherClientInterface | undefined,
|
|
88
|
-
protected l2BlockSource: L2BlockSource & L2BlockSink,
|
|
114
|
+
protected l2BlockSource: L2BlockSource & L2BlockSink & ProposedCheckpointSink,
|
|
89
115
|
protected l1ToL2MessageSource: L1ToL2MessageSource,
|
|
90
116
|
protected checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
91
117
|
protected l1Constants: SequencerRollupConstants,
|
|
92
118
|
protected dateProvider: DateProvider,
|
|
93
119
|
protected epochCache: EpochCache,
|
|
94
120
|
protected rollupContract: RollupContract,
|
|
95
|
-
config: SequencerConfig,
|
|
121
|
+
config: SequencerConfig & Pick<ChainConfig, 'l1ChainId' | 'rollupAddress'>,
|
|
96
122
|
protected telemetry: TelemetryClient = getTelemetryClient(),
|
|
97
123
|
protected log = createLogger('sequencer'),
|
|
98
124
|
) {
|
|
99
125
|
super();
|
|
126
|
+
this.stateLog = log.createChild('state');
|
|
100
127
|
|
|
101
128
|
// Add [FISHERMAN] prefix to logger if in fisherman mode
|
|
102
129
|
if (config.fishermanMode) {
|
|
103
130
|
this.log = log.createChild('[FISHERMAN]');
|
|
104
131
|
}
|
|
105
132
|
|
|
133
|
+
this.signatureContext = {
|
|
134
|
+
chainId: config.l1ChainId,
|
|
135
|
+
rollupAddress: config.rollupAddress,
|
|
136
|
+
};
|
|
106
137
|
this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
|
|
138
|
+
this.checkpointProposalJobMetrics = new CheckpointProposalJobMetrics(telemetry);
|
|
107
139
|
this.updateConfig(config);
|
|
108
140
|
}
|
|
109
141
|
|
|
110
142
|
/** Updates sequencer config by the defined values and updates the timetable */
|
|
111
143
|
public updateConfig(config: Partial<SequencerConfig>) {
|
|
112
144
|
const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
|
|
113
|
-
this.log.info(`Updated sequencer config`, omit(filteredConfig, '
|
|
145
|
+
this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
|
|
114
146
|
this.config = merge(this.config, filteredConfig);
|
|
147
|
+
const p2pPropagationTime = this.config.attestationPropagationTime;
|
|
115
148
|
this.timetable = new SequencerTimetable(
|
|
116
149
|
{
|
|
117
150
|
ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
|
|
118
151
|
aztecSlotDuration: this.aztecSlotDuration,
|
|
119
152
|
l1PublishingTime: this.l1PublishingTime,
|
|
120
|
-
p2pPropagationTime
|
|
153
|
+
p2pPropagationTime,
|
|
121
154
|
blockDurationMs: this.config.blockDurationMs,
|
|
122
155
|
enforce: this.config.enforceTimeTable,
|
|
156
|
+
pipelining: this.epochCache.isProposerPipeliningEnabled(),
|
|
123
157
|
},
|
|
124
158
|
this.metrics,
|
|
125
159
|
this.log,
|
|
@@ -143,12 +177,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
143
177
|
this.log.info('Started sequencer');
|
|
144
178
|
}
|
|
145
179
|
|
|
180
|
+
/** Triggers an immediate run of the sequencer, bypassing the polling interval. */
|
|
181
|
+
public trigger() {
|
|
182
|
+
return this.runningPromise?.trigger();
|
|
183
|
+
}
|
|
184
|
+
|
|
146
185
|
/** Stops the sequencer from building blocks and moves to STOPPED state. */
|
|
147
186
|
public async stop(): Promise<void> {
|
|
148
187
|
this.log.info(`Stopping sequencer`);
|
|
149
188
|
this.setState(SequencerState.STOPPING, undefined, { force: true });
|
|
150
|
-
this.publisherFactory.
|
|
189
|
+
await this.publisherFactory.stopAll();
|
|
151
190
|
await this.runningPromise?.stop();
|
|
191
|
+
await this.lastCheckpointProposalJob?.awaitPendingSubmission();
|
|
152
192
|
this.setState(SequencerState.STOPPED, undefined, { force: true });
|
|
153
193
|
this.log.info('Stopped sequencer');
|
|
154
194
|
}
|
|
@@ -158,17 +198,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
158
198
|
try {
|
|
159
199
|
await this.work();
|
|
160
200
|
} catch (err) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)
|
|
167
|
-
? ('debug' as const)
|
|
168
|
-
: ('warn' as const);
|
|
169
|
-
this.log[logLvl](err.message, { now: this.dateProvider.nowInSeconds() });
|
|
201
|
+
if (
|
|
202
|
+
err instanceof SequencerTooSlowError &&
|
|
203
|
+
[SequencerState.INITIALIZING_CHECKPOINT, SequencerState.PROPOSER_CHECK].includes(err.proposedState)
|
|
204
|
+
) {
|
|
205
|
+
// No need to alert if we just didn't get to start in time
|
|
206
|
+
this.log.debug(err.message, { now: this.dateProvider.nowInSeconds() });
|
|
170
207
|
} else {
|
|
171
208
|
// Re-throw other errors
|
|
209
|
+
this.emit('checkpoint-error', { error: err as Error });
|
|
172
210
|
throw err;
|
|
173
211
|
}
|
|
174
212
|
} finally {
|
|
@@ -192,14 +230,24 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
192
230
|
@trackSpan('Sequencer.work')
|
|
193
231
|
protected async work() {
|
|
194
232
|
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
195
|
-
const { slot,
|
|
233
|
+
const { slot, targetSlot, epoch, targetEpoch, ts, nowSeconds } = this.getSlotContextInNextL1Slot();
|
|
196
234
|
|
|
197
235
|
// Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
|
|
198
|
-
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
236
|
+
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
237
|
+
slot,
|
|
238
|
+
targetSlot,
|
|
239
|
+
epoch,
|
|
240
|
+
targetEpoch,
|
|
241
|
+
ts,
|
|
242
|
+
nowSeconds,
|
|
243
|
+
);
|
|
199
244
|
if (!checkpointProposalJob) {
|
|
200
245
|
return;
|
|
201
246
|
}
|
|
202
247
|
|
|
248
|
+
// Track the job so we can await its pending L1 submission during shutdown
|
|
249
|
+
this.lastCheckpointProposalJob = checkpointProposalJob;
|
|
250
|
+
|
|
203
251
|
// Execute the checkpoint proposal job
|
|
204
252
|
const checkpoint = await checkpointProposalJob.execute();
|
|
205
253
|
|
|
@@ -208,64 +256,87 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
208
256
|
this.lastCheckpointProposed = checkpoint;
|
|
209
257
|
}
|
|
210
258
|
|
|
211
|
-
// Log fee strategy comparison if on fisherman
|
|
259
|
+
// Log fee strategy comparison if on fisherman (uses target epoch since we mirror the proposer's perspective)
|
|
212
260
|
if (
|
|
213
261
|
this.config.fishermanMode &&
|
|
214
|
-
(this.lastEpochForStrategyComparison === undefined ||
|
|
262
|
+
(this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
|
|
215
263
|
) {
|
|
216
|
-
this.logStrategyComparison(
|
|
217
|
-
this.lastEpochForStrategyComparison =
|
|
264
|
+
this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
|
|
265
|
+
this.lastEpochForStrategyComparison = targetEpoch;
|
|
218
266
|
}
|
|
219
267
|
|
|
220
268
|
return checkpoint;
|
|
221
269
|
}
|
|
222
270
|
|
|
271
|
+
/** Returns slot and target slot from a single clock snapshot. */
|
|
272
|
+
protected getSlotContextInNextL1Slot(): SequencerSlotContext {
|
|
273
|
+
const { slot, ts, nowSeconds, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
274
|
+
const targetSlot = SlotNumber(
|
|
275
|
+
slot + (this.epochCache.isProposerPipeliningEnabled() ? PROPOSER_PIPELINING_SLOT_OFFSET : 0),
|
|
276
|
+
);
|
|
277
|
+
return { slot, targetSlot, epoch, targetEpoch: getEpochAtSlot(targetSlot, this.l1Constants), ts, nowSeconds };
|
|
278
|
+
}
|
|
279
|
+
|
|
223
280
|
/**
|
|
224
281
|
* Prepares the checkpoint proposal by performing all necessary checks and setup.
|
|
225
282
|
* This is the initial step in the main loop.
|
|
226
283
|
* @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
|
|
227
284
|
*/
|
|
228
285
|
@trackSpan('Sequencer.prepareCheckpointProposal')
|
|
229
|
-
|
|
230
|
-
epoch: EpochNumber,
|
|
286
|
+
protected async prepareCheckpointProposal(
|
|
231
287
|
slot: SlotNumber,
|
|
288
|
+
targetSlot: SlotNumber,
|
|
289
|
+
epoch: EpochNumber,
|
|
290
|
+
targetEpoch: EpochNumber,
|
|
232
291
|
ts: bigint,
|
|
233
|
-
|
|
292
|
+
nowSeconds: bigint,
|
|
234
293
|
): Promise<CheckpointProposalJob | undefined> {
|
|
235
|
-
// Check we have not already processed this slot (cheapest check)
|
|
294
|
+
// Check we have not already processed this target slot (cheapest check)
|
|
236
295
|
// We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
|
|
237
296
|
// running against actual time (eg when we use sandbox-style automining)
|
|
238
297
|
if (
|
|
239
298
|
this.lastSlotForCheckpointProposalJob &&
|
|
240
|
-
this.lastSlotForCheckpointProposalJob >=
|
|
299
|
+
this.lastSlotForCheckpointProposalJob >= targetSlot &&
|
|
241
300
|
this.config.enforceTimeTable
|
|
242
301
|
) {
|
|
243
|
-
this.log.trace(`
|
|
302
|
+
this.log.trace(`Target slot ${targetSlot} has already been processed`);
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// But if we have already proposed for this slot, then we definitely have to skip it, automining or not
|
|
307
|
+
if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= targetSlot) {
|
|
308
|
+
this.log.trace(
|
|
309
|
+
`Slot ${targetSlot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`,
|
|
310
|
+
);
|
|
244
311
|
return undefined;
|
|
245
312
|
}
|
|
246
313
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
314
|
+
// Test-only: skip proposing for explicitly paused slots. Attestation paths run in the validator
|
|
315
|
+
// client and are not gated by this hook, so paused proposers still attest to others' proposals.
|
|
316
|
+
if (this.config.pauseProposingForSlots?.some(s => s === targetSlot)) {
|
|
317
|
+
this.log.warn(`Skipping proposal for paused slot ${targetSlot} (test-only pauseProposingForSlots hook)`, {
|
|
318
|
+
targetSlot,
|
|
319
|
+
});
|
|
250
320
|
return undefined;
|
|
251
321
|
}
|
|
252
322
|
|
|
253
323
|
// Check all components are synced to latest as seen by the archiver (queries all subsystems)
|
|
254
324
|
const syncedTo = await this.checkSync({ ts, slot });
|
|
255
325
|
if (!syncedTo) {
|
|
256
|
-
await this.tryVoteWhenSyncFails({ slot, ts });
|
|
326
|
+
await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
|
|
257
327
|
return undefined;
|
|
258
328
|
}
|
|
259
329
|
|
|
260
|
-
// If escape hatch is open for
|
|
330
|
+
// If escape hatch is open for the target epoch, do not start checkpoint proposal work and do not attempt invalidations.
|
|
261
331
|
// Still perform governance/slashing voting (as proposer) once per slot.
|
|
262
|
-
|
|
332
|
+
// When pipelining, we check the target epoch (slot+1's epoch) since that's the epoch we're building for.
|
|
333
|
+
const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(targetEpoch);
|
|
263
334
|
|
|
264
335
|
if (isEscapeHatchOpen) {
|
|
265
336
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
266
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
337
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
267
338
|
if (canPropose) {
|
|
268
|
-
await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
|
|
339
|
+
await this.tryVoteWhenEscapeHatchOpen({ slot, targetSlot, proposer });
|
|
269
340
|
} else {
|
|
270
341
|
this.log.trace(`Escape hatch open but we are not proposer, skipping vote-only actions`, {
|
|
271
342
|
slot,
|
|
@@ -280,18 +351,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
280
351
|
const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
|
|
281
352
|
|
|
282
353
|
const logCtx = {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
|
|
354
|
+
nowSeconds,
|
|
355
|
+
syncedToL2Slot: syncedTo.syncedL2Slot,
|
|
286
356
|
slot,
|
|
357
|
+
targetSlot,
|
|
287
358
|
slotTs: ts,
|
|
288
359
|
checkpointNumber,
|
|
289
360
|
isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
|
|
290
361
|
};
|
|
291
362
|
|
|
292
|
-
// Check that we are a proposer for the
|
|
363
|
+
// Check that we are a proposer for the target slot.
|
|
293
364
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
294
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
365
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
295
366
|
|
|
296
367
|
// If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
|
|
297
368
|
if (!canPropose) {
|
|
@@ -299,10 +370,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
299
370
|
return undefined;
|
|
300
371
|
}
|
|
301
372
|
|
|
302
|
-
//
|
|
303
|
-
|
|
373
|
+
// Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
|
|
374
|
+
// confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
|
|
375
|
+
const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
|
|
376
|
+
if (checkpointNumber > confirmedCkpt + 2) {
|
|
377
|
+
this.log.verbose(
|
|
378
|
+
`Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
|
|
379
|
+
);
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
|
|
384
|
+
if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
|
|
304
385
|
this.log.warn(
|
|
305
|
-
`Cannot propose block at
|
|
386
|
+
`Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`,
|
|
306
387
|
{ ...logCtx, block: syncedTo.blockData.header.toInspect() },
|
|
307
388
|
);
|
|
308
389
|
this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
|
|
@@ -317,37 +398,109 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
317
398
|
const { attestorAddress, publisher } = await this.publisherFactory.create(proposerForPublisher);
|
|
318
399
|
this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
|
|
319
400
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
401
|
+
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
402
|
+
|
|
403
|
+
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need).
|
|
404
|
+
// Only simulate invalidation when there's no proposed parent, since we assume the proposed parent
|
|
405
|
+
// will invalidate the currently invalid checkpoint on L1.
|
|
406
|
+
const invalidateCheckpoint =
|
|
407
|
+
(isPipelining && syncedTo.hasProposedCheckpoint) || syncedTo.pendingChainValidationStatus.valid
|
|
408
|
+
? undefined
|
|
409
|
+
: await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
|
|
410
|
+
|
|
411
|
+
// Determine the correct archive and L1 state overrides for the canProposeAt check.
|
|
412
|
+
// The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
|
|
413
|
+
// When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
|
|
414
|
+
let archiveForCheck = syncedTo.archive;
|
|
415
|
+
|
|
416
|
+
if (isPipelining && syncedTo.hasProposedCheckpoint) {
|
|
417
|
+
this.metrics.recordPipelineDepth(syncedTo.checkpointNumber - syncedTo.checkpointedCheckpointNumber);
|
|
418
|
+
this.log.verbose(
|
|
419
|
+
`Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber}) for target slot ${targetSlot}`,
|
|
420
|
+
{ targetSlot, parentCheckpointNumber: CheckpointNumber(checkpointNumber - 1) },
|
|
421
|
+
);
|
|
422
|
+
// Match what L1 will see at archives[pending] once the proposed parent lands: the parent's
|
|
423
|
+
// own archive root from the gossiped proposal. `syncedTo.archive` is the world-state-local
|
|
424
|
+
// view and can transiently diverge from the proposed parent (e.g. before the proposed
|
|
425
|
+
// parent's blocks have been applied locally); diverging here would cause the canProposeAt
|
|
426
|
+
// override to set archives[pending] to one value while we present another for comparison.
|
|
427
|
+
archiveForCheck = syncedTo.proposedCheckpointData!.archive.root;
|
|
428
|
+
} else if (invalidateCheckpoint) {
|
|
429
|
+
// After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
|
|
430
|
+
// exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
|
|
431
|
+
archiveForCheck = invalidateCheckpoint.lastArchive;
|
|
432
|
+
this.metrics.recordPipelineDepth(0);
|
|
433
|
+
} else {
|
|
434
|
+
this.metrics.recordPipelineDepth(0);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Build the simulation plan: pending/proven override from pipelining or invalidation (or the
|
|
438
|
+
// current snapshot when neither applies, to short-circuit any pending prune in simulation),
|
|
439
|
+
// plus the parent checkpoint cell and fee header when pipelining.
|
|
440
|
+
const simulationOverridesPlan = await buildCheckpointSimulationOverridesPlan({
|
|
441
|
+
checkpointNumber,
|
|
442
|
+
proposedCheckpointData:
|
|
443
|
+
isPipelining && syncedTo.hasProposedCheckpoint ? syncedTo.proposedCheckpointData : undefined,
|
|
444
|
+
invalidateToPendingCheckpointNumber: invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
445
|
+
checkpointedCheckpointNumber: syncedTo.checkpointedCheckpointNumber,
|
|
446
|
+
rollup: this.rollupContract,
|
|
447
|
+
signatureContext: this.signatureContext,
|
|
448
|
+
log: this.log,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// The plan always pins both pending/proven (to short-circuit `canPruneAtTime` in simulation),
|
|
452
|
+
// so `provenOverride` always reflects the assumed proven checkpoint we are pinning the
|
|
453
|
+
// simulation to. We additionally warn when the pin is load-bearing — i.e. when a prune would
|
|
454
|
+
// actually fire at the target slot without it — so observers can spot "we are building
|
|
455
|
+
// optimistically across a pruning boundary" in the logs.
|
|
456
|
+
const provenOverride = simulationOverridesPlan?.chainTipsOverride?.proven;
|
|
457
|
+
if (provenOverride !== undefined && (await this.l2BlockSource.isPruneDueAtSlot(targetSlot))) {
|
|
458
|
+
this.log.warn(
|
|
459
|
+
`Assuming proof for epoch ending at checkpoint ${provenOverride} lands by target slot ${targetSlot}`,
|
|
460
|
+
{ checkpointNumber, slot, targetSlot, provenOverride },
|
|
461
|
+
);
|
|
324
462
|
}
|
|
325
463
|
|
|
326
|
-
|
|
327
|
-
|
|
464
|
+
this.emit('preparing-checkpoint', {
|
|
465
|
+
targetSlot,
|
|
466
|
+
checkpointNumber,
|
|
467
|
+
hadProposedParent: this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint,
|
|
468
|
+
provenOverride,
|
|
469
|
+
simulatedPending: simulationOverridesPlan?.chainTipsOverride?.pending,
|
|
470
|
+
});
|
|
328
471
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const canProposeCheck = await publisher.canProposeAtNextEthBlock(
|
|
332
|
-
syncedTo.archive,
|
|
472
|
+
const canProposeCheck = await publisher.canProposeAt(
|
|
473
|
+
archiveForCheck,
|
|
333
474
|
proposer ?? EthAddress.ZERO,
|
|
334
|
-
|
|
475
|
+
simulationOverridesPlan,
|
|
335
476
|
);
|
|
336
477
|
|
|
478
|
+
const proposeContext = {
|
|
479
|
+
hasProposedCheckpoint: syncedTo.hasProposedCheckpoint,
|
|
480
|
+
proposedCheckpointNumber: syncedTo.proposedCheckpointData?.checkpointNumber,
|
|
481
|
+
checkpointedCheckpointNumber: syncedTo.checkpointedCheckpointNumber,
|
|
482
|
+
isInvalidating: !!invalidateCheckpoint,
|
|
483
|
+
invalidatingCheckpointNumber: invalidateCheckpoint?.checkpointNumber,
|
|
484
|
+
archiveForCheck: archiveForCheck.toString(),
|
|
485
|
+
overridePendingCheckpointNumber: simulationOverridesPlan?.chainTipsOverride?.pending,
|
|
486
|
+
overrideArchive: simulationOverridesPlan?.pendingCheckpointState?.archive,
|
|
487
|
+
overrideFeeHeader: simulationOverridesPlan?.pendingCheckpointState?.feeHeader,
|
|
488
|
+
};
|
|
489
|
+
|
|
337
490
|
if (canProposeCheck === undefined) {
|
|
338
491
|
this.log.warn(
|
|
339
492
|
`Cannot propose checkpoint ${checkpointNumber} at slot ${slot} due to failed rollup contract check`,
|
|
340
|
-
logCtx,
|
|
493
|
+
{ ...logCtx, ...proposeContext },
|
|
341
494
|
);
|
|
342
495
|
this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed', slot });
|
|
343
496
|
this.metrics.recordCheckpointPrecheckFailed('rollup_contract_check_failed');
|
|
344
497
|
return undefined;
|
|
345
498
|
}
|
|
346
499
|
|
|
347
|
-
if (canProposeCheck.slot !==
|
|
500
|
+
if (canProposeCheck.slot !== targetSlot) {
|
|
348
501
|
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:
|
|
502
|
+
`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}.`,
|
|
503
|
+
{ ...logCtx, ...proposeContext, rollup: canProposeCheck, expectedSlot: targetSlot },
|
|
351
504
|
);
|
|
352
505
|
this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
|
|
353
506
|
this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
|
|
@@ -357,45 +510,62 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
357
510
|
if (canProposeCheck.checkpointNumber !== checkpointNumber) {
|
|
358
511
|
this.log.warn(
|
|
359
512
|
`Cannot propose due to block mismatch with rollup contract (this can be caused by a pending archiver sync). Expected checkpoint ${checkpointNumber} but got ${canProposeCheck.checkpointNumber}.`,
|
|
360
|
-
{ ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
|
|
513
|
+
{ ...logCtx, ...proposeContext, rollup: canProposeCheck, expectedSlot: slot },
|
|
361
514
|
);
|
|
362
515
|
this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch', slot });
|
|
363
516
|
this.metrics.recordCheckpointPrecheckFailed('block_number_mismatch');
|
|
364
517
|
return undefined;
|
|
365
518
|
}
|
|
366
519
|
|
|
367
|
-
this.lastSlotForCheckpointProposalJob =
|
|
368
|
-
|
|
369
|
-
this.
|
|
520
|
+
this.lastSlotForCheckpointProposalJob = targetSlot;
|
|
521
|
+
|
|
522
|
+
await this.p2pClient.prepareForSlot(targetSlot);
|
|
523
|
+
this.log.info(
|
|
524
|
+
`Preparing checkpoint proposal ${checkpointNumber} for target slot ${targetSlot} during wall-clock slot ${slot}`,
|
|
525
|
+
{
|
|
526
|
+
...logCtx,
|
|
527
|
+
...proposeContext,
|
|
528
|
+
proposer,
|
|
529
|
+
pipeliningEnabled: this.epochCache.isProposerPipeliningEnabled(),
|
|
530
|
+
},
|
|
531
|
+
);
|
|
370
532
|
|
|
371
533
|
// Create and return the checkpoint proposal job
|
|
372
534
|
return this.createCheckpointProposalJob(
|
|
373
|
-
epoch,
|
|
374
535
|
slot,
|
|
536
|
+
targetSlot,
|
|
537
|
+
targetEpoch,
|
|
375
538
|
checkpointNumber,
|
|
376
539
|
syncedTo.blockNumber,
|
|
540
|
+
syncedTo.checkpointedCheckpointNumber,
|
|
377
541
|
proposer,
|
|
378
542
|
publisher,
|
|
379
543
|
attestorAddress,
|
|
380
544
|
invalidateCheckpoint,
|
|
545
|
+
syncedTo.proposedCheckpointData,
|
|
381
546
|
);
|
|
382
547
|
}
|
|
383
548
|
|
|
384
549
|
protected createCheckpointProposalJob(
|
|
385
|
-
epoch: EpochNumber,
|
|
386
550
|
slot: SlotNumber,
|
|
551
|
+
targetSlot: SlotNumber,
|
|
552
|
+
targetEpoch: EpochNumber,
|
|
387
553
|
checkpointNumber: CheckpointNumber,
|
|
388
554
|
syncedToBlockNumber: BlockNumber,
|
|
555
|
+
checkpointedCheckpointNumber: CheckpointNumber,
|
|
389
556
|
proposer: EthAddress | undefined,
|
|
390
557
|
publisher: SequencerPublisher,
|
|
391
558
|
attestorAddress: EthAddress,
|
|
392
559
|
invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
|
|
560
|
+
proposedCheckpointData?: ProposedCheckpointData,
|
|
393
561
|
): CheckpointProposalJob {
|
|
394
562
|
return new CheckpointProposalJob(
|
|
395
|
-
epoch,
|
|
396
563
|
slot,
|
|
564
|
+
targetSlot,
|
|
565
|
+
targetEpoch,
|
|
397
566
|
checkpointNumber,
|
|
398
567
|
syncedToBlockNumber,
|
|
568
|
+
checkpointedCheckpointNumber,
|
|
399
569
|
proposer,
|
|
400
570
|
publisher,
|
|
401
571
|
attestorAddress,
|
|
@@ -409,16 +579,19 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
409
579
|
this.checkpointsBuilder,
|
|
410
580
|
this.l2BlockSource,
|
|
411
581
|
this.l1Constants,
|
|
582
|
+
this.signatureContext,
|
|
412
583
|
this.config,
|
|
413
584
|
this.timetable,
|
|
414
585
|
this.slasherClient,
|
|
415
586
|
this.epochCache,
|
|
416
587
|
this.dateProvider,
|
|
417
588
|
this.metrics,
|
|
589
|
+
this.checkpointProposalJobMetrics.createRecorder(),
|
|
418
590
|
this,
|
|
419
591
|
this.setState.bind(this),
|
|
420
592
|
this.tracer,
|
|
421
593
|
this.log.getBindings(),
|
|
594
|
+
proposedCheckpointData,
|
|
422
595
|
);
|
|
423
596
|
}
|
|
424
597
|
|
|
@@ -454,19 +627,35 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
454
627
|
this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
|
|
455
628
|
}
|
|
456
629
|
|
|
630
|
+
const oldState = this.state;
|
|
631
|
+
const oldStateSlotNumber = this.stateSlotNumber;
|
|
632
|
+
const stateChanged = proposedState !== oldState;
|
|
633
|
+
const transitionAtMs = performance.now();
|
|
634
|
+
const stateDurationMs = transitionAtMs - this.stateEnteredAtMs;
|
|
635
|
+
|
|
457
636
|
const boringStates = [SequencerState.IDLE, SequencerState.SYNCHRONIZING];
|
|
458
637
|
const logLevel =
|
|
459
|
-
boringStates.includes(proposedState) && boringStates.includes(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
638
|
+
boringStates.includes(proposedState) && boringStates.includes(oldState) ? ('trace' as const) : ('debug' as const);
|
|
639
|
+
this.stateLog[logLevel](`Transitioning from ${oldState} to ${proposedState}`, {
|
|
640
|
+
oldState,
|
|
641
|
+
newState: proposedState,
|
|
642
|
+
slotNumber,
|
|
643
|
+
stateSlotNumber: oldStateSlotNumber,
|
|
644
|
+
secondsIntoSlot,
|
|
645
|
+
...(stateChanged && { stateDurationMs: Math.ceil(stateDurationMs) }),
|
|
646
|
+
});
|
|
463
647
|
|
|
464
648
|
this.emit('state-changed', {
|
|
465
|
-
oldState
|
|
649
|
+
oldState,
|
|
466
650
|
newState: proposedState,
|
|
467
651
|
secondsIntoSlot,
|
|
468
652
|
slot: slotNumber,
|
|
469
653
|
});
|
|
654
|
+
if (stateChanged) {
|
|
655
|
+
this.metrics.recordStateDuration(stateDurationMs, oldState);
|
|
656
|
+
this.stateEnteredAtMs = transitionAtMs;
|
|
657
|
+
this.stateSlotNumber = slotNumber;
|
|
658
|
+
}
|
|
470
659
|
this.state = proposedState;
|
|
471
660
|
}
|
|
472
661
|
|
|
@@ -475,16 +664,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
475
664
|
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
476
665
|
*/
|
|
477
666
|
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 (
|
|
667
|
+
// Check that the archiver has fully synced the L2 slot before the one we want to propose in.
|
|
668
|
+
// The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
|
|
669
|
+
// See getSyncedL2SlotNumber for how missed L1 blocks are handled.
|
|
670
|
+
const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
|
|
671
|
+
const { slot } = args;
|
|
672
|
+
if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
|
|
484
673
|
this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
|
|
485
674
|
slot,
|
|
486
|
-
|
|
487
|
-
l1Timestamp,
|
|
675
|
+
syncedL2Slot,
|
|
488
676
|
});
|
|
489
677
|
return undefined;
|
|
490
678
|
}
|
|
@@ -494,45 +682,102 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
494
682
|
number: syncSummary.latestBlockNumber,
|
|
495
683
|
hash: syncSummary.latestBlockHash,
|
|
496
684
|
})),
|
|
497
|
-
this.l2BlockSource
|
|
685
|
+
this.l2BlockSource
|
|
686
|
+
.getL2Tips()
|
|
687
|
+
.then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
|
|
498
688
|
this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
|
|
499
|
-
this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
|
|
689
|
+
this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
|
|
500
690
|
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
691
|
+
this.l2BlockSource.getProposedCheckpointData(),
|
|
501
692
|
] as const);
|
|
502
693
|
|
|
503
|
-
const [worldState,
|
|
694
|
+
const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
|
|
695
|
+
syncedBlocks;
|
|
504
696
|
|
|
505
|
-
// 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
|
-
// as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
|
|
507
|
-
// TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
|
|
508
697
|
const result =
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
698
|
+
worldState.hash === l2Tips.proposed.hash &&
|
|
699
|
+
p2p.hash === l2Tips.proposed.hash &&
|
|
700
|
+
l1ToL2MessageSourceTips.proposed.hash === l2Tips.proposed.hash &&
|
|
701
|
+
l1ToL2MessageSourceTips.checkpointed.block.hash === l2Tips.checkpointed.block.hash &&
|
|
702
|
+
l1ToL2MessageSourceTips.checkpointed.checkpoint.hash === l2Tips.checkpointed.checkpoint.hash;
|
|
513
703
|
|
|
514
704
|
if (!result) {
|
|
515
|
-
this.log.debug(`Sequencer sync check failed`, {
|
|
705
|
+
this.log.debug(`Sequencer sync check failed`, {
|
|
706
|
+
worldState,
|
|
707
|
+
l2BlockSource: l2Tips.proposed,
|
|
708
|
+
p2p,
|
|
709
|
+
l1ToL2MessageSourceTips,
|
|
710
|
+
});
|
|
516
711
|
return undefined;
|
|
517
712
|
}
|
|
518
713
|
|
|
519
|
-
// Special case for genesis state
|
|
520
714
|
const blockNumber = worldState.number;
|
|
521
|
-
|
|
522
|
-
const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
523
|
-
return {
|
|
524
|
-
checkpointNumber: CheckpointNumber.ZERO,
|
|
525
|
-
blockNumber: BlockNumber.ZERO,
|
|
526
|
-
archive,
|
|
527
|
-
l1Timestamp,
|
|
528
|
-
pendingChainValidationStatus,
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const blockData = await this.l2BlockSource.getBlockData(blockNumber);
|
|
715
|
+
const blockData = await this.l2BlockSource.getBlockData({ number: blockNumber });
|
|
533
716
|
if (!blockData) {
|
|
534
|
-
|
|
535
|
-
|
|
717
|
+
this.log.warn(`Sequencer sync check failed: failed to get L2 block data ${blockNumber} from the archiver`, {
|
|
718
|
+
blockNumber,
|
|
719
|
+
l2Tips,
|
|
720
|
+
syncedL2Slot,
|
|
721
|
+
...args,
|
|
722
|
+
});
|
|
723
|
+
return undefined;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Refuse to build a checkpoint on top of a proposed block whose enclosing checkpoint was never
|
|
727
|
+
// proposed. Under pipelining we may have received and reexecuted such a block locally — advancing
|
|
728
|
+
// our world-state tip past the checkpointed tip — while the proposing node never published the
|
|
729
|
+
// matching proposed checkpoint (e.g. it crashed before assembling it). Building on this orphan block
|
|
730
|
+
// would fork the chain off a tip no other node can follow. The archiver prunes these orphan blocks
|
|
731
|
+
// once their build slot ends; this guard is the correctness barrier during the grace window before.
|
|
732
|
+
if (
|
|
733
|
+
blockData.checkpointNumber > l2Tips.checkpointed.checkpoint.number &&
|
|
734
|
+
(l2Tips.proposedCheckpoint.checkpoint.number !== blockData.checkpointNumber ||
|
|
735
|
+
proposedCheckpointData?.checkpointNumber !== blockData.checkpointNumber)
|
|
736
|
+
) {
|
|
737
|
+
this.log.warn(`Sequencer sync check failed: proposed block has no matching proposed checkpoint`, {
|
|
738
|
+
blockCheckpointNumber: blockData.checkpointNumber,
|
|
739
|
+
checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
|
|
740
|
+
proposedCheckpointTipNumber: l2Tips.proposedCheckpoint.checkpoint.number,
|
|
741
|
+
proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber,
|
|
742
|
+
blockNumber: blockData.header.getBlockNumber(),
|
|
743
|
+
blockSlot: blockData.header.getSlot(),
|
|
744
|
+
syncedL2Slot,
|
|
745
|
+
...args,
|
|
746
|
+
});
|
|
747
|
+
return undefined;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
|
|
751
|
+
|
|
752
|
+
// The l2Tips and proposedCheckpointData reads above come from independent archiver snapshots
|
|
753
|
+
// (a JS-side tips cache vs. a direct store read on `#proposedCheckpoints`). A concurrent archiver
|
|
754
|
+
// write that mutates both can be observed split, leaving us with `hasProposedCheckpoint=true` but
|
|
755
|
+
// no proposedCheckpointData (or one whose number doesn't match the tip). Refuse to proceed in that
|
|
756
|
+
// window — the next checkSync tick will see a coherent snapshot.
|
|
757
|
+
if (
|
|
758
|
+
hasProposedCheckpoint &&
|
|
759
|
+
(!proposedCheckpointData ||
|
|
760
|
+
proposedCheckpointData.checkpointNumber !== l2Tips.proposedCheckpoint.checkpoint.number)
|
|
761
|
+
) {
|
|
762
|
+
this.log.warn(`Sequencer sync check failed: inconsistent proposed-checkpoint state`, {
|
|
763
|
+
proposedCheckpointTipNumber: l2Tips.proposedCheckpoint.checkpoint.number,
|
|
764
|
+
checkpointedTipNumber: l2Tips.checkpointed.checkpoint.number,
|
|
765
|
+
proposedCheckpointDataNumber: proposedCheckpointData?.checkpointNumber,
|
|
766
|
+
syncedL2Slot,
|
|
767
|
+
...args,
|
|
768
|
+
});
|
|
769
|
+
return undefined;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Check that the proposed checkpoint is indeed the parent of the checkpoint we'll be building
|
|
773
|
+
// The checkpoint number to build is derived as blockData.checkpointNumber + 1
|
|
774
|
+
if (proposedCheckpointData && proposedCheckpointData.checkpointNumber !== blockData.checkpointNumber) {
|
|
775
|
+
this.log.warn(`Sequencer sync check failed: proposed checkpoint number mismatch`, {
|
|
776
|
+
proposedCheckpointNumber: proposedCheckpointData.checkpointNumber,
|
|
777
|
+
blockCheckpointNumber: blockData.checkpointNumber,
|
|
778
|
+
syncedL2Slot,
|
|
779
|
+
...args,
|
|
780
|
+
});
|
|
536
781
|
return undefined;
|
|
537
782
|
}
|
|
538
783
|
|
|
@@ -540,8 +785,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
540
785
|
blockData,
|
|
541
786
|
blockNumber: blockData.header.getBlockNumber(),
|
|
542
787
|
checkpointNumber: blockData.checkpointNumber,
|
|
788
|
+
checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
|
|
543
789
|
archive: blockData.archive.root,
|
|
544
|
-
|
|
790
|
+
hasProposedCheckpoint,
|
|
791
|
+
proposedCheckpointData,
|
|
792
|
+
syncedL2Slot,
|
|
545
793
|
pendingChainValidationStatus,
|
|
546
794
|
};
|
|
547
795
|
}
|
|
@@ -550,20 +798,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
550
798
|
* Checks if we are the proposer for the next slot.
|
|
551
799
|
* @returns True if we can propose, and the proposer address (undefined if anyone can propose)
|
|
552
800
|
*/
|
|
553
|
-
protected async checkCanPropose(
|
|
801
|
+
protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
|
|
554
802
|
let proposer: EthAddress | undefined;
|
|
555
803
|
|
|
556
804
|
try {
|
|
557
|
-
proposer = await this.epochCache.getProposerAttesterAddressInSlot(
|
|
805
|
+
proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
|
|
558
806
|
} catch (e) {
|
|
559
807
|
if (e instanceof NoCommitteeError) {
|
|
560
|
-
if (this.lastSlotForNoCommitteeWarning !==
|
|
561
|
-
this.lastSlotForNoCommitteeWarning =
|
|
562
|
-
this.log.warn(`Cannot propose at
|
|
808
|
+
if (this.lastSlotForNoCommitteeWarning !== targetSlot) {
|
|
809
|
+
this.lastSlotForNoCommitteeWarning = targetSlot;
|
|
810
|
+
this.log.warn(`Cannot propose at target slot ${targetSlot} since the committee does not exist on L1`);
|
|
563
811
|
}
|
|
564
812
|
return [false, undefined];
|
|
565
813
|
}
|
|
566
|
-
this.log.error(`Error getting proposer for slot ${
|
|
814
|
+
this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
|
|
567
815
|
return [false, undefined];
|
|
568
816
|
}
|
|
569
817
|
|
|
@@ -580,10 +828,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
580
828
|
const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
|
|
581
829
|
|
|
582
830
|
if (!weAreProposer) {
|
|
583
|
-
this.log.debug(`Cannot propose at slot ${
|
|
831
|
+
this.log.debug(`Cannot propose at target slot ${targetSlot} since we are not a proposer`, {
|
|
832
|
+
targetSlot,
|
|
833
|
+
validatorAddresses,
|
|
834
|
+
proposer,
|
|
835
|
+
});
|
|
584
836
|
return [false, proposer];
|
|
585
837
|
}
|
|
586
838
|
|
|
839
|
+
this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
|
|
840
|
+
targetSlot,
|
|
841
|
+
proposer,
|
|
842
|
+
});
|
|
587
843
|
return [true, proposer];
|
|
588
844
|
}
|
|
589
845
|
|
|
@@ -592,8 +848,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
592
848
|
* This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
|
|
593
849
|
*/
|
|
594
850
|
@trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
|
|
595
|
-
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
|
|
596
|
-
const { slot } = args;
|
|
851
|
+
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
|
|
852
|
+
const { slot, targetSlot } = args;
|
|
597
853
|
|
|
598
854
|
// Prevent duplicate attempts in the same slot
|
|
599
855
|
if (this.lastSlotForFallbackVote === slot) {
|
|
@@ -621,7 +877,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
621
877
|
});
|
|
622
878
|
|
|
623
879
|
// Check if we're a proposer or proposal is open
|
|
624
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
880
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
625
881
|
if (!canPropose) {
|
|
626
882
|
this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
|
|
627
883
|
return;
|
|
@@ -638,9 +894,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
638
894
|
slot,
|
|
639
895
|
});
|
|
640
896
|
|
|
641
|
-
// Enqueue governance and slashing votes
|
|
897
|
+
// Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
|
|
642
898
|
const voter = new CheckpointVoter(
|
|
643
|
-
|
|
899
|
+
targetSlot,
|
|
644
900
|
publisher,
|
|
645
901
|
attestorAddress,
|
|
646
902
|
this.validatorClient,
|
|
@@ -659,7 +915,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
659
915
|
}
|
|
660
916
|
|
|
661
917
|
this.log.info(`Voting in slot ${slot} despite sync failure`, { slot });
|
|
662
|
-
|
|
918
|
+
// Votes are EIP-712-signed for `targetSlot` (the pipelined slot in which the multicall is
|
|
919
|
+
// expected to mine). Delay submission to the start of `targetSlot` so the tx mines in the
|
|
920
|
+
// slot the votes were signed for. We fire-and-forget so we don't block the sequencer's
|
|
921
|
+
// work loop while waiting for the target slot to start.
|
|
922
|
+
void publisher.sendRequestsAt(targetSlot).catch(err => {
|
|
923
|
+
this.log.error(`Failed to publish votes despite sync failure for slot ${slot}`, err, { slot });
|
|
924
|
+
});
|
|
663
925
|
}
|
|
664
926
|
|
|
665
927
|
/**
|
|
@@ -669,9 +931,10 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
669
931
|
@trackSpan('Sequencer.tryVoteWhenEscapeHatchOpen', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
|
|
670
932
|
protected async tryVoteWhenEscapeHatchOpen(args: {
|
|
671
933
|
slot: SlotNumber;
|
|
934
|
+
targetSlot: SlotNumber;
|
|
672
935
|
proposer: EthAddress | undefined;
|
|
673
936
|
}): Promise<void> {
|
|
674
|
-
const { slot, proposer } = args;
|
|
937
|
+
const { slot, targetSlot, proposer } = args;
|
|
675
938
|
|
|
676
939
|
// Prevent duplicate attempts in the same slot
|
|
677
940
|
if (this.lastSlotForFallbackVote === slot) {
|
|
@@ -684,10 +947,19 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
684
947
|
|
|
685
948
|
const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
|
|
686
949
|
|
|
687
|
-
this.log.debug(`Escape hatch open for slot ${slot}, attempting vote-only actions`, {
|
|
950
|
+
this.log.debug(`Escape hatch open for slot ${slot}, attempting vote-only actions`, {
|
|
951
|
+
slot,
|
|
952
|
+
targetSlot,
|
|
953
|
+
attestorAddress,
|
|
954
|
+
});
|
|
688
955
|
|
|
956
|
+
// Under proposer pipelining, the multicall is expected to mine in `targetSlot` (slot + 1).
|
|
957
|
+
// Governance and slashing votes are EIP-712-signed against the slot they will mine in, and the
|
|
958
|
+
// L1 contract checks `msg.sender == getCurrentProposer()` using the mining slot. So we must
|
|
959
|
+
// sign for `targetSlot` and delay submission to the start of `targetSlot`. When pipelining is
|
|
960
|
+
// disabled `targetSlot == slot` and `sendRequestsAt` resolves with no extra sleep.
|
|
689
961
|
const voter = new CheckpointVoter(
|
|
690
|
-
|
|
962
|
+
targetSlot,
|
|
691
963
|
publisher,
|
|
692
964
|
attestorAddress,
|
|
693
965
|
this.validatorClient,
|
|
@@ -706,8 +978,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
706
978
|
return;
|
|
707
979
|
}
|
|
708
980
|
|
|
709
|
-
this.log.info(`Voting in slot ${slot} (escape hatch open)`, { slot });
|
|
710
|
-
|
|
981
|
+
this.log.info(`Voting in slot ${slot} (escape hatch open)`, { slot, targetSlot });
|
|
982
|
+
// Votes are EIP-712-signed for `targetSlot`. Delay submission to the start of `targetSlot` so
|
|
983
|
+
// the multicall mines in the slot the votes were signed for; otherwise the L1 contract reads
|
|
984
|
+
// `signaler = getCurrentProposer()` against the wrong slot and signature verification fails
|
|
985
|
+
// silently inside Multicall3. Fire-and-forget so we don't block the sequencer's work loop while
|
|
986
|
+
// waiting for the target slot to start, mirroring tryVoteWhenSyncFails.
|
|
987
|
+
void publisher.sendRequestsAt(targetSlot).catch(err => {
|
|
988
|
+
this.log.error(`Failed to publish escape-hatch votes for slot ${slot}`, err, { slot, targetSlot });
|
|
989
|
+
});
|
|
711
990
|
}
|
|
712
991
|
|
|
713
992
|
/**
|
|
@@ -720,7 +999,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
720
999
|
syncedTo: SequencerSyncCheckResult,
|
|
721
1000
|
currentSlot: SlotNumber,
|
|
722
1001
|
): Promise<void> {
|
|
723
|
-
const { pendingChainValidationStatus,
|
|
1002
|
+
const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
|
|
724
1003
|
if (pendingChainValidationStatus.valid) {
|
|
725
1004
|
return;
|
|
726
1005
|
}
|
|
@@ -735,7 +1014,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
735
1014
|
|
|
736
1015
|
const logData = {
|
|
737
1016
|
invalidL1Timestamp: invalidCheckpointTimestamp,
|
|
738
|
-
|
|
1017
|
+
syncedL2Slot,
|
|
739
1018
|
invalidCheckpoint: pendingChainValidationStatus.checkpoint,
|
|
740
1019
|
secondsBeforeInvalidatingBlockAsCommitteeMember,
|
|
741
1020
|
secondsBeforeInvalidatingBlockAsNonCommitteeMember,
|
|
@@ -880,8 +1159,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
880
1159
|
type SequencerSyncCheckResult = {
|
|
881
1160
|
blockData?: BlockData;
|
|
882
1161
|
checkpointNumber: CheckpointNumber;
|
|
1162
|
+
checkpointedCheckpointNumber: CheckpointNumber;
|
|
883
1163
|
blockNumber: BlockNumber;
|
|
884
1164
|
archive: Fr;
|
|
885
|
-
|
|
1165
|
+
hasProposedCheckpoint: boolean;
|
|
1166
|
+
proposedCheckpointData?: ProposedCheckpointData;
|
|
1167
|
+
syncedL2Slot: SlotNumber;
|
|
886
1168
|
pendingChainValidationStatus: ValidateCheckpointResult;
|
|
887
1169
|
};
|