@aztec/sequencer-client 0.0.1-commit.0c875d939 → 0.0.1-commit.0ec55a70b
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 +17 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +63 -30
- package/dest/config.d.ts +27 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +55 -30
- 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 +15 -16
- 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/publisher/config.d.ts +47 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +121 -42
- package/dest/publisher/index.d.ts +2 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
- package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +58 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/index.js +2 -0
- package/dest/publisher/sequencer-publisher-factory.d.ts +11 -5
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +27 -3
- package/dest/publisher/sequencer-publisher.d.ts +68 -48
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +421 -137
- package/dest/sequencer/chain_state_overrides.d.ts +25 -0
- package/dest/sequencer/chain_state_overrides.d.ts.map +1 -0
- package/dest/sequencer/chain_state_overrides.js +39 -0
- package/dest/sequencer/checkpoint_proposal_job.d.ts +46 -10
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +700 -220
- 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 +7 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +20 -8
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +92 -21
- package/dest/sequencer/sequencer.d.ts +53 -19
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +188 -102
- 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/index.d.ts +3 -5
- package/dest/test/index.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 +81 -30
- package/src/config.ts +68 -35
- 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 +27 -63
- package/src/global_variable_builder/index.ts +3 -1
- package/src/publisher/config.ts +157 -45
- package/src/publisher/index.ts +3 -0
- package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +57 -0
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
- package/src/publisher/l1_tx_failed_store/index.ts +3 -0
- package/src/publisher/sequencer-publisher-factory.ts +38 -9
- package/src/publisher/sequencer-publisher.ts +474 -197
- package/src/sequencer/README.md +82 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +866 -241
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/events.ts +6 -1
- package/src/sequencer/metrics.ts +108 -25
- package/src/sequencer/sequencer.ts +262 -116
- package/src/sequencer/timetable.ts +64 -52
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +51 -48
- package/src/test/utils.ts +28 -10
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { getKzg } from '@aztec/blob-lib';
|
|
2
2
|
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
3
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
|
-
import { NoCommitteeError, type RollupContract } from '@aztec/ethereum/contracts';
|
|
4
|
+
import { NoCommitteeError, type RollupContract, SimulationOverridesBuilder } from '@aztec/ethereum/contracts';
|
|
5
5
|
import { BlockNumber, CheckpointNumber, EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
6
6
|
import { merge, omit, pick } from '@aztec/foundation/collection';
|
|
7
7
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
8
8
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
9
|
-
import { createLogger } from '@aztec/foundation/log';
|
|
9
|
+
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
10
10
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
11
11
|
import type { DateProvider } from '@aztec/foundation/timer';
|
|
12
12
|
import type { TypedEventEmitter } from '@aztec/foundation/types';
|
|
13
13
|
import type { P2P } from '@aztec/p2p';
|
|
14
14
|
import type { SlasherClientInterface } from '@aztec/slasher';
|
|
15
|
-
import type {
|
|
16
|
-
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
17
|
-
import {
|
|
15
|
+
import type { BlockData, L2BlockSink, L2BlockSource, ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
16
|
+
import type { Checkpoint, ProposedCheckpointData } from '@aztec/stdlib/checkpoint';
|
|
17
|
+
import type { ChainConfig } from '@aztec/stdlib/config';
|
|
18
|
+
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
18
19
|
import {
|
|
19
20
|
type ResolvedSequencerConfig,
|
|
20
21
|
type SequencerConfig,
|
|
@@ -22,10 +23,11 @@ import {
|
|
|
22
23
|
type WorldStateSynchronizer,
|
|
23
24
|
} from '@aztec/stdlib/interfaces/server';
|
|
24
25
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
26
|
+
import type { CoordinationSignatureContext } from '@aztec/stdlib/p2p';
|
|
25
27
|
import { pickFromSchema } from '@aztec/stdlib/schemas';
|
|
26
28
|
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
27
29
|
import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
28
|
-
import { FullNodeCheckpointsBuilder, type ValidatorClient } from '@aztec/validator-client';
|
|
30
|
+
import { FullNodeCheckpointsBuilder, NodeKeystoreAdapter, type ValidatorClient } from '@aztec/validator-client';
|
|
29
31
|
|
|
30
32
|
import EventEmitter from 'node:events';
|
|
31
33
|
|
|
@@ -34,6 +36,7 @@ import type { GlobalVariableBuilder } from '../global_variable_builder/global_bu
|
|
|
34
36
|
import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
|
|
35
37
|
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
36
38
|
import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
|
|
39
|
+
import { CheckpointProposalJobMetrics } from './checkpoint_proposal_job_metrics.js';
|
|
37
40
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
38
41
|
import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
|
|
39
42
|
import type { SequencerEvents } from './events.js';
|
|
@@ -55,7 +58,11 @@ export { SequencerState };
|
|
|
55
58
|
export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<SequencerEvents>) {
|
|
56
59
|
private runningPromise?: RunningPromise;
|
|
57
60
|
private state = SequencerState.STOPPED;
|
|
61
|
+
private stateSlotNumber: SlotNumber | undefined;
|
|
62
|
+
private stateEnteredAtMs = performance.now();
|
|
58
63
|
private metrics: SequencerMetrics;
|
|
64
|
+
private checkpointProposalJobMetrics: CheckpointProposalJobMetrics;
|
|
65
|
+
private readonly stateLog: Logger;
|
|
59
66
|
|
|
60
67
|
/** The last slot for which we attempted to perform our voting duties with degraded block production */
|
|
61
68
|
private lastSlotForFallbackVote: SlotNumber | undefined;
|
|
@@ -72,19 +79,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
72
79
|
/** The last epoch for which we logged strategy comparison in fisherman mode. */
|
|
73
80
|
private lastEpochForStrategyComparison: EpochNumber | undefined;
|
|
74
81
|
|
|
82
|
+
/** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */
|
|
83
|
+
protected lastCheckpointProposalJob: CheckpointProposalJob | undefined;
|
|
84
|
+
|
|
75
85
|
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
|
|
76
86
|
protected timetable!: SequencerTimetable;
|
|
77
87
|
|
|
78
|
-
// This shouldn't be here as this gets re-created each time we build/propose a block.
|
|
79
|
-
// But we have a number of tests that abuse/rely on this class having a permanent publisher.
|
|
80
|
-
// As long as those tests only configure a single publisher they will continue to work.
|
|
81
|
-
// This will get re-assigned every time the sequencer goes to build a new block to a publisher that is valid
|
|
82
|
-
// for the block proposer.
|
|
83
|
-
// TODO(palla/mbps): Remove this field and fix tests
|
|
84
|
-
protected publisher: SequencerPublisher | undefined;
|
|
85
|
-
|
|
86
88
|
/** Config for the sequencer */
|
|
87
89
|
protected config: ResolvedSequencerConfig = DefaultSequencerConfig;
|
|
90
|
+
private readonly signatureContext: CoordinationSignatureContext;
|
|
88
91
|
|
|
89
92
|
constructor(
|
|
90
93
|
protected publisherFactory: SequencerPublisherFactory,
|
|
@@ -100,44 +103,51 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
100
103
|
protected dateProvider: DateProvider,
|
|
101
104
|
protected epochCache: EpochCache,
|
|
102
105
|
protected rollupContract: RollupContract,
|
|
103
|
-
config: SequencerConfig,
|
|
106
|
+
config: SequencerConfig & Pick<ChainConfig, 'l1ChainId' | 'l1Contracts'>,
|
|
104
107
|
protected telemetry: TelemetryClient = getTelemetryClient(),
|
|
105
108
|
protected log = createLogger('sequencer'),
|
|
106
109
|
) {
|
|
107
110
|
super();
|
|
111
|
+
this.stateLog = log.createChild('state');
|
|
108
112
|
|
|
109
113
|
// Add [FISHERMAN] prefix to logger if in fisherman mode
|
|
110
114
|
if (config.fishermanMode) {
|
|
111
115
|
this.log = log.createChild('[FISHERMAN]');
|
|
112
116
|
}
|
|
113
117
|
|
|
118
|
+
this.signatureContext = {
|
|
119
|
+
chainId: config.l1ChainId,
|
|
120
|
+
rollupAddress: config.l1Contracts.rollupAddress,
|
|
121
|
+
};
|
|
114
122
|
this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
|
|
123
|
+
this.checkpointProposalJobMetrics = new CheckpointProposalJobMetrics(telemetry);
|
|
115
124
|
this.updateConfig(config);
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
/** Updates sequencer config by the defined values and updates the timetable */
|
|
119
128
|
public updateConfig(config: Partial<SequencerConfig>) {
|
|
120
129
|
const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
|
|
121
|
-
this.log.info(`Updated sequencer config`, omit(filteredConfig, '
|
|
130
|
+
this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
|
|
122
131
|
this.config = merge(this.config, filteredConfig);
|
|
132
|
+
const p2pPropagationTime = this.config.attestationPropagationTime;
|
|
123
133
|
this.timetable = new SequencerTimetable(
|
|
124
134
|
{
|
|
125
135
|
ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
|
|
126
136
|
aztecSlotDuration: this.aztecSlotDuration,
|
|
127
137
|
l1PublishingTime: this.l1PublishingTime,
|
|
128
|
-
p2pPropagationTime
|
|
138
|
+
p2pPropagationTime,
|
|
129
139
|
blockDurationMs: this.config.blockDurationMs,
|
|
130
140
|
enforce: this.config.enforceTimeTable,
|
|
141
|
+
pipelining: this.epochCache.isProposerPipeliningEnabled(),
|
|
131
142
|
},
|
|
132
143
|
this.metrics,
|
|
133
144
|
this.log,
|
|
134
145
|
);
|
|
135
146
|
}
|
|
136
147
|
|
|
137
|
-
/** Initializes the sequencer (precomputes tables
|
|
138
|
-
public
|
|
148
|
+
/** Initializes the sequencer (precomputes tables). Takes about 3s. */
|
|
149
|
+
public init() {
|
|
139
150
|
getKzg();
|
|
140
|
-
this.publisher = (await this.publisherFactory.create(undefined)).publisher;
|
|
141
151
|
}
|
|
142
152
|
|
|
143
153
|
/** Starts the sequencer and moves to IDLE state. */
|
|
@@ -152,12 +162,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
152
162
|
this.log.info('Started sequencer');
|
|
153
163
|
}
|
|
154
164
|
|
|
165
|
+
/** Triggers an immediate run of the sequencer, bypassing the polling interval. */
|
|
166
|
+
public trigger() {
|
|
167
|
+
return this.runningPromise?.trigger();
|
|
168
|
+
}
|
|
169
|
+
|
|
155
170
|
/** Stops the sequencer from building blocks and moves to STOPPED state. */
|
|
156
171
|
public async stop(): Promise<void> {
|
|
157
172
|
this.log.info(`Stopping sequencer`);
|
|
158
173
|
this.setState(SequencerState.STOPPING, undefined, { force: true });
|
|
159
|
-
this.
|
|
174
|
+
await this.publisherFactory.stopAll();
|
|
160
175
|
await this.runningPromise?.stop();
|
|
176
|
+
await this.lastCheckpointProposalJob?.awaitPendingSubmission();
|
|
161
177
|
this.setState(SequencerState.STOPPED, undefined, { force: true });
|
|
162
178
|
this.log.info('Stopped sequencer');
|
|
163
179
|
}
|
|
@@ -169,7 +185,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
169
185
|
} catch (err) {
|
|
170
186
|
this.emit('checkpoint-error', { error: err as Error });
|
|
171
187
|
if (err instanceof SequencerTooSlowError) {
|
|
172
|
-
// TODO(palla/mbps): Add missing states
|
|
173
188
|
// Log as warn only if we had to abort halfway through the block proposal
|
|
174
189
|
const logLvl = [SequencerState.INITIALIZING_CHECKPOINT, SequencerState.PROPOSER_CHECK].includes(
|
|
175
190
|
err.proposedState,
|
|
@@ -202,14 +217,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
202
217
|
@trackSpan('Sequencer.work')
|
|
203
218
|
protected async work() {
|
|
204
219
|
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
205
|
-
const { slot, ts,
|
|
220
|
+
const { slot, ts, nowSeconds, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
221
|
+
const { slot: targetSlot, epoch: targetEpoch } = this.epochCache.getTargetEpochAndSlotInNextL1Slot();
|
|
206
222
|
|
|
207
223
|
// Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
|
|
208
|
-
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
224
|
+
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
225
|
+
slot,
|
|
226
|
+
targetSlot,
|
|
227
|
+
epoch,
|
|
228
|
+
targetEpoch,
|
|
229
|
+
ts,
|
|
230
|
+
nowSeconds,
|
|
231
|
+
);
|
|
209
232
|
if (!checkpointProposalJob) {
|
|
210
233
|
return;
|
|
211
234
|
}
|
|
212
235
|
|
|
236
|
+
// Track the job so we can await its pending L1 submission during shutdown
|
|
237
|
+
this.lastCheckpointProposalJob = checkpointProposalJob;
|
|
238
|
+
|
|
213
239
|
// Execute the checkpoint proposal job
|
|
214
240
|
const checkpoint = await checkpointProposalJob.execute();
|
|
215
241
|
|
|
@@ -218,13 +244,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
218
244
|
this.lastCheckpointProposed = checkpoint;
|
|
219
245
|
}
|
|
220
246
|
|
|
221
|
-
// Log fee strategy comparison if on fisherman
|
|
247
|
+
// Log fee strategy comparison if on fisherman (uses target epoch since we mirror the proposer's perspective)
|
|
222
248
|
if (
|
|
223
249
|
this.config.fishermanMode &&
|
|
224
|
-
(this.lastEpochForStrategyComparison === undefined ||
|
|
250
|
+
(this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
|
|
225
251
|
) {
|
|
226
|
-
this.logStrategyComparison(
|
|
227
|
-
this.lastEpochForStrategyComparison =
|
|
252
|
+
this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
|
|
253
|
+
this.lastEpochForStrategyComparison = targetEpoch;
|
|
228
254
|
}
|
|
229
255
|
|
|
230
256
|
return checkpoint;
|
|
@@ -236,44 +262,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
236
262
|
* @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
|
|
237
263
|
*/
|
|
238
264
|
@trackSpan('Sequencer.prepareCheckpointProposal')
|
|
239
|
-
|
|
240
|
-
epoch: EpochNumber,
|
|
265
|
+
protected async prepareCheckpointProposal(
|
|
241
266
|
slot: SlotNumber,
|
|
267
|
+
targetSlot: SlotNumber,
|
|
268
|
+
epoch: EpochNumber,
|
|
269
|
+
targetEpoch: EpochNumber,
|
|
242
270
|
ts: bigint,
|
|
243
|
-
|
|
271
|
+
nowSeconds: bigint,
|
|
244
272
|
): Promise<CheckpointProposalJob | undefined> {
|
|
245
|
-
// Check we have not already processed this slot (cheapest check)
|
|
273
|
+
// Check we have not already processed this target slot (cheapest check)
|
|
246
274
|
// We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
|
|
247
275
|
// running against actual time (eg when we use sandbox-style automining)
|
|
248
276
|
if (
|
|
249
277
|
this.lastSlotForCheckpointProposalJob &&
|
|
250
|
-
this.lastSlotForCheckpointProposalJob >=
|
|
278
|
+
this.lastSlotForCheckpointProposalJob >= targetSlot &&
|
|
251
279
|
this.config.enforceTimeTable
|
|
252
280
|
) {
|
|
253
|
-
this.log.trace(`
|
|
281
|
+
this.log.trace(`Target slot ${targetSlot} has already been processed`);
|
|
254
282
|
return undefined;
|
|
255
283
|
}
|
|
256
284
|
|
|
257
|
-
// But if we have already proposed for this slot,
|
|
258
|
-
if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >=
|
|
259
|
-
this.log.trace(
|
|
285
|
+
// But if we have already proposed for this slot, then we definitely have to skip it, automining or not
|
|
286
|
+
if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= targetSlot) {
|
|
287
|
+
this.log.trace(
|
|
288
|
+
`Slot ${targetSlot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`,
|
|
289
|
+
);
|
|
260
290
|
return undefined;
|
|
261
291
|
}
|
|
262
292
|
|
|
263
293
|
// Check all components are synced to latest as seen by the archiver (queries all subsystems)
|
|
264
294
|
const syncedTo = await this.checkSync({ ts, slot });
|
|
265
295
|
if (!syncedTo) {
|
|
266
|
-
await this.tryVoteWhenSyncFails({ slot, ts });
|
|
296
|
+
await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
|
|
267
297
|
return undefined;
|
|
268
298
|
}
|
|
269
299
|
|
|
270
|
-
// If escape hatch is open for
|
|
300
|
+
// If escape hatch is open for the target epoch, do not start checkpoint proposal work and do not attempt invalidations.
|
|
271
301
|
// Still perform governance/slashing voting (as proposer) once per slot.
|
|
272
|
-
|
|
302
|
+
// When pipelining, we check the target epoch (slot+1's epoch) since that's the epoch we're building for.
|
|
303
|
+
const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(targetEpoch);
|
|
273
304
|
|
|
274
305
|
if (isEscapeHatchOpen) {
|
|
275
306
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
276
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
307
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
277
308
|
if (canPropose) {
|
|
278
309
|
await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
|
|
279
310
|
} else {
|
|
@@ -290,18 +321,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
290
321
|
const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
|
|
291
322
|
|
|
292
323
|
const logCtx = {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
|
|
324
|
+
nowSeconds,
|
|
325
|
+
syncedToL2Slot: syncedTo.syncedL2Slot,
|
|
296
326
|
slot,
|
|
327
|
+
targetSlot,
|
|
297
328
|
slotTs: ts,
|
|
298
329
|
checkpointNumber,
|
|
299
330
|
isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
|
|
300
331
|
};
|
|
301
332
|
|
|
302
|
-
// Check that we are a proposer for the
|
|
333
|
+
// Check that we are a proposer for the target slot.
|
|
303
334
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
304
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
335
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
305
336
|
|
|
306
337
|
// If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
|
|
307
338
|
if (!canPropose) {
|
|
@@ -309,13 +340,23 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
309
340
|
return undefined;
|
|
310
341
|
}
|
|
311
342
|
|
|
312
|
-
//
|
|
313
|
-
|
|
343
|
+
// Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
|
|
344
|
+
// confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
|
|
345
|
+
const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
|
|
346
|
+
if (checkpointNumber > confirmedCkpt + 2) {
|
|
347
|
+
this.log.verbose(
|
|
348
|
+
`Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
|
|
349
|
+
);
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
|
|
354
|
+
if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
|
|
314
355
|
this.log.warn(
|
|
315
|
-
`Cannot propose block at
|
|
316
|
-
{ ...logCtx, block: syncedTo.
|
|
356
|
+
`Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`,
|
|
357
|
+
{ ...logCtx, block: syncedTo.blockData.header.toInspect() },
|
|
317
358
|
);
|
|
318
|
-
this.metrics.
|
|
359
|
+
this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
|
|
319
360
|
return undefined;
|
|
320
361
|
}
|
|
321
362
|
|
|
@@ -326,7 +367,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
326
367
|
const proposerForPublisher = this.config.fishermanMode ? undefined : proposer;
|
|
327
368
|
const { attestorAddress, publisher } = await this.publisherFactory.create(proposerForPublisher);
|
|
328
369
|
this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
|
|
329
|
-
this.publisher = publisher;
|
|
330
370
|
|
|
331
371
|
// In fisherman mode, set the actual proposer's address for simulations
|
|
332
372
|
if (this.config.fishermanMode && proposer) {
|
|
@@ -335,14 +375,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
335
375
|
}
|
|
336
376
|
|
|
337
377
|
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
338
|
-
|
|
378
|
+
let invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
|
|
379
|
+
|
|
380
|
+
// Determine the correct archive and L1 state overrides for the canProposeAt check.
|
|
381
|
+
// The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
|
|
382
|
+
// When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
|
|
383
|
+
let archiveForCheck = syncedTo.archive;
|
|
384
|
+
const l1SimulationOverridesBuilder = new SimulationOverridesBuilder();
|
|
385
|
+
|
|
386
|
+
if (this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint) {
|
|
387
|
+
// Parent checkpoint hasn't landed on L1 yet. Override both the proposed checkpoint number
|
|
388
|
+
// and the archive at that checkpoint so L1 simulation sees the correct chain tip.
|
|
389
|
+
const parentCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
|
|
390
|
+
l1SimulationOverridesBuilder.forPendingCheckpoint(parentCheckpointNumber).withPendingArchive(syncedTo.archive);
|
|
391
|
+
this.metrics.recordPipelineDepth(syncedTo.checkpointNumber - syncedTo.checkpointedCheckpointNumber);
|
|
392
|
+
|
|
393
|
+
this.log.verbose(
|
|
394
|
+
`Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber})`,
|
|
395
|
+
);
|
|
396
|
+
// Clear the invalidation - the proposed checkpoint should handle it.
|
|
397
|
+
invalidateCheckpoint = undefined;
|
|
398
|
+
} else if (invalidateCheckpoint) {
|
|
399
|
+
// After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
|
|
400
|
+
// exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
|
|
401
|
+
archiveForCheck = invalidateCheckpoint.lastArchive;
|
|
402
|
+
l1SimulationOverridesBuilder.forPendingCheckpoint(invalidateCheckpoint.forcePendingCheckpointNumber);
|
|
403
|
+
this.metrics.recordPipelineDepth(0);
|
|
404
|
+
} else {
|
|
405
|
+
this.metrics.recordPipelineDepth(0);
|
|
406
|
+
}
|
|
339
407
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
syncedTo.archive,
|
|
408
|
+
const simulationOverridesPlan = l1SimulationOverridesBuilder.build();
|
|
409
|
+
const canProposeCheck = await publisher.canProposeAt(
|
|
410
|
+
archiveForCheck,
|
|
344
411
|
proposer ?? EthAddress.ZERO,
|
|
345
|
-
|
|
412
|
+
simulationOverridesPlan,
|
|
346
413
|
);
|
|
347
414
|
|
|
348
415
|
if (canProposeCheck === undefined) {
|
|
@@ -351,17 +418,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
351
418
|
logCtx,
|
|
352
419
|
);
|
|
353
420
|
this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed', slot });
|
|
354
|
-
this.metrics.
|
|
421
|
+
this.metrics.recordCheckpointPrecheckFailed('rollup_contract_check_failed');
|
|
355
422
|
return undefined;
|
|
356
423
|
}
|
|
357
424
|
|
|
358
|
-
if (canProposeCheck.slot !==
|
|
425
|
+
if (canProposeCheck.slot !== targetSlot) {
|
|
359
426
|
this.log.warn(
|
|
360
|
-
`Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${
|
|
361
|
-
{ ...logCtx, rollup: canProposeCheck, expectedSlot:
|
|
427
|
+
`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}.`,
|
|
428
|
+
{ ...logCtx, rollup: canProposeCheck, expectedSlot: targetSlot },
|
|
362
429
|
);
|
|
363
430
|
this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
|
|
364
|
-
this.metrics.
|
|
431
|
+
this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
|
|
365
432
|
return undefined;
|
|
366
433
|
}
|
|
367
434
|
|
|
@@ -371,40 +438,53 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
371
438
|
{ ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
|
|
372
439
|
);
|
|
373
440
|
this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch', slot });
|
|
374
|
-
this.metrics.
|
|
441
|
+
this.metrics.recordCheckpointPrecheckFailed('block_number_mismatch');
|
|
375
442
|
return undefined;
|
|
376
443
|
}
|
|
377
444
|
|
|
378
|
-
this.lastSlotForCheckpointProposalJob =
|
|
379
|
-
|
|
380
|
-
this.
|
|
445
|
+
this.lastSlotForCheckpointProposalJob = targetSlot;
|
|
446
|
+
|
|
447
|
+
await this.p2pClient.prepareForSlot(targetSlot);
|
|
448
|
+
this.log.info(
|
|
449
|
+
`Preparing checkpoint proposal ${checkpointNumber} for target slot ${targetSlot} during wall-clock slot ${slot}`,
|
|
450
|
+
{
|
|
451
|
+
...logCtx,
|
|
452
|
+
proposer,
|
|
453
|
+
pipeliningEnabled: this.epochCache.isProposerPipeliningEnabled(),
|
|
454
|
+
},
|
|
455
|
+
);
|
|
381
456
|
|
|
382
457
|
// Create and return the checkpoint proposal job
|
|
383
458
|
return this.createCheckpointProposalJob(
|
|
384
|
-
epoch,
|
|
385
459
|
slot,
|
|
460
|
+
targetSlot,
|
|
461
|
+
targetEpoch,
|
|
386
462
|
checkpointNumber,
|
|
387
463
|
syncedTo.blockNumber,
|
|
388
464
|
proposer,
|
|
389
465
|
publisher,
|
|
390
466
|
attestorAddress,
|
|
391
467
|
invalidateCheckpoint,
|
|
468
|
+
syncedTo.proposedCheckpointData,
|
|
392
469
|
);
|
|
393
470
|
}
|
|
394
471
|
|
|
395
472
|
protected createCheckpointProposalJob(
|
|
396
|
-
epoch: EpochNumber,
|
|
397
473
|
slot: SlotNumber,
|
|
474
|
+
targetSlot: SlotNumber,
|
|
475
|
+
targetEpoch: EpochNumber,
|
|
398
476
|
checkpointNumber: CheckpointNumber,
|
|
399
477
|
syncedToBlockNumber: BlockNumber,
|
|
400
478
|
proposer: EthAddress | undefined,
|
|
401
479
|
publisher: SequencerPublisher,
|
|
402
480
|
attestorAddress: EthAddress,
|
|
403
481
|
invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
|
|
482
|
+
proposedCheckpointData?: ProposedCheckpointData,
|
|
404
483
|
): CheckpointProposalJob {
|
|
405
484
|
return new CheckpointProposalJob(
|
|
406
|
-
epoch,
|
|
407
485
|
slot,
|
|
486
|
+
targetSlot,
|
|
487
|
+
targetEpoch,
|
|
408
488
|
checkpointNumber,
|
|
409
489
|
syncedToBlockNumber,
|
|
410
490
|
proposer,
|
|
@@ -420,19 +500,29 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
420
500
|
this.checkpointsBuilder,
|
|
421
501
|
this.l2BlockSource,
|
|
422
502
|
this.l1Constants,
|
|
503
|
+
this.signatureContext,
|
|
423
504
|
this.config,
|
|
424
505
|
this.timetable,
|
|
425
506
|
this.slasherClient,
|
|
426
507
|
this.epochCache,
|
|
427
508
|
this.dateProvider,
|
|
428
509
|
this.metrics,
|
|
510
|
+
this.checkpointProposalJobMetrics.createRecorder(),
|
|
429
511
|
this,
|
|
430
512
|
this.setState.bind(this),
|
|
431
513
|
this.tracer,
|
|
432
514
|
this.log.getBindings(),
|
|
515
|
+
proposedCheckpointData,
|
|
433
516
|
);
|
|
434
517
|
}
|
|
435
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Returns the current sequencer state.
|
|
521
|
+
*/
|
|
522
|
+
public getState(): SequencerState {
|
|
523
|
+
return this.state;
|
|
524
|
+
}
|
|
525
|
+
|
|
436
526
|
/**
|
|
437
527
|
* Internal helper for setting the sequencer state and checks if we have enough time left in the slot to transition to the new state.
|
|
438
528
|
* @param proposedState - The new state to transition to.
|
|
@@ -458,19 +548,35 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
458
548
|
this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
|
|
459
549
|
}
|
|
460
550
|
|
|
551
|
+
const oldState = this.state;
|
|
552
|
+
const oldStateSlotNumber = this.stateSlotNumber;
|
|
553
|
+
const stateChanged = proposedState !== oldState;
|
|
554
|
+
const transitionAtMs = performance.now();
|
|
555
|
+
const stateDurationMs = transitionAtMs - this.stateEnteredAtMs;
|
|
556
|
+
|
|
461
557
|
const boringStates = [SequencerState.IDLE, SequencerState.SYNCHRONIZING];
|
|
462
558
|
const logLevel =
|
|
463
|
-
boringStates.includes(proposedState) && boringStates.includes(
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
559
|
+
boringStates.includes(proposedState) && boringStates.includes(oldState) ? ('trace' as const) : ('debug' as const);
|
|
560
|
+
this.stateLog[logLevel](`Transitioning from ${oldState} to ${proposedState}`, {
|
|
561
|
+
oldState,
|
|
562
|
+
newState: proposedState,
|
|
563
|
+
slotNumber,
|
|
564
|
+
stateSlotNumber: oldStateSlotNumber,
|
|
565
|
+
secondsIntoSlot,
|
|
566
|
+
...(stateChanged && { stateDurationMs: Math.ceil(stateDurationMs) }),
|
|
567
|
+
});
|
|
467
568
|
|
|
468
569
|
this.emit('state-changed', {
|
|
469
|
-
oldState
|
|
570
|
+
oldState,
|
|
470
571
|
newState: proposedState,
|
|
471
572
|
secondsIntoSlot,
|
|
472
573
|
slot: slotNumber,
|
|
473
574
|
});
|
|
575
|
+
if (stateChanged) {
|
|
576
|
+
this.metrics.recordStateDuration(stateDurationMs, oldState);
|
|
577
|
+
this.stateEnteredAtMs = transitionAtMs;
|
|
578
|
+
this.stateSlotNumber = slotNumber;
|
|
579
|
+
}
|
|
474
580
|
this.state = proposedState;
|
|
475
581
|
}
|
|
476
582
|
|
|
@@ -479,16 +585,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
479
585
|
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
480
586
|
*/
|
|
481
587
|
protected async checkSync(args: { ts: bigint; slot: SlotNumber }): Promise<SequencerSyncCheckResult | undefined> {
|
|
482
|
-
// Check that the archiver
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
const
|
|
486
|
-
const { slot
|
|
487
|
-
if (
|
|
588
|
+
// Check that the archiver has fully synced the L2 slot before the one we want to propose in.
|
|
589
|
+
// The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
|
|
590
|
+
// See getSyncedL2SlotNumber for how missed L1 blocks are handled.
|
|
591
|
+
const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
|
|
592
|
+
const { slot } = args;
|
|
593
|
+
if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
|
|
488
594
|
this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
|
|
489
595
|
slot,
|
|
490
|
-
|
|
491
|
-
l1Timestamp,
|
|
596
|
+
syncedL2Slot,
|
|
492
597
|
});
|
|
493
598
|
return undefined;
|
|
494
599
|
}
|
|
@@ -498,25 +603,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
498
603
|
number: syncSummary.latestBlockNumber,
|
|
499
604
|
hash: syncSummary.latestBlockHash,
|
|
500
605
|
})),
|
|
501
|
-
this.l2BlockSource
|
|
606
|
+
this.l2BlockSource
|
|
607
|
+
.getL2Tips()
|
|
608
|
+
.then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
|
|
502
609
|
this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
|
|
503
|
-
this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
|
|
610
|
+
this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
|
|
504
611
|
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
612
|
+
this.l2BlockSource.getLastProposedCheckpoint(),
|
|
505
613
|
] as const);
|
|
506
614
|
|
|
507
|
-
const [worldState,
|
|
615
|
+
const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
|
|
616
|
+
syncedBlocks;
|
|
508
617
|
|
|
509
618
|
// 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,
|
|
510
619
|
// as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
|
|
511
620
|
// TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
|
|
512
621
|
const result =
|
|
513
|
-
(
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
622
|
+
(l2Tips.proposed.number === 0 &&
|
|
623
|
+
l2Tips.checkpointed.block.number === 0 &&
|
|
624
|
+
l2Tips.checkpointed.checkpoint.number === 0 &&
|
|
625
|
+
worldState.number === 0 &&
|
|
626
|
+
p2p.number === 0 &&
|
|
627
|
+
l1ToL2MessageSourceTips.proposed.number === 0 &&
|
|
628
|
+
l1ToL2MessageSourceTips.checkpointed.block.number === 0 &&
|
|
629
|
+
l1ToL2MessageSourceTips.checkpointed.checkpoint.number === 0) ||
|
|
630
|
+
(worldState.hash === l2Tips.proposed.hash &&
|
|
631
|
+
p2p.hash === l2Tips.proposed.hash &&
|
|
632
|
+
l1ToL2MessageSourceTips.proposed.hash === l2Tips.proposed.hash &&
|
|
633
|
+
l1ToL2MessageSourceTips.checkpointed.block.hash === l2Tips.checkpointed.block.hash &&
|
|
634
|
+
l1ToL2MessageSourceTips.checkpointed.checkpoint.hash === l2Tips.checkpointed.checkpoint.hash);
|
|
517
635
|
|
|
518
636
|
if (!result) {
|
|
519
|
-
this.log.debug(`Sequencer sync check failed`, {
|
|
637
|
+
this.log.debug(`Sequencer sync check failed`, {
|
|
638
|
+
worldState,
|
|
639
|
+
l2BlockSource: l2Tips.proposed,
|
|
640
|
+
p2p,
|
|
641
|
+
l1ToL2MessageSourceTips,
|
|
642
|
+
});
|
|
520
643
|
return undefined;
|
|
521
644
|
}
|
|
522
645
|
|
|
@@ -526,26 +649,33 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
526
649
|
const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
527
650
|
return {
|
|
528
651
|
checkpointNumber: CheckpointNumber.ZERO,
|
|
652
|
+
checkpointedCheckpointNumber: CheckpointNumber.ZERO,
|
|
529
653
|
blockNumber: BlockNumber.ZERO,
|
|
530
654
|
archive,
|
|
531
|
-
|
|
655
|
+
hasProposedCheckpoint: false,
|
|
656
|
+
syncedL2Slot,
|
|
532
657
|
pendingChainValidationStatus,
|
|
533
658
|
};
|
|
534
659
|
}
|
|
535
660
|
|
|
536
|
-
const
|
|
537
|
-
if (!
|
|
661
|
+
const blockData = await this.l2BlockSource.getBlockData(blockNumber);
|
|
662
|
+
if (!blockData) {
|
|
538
663
|
// this shouldn't really happen because a moment ago we checked that all components were in sync
|
|
539
|
-
this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
|
|
664
|
+
this.log.error(`Failed to get L2 block data ${blockNumber} from the archiver with all components in sync`);
|
|
540
665
|
return undefined;
|
|
541
666
|
}
|
|
542
667
|
|
|
668
|
+
const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
|
|
669
|
+
|
|
543
670
|
return {
|
|
544
|
-
|
|
545
|
-
blockNumber:
|
|
546
|
-
checkpointNumber:
|
|
547
|
-
|
|
548
|
-
|
|
671
|
+
blockData,
|
|
672
|
+
blockNumber: blockData.header.getBlockNumber(),
|
|
673
|
+
checkpointNumber: blockData.checkpointNumber,
|
|
674
|
+
checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
|
|
675
|
+
archive: blockData.archive.root,
|
|
676
|
+
hasProposedCheckpoint,
|
|
677
|
+
proposedCheckpointData,
|
|
678
|
+
syncedL2Slot,
|
|
549
679
|
pendingChainValidationStatus,
|
|
550
680
|
};
|
|
551
681
|
}
|
|
@@ -554,20 +684,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
554
684
|
* Checks if we are the proposer for the next slot.
|
|
555
685
|
* @returns True if we can propose, and the proposer address (undefined if anyone can propose)
|
|
556
686
|
*/
|
|
557
|
-
protected async checkCanPropose(
|
|
687
|
+
protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
|
|
558
688
|
let proposer: EthAddress | undefined;
|
|
559
689
|
|
|
560
690
|
try {
|
|
561
|
-
proposer = await this.epochCache.getProposerAttesterAddressInSlot(
|
|
691
|
+
proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
|
|
562
692
|
} catch (e) {
|
|
563
693
|
if (e instanceof NoCommitteeError) {
|
|
564
|
-
if (this.lastSlotForNoCommitteeWarning !==
|
|
565
|
-
this.lastSlotForNoCommitteeWarning =
|
|
566
|
-
this.log.warn(`Cannot propose at
|
|
694
|
+
if (this.lastSlotForNoCommitteeWarning !== targetSlot) {
|
|
695
|
+
this.lastSlotForNoCommitteeWarning = targetSlot;
|
|
696
|
+
this.log.warn(`Cannot propose at target slot ${targetSlot} since the committee does not exist on L1`);
|
|
567
697
|
}
|
|
568
698
|
return [false, undefined];
|
|
569
699
|
}
|
|
570
|
-
this.log.error(`Error getting proposer for slot ${
|
|
700
|
+
this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
|
|
571
701
|
return [false, undefined];
|
|
572
702
|
}
|
|
573
703
|
|
|
@@ -584,10 +714,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
584
714
|
const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
|
|
585
715
|
|
|
586
716
|
if (!weAreProposer) {
|
|
587
|
-
this.log.debug(`Cannot propose at slot ${
|
|
717
|
+
this.log.debug(`Cannot propose at target slot ${targetSlot} since we are not a proposer`, {
|
|
718
|
+
targetSlot,
|
|
719
|
+
validatorAddresses,
|
|
720
|
+
proposer,
|
|
721
|
+
});
|
|
588
722
|
return [false, proposer];
|
|
589
723
|
}
|
|
590
724
|
|
|
725
|
+
this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
|
|
726
|
+
targetSlot,
|
|
727
|
+
proposer,
|
|
728
|
+
});
|
|
591
729
|
return [true, proposer];
|
|
592
730
|
}
|
|
593
731
|
|
|
@@ -596,8 +734,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
596
734
|
* This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
|
|
597
735
|
*/
|
|
598
736
|
@trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
|
|
599
|
-
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
|
|
600
|
-
const { slot } = args;
|
|
737
|
+
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
|
|
738
|
+
const { slot, targetSlot } = args;
|
|
601
739
|
|
|
602
740
|
// Prevent duplicate attempts in the same slot
|
|
603
741
|
if (this.lastSlotForFallbackVote === slot) {
|
|
@@ -625,7 +763,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
625
763
|
});
|
|
626
764
|
|
|
627
765
|
// Check if we're a proposer or proposal is open
|
|
628
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
766
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
629
767
|
if (!canPropose) {
|
|
630
768
|
this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
|
|
631
769
|
return;
|
|
@@ -642,9 +780,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
642
780
|
slot,
|
|
643
781
|
});
|
|
644
782
|
|
|
645
|
-
// Enqueue governance and slashing votes
|
|
783
|
+
// Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
|
|
646
784
|
const voter = new CheckpointVoter(
|
|
647
|
-
|
|
785
|
+
targetSlot,
|
|
648
786
|
publisher,
|
|
649
787
|
attestorAddress,
|
|
650
788
|
this.validatorClient,
|
|
@@ -724,7 +862,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
724
862
|
syncedTo: SequencerSyncCheckResult,
|
|
725
863
|
currentSlot: SlotNumber,
|
|
726
864
|
): Promise<void> {
|
|
727
|
-
const { pendingChainValidationStatus,
|
|
865
|
+
const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
|
|
728
866
|
if (pendingChainValidationStatus.valid) {
|
|
729
867
|
return;
|
|
730
868
|
}
|
|
@@ -739,7 +877,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
739
877
|
|
|
740
878
|
const logData = {
|
|
741
879
|
invalidL1Timestamp: invalidCheckpointTimestamp,
|
|
742
|
-
|
|
880
|
+
syncedL2Slot,
|
|
743
881
|
invalidCheckpoint: pendingChainValidationStatus.checkpoint,
|
|
744
882
|
secondsBeforeInvalidatingBlockAsCommitteeMember,
|
|
745
883
|
secondsBeforeInvalidatingBlockAsNonCommitteeMember,
|
|
@@ -867,6 +1005,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
867
1005
|
return this.validatorClient?.getValidatorAddresses();
|
|
868
1006
|
}
|
|
869
1007
|
|
|
1008
|
+
/** Updates the publisher factory's node keystore adapter after a keystore reload. */
|
|
1009
|
+
public updatePublisherNodeKeyStore(adapter: NodeKeystoreAdapter): void {
|
|
1010
|
+
this.publisherFactory.updateNodeKeyStore(adapter);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
870
1013
|
public getConfig() {
|
|
871
1014
|
return this.config;
|
|
872
1015
|
}
|
|
@@ -877,10 +1020,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
877
1020
|
}
|
|
878
1021
|
|
|
879
1022
|
type SequencerSyncCheckResult = {
|
|
880
|
-
|
|
1023
|
+
blockData?: BlockData;
|
|
881
1024
|
checkpointNumber: CheckpointNumber;
|
|
1025
|
+
checkpointedCheckpointNumber: CheckpointNumber;
|
|
882
1026
|
blockNumber: BlockNumber;
|
|
883
1027
|
archive: Fr;
|
|
884
|
-
|
|
1028
|
+
hasProposedCheckpoint: boolean;
|
|
1029
|
+
proposedCheckpointData?: ProposedCheckpointData;
|
|
1030
|
+
syncedL2Slot: SlotNumber;
|
|
885
1031
|
pendingChainValidationStatus: ValidateCheckpointResult;
|
|
886
1032
|
};
|