@aztec/sequencer-client 0.0.1-commit.1142ef1 → 0.0.1-commit.11bf3dd6e
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 +26 -7
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +52 -38
- 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/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/publisher/config.d.ts +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-metrics.d.ts +1 -1
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +12 -4
- package/dest/publisher/sequencer-publisher.d.ts +75 -49
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +470 -156
- 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 +54 -12
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +450 -184
- 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 +3 -3
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +34 -13
- package/dest/sequencer/events.d.ts +2 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/index.d.ts +1 -2
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +0 -1
- package/dest/sequencer/metrics.d.ts +19 -8
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +120 -36
- package/dest/sequencer/sequencer.d.ts +58 -22
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +218 -96
- 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 +23 -19
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +67 -38
- package/dest/test/utils.d.ts +8 -8
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +12 -11
- package/package.json +29 -28
- package/src/client/sequencer-client.ts +81 -30
- package/src/config.ts +71 -49
- 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/index.ts +1 -6
- 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-metrics.ts +7 -3
- package/src/publisher/sequencer-publisher.ts +531 -214
- package/src/sequencer/README.md +83 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +601 -206
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/checkpoint_voter.ts +31 -17
- package/src/sequencer/events.ts +1 -1
- package/src/sequencer/index.ts +0 -1
- package/src/sequencer/metrics.ts +136 -40
- package/src/sequencer/sequencer.ts +302 -105
- 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 +122 -78
- package/src/test/utils.ts +24 -14
- package/dest/sequencer/block_builder.d.ts +0 -26
- package/dest/sequencer/block_builder.d.ts.map +0 -1
- package/dest/sequencer/block_builder.js +0 -129
- package/src/sequencer/block_builder.ts +0 -216
|
@@ -1,7 +1,7 @@
|
|
|
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';
|
|
@@ -12,9 +12,9 @@ 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 { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
18
18
|
import {
|
|
19
19
|
type ResolvedSequencerConfig,
|
|
20
20
|
type SequencerConfig,
|
|
@@ -25,7 +25,7 @@ import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
|
25
25
|
import { pickFromSchema } from '@aztec/stdlib/schemas';
|
|
26
26
|
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
27
27
|
import { Attributes, type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client';
|
|
28
|
-
import { FullNodeCheckpointsBuilder, type ValidatorClient } from '@aztec/validator-client';
|
|
28
|
+
import { FullNodeCheckpointsBuilder, NodeKeystoreAdapter, type ValidatorClient } from '@aztec/validator-client';
|
|
29
29
|
|
|
30
30
|
import EventEmitter from 'node:events';
|
|
31
31
|
|
|
@@ -34,6 +34,7 @@ import type { GlobalVariableBuilder } from '../global_variable_builder/global_bu
|
|
|
34
34
|
import type { SequencerPublisherFactory } from '../publisher/sequencer-publisher-factory.js';
|
|
35
35
|
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
36
36
|
import { CheckpointProposalJob } from './checkpoint_proposal_job.js';
|
|
37
|
+
import { CheckpointProposalJobMetrics } from './checkpoint_proposal_job_metrics.js';
|
|
37
38
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
38
39
|
import { SequencerInterruptedError, SequencerTooSlowError } from './errors.js';
|
|
39
40
|
import type { SequencerEvents } from './events.js';
|
|
@@ -56,9 +57,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
56
57
|
private runningPromise?: RunningPromise;
|
|
57
58
|
private state = SequencerState.STOPPED;
|
|
58
59
|
private metrics: SequencerMetrics;
|
|
60
|
+
private checkpointProposalJobMetrics: CheckpointProposalJobMetrics;
|
|
59
61
|
|
|
60
|
-
/** The last slot for which we attempted to
|
|
61
|
-
private
|
|
62
|
+
/** The last slot for which we attempted to perform our voting duties with degraded block production */
|
|
63
|
+
private lastSlotForFallbackVote: SlotNumber | undefined;
|
|
64
|
+
|
|
65
|
+
/** The last slot for which we logged "no committee" warning, to avoid spam */
|
|
66
|
+
private lastSlotForNoCommitteeWarning: SlotNumber | undefined;
|
|
62
67
|
|
|
63
68
|
/** The last slot for which we triggered a checkpoint proposal job, to prevent duplicate attempts. */
|
|
64
69
|
private lastSlotForCheckpointProposalJob: SlotNumber | undefined;
|
|
@@ -69,17 +74,12 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
69
74
|
/** The last epoch for which we logged strategy comparison in fisherman mode. */
|
|
70
75
|
private lastEpochForStrategyComparison: EpochNumber | undefined;
|
|
71
76
|
|
|
77
|
+
/** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */
|
|
78
|
+
protected lastCheckpointProposalJob: CheckpointProposalJob | undefined;
|
|
79
|
+
|
|
72
80
|
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
|
|
73
81
|
protected timetable!: SequencerTimetable;
|
|
74
82
|
|
|
75
|
-
// This shouldn't be here as this gets re-created each time we build/propose a block.
|
|
76
|
-
// But we have a number of tests that abuse/rely on this class having a permanent publisher.
|
|
77
|
-
// As long as those tests only configure a single publisher they will continue to work.
|
|
78
|
-
// This will get re-assigned every time the sequencer goes to build a new block to a publisher that is valid
|
|
79
|
-
// for the block proposer.
|
|
80
|
-
// TODO(palla/mbps): Remove this field and fix tests
|
|
81
|
-
protected publisher: SequencerPublisher | undefined;
|
|
82
|
-
|
|
83
83
|
/** Config for the sequencer */
|
|
84
84
|
protected config: ResolvedSequencerConfig = DefaultSequencerConfig;
|
|
85
85
|
|
|
@@ -109,32 +109,34 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
|
|
112
|
+
this.checkpointProposalJobMetrics = new CheckpointProposalJobMetrics(telemetry);
|
|
112
113
|
this.updateConfig(config);
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
/** Updates sequencer config by the defined values and updates the timetable */
|
|
116
117
|
public updateConfig(config: Partial<SequencerConfig>) {
|
|
117
118
|
const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
|
|
118
|
-
this.log.info(`Updated sequencer config`, omit(filteredConfig, '
|
|
119
|
+
this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
|
|
119
120
|
this.config = merge(this.config, filteredConfig);
|
|
121
|
+
const p2pPropagationTime = this.config.attestationPropagationTime;
|
|
120
122
|
this.timetable = new SequencerTimetable(
|
|
121
123
|
{
|
|
122
124
|
ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
|
|
123
125
|
aztecSlotDuration: this.aztecSlotDuration,
|
|
124
126
|
l1PublishingTime: this.l1PublishingTime,
|
|
125
|
-
p2pPropagationTime
|
|
127
|
+
p2pPropagationTime,
|
|
126
128
|
blockDurationMs: this.config.blockDurationMs,
|
|
127
129
|
enforce: this.config.enforceTimeTable,
|
|
130
|
+
pipelining: this.epochCache.isProposerPipeliningEnabled(),
|
|
128
131
|
},
|
|
129
132
|
this.metrics,
|
|
130
133
|
this.log,
|
|
131
134
|
);
|
|
132
135
|
}
|
|
133
136
|
|
|
134
|
-
/** Initializes the sequencer (precomputes tables
|
|
135
|
-
public
|
|
137
|
+
/** Initializes the sequencer (precomputes tables). Takes about 3s. */
|
|
138
|
+
public init() {
|
|
136
139
|
getKzg();
|
|
137
|
-
this.publisher = (await this.publisherFactory.create(undefined)).publisher;
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
/** Starts the sequencer and moves to IDLE state. */
|
|
@@ -149,12 +151,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
149
151
|
this.log.info('Started sequencer');
|
|
150
152
|
}
|
|
151
153
|
|
|
154
|
+
/** Triggers an immediate run of the sequencer, bypassing the polling interval. */
|
|
155
|
+
public trigger() {
|
|
156
|
+
return this.runningPromise?.trigger();
|
|
157
|
+
}
|
|
158
|
+
|
|
152
159
|
/** Stops the sequencer from building blocks and moves to STOPPED state. */
|
|
153
160
|
public async stop(): Promise<void> {
|
|
154
161
|
this.log.info(`Stopping sequencer`);
|
|
155
162
|
this.setState(SequencerState.STOPPING, undefined, { force: true });
|
|
156
|
-
this.
|
|
163
|
+
await this.publisherFactory.stopAll();
|
|
157
164
|
await this.runningPromise?.stop();
|
|
165
|
+
await this.lastCheckpointProposalJob?.awaitPendingSubmission();
|
|
158
166
|
this.setState(SequencerState.STOPPED, undefined, { force: true });
|
|
159
167
|
this.log.info('Stopped sequencer');
|
|
160
168
|
}
|
|
@@ -166,7 +174,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
166
174
|
} catch (err) {
|
|
167
175
|
this.emit('checkpoint-error', { error: err as Error });
|
|
168
176
|
if (err instanceof SequencerTooSlowError) {
|
|
169
|
-
// TODO(palla/mbps): Add missing states
|
|
170
177
|
// Log as warn only if we had to abort halfway through the block proposal
|
|
171
178
|
const logLvl = [SequencerState.INITIALIZING_CHECKPOINT, SequencerState.PROPOSER_CHECK].includes(
|
|
172
179
|
err.proposedState,
|
|
@@ -199,14 +206,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
199
206
|
@trackSpan('Sequencer.work')
|
|
200
207
|
protected async work() {
|
|
201
208
|
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
202
|
-
const { slot, ts,
|
|
209
|
+
const { slot, ts, nowSeconds, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
210
|
+
const { slot: targetSlot, epoch: targetEpoch } = this.epochCache.getTargetEpochAndSlotInNextL1Slot();
|
|
203
211
|
|
|
204
212
|
// Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
|
|
205
|
-
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
213
|
+
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
214
|
+
slot,
|
|
215
|
+
targetSlot,
|
|
216
|
+
epoch,
|
|
217
|
+
targetEpoch,
|
|
218
|
+
ts,
|
|
219
|
+
nowSeconds,
|
|
220
|
+
);
|
|
206
221
|
if (!checkpointProposalJob) {
|
|
207
222
|
return;
|
|
208
223
|
}
|
|
209
224
|
|
|
225
|
+
// Track the job so we can await its pending L1 submission during shutdown
|
|
226
|
+
this.lastCheckpointProposalJob = checkpointProposalJob;
|
|
227
|
+
|
|
210
228
|
// Execute the checkpoint proposal job
|
|
211
229
|
const checkpoint = await checkpointProposalJob.execute();
|
|
212
230
|
|
|
@@ -215,13 +233,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
215
233
|
this.lastCheckpointProposed = checkpoint;
|
|
216
234
|
}
|
|
217
235
|
|
|
218
|
-
// Log fee strategy comparison if on fisherman
|
|
236
|
+
// Log fee strategy comparison if on fisherman (uses target epoch since we mirror the proposer's perspective)
|
|
219
237
|
if (
|
|
220
238
|
this.config.fishermanMode &&
|
|
221
|
-
(this.lastEpochForStrategyComparison === undefined ||
|
|
239
|
+
(this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
|
|
222
240
|
) {
|
|
223
|
-
this.logStrategyComparison(
|
|
224
|
-
this.lastEpochForStrategyComparison =
|
|
241
|
+
this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
|
|
242
|
+
this.lastEpochForStrategyComparison = targetEpoch;
|
|
225
243
|
}
|
|
226
244
|
|
|
227
245
|
return checkpoint;
|
|
@@ -233,33 +251,58 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
233
251
|
* @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
|
|
234
252
|
*/
|
|
235
253
|
@trackSpan('Sequencer.prepareCheckpointProposal')
|
|
236
|
-
|
|
254
|
+
protected async prepareCheckpointProposal(
|
|
237
255
|
slot: SlotNumber,
|
|
256
|
+
targetSlot: SlotNumber,
|
|
257
|
+
epoch: EpochNumber,
|
|
258
|
+
targetEpoch: EpochNumber,
|
|
238
259
|
ts: bigint,
|
|
239
|
-
|
|
260
|
+
nowSeconds: bigint,
|
|
240
261
|
): Promise<CheckpointProposalJob | undefined> {
|
|
241
|
-
// Check we have not already processed this slot (cheapest check)
|
|
262
|
+
// Check we have not already processed this target slot (cheapest check)
|
|
242
263
|
// We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
|
|
243
264
|
// running against actual time (eg when we use sandbox-style automining)
|
|
244
265
|
if (
|
|
245
266
|
this.lastSlotForCheckpointProposalJob &&
|
|
246
|
-
this.lastSlotForCheckpointProposalJob >=
|
|
267
|
+
this.lastSlotForCheckpointProposalJob >= targetSlot &&
|
|
247
268
|
this.config.enforceTimeTable
|
|
248
269
|
) {
|
|
249
|
-
this.log.trace(`
|
|
270
|
+
this.log.trace(`Target slot ${targetSlot} has already been processed`);
|
|
250
271
|
return undefined;
|
|
251
272
|
}
|
|
252
273
|
|
|
253
|
-
// But if we have already proposed for this slot,
|
|
254
|
-
if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >=
|
|
255
|
-
this.log.trace(
|
|
274
|
+
// But if we have already proposed for this slot, then we definitely have to skip it, automining or not
|
|
275
|
+
if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >= targetSlot) {
|
|
276
|
+
this.log.trace(
|
|
277
|
+
`Slot ${targetSlot} has already been published as checkpoint ${this.lastCheckpointProposed.number}`,
|
|
278
|
+
);
|
|
256
279
|
return undefined;
|
|
257
280
|
}
|
|
258
281
|
|
|
259
282
|
// Check all components are synced to latest as seen by the archiver (queries all subsystems)
|
|
260
283
|
const syncedTo = await this.checkSync({ ts, slot });
|
|
261
284
|
if (!syncedTo) {
|
|
262
|
-
await this.tryVoteWhenSyncFails({ slot, ts });
|
|
285
|
+
await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// If escape hatch is open for the target epoch, do not start checkpoint proposal work and do not attempt invalidations.
|
|
290
|
+
// Still perform governance/slashing voting (as proposer) once per slot.
|
|
291
|
+
// When pipelining, we check the target epoch (slot+1's epoch) since that's the epoch we're building for.
|
|
292
|
+
const isEscapeHatchOpen = await this.epochCache.isEscapeHatchOpen(targetEpoch);
|
|
293
|
+
|
|
294
|
+
if (isEscapeHatchOpen) {
|
|
295
|
+
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
296
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
297
|
+
if (canPropose) {
|
|
298
|
+
await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
|
|
299
|
+
} else {
|
|
300
|
+
this.log.trace(`Escape hatch open but we are not proposer, skipping vote-only actions`, {
|
|
301
|
+
slot,
|
|
302
|
+
epoch,
|
|
303
|
+
proposer,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
263
306
|
return undefined;
|
|
264
307
|
}
|
|
265
308
|
|
|
@@ -267,18 +310,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
267
310
|
const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
|
|
268
311
|
|
|
269
312
|
const logCtx = {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
|
|
313
|
+
nowSeconds,
|
|
314
|
+
syncedToL2Slot: syncedTo.syncedL2Slot,
|
|
273
315
|
slot,
|
|
316
|
+
targetSlot,
|
|
274
317
|
slotTs: ts,
|
|
275
318
|
checkpointNumber,
|
|
276
319
|
isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
|
|
277
320
|
};
|
|
278
321
|
|
|
279
|
-
// Check that we are a proposer for the
|
|
322
|
+
// Check that we are a proposer for the target slot.
|
|
280
323
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
281
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
324
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
282
325
|
|
|
283
326
|
// If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
|
|
284
327
|
if (!canPropose) {
|
|
@@ -286,13 +329,23 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
286
329
|
return undefined;
|
|
287
330
|
}
|
|
288
331
|
|
|
289
|
-
//
|
|
290
|
-
|
|
332
|
+
// Guard: don't exceed 1-deep pipeline. Without a proposed checkpoint, we can only build
|
|
333
|
+
// confirmed + 1. With a proposed checkpoint, we can build confirmed + 2.
|
|
334
|
+
const confirmedCkpt = syncedTo.checkpointedCheckpointNumber;
|
|
335
|
+
if (checkpointNumber > confirmedCkpt + 2) {
|
|
336
|
+
this.log.verbose(
|
|
337
|
+
`Skipping slot ${targetSlot}: checkpoint ${checkpointNumber} exceeds max pipeline depth (confirmed=${confirmedCkpt})`,
|
|
338
|
+
);
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Check that the target slot is not taken by a block already (should never happen, since only us can propose for this slot)
|
|
343
|
+
if (syncedTo.blockData && syncedTo.blockData.header.getSlot() >= targetSlot) {
|
|
291
344
|
this.log.warn(
|
|
292
|
-
`Cannot propose block at
|
|
293
|
-
{ ...logCtx, block: syncedTo.
|
|
345
|
+
`Cannot propose block at target slot ${targetSlot} since that slot was taken by block ${syncedTo.blockNumber}`,
|
|
346
|
+
{ ...logCtx, block: syncedTo.blockData.header.toInspect() },
|
|
294
347
|
);
|
|
295
|
-
this.metrics.
|
|
348
|
+
this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
|
|
296
349
|
return undefined;
|
|
297
350
|
}
|
|
298
351
|
|
|
@@ -303,7 +356,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
303
356
|
const proposerForPublisher = this.config.fishermanMode ? undefined : proposer;
|
|
304
357
|
const { attestorAddress, publisher } = await this.publisherFactory.create(proposerForPublisher);
|
|
305
358
|
this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
|
|
306
|
-
this.publisher = publisher;
|
|
307
359
|
|
|
308
360
|
// In fisherman mode, set the actual proposer's address for simulations
|
|
309
361
|
if (this.config.fishermanMode && proposer) {
|
|
@@ -312,14 +364,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
312
364
|
}
|
|
313
365
|
|
|
314
366
|
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
315
|
-
|
|
367
|
+
let invalidateCheckpoint = await publisher.simulateInvalidateCheckpoint(syncedTo.pendingChainValidationStatus);
|
|
368
|
+
|
|
369
|
+
// Determine the correct archive and L1 state overrides for the canProposeAt check.
|
|
370
|
+
// The L1 contract reads archives[proposedCheckpointNumber] and compares it with the provided archive.
|
|
371
|
+
// When invalidating or pipelining, the local archive may differ from L1's, so we adjust accordingly.
|
|
372
|
+
let archiveForCheck = syncedTo.archive;
|
|
373
|
+
const l1SimulationOverridesBuilder = new SimulationOverridesBuilder();
|
|
374
|
+
|
|
375
|
+
if (this.epochCache.isProposerPipeliningEnabled() && syncedTo.hasProposedCheckpoint) {
|
|
376
|
+
// Parent checkpoint hasn't landed on L1 yet. Override both the proposed checkpoint number
|
|
377
|
+
// and the archive at that checkpoint so L1 simulation sees the correct chain tip.
|
|
378
|
+
const parentCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
|
|
379
|
+
l1SimulationOverridesBuilder.forPendingCheckpoint(parentCheckpointNumber).withPendingArchive(syncedTo.archive);
|
|
380
|
+
this.metrics.recordPipelineDepth(1);
|
|
381
|
+
|
|
382
|
+
this.log.verbose(
|
|
383
|
+
`Building on top of proposed checkpoint (pending=${syncedTo.proposedCheckpointData?.checkpointNumber})`,
|
|
384
|
+
);
|
|
385
|
+
// Clear the invalidation - the proposed checkpoint should handle it.
|
|
386
|
+
invalidateCheckpoint = undefined;
|
|
387
|
+
} else if (invalidateCheckpoint) {
|
|
388
|
+
// After invalidation, L1 will roll back to checkpoint N-1. The archive at N-1 already
|
|
389
|
+
// exists on L1, so we just pass the matching archive (the lastArchive of the invalid checkpoint).
|
|
390
|
+
archiveForCheck = invalidateCheckpoint.lastArchive;
|
|
391
|
+
l1SimulationOverridesBuilder.forPendingCheckpoint(invalidateCheckpoint.forcePendingCheckpointNumber);
|
|
392
|
+
this.metrics.recordPipelineDepth(0);
|
|
393
|
+
} else {
|
|
394
|
+
this.metrics.recordPipelineDepth(0);
|
|
395
|
+
}
|
|
316
396
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
syncedTo.archive,
|
|
397
|
+
const simulationOverridesPlan = l1SimulationOverridesBuilder.build();
|
|
398
|
+
const canProposeCheck = await publisher.canProposeAt(
|
|
399
|
+
archiveForCheck,
|
|
321
400
|
proposer ?? EthAddress.ZERO,
|
|
322
|
-
|
|
401
|
+
simulationOverridesPlan,
|
|
323
402
|
);
|
|
324
403
|
|
|
325
404
|
if (canProposeCheck === undefined) {
|
|
@@ -328,17 +407,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
328
407
|
logCtx,
|
|
329
408
|
);
|
|
330
409
|
this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed', slot });
|
|
331
|
-
this.metrics.
|
|
410
|
+
this.metrics.recordCheckpointPrecheckFailed('rollup_contract_check_failed');
|
|
332
411
|
return undefined;
|
|
333
412
|
}
|
|
334
413
|
|
|
335
|
-
if (canProposeCheck.slot !==
|
|
414
|
+
if (canProposeCheck.slot !== targetSlot) {
|
|
336
415
|
this.log.warn(
|
|
337
|
-
`Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${
|
|
338
|
-
{ ...logCtx, rollup: canProposeCheck, expectedSlot:
|
|
416
|
+
`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}.`,
|
|
417
|
+
{ ...logCtx, rollup: canProposeCheck, expectedSlot: targetSlot },
|
|
339
418
|
);
|
|
340
419
|
this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
|
|
341
|
-
this.metrics.
|
|
420
|
+
this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
|
|
342
421
|
return undefined;
|
|
343
422
|
}
|
|
344
423
|
|
|
@@ -348,36 +427,53 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
348
427
|
{ ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
|
|
349
428
|
);
|
|
350
429
|
this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch', slot });
|
|
351
|
-
this.metrics.
|
|
430
|
+
this.metrics.recordCheckpointPrecheckFailed('block_number_mismatch');
|
|
352
431
|
return undefined;
|
|
353
432
|
}
|
|
354
433
|
|
|
355
|
-
this.lastSlotForCheckpointProposalJob =
|
|
356
|
-
|
|
434
|
+
this.lastSlotForCheckpointProposalJob = targetSlot;
|
|
435
|
+
|
|
436
|
+
await this.p2pClient.prepareForSlot(targetSlot);
|
|
437
|
+
this.log.info(
|
|
438
|
+
`Preparing checkpoint proposal ${checkpointNumber} for target slot ${targetSlot} during wall-clock slot ${slot}`,
|
|
439
|
+
{
|
|
440
|
+
...logCtx,
|
|
441
|
+
proposer,
|
|
442
|
+
pipeliningEnabled: this.epochCache.isProposerPipeliningEnabled(),
|
|
443
|
+
},
|
|
444
|
+
);
|
|
357
445
|
|
|
358
446
|
// Create and return the checkpoint proposal job
|
|
359
447
|
return this.createCheckpointProposalJob(
|
|
360
448
|
slot,
|
|
449
|
+
targetSlot,
|
|
450
|
+
targetEpoch,
|
|
361
451
|
checkpointNumber,
|
|
362
452
|
syncedTo.blockNumber,
|
|
363
453
|
proposer,
|
|
364
454
|
publisher,
|
|
365
455
|
attestorAddress,
|
|
366
456
|
invalidateCheckpoint,
|
|
457
|
+
syncedTo.proposedCheckpointData,
|
|
367
458
|
);
|
|
368
459
|
}
|
|
369
460
|
|
|
370
461
|
protected createCheckpointProposalJob(
|
|
371
462
|
slot: SlotNumber,
|
|
463
|
+
targetSlot: SlotNumber,
|
|
464
|
+
targetEpoch: EpochNumber,
|
|
372
465
|
checkpointNumber: CheckpointNumber,
|
|
373
466
|
syncedToBlockNumber: BlockNumber,
|
|
374
467
|
proposer: EthAddress | undefined,
|
|
375
468
|
publisher: SequencerPublisher,
|
|
376
469
|
attestorAddress: EthAddress,
|
|
377
470
|
invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
|
|
471
|
+
proposedCheckpointData?: ProposedCheckpointData,
|
|
378
472
|
): CheckpointProposalJob {
|
|
379
473
|
return new CheckpointProposalJob(
|
|
380
474
|
slot,
|
|
475
|
+
targetSlot,
|
|
476
|
+
targetEpoch,
|
|
381
477
|
checkpointNumber,
|
|
382
478
|
syncedToBlockNumber,
|
|
383
479
|
proposer,
|
|
@@ -389,6 +485,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
389
485
|
this.p2pClient,
|
|
390
486
|
this.worldState,
|
|
391
487
|
this.l1ToL2MessageSource,
|
|
488
|
+
this.l2BlockSource,
|
|
392
489
|
this.checkpointsBuilder,
|
|
393
490
|
this.l2BlockSource,
|
|
394
491
|
this.l1Constants,
|
|
@@ -398,13 +495,22 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
398
495
|
this.epochCache,
|
|
399
496
|
this.dateProvider,
|
|
400
497
|
this.metrics,
|
|
498
|
+
this.checkpointProposalJobMetrics.createRecorder(),
|
|
401
499
|
this,
|
|
402
500
|
this.setState.bind(this),
|
|
403
|
-
this.log,
|
|
404
501
|
this.tracer,
|
|
502
|
+
this.log.getBindings(),
|
|
503
|
+
proposedCheckpointData,
|
|
405
504
|
);
|
|
406
505
|
}
|
|
407
506
|
|
|
507
|
+
/**
|
|
508
|
+
* Returns the current sequencer state.
|
|
509
|
+
*/
|
|
510
|
+
public getState(): SequencerState {
|
|
511
|
+
return this.state;
|
|
512
|
+
}
|
|
513
|
+
|
|
408
514
|
/**
|
|
409
515
|
* Internal helper for setting the sequencer state and checks if we have enough time left in the slot to transition to the new state.
|
|
410
516
|
* @param proposedState - The new state to transition to.
|
|
@@ -451,16 +557,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
451
557
|
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
452
558
|
*/
|
|
453
559
|
protected async checkSync(args: { ts: bigint; slot: SlotNumber }): Promise<SequencerSyncCheckResult | undefined> {
|
|
454
|
-
// Check that the archiver
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
const
|
|
458
|
-
const { slot
|
|
459
|
-
if (
|
|
560
|
+
// Check that the archiver has fully synced the L2 slot before the one we want to propose in.
|
|
561
|
+
// The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
|
|
562
|
+
// See getSyncedL2SlotNumber for how missed L1 blocks are handled.
|
|
563
|
+
const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
|
|
564
|
+
const { slot } = args;
|
|
565
|
+
if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
|
|
460
566
|
this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
|
|
461
567
|
slot,
|
|
462
|
-
|
|
463
|
-
l1Timestamp,
|
|
568
|
+
syncedL2Slot,
|
|
464
569
|
});
|
|
465
570
|
return undefined;
|
|
466
571
|
}
|
|
@@ -470,25 +575,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
470
575
|
number: syncSummary.latestBlockNumber,
|
|
471
576
|
hash: syncSummary.latestBlockHash,
|
|
472
577
|
})),
|
|
473
|
-
this.l2BlockSource
|
|
578
|
+
this.l2BlockSource
|
|
579
|
+
.getL2Tips()
|
|
580
|
+
.then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
|
|
474
581
|
this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
|
|
475
|
-
this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
|
|
582
|
+
this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
|
|
476
583
|
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
584
|
+
this.l2BlockSource.getProposedCheckpointOnly(),
|
|
477
585
|
] as const);
|
|
478
586
|
|
|
479
|
-
const [worldState,
|
|
587
|
+
const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
|
|
588
|
+
syncedBlocks;
|
|
480
589
|
|
|
481
590
|
// 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,
|
|
482
591
|
// as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
|
|
483
592
|
// TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
|
|
484
593
|
const result =
|
|
485
|
-
(
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
594
|
+
(l2Tips.proposed.number === 0 &&
|
|
595
|
+
l2Tips.checkpointed.block.number === 0 &&
|
|
596
|
+
l2Tips.checkpointed.checkpoint.number === 0 &&
|
|
597
|
+
worldState.number === 0 &&
|
|
598
|
+
p2p.number === 0 &&
|
|
599
|
+
l1ToL2MessageSourceTips.proposed.number === 0 &&
|
|
600
|
+
l1ToL2MessageSourceTips.checkpointed.block.number === 0 &&
|
|
601
|
+
l1ToL2MessageSourceTips.checkpointed.checkpoint.number === 0) ||
|
|
602
|
+
(worldState.hash === l2Tips.proposed.hash &&
|
|
603
|
+
p2p.hash === l2Tips.proposed.hash &&
|
|
604
|
+
l1ToL2MessageSourceTips.proposed.hash === l2Tips.proposed.hash &&
|
|
605
|
+
l1ToL2MessageSourceTips.checkpointed.block.hash === l2Tips.checkpointed.block.hash &&
|
|
606
|
+
l1ToL2MessageSourceTips.checkpointed.checkpoint.hash === l2Tips.checkpointed.checkpoint.hash);
|
|
489
607
|
|
|
490
608
|
if (!result) {
|
|
491
|
-
this.log.debug(`Sequencer sync check failed`, {
|
|
609
|
+
this.log.debug(`Sequencer sync check failed`, {
|
|
610
|
+
worldState,
|
|
611
|
+
l2BlockSource: l2Tips.proposed,
|
|
612
|
+
p2p,
|
|
613
|
+
l1ToL2MessageSourceTips,
|
|
614
|
+
});
|
|
492
615
|
return undefined;
|
|
493
616
|
}
|
|
494
617
|
|
|
@@ -498,26 +621,33 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
498
621
|
const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
499
622
|
return {
|
|
500
623
|
checkpointNumber: CheckpointNumber.ZERO,
|
|
624
|
+
checkpointedCheckpointNumber: CheckpointNumber.ZERO,
|
|
501
625
|
blockNumber: BlockNumber.ZERO,
|
|
502
626
|
archive,
|
|
503
|
-
|
|
627
|
+
hasProposedCheckpoint: false,
|
|
628
|
+
syncedL2Slot,
|
|
504
629
|
pendingChainValidationStatus,
|
|
505
630
|
};
|
|
506
631
|
}
|
|
507
632
|
|
|
508
|
-
const
|
|
509
|
-
if (!
|
|
633
|
+
const blockData = await this.l2BlockSource.getBlockData(blockNumber);
|
|
634
|
+
if (!blockData) {
|
|
510
635
|
// this shouldn't really happen because a moment ago we checked that all components were in sync
|
|
511
|
-
this.log.error(`Failed to get L2 block ${blockNumber} from the archiver with all components in sync`);
|
|
636
|
+
this.log.error(`Failed to get L2 block data ${blockNumber} from the archiver with all components in sync`);
|
|
512
637
|
return undefined;
|
|
513
638
|
}
|
|
514
639
|
|
|
640
|
+
const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
|
|
641
|
+
|
|
515
642
|
return {
|
|
516
|
-
|
|
517
|
-
blockNumber:
|
|
518
|
-
checkpointNumber:
|
|
519
|
-
|
|
520
|
-
|
|
643
|
+
blockData,
|
|
644
|
+
blockNumber: blockData.header.getBlockNumber(),
|
|
645
|
+
checkpointNumber: blockData.checkpointNumber,
|
|
646
|
+
checkpointedCheckpointNumber: l2Tips.checkpointed.checkpoint.number,
|
|
647
|
+
archive: blockData.archive.root,
|
|
648
|
+
hasProposedCheckpoint,
|
|
649
|
+
proposedCheckpointData,
|
|
650
|
+
syncedL2Slot,
|
|
521
651
|
pendingChainValidationStatus,
|
|
522
652
|
};
|
|
523
653
|
}
|
|
@@ -526,17 +656,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
526
656
|
* Checks if we are the proposer for the next slot.
|
|
527
657
|
* @returns True if we can propose, and the proposer address (undefined if anyone can propose)
|
|
528
658
|
*/
|
|
529
|
-
protected async checkCanPropose(
|
|
659
|
+
protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
|
|
530
660
|
let proposer: EthAddress | undefined;
|
|
531
661
|
|
|
532
662
|
try {
|
|
533
|
-
proposer = await this.epochCache.getProposerAttesterAddressInSlot(
|
|
663
|
+
proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
|
|
534
664
|
} catch (e) {
|
|
535
665
|
if (e instanceof NoCommitteeError) {
|
|
536
|
-
this.
|
|
666
|
+
if (this.lastSlotForNoCommitteeWarning !== targetSlot) {
|
|
667
|
+
this.lastSlotForNoCommitteeWarning = targetSlot;
|
|
668
|
+
this.log.warn(`Cannot propose at target slot ${targetSlot} since the committee does not exist on L1`);
|
|
669
|
+
}
|
|
537
670
|
return [false, undefined];
|
|
538
671
|
}
|
|
539
|
-
this.log.error(`Error getting proposer for slot ${
|
|
672
|
+
this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
|
|
540
673
|
return [false, undefined];
|
|
541
674
|
}
|
|
542
675
|
|
|
@@ -553,10 +686,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
553
686
|
const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
|
|
554
687
|
|
|
555
688
|
if (!weAreProposer) {
|
|
556
|
-
this.log.debug(`Cannot propose at slot ${
|
|
689
|
+
this.log.debug(`Cannot propose at target slot ${targetSlot} since we are not a proposer`, {
|
|
690
|
+
targetSlot,
|
|
691
|
+
validatorAddresses,
|
|
692
|
+
proposer,
|
|
693
|
+
});
|
|
557
694
|
return [false, proposer];
|
|
558
695
|
}
|
|
559
696
|
|
|
697
|
+
this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
|
|
698
|
+
targetSlot,
|
|
699
|
+
proposer,
|
|
700
|
+
});
|
|
560
701
|
return [true, proposer];
|
|
561
702
|
}
|
|
562
703
|
|
|
@@ -565,11 +706,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
565
706
|
* This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
|
|
566
707
|
*/
|
|
567
708
|
@trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
|
|
568
|
-
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
|
|
569
|
-
const { slot } = args;
|
|
709
|
+
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
|
|
710
|
+
const { slot, targetSlot } = args;
|
|
570
711
|
|
|
571
712
|
// Prevent duplicate attempts in the same slot
|
|
572
|
-
if (this.
|
|
713
|
+
if (this.lastSlotForFallbackVote === slot) {
|
|
573
714
|
this.log.trace(`Already attempted to vote in slot ${slot} (skipping)`);
|
|
574
715
|
return;
|
|
575
716
|
}
|
|
@@ -594,14 +735,14 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
594
735
|
});
|
|
595
736
|
|
|
596
737
|
// Check if we're a proposer or proposal is open
|
|
597
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
738
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
598
739
|
if (!canPropose) {
|
|
599
740
|
this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
|
|
600
741
|
return;
|
|
601
742
|
}
|
|
602
743
|
|
|
603
744
|
// Mark this slot as attempted
|
|
604
|
-
this.
|
|
745
|
+
this.lastSlotForFallbackVote = slot;
|
|
605
746
|
|
|
606
747
|
// Get a publisher for voting
|
|
607
748
|
const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
|
|
@@ -611,9 +752,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
611
752
|
slot,
|
|
612
753
|
});
|
|
613
754
|
|
|
614
|
-
// Enqueue governance and slashing votes
|
|
755
|
+
// Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
|
|
615
756
|
const voter = new CheckpointVoter(
|
|
616
|
-
|
|
757
|
+
targetSlot,
|
|
617
758
|
publisher,
|
|
618
759
|
attestorAddress,
|
|
619
760
|
this.validatorClient,
|
|
@@ -636,7 +777,55 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
636
777
|
}
|
|
637
778
|
|
|
638
779
|
/**
|
|
639
|
-
*
|
|
780
|
+
* Tries to vote on slashing actions and governance proposals when escape hatch is open.
|
|
781
|
+
* This allows the sequencer to participate in voting without performing checkpoint proposal work.
|
|
782
|
+
*/
|
|
783
|
+
@trackSpan('Sequencer.tryVoteWhenEscapeHatchOpen', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
|
|
784
|
+
protected async tryVoteWhenEscapeHatchOpen(args: {
|
|
785
|
+
slot: SlotNumber;
|
|
786
|
+
proposer: EthAddress | undefined;
|
|
787
|
+
}): Promise<void> {
|
|
788
|
+
const { slot, proposer } = args;
|
|
789
|
+
|
|
790
|
+
// Prevent duplicate attempts in the same slot
|
|
791
|
+
if (this.lastSlotForFallbackVote === slot) {
|
|
792
|
+
this.log.trace(`Already attempted to vote in slot ${slot} (escape hatch open, skipping)`);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Mark this slot as attempted
|
|
797
|
+
this.lastSlotForFallbackVote = slot;
|
|
798
|
+
|
|
799
|
+
const { attestorAddress, publisher } = await this.publisherFactory.create(proposer);
|
|
800
|
+
|
|
801
|
+
this.log.debug(`Escape hatch open for slot ${slot}, attempting vote-only actions`, { slot, attestorAddress });
|
|
802
|
+
|
|
803
|
+
const voter = new CheckpointVoter(
|
|
804
|
+
slot,
|
|
805
|
+
publisher,
|
|
806
|
+
attestorAddress,
|
|
807
|
+
this.validatorClient,
|
|
808
|
+
this.slasherClient,
|
|
809
|
+
this.l1Constants,
|
|
810
|
+
this.config,
|
|
811
|
+
this.metrics,
|
|
812
|
+
this.log,
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
const votesPromises = voter.enqueueVotes();
|
|
816
|
+
const votes = await Promise.all(votesPromises);
|
|
817
|
+
|
|
818
|
+
if (votes.every(p => !p)) {
|
|
819
|
+
this.log.debug(`No votes to enqueue for slot ${slot} (escape hatch open)`);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
this.log.info(`Voting in slot ${slot} (escape hatch open)`, { slot });
|
|
824
|
+
await publisher.sendRequests();
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Considers invalidating a block if the pending chain is invalid. Depends on how long the invalid block
|
|
640
829
|
* has been there without being invalidated and whether the sequencer is in the committee or not. We always
|
|
641
830
|
* have the proposer try to invalidate, but if they fail, the sequencers in the committee are expected to try,
|
|
642
831
|
* and if they fail, any sequencer will try as well.
|
|
@@ -645,7 +834,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
645
834
|
syncedTo: SequencerSyncCheckResult,
|
|
646
835
|
currentSlot: SlotNumber,
|
|
647
836
|
): Promise<void> {
|
|
648
|
-
const { pendingChainValidationStatus,
|
|
837
|
+
const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
|
|
649
838
|
if (pendingChainValidationStatus.valid) {
|
|
650
839
|
return;
|
|
651
840
|
}
|
|
@@ -660,7 +849,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
660
849
|
|
|
661
850
|
const logData = {
|
|
662
851
|
invalidL1Timestamp: invalidCheckpointTimestamp,
|
|
663
|
-
|
|
852
|
+
syncedL2Slot,
|
|
664
853
|
invalidCheckpoint: pendingChainValidationStatus.checkpoint,
|
|
665
854
|
secondsBeforeInvalidatingBlockAsCommitteeMember,
|
|
666
855
|
secondsBeforeInvalidatingBlockAsNonCommitteeMember,
|
|
@@ -788,6 +977,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
788
977
|
return this.validatorClient?.getValidatorAddresses();
|
|
789
978
|
}
|
|
790
979
|
|
|
980
|
+
/** Updates the publisher factory's node keystore adapter after a keystore reload. */
|
|
981
|
+
public updatePublisherNodeKeyStore(adapter: NodeKeystoreAdapter): void {
|
|
982
|
+
this.publisherFactory.updateNodeKeyStore(adapter);
|
|
983
|
+
}
|
|
984
|
+
|
|
791
985
|
public getConfig() {
|
|
792
986
|
return this.config;
|
|
793
987
|
}
|
|
@@ -798,10 +992,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
798
992
|
}
|
|
799
993
|
|
|
800
994
|
type SequencerSyncCheckResult = {
|
|
801
|
-
|
|
995
|
+
blockData?: BlockData;
|
|
802
996
|
checkpointNumber: CheckpointNumber;
|
|
997
|
+
checkpointedCheckpointNumber: CheckpointNumber;
|
|
803
998
|
blockNumber: BlockNumber;
|
|
804
999
|
archive: Fr;
|
|
805
|
-
|
|
1000
|
+
hasProposedCheckpoint: boolean;
|
|
1001
|
+
proposedCheckpointData?: ProposedCheckpointData;
|
|
1002
|
+
syncedL2Slot: SlotNumber;
|
|
806
1003
|
pendingChainValidationStatus: ValidateCheckpointResult;
|
|
807
1004
|
};
|