@aztec/sequencer-client 0.0.1-commit.e558bd1c → 0.0.1-commit.e5a3663dd
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 -7
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +58 -37
- 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 +75 -48
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +434 -141
- 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 +705 -223
- 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 +23 -8
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +117 -21
- package/dest/sequencer/sequencer.d.ts +53 -19
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +188 -101
- package/dest/sequencer/timetable.d.ts +17 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +51 -46
- 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 +11 -11
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +45 -34
- package/dest/test/utils.d.ts +3 -3
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +10 -8
- package/package.json +27 -28
- package/src/client/sequencer-client.ts +81 -30
- package/src/config.ts +77 -46
- 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 +501 -202
- package/src/sequencer/README.md +82 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +873 -243
- 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 +132 -25
- package/src/sequencer/sequencer.ts +262 -115
- package/src/sequencer/timetable.ts +70 -57
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +63 -49
- package/src/test/utils.ts +31 -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,39 +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
|
-
|
|
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
|
+
);
|
|
380
456
|
|
|
381
457
|
// Create and return the checkpoint proposal job
|
|
382
458
|
return this.createCheckpointProposalJob(
|
|
383
|
-
epoch,
|
|
384
459
|
slot,
|
|
460
|
+
targetSlot,
|
|
461
|
+
targetEpoch,
|
|
385
462
|
checkpointNumber,
|
|
386
463
|
syncedTo.blockNumber,
|
|
387
464
|
proposer,
|
|
388
465
|
publisher,
|
|
389
466
|
attestorAddress,
|
|
390
467
|
invalidateCheckpoint,
|
|
468
|
+
syncedTo.proposedCheckpointData,
|
|
391
469
|
);
|
|
392
470
|
}
|
|
393
471
|
|
|
394
472
|
protected createCheckpointProposalJob(
|
|
395
|
-
epoch: EpochNumber,
|
|
396
473
|
slot: SlotNumber,
|
|
474
|
+
targetSlot: SlotNumber,
|
|
475
|
+
targetEpoch: EpochNumber,
|
|
397
476
|
checkpointNumber: CheckpointNumber,
|
|
398
477
|
syncedToBlockNumber: BlockNumber,
|
|
399
478
|
proposer: EthAddress | undefined,
|
|
400
479
|
publisher: SequencerPublisher,
|
|
401
480
|
attestorAddress: EthAddress,
|
|
402
481
|
invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
|
|
482
|
+
proposedCheckpointData?: ProposedCheckpointData,
|
|
403
483
|
): CheckpointProposalJob {
|
|
404
484
|
return new CheckpointProposalJob(
|
|
405
|
-
epoch,
|
|
406
485
|
slot,
|
|
486
|
+
targetSlot,
|
|
487
|
+
targetEpoch,
|
|
407
488
|
checkpointNumber,
|
|
408
489
|
syncedToBlockNumber,
|
|
409
490
|
proposer,
|
|
@@ -419,19 +500,29 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
419
500
|
this.checkpointsBuilder,
|
|
420
501
|
this.l2BlockSource,
|
|
421
502
|
this.l1Constants,
|
|
503
|
+
this.signatureContext,
|
|
422
504
|
this.config,
|
|
423
505
|
this.timetable,
|
|
424
506
|
this.slasherClient,
|
|
425
507
|
this.epochCache,
|
|
426
508
|
this.dateProvider,
|
|
427
509
|
this.metrics,
|
|
510
|
+
this.checkpointProposalJobMetrics.createRecorder(),
|
|
428
511
|
this,
|
|
429
512
|
this.setState.bind(this),
|
|
430
513
|
this.tracer,
|
|
431
514
|
this.log.getBindings(),
|
|
515
|
+
proposedCheckpointData,
|
|
432
516
|
);
|
|
433
517
|
}
|
|
434
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Returns the current sequencer state.
|
|
521
|
+
*/
|
|
522
|
+
public getState(): SequencerState {
|
|
523
|
+
return this.state;
|
|
524
|
+
}
|
|
525
|
+
|
|
435
526
|
/**
|
|
436
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.
|
|
437
528
|
* @param proposedState - The new state to transition to.
|
|
@@ -457,19 +548,35 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
457
548
|
this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
|
|
458
549
|
}
|
|
459
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
|
+
|
|
460
557
|
const boringStates = [SequencerState.IDLE, SequencerState.SYNCHRONIZING];
|
|
461
558
|
const logLevel =
|
|
462
|
-
boringStates.includes(proposedState) && boringStates.includes(
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
+
});
|
|
466
568
|
|
|
467
569
|
this.emit('state-changed', {
|
|
468
|
-
oldState
|
|
570
|
+
oldState,
|
|
469
571
|
newState: proposedState,
|
|
470
572
|
secondsIntoSlot,
|
|
471
573
|
slot: slotNumber,
|
|
472
574
|
});
|
|
575
|
+
if (stateChanged) {
|
|
576
|
+
this.metrics.recordStateDuration(stateDurationMs, oldState);
|
|
577
|
+
this.stateEnteredAtMs = transitionAtMs;
|
|
578
|
+
this.stateSlotNumber = slotNumber;
|
|
579
|
+
}
|
|
473
580
|
this.state = proposedState;
|
|
474
581
|
}
|
|
475
582
|
|
|
@@ -478,16 +585,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
478
585
|
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
479
586
|
*/
|
|
480
587
|
protected async checkSync(args: { ts: bigint; slot: SlotNumber }): Promise<SequencerSyncCheckResult | undefined> {
|
|
481
|
-
// Check that the archiver
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
const
|
|
485
|
-
const { slot
|
|
486
|
-
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) {
|
|
487
594
|
this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
|
|
488
595
|
slot,
|
|
489
|
-
|
|
490
|
-
l1Timestamp,
|
|
596
|
+
syncedL2Slot,
|
|
491
597
|
});
|
|
492
598
|
return undefined;
|
|
493
599
|
}
|
|
@@ -497,25 +603,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
497
603
|
number: syncSummary.latestBlockNumber,
|
|
498
604
|
hash: syncSummary.latestBlockHash,
|
|
499
605
|
})),
|
|
500
|
-
this.l2BlockSource
|
|
606
|
+
this.l2BlockSource
|
|
607
|
+
.getL2Tips()
|
|
608
|
+
.then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
|
|
501
609
|
this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
|
|
502
|
-
this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
|
|
610
|
+
this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
|
|
503
611
|
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
612
|
+
this.l2BlockSource.getLastProposedCheckpoint(),
|
|
504
613
|
] as const);
|
|
505
614
|
|
|
506
|
-
const [worldState,
|
|
615
|
+
const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
|
|
616
|
+
syncedBlocks;
|
|
507
617
|
|
|
508
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,
|
|
509
619
|
// as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
|
|
510
620
|
// TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
|
|
511
621
|
const result =
|
|
512
|
-
(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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);
|
|
516
635
|
|
|
517
636
|
if (!result) {
|
|
518
|
-
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
|
+
});
|
|
519
643
|
return undefined;
|
|
520
644
|
}
|
|
521
645
|
|
|
@@ -525,26 +649,33 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
525
649
|
const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
526
650
|
return {
|
|
527
651
|
checkpointNumber: CheckpointNumber.ZERO,
|
|
652
|
+
checkpointedCheckpointNumber: CheckpointNumber.ZERO,
|
|
528
653
|
blockNumber: BlockNumber.ZERO,
|
|
529
654
|
archive,
|
|
530
|
-
|
|
655
|
+
hasProposedCheckpoint: false,
|
|
656
|
+
syncedL2Slot,
|
|
531
657
|
pendingChainValidationStatus,
|
|
532
658
|
};
|
|
533
659
|
}
|
|
534
660
|
|
|
535
|
-
const
|
|
536
|
-
if (!
|
|
661
|
+
const blockData = await this.l2BlockSource.getBlockData(blockNumber);
|
|
662
|
+
if (!blockData) {
|
|
537
663
|
// this shouldn't really happen because a moment ago we checked that all components were in sync
|
|
538
|
-
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`);
|
|
539
665
|
return undefined;
|
|
540
666
|
}
|
|
541
667
|
|
|
668
|
+
const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
|
|
669
|
+
|
|
542
670
|
return {
|
|
543
|
-
|
|
544
|
-
blockNumber:
|
|
545
|
-
checkpointNumber:
|
|
546
|
-
|
|
547
|
-
|
|
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,
|
|
548
679
|
pendingChainValidationStatus,
|
|
549
680
|
};
|
|
550
681
|
}
|
|
@@ -553,20 +684,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
553
684
|
* Checks if we are the proposer for the next slot.
|
|
554
685
|
* @returns True if we can propose, and the proposer address (undefined if anyone can propose)
|
|
555
686
|
*/
|
|
556
|
-
protected async checkCanPropose(
|
|
687
|
+
protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
|
|
557
688
|
let proposer: EthAddress | undefined;
|
|
558
689
|
|
|
559
690
|
try {
|
|
560
|
-
proposer = await this.epochCache.getProposerAttesterAddressInSlot(
|
|
691
|
+
proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
|
|
561
692
|
} catch (e) {
|
|
562
693
|
if (e instanceof NoCommitteeError) {
|
|
563
|
-
if (this.lastSlotForNoCommitteeWarning !==
|
|
564
|
-
this.lastSlotForNoCommitteeWarning =
|
|
565
|
-
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`);
|
|
566
697
|
}
|
|
567
698
|
return [false, undefined];
|
|
568
699
|
}
|
|
569
|
-
this.log.error(`Error getting proposer for slot ${
|
|
700
|
+
this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
|
|
570
701
|
return [false, undefined];
|
|
571
702
|
}
|
|
572
703
|
|
|
@@ -583,10 +714,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
583
714
|
const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
|
|
584
715
|
|
|
585
716
|
if (!weAreProposer) {
|
|
586
|
-
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
|
+
});
|
|
587
722
|
return [false, proposer];
|
|
588
723
|
}
|
|
589
724
|
|
|
725
|
+
this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
|
|
726
|
+
targetSlot,
|
|
727
|
+
proposer,
|
|
728
|
+
});
|
|
590
729
|
return [true, proposer];
|
|
591
730
|
}
|
|
592
731
|
|
|
@@ -595,8 +734,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
595
734
|
* This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
|
|
596
735
|
*/
|
|
597
736
|
@trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
|
|
598
|
-
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
|
|
599
|
-
const { slot } = args;
|
|
737
|
+
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
|
|
738
|
+
const { slot, targetSlot } = args;
|
|
600
739
|
|
|
601
740
|
// Prevent duplicate attempts in the same slot
|
|
602
741
|
if (this.lastSlotForFallbackVote === slot) {
|
|
@@ -624,7 +763,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
624
763
|
});
|
|
625
764
|
|
|
626
765
|
// Check if we're a proposer or proposal is open
|
|
627
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
766
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
628
767
|
if (!canPropose) {
|
|
629
768
|
this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
|
|
630
769
|
return;
|
|
@@ -641,9 +780,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
641
780
|
slot,
|
|
642
781
|
});
|
|
643
782
|
|
|
644
|
-
// Enqueue governance and slashing votes
|
|
783
|
+
// Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
|
|
645
784
|
const voter = new CheckpointVoter(
|
|
646
|
-
|
|
785
|
+
targetSlot,
|
|
647
786
|
publisher,
|
|
648
787
|
attestorAddress,
|
|
649
788
|
this.validatorClient,
|
|
@@ -723,7 +862,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
723
862
|
syncedTo: SequencerSyncCheckResult,
|
|
724
863
|
currentSlot: SlotNumber,
|
|
725
864
|
): Promise<void> {
|
|
726
|
-
const { pendingChainValidationStatus,
|
|
865
|
+
const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
|
|
727
866
|
if (pendingChainValidationStatus.valid) {
|
|
728
867
|
return;
|
|
729
868
|
}
|
|
@@ -738,7 +877,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
738
877
|
|
|
739
878
|
const logData = {
|
|
740
879
|
invalidL1Timestamp: invalidCheckpointTimestamp,
|
|
741
|
-
|
|
880
|
+
syncedL2Slot,
|
|
742
881
|
invalidCheckpoint: pendingChainValidationStatus.checkpoint,
|
|
743
882
|
secondsBeforeInvalidatingBlockAsCommitteeMember,
|
|
744
883
|
secondsBeforeInvalidatingBlockAsNonCommitteeMember,
|
|
@@ -866,6 +1005,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
866
1005
|
return this.validatorClient?.getValidatorAddresses();
|
|
867
1006
|
}
|
|
868
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
|
+
|
|
869
1013
|
public getConfig() {
|
|
870
1014
|
return this.config;
|
|
871
1015
|
}
|
|
@@ -876,10 +1020,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
876
1020
|
}
|
|
877
1021
|
|
|
878
1022
|
type SequencerSyncCheckResult = {
|
|
879
|
-
|
|
1023
|
+
blockData?: BlockData;
|
|
880
1024
|
checkpointNumber: CheckpointNumber;
|
|
1025
|
+
checkpointedCheckpointNumber: CheckpointNumber;
|
|
881
1026
|
blockNumber: BlockNumber;
|
|
882
1027
|
archive: Fr;
|
|
883
|
-
|
|
1028
|
+
hasProposedCheckpoint: boolean;
|
|
1029
|
+
proposedCheckpointData?: ProposedCheckpointData;
|
|
1030
|
+
syncedL2Slot: SlotNumber;
|
|
884
1031
|
pendingChainValidationStatus: ValidateCheckpointResult;
|
|
885
1032
|
};
|