@aztec/sequencer-client 0.0.1-commit.9372f48 → 0.0.1-commit.949a33fd8
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 +57 -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 +52 -10
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +718 -245
- 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 +54 -19
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +190 -99
- 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 +75 -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 +889 -265
- 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 +266 -113
- 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,11 +58,18 @@ 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;
|
|
62
69
|
|
|
70
|
+
/** The last slot for which we logged "no committee" warning, to avoid spam */
|
|
71
|
+
private lastSlotForNoCommitteeWarning: SlotNumber | undefined;
|
|
72
|
+
|
|
63
73
|
/** The last slot for which we triggered a checkpoint proposal job, to prevent duplicate attempts. */
|
|
64
74
|
private lastSlotForCheckpointProposalJob: SlotNumber | undefined;
|
|
65
75
|
|
|
@@ -69,19 +79,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
69
79
|
/** The last epoch for which we logged strategy comparison in fisherman mode. */
|
|
70
80
|
private lastEpochForStrategyComparison: EpochNumber | undefined;
|
|
71
81
|
|
|
82
|
+
/** The last checkpoint proposal job, tracked so we can await its pending L1 submission during shutdown. */
|
|
83
|
+
protected lastCheckpointProposalJob: CheckpointProposalJob | undefined;
|
|
84
|
+
|
|
72
85
|
/** The maximum number of seconds that the sequencer can be into a slot to transition to a particular state. */
|
|
73
86
|
protected timetable!: SequencerTimetable;
|
|
74
87
|
|
|
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
88
|
/** Config for the sequencer */
|
|
84
89
|
protected config: ResolvedSequencerConfig = DefaultSequencerConfig;
|
|
90
|
+
private readonly signatureContext: CoordinationSignatureContext;
|
|
85
91
|
|
|
86
92
|
constructor(
|
|
87
93
|
protected publisherFactory: SequencerPublisherFactory,
|
|
@@ -97,44 +103,51 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
97
103
|
protected dateProvider: DateProvider,
|
|
98
104
|
protected epochCache: EpochCache,
|
|
99
105
|
protected rollupContract: RollupContract,
|
|
100
|
-
config: SequencerConfig,
|
|
106
|
+
config: SequencerConfig & Pick<ChainConfig, 'l1ChainId' | 'l1Contracts'>,
|
|
101
107
|
protected telemetry: TelemetryClient = getTelemetryClient(),
|
|
102
108
|
protected log = createLogger('sequencer'),
|
|
103
109
|
) {
|
|
104
110
|
super();
|
|
111
|
+
this.stateLog = log.createChild('state');
|
|
105
112
|
|
|
106
113
|
// Add [FISHERMAN] prefix to logger if in fisherman mode
|
|
107
114
|
if (config.fishermanMode) {
|
|
108
115
|
this.log = log.createChild('[FISHERMAN]');
|
|
109
116
|
}
|
|
110
117
|
|
|
118
|
+
this.signatureContext = {
|
|
119
|
+
chainId: config.l1ChainId,
|
|
120
|
+
rollupAddress: config.l1Contracts.rollupAddress,
|
|
121
|
+
};
|
|
111
122
|
this.metrics = new SequencerMetrics(telemetry, this.rollupContract, 'Sequencer');
|
|
123
|
+
this.checkpointProposalJobMetrics = new CheckpointProposalJobMetrics(telemetry);
|
|
112
124
|
this.updateConfig(config);
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
/** Updates sequencer config by the defined values and updates the timetable */
|
|
116
128
|
public updateConfig(config: Partial<SequencerConfig>) {
|
|
117
129
|
const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
|
|
118
|
-
this.log.info(`Updated sequencer config`, omit(filteredConfig, '
|
|
130
|
+
this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
|
|
119
131
|
this.config = merge(this.config, filteredConfig);
|
|
132
|
+
const p2pPropagationTime = this.config.attestationPropagationTime;
|
|
120
133
|
this.timetable = new SequencerTimetable(
|
|
121
134
|
{
|
|
122
135
|
ethereumSlotDuration: this.l1Constants.ethereumSlotDuration,
|
|
123
136
|
aztecSlotDuration: this.aztecSlotDuration,
|
|
124
137
|
l1PublishingTime: this.l1PublishingTime,
|
|
125
|
-
p2pPropagationTime
|
|
138
|
+
p2pPropagationTime,
|
|
126
139
|
blockDurationMs: this.config.blockDurationMs,
|
|
127
140
|
enforce: this.config.enforceTimeTable,
|
|
141
|
+
pipelining: this.epochCache.isProposerPipeliningEnabled(),
|
|
128
142
|
},
|
|
129
143
|
this.metrics,
|
|
130
144
|
this.log,
|
|
131
145
|
);
|
|
132
146
|
}
|
|
133
147
|
|
|
134
|
-
/** Initializes the sequencer (precomputes tables
|
|
135
|
-
public
|
|
148
|
+
/** Initializes the sequencer (precomputes tables). Takes about 3s. */
|
|
149
|
+
public init() {
|
|
136
150
|
getKzg();
|
|
137
|
-
this.publisher = (await this.publisherFactory.create(undefined)).publisher;
|
|
138
151
|
}
|
|
139
152
|
|
|
140
153
|
/** Starts the sequencer and moves to IDLE state. */
|
|
@@ -149,12 +162,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
149
162
|
this.log.info('Started sequencer');
|
|
150
163
|
}
|
|
151
164
|
|
|
165
|
+
/** Triggers an immediate run of the sequencer, bypassing the polling interval. */
|
|
166
|
+
public trigger() {
|
|
167
|
+
return this.runningPromise?.trigger();
|
|
168
|
+
}
|
|
169
|
+
|
|
152
170
|
/** Stops the sequencer from building blocks and moves to STOPPED state. */
|
|
153
171
|
public async stop(): Promise<void> {
|
|
154
172
|
this.log.info(`Stopping sequencer`);
|
|
155
173
|
this.setState(SequencerState.STOPPING, undefined, { force: true });
|
|
156
|
-
this.
|
|
174
|
+
await this.publisherFactory.stopAll();
|
|
157
175
|
await this.runningPromise?.stop();
|
|
176
|
+
await this.lastCheckpointProposalJob?.awaitPendingSubmission();
|
|
158
177
|
this.setState(SequencerState.STOPPED, undefined, { force: true });
|
|
159
178
|
this.log.info('Stopped sequencer');
|
|
160
179
|
}
|
|
@@ -166,7 +185,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
166
185
|
} catch (err) {
|
|
167
186
|
this.emit('checkpoint-error', { error: err as Error });
|
|
168
187
|
if (err instanceof SequencerTooSlowError) {
|
|
169
|
-
// TODO(palla/mbps): Add missing states
|
|
170
188
|
// Log as warn only if we had to abort halfway through the block proposal
|
|
171
189
|
const logLvl = [SequencerState.INITIALIZING_CHECKPOINT, SequencerState.PROPOSER_CHECK].includes(
|
|
172
190
|
err.proposedState,
|
|
@@ -199,14 +217,25 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
199
217
|
@trackSpan('Sequencer.work')
|
|
200
218
|
protected async work() {
|
|
201
219
|
this.setState(SequencerState.SYNCHRONIZING, undefined);
|
|
202
|
-
const { slot, ts,
|
|
220
|
+
const { slot, ts, nowSeconds, epoch } = this.epochCache.getEpochAndSlotInNextL1Slot();
|
|
221
|
+
const { slot: targetSlot, epoch: targetEpoch } = this.epochCache.getTargetEpochAndSlotInNextL1Slot();
|
|
203
222
|
|
|
204
223
|
// Check if we are synced and it's our slot, grab a publisher, check previous block invalidation, etc
|
|
205
|
-
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
224
|
+
const checkpointProposalJob = await this.prepareCheckpointProposal(
|
|
225
|
+
slot,
|
|
226
|
+
targetSlot,
|
|
227
|
+
epoch,
|
|
228
|
+
targetEpoch,
|
|
229
|
+
ts,
|
|
230
|
+
nowSeconds,
|
|
231
|
+
);
|
|
206
232
|
if (!checkpointProposalJob) {
|
|
207
233
|
return;
|
|
208
234
|
}
|
|
209
235
|
|
|
236
|
+
// Track the job so we can await its pending L1 submission during shutdown
|
|
237
|
+
this.lastCheckpointProposalJob = checkpointProposalJob;
|
|
238
|
+
|
|
210
239
|
// Execute the checkpoint proposal job
|
|
211
240
|
const checkpoint = await checkpointProposalJob.execute();
|
|
212
241
|
|
|
@@ -215,13 +244,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
215
244
|
this.lastCheckpointProposed = checkpoint;
|
|
216
245
|
}
|
|
217
246
|
|
|
218
|
-
// 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)
|
|
219
248
|
if (
|
|
220
249
|
this.config.fishermanMode &&
|
|
221
|
-
(this.lastEpochForStrategyComparison === undefined ||
|
|
250
|
+
(this.lastEpochForStrategyComparison === undefined || targetEpoch > this.lastEpochForStrategyComparison)
|
|
222
251
|
) {
|
|
223
|
-
this.logStrategyComparison(
|
|
224
|
-
this.lastEpochForStrategyComparison =
|
|
252
|
+
this.logStrategyComparison(targetEpoch, checkpointProposalJob.getPublisher());
|
|
253
|
+
this.lastEpochForStrategyComparison = targetEpoch;
|
|
225
254
|
}
|
|
226
255
|
|
|
227
256
|
return checkpoint;
|
|
@@ -233,44 +262,49 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
233
262
|
* @returns CheckpointProposalJob if successful, undefined if we are not yet synced or are not the proposer.
|
|
234
263
|
*/
|
|
235
264
|
@trackSpan('Sequencer.prepareCheckpointProposal')
|
|
236
|
-
|
|
237
|
-
epoch: EpochNumber,
|
|
265
|
+
protected async prepareCheckpointProposal(
|
|
238
266
|
slot: SlotNumber,
|
|
267
|
+
targetSlot: SlotNumber,
|
|
268
|
+
epoch: EpochNumber,
|
|
269
|
+
targetEpoch: EpochNumber,
|
|
239
270
|
ts: bigint,
|
|
240
|
-
|
|
271
|
+
nowSeconds: bigint,
|
|
241
272
|
): Promise<CheckpointProposalJob | undefined> {
|
|
242
|
-
// Check we have not already processed this slot (cheapest check)
|
|
273
|
+
// Check we have not already processed this target slot (cheapest check)
|
|
243
274
|
// We only check this if enforce timetable is set, since we want to keep processing the same slot if we are not
|
|
244
275
|
// running against actual time (eg when we use sandbox-style automining)
|
|
245
276
|
if (
|
|
246
277
|
this.lastSlotForCheckpointProposalJob &&
|
|
247
|
-
this.lastSlotForCheckpointProposalJob >=
|
|
278
|
+
this.lastSlotForCheckpointProposalJob >= targetSlot &&
|
|
248
279
|
this.config.enforceTimeTable
|
|
249
280
|
) {
|
|
250
|
-
this.log.trace(`
|
|
281
|
+
this.log.trace(`Target slot ${targetSlot} has already been processed`);
|
|
251
282
|
return undefined;
|
|
252
283
|
}
|
|
253
284
|
|
|
254
|
-
// But if we have already proposed for this slot,
|
|
255
|
-
if (this.lastCheckpointProposed && this.lastCheckpointProposed.header.slotNumber >=
|
|
256
|
-
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
|
+
);
|
|
257
290
|
return undefined;
|
|
258
291
|
}
|
|
259
292
|
|
|
260
293
|
// Check all components are synced to latest as seen by the archiver (queries all subsystems)
|
|
261
294
|
const syncedTo = await this.checkSync({ ts, slot });
|
|
262
295
|
if (!syncedTo) {
|
|
263
|
-
await this.tryVoteWhenSyncFails({ slot, ts });
|
|
296
|
+
await this.tryVoteWhenSyncFails({ slot, targetSlot, ts });
|
|
264
297
|
return undefined;
|
|
265
298
|
}
|
|
266
299
|
|
|
267
|
-
// 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.
|
|
268
301
|
// Still perform governance/slashing voting (as proposer) once per slot.
|
|
269
|
-
|
|
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);
|
|
270
304
|
|
|
271
305
|
if (isEscapeHatchOpen) {
|
|
272
306
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
273
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
307
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
274
308
|
if (canPropose) {
|
|
275
309
|
await this.tryVoteWhenEscapeHatchOpen({ slot, proposer });
|
|
276
310
|
} else {
|
|
@@ -287,18 +321,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
287
321
|
const checkpointNumber = CheckpointNumber(syncedTo.checkpointNumber + 1);
|
|
288
322
|
|
|
289
323
|
const logCtx = {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
|
|
324
|
+
nowSeconds,
|
|
325
|
+
syncedToL2Slot: syncedTo.syncedL2Slot,
|
|
293
326
|
slot,
|
|
327
|
+
targetSlot,
|
|
294
328
|
slotTs: ts,
|
|
295
329
|
checkpointNumber,
|
|
296
330
|
isPendingChainValid: pick(syncedTo.pendingChainValidationStatus, 'valid', 'reason', 'invalidIndex'),
|
|
297
331
|
};
|
|
298
332
|
|
|
299
|
-
// Check that we are a proposer for the
|
|
333
|
+
// Check that we are a proposer for the target slot.
|
|
300
334
|
this.setState(SequencerState.PROPOSER_CHECK, slot);
|
|
301
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
335
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
302
336
|
|
|
303
337
|
// If we are not a proposer check if we should invalidate an invalid checkpoint, and bail
|
|
304
338
|
if (!canPropose) {
|
|
@@ -306,13 +340,23 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
306
340
|
return undefined;
|
|
307
341
|
}
|
|
308
342
|
|
|
309
|
-
//
|
|
310
|
-
|
|
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) {
|
|
311
355
|
this.log.warn(
|
|
312
|
-
`Cannot propose block at
|
|
313
|
-
{ ...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() },
|
|
314
358
|
);
|
|
315
|
-
this.metrics.
|
|
359
|
+
this.metrics.recordCheckpointPrecheckFailed('slot_already_taken');
|
|
316
360
|
return undefined;
|
|
317
361
|
}
|
|
318
362
|
|
|
@@ -323,7 +367,6 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
323
367
|
const proposerForPublisher = this.config.fishermanMode ? undefined : proposer;
|
|
324
368
|
const { attestorAddress, publisher } = await this.publisherFactory.create(proposerForPublisher);
|
|
325
369
|
this.log.verbose(`Created publisher at address ${publisher.getSenderAddress()} for attestor ${attestorAddress}`);
|
|
326
|
-
this.publisher = publisher;
|
|
327
370
|
|
|
328
371
|
// In fisherman mode, set the actual proposer's address for simulations
|
|
329
372
|
if (this.config.fishermanMode && proposer) {
|
|
@@ -332,14 +375,41 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
332
375
|
}
|
|
333
376
|
|
|
334
377
|
// Prepare invalidation request if the pending chain is invalid (returns undefined if no need)
|
|
335
|
-
|
|
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
|
+
}
|
|
336
407
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
syncedTo.archive,
|
|
408
|
+
const simulationOverridesPlan = l1SimulationOverridesBuilder.build();
|
|
409
|
+
const canProposeCheck = await publisher.canProposeAt(
|
|
410
|
+
archiveForCheck,
|
|
341
411
|
proposer ?? EthAddress.ZERO,
|
|
342
|
-
|
|
412
|
+
simulationOverridesPlan,
|
|
343
413
|
);
|
|
344
414
|
|
|
345
415
|
if (canProposeCheck === undefined) {
|
|
@@ -348,17 +418,17 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
348
418
|
logCtx,
|
|
349
419
|
);
|
|
350
420
|
this.emit('proposer-rollup-check-failed', { reason: 'Rollup contract check failed', slot });
|
|
351
|
-
this.metrics.
|
|
421
|
+
this.metrics.recordCheckpointPrecheckFailed('rollup_contract_check_failed');
|
|
352
422
|
return undefined;
|
|
353
423
|
}
|
|
354
424
|
|
|
355
|
-
if (canProposeCheck.slot !==
|
|
425
|
+
if (canProposeCheck.slot !== targetSlot) {
|
|
356
426
|
this.log.warn(
|
|
357
|
-
`Cannot propose block due to slot mismatch with rollup contract (this can be caused by a clock out of sync). Expected slot ${
|
|
358
|
-
{ ...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 },
|
|
359
429
|
);
|
|
360
430
|
this.emit('proposer-rollup-check-failed', { reason: 'Slot mismatch', slot });
|
|
361
|
-
this.metrics.
|
|
431
|
+
this.metrics.recordCheckpointPrecheckFailed('slot_mismatch');
|
|
362
432
|
return undefined;
|
|
363
433
|
}
|
|
364
434
|
|
|
@@ -368,39 +438,53 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
368
438
|
{ ...logCtx, rollup: canProposeCheck, expectedSlot: slot },
|
|
369
439
|
);
|
|
370
440
|
this.emit('proposer-rollup-check-failed', { reason: 'Block mismatch', slot });
|
|
371
|
-
this.metrics.
|
|
441
|
+
this.metrics.recordCheckpointPrecheckFailed('block_number_mismatch');
|
|
372
442
|
return undefined;
|
|
373
443
|
}
|
|
374
444
|
|
|
375
|
-
this.lastSlotForCheckpointProposalJob =
|
|
376
|
-
|
|
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
|
+
);
|
|
377
456
|
|
|
378
457
|
// Create and return the checkpoint proposal job
|
|
379
458
|
return this.createCheckpointProposalJob(
|
|
380
|
-
epoch,
|
|
381
459
|
slot,
|
|
460
|
+
targetSlot,
|
|
461
|
+
targetEpoch,
|
|
382
462
|
checkpointNumber,
|
|
383
463
|
syncedTo.blockNumber,
|
|
384
464
|
proposer,
|
|
385
465
|
publisher,
|
|
386
466
|
attestorAddress,
|
|
387
467
|
invalidateCheckpoint,
|
|
468
|
+
syncedTo.proposedCheckpointData,
|
|
388
469
|
);
|
|
389
470
|
}
|
|
390
471
|
|
|
391
472
|
protected createCheckpointProposalJob(
|
|
392
|
-
epoch: EpochNumber,
|
|
393
473
|
slot: SlotNumber,
|
|
474
|
+
targetSlot: SlotNumber,
|
|
475
|
+
targetEpoch: EpochNumber,
|
|
394
476
|
checkpointNumber: CheckpointNumber,
|
|
395
477
|
syncedToBlockNumber: BlockNumber,
|
|
396
478
|
proposer: EthAddress | undefined,
|
|
397
479
|
publisher: SequencerPublisher,
|
|
398
480
|
attestorAddress: EthAddress,
|
|
399
481
|
invalidateCheckpoint: InvalidateCheckpointRequest | undefined,
|
|
482
|
+
proposedCheckpointData?: ProposedCheckpointData,
|
|
400
483
|
): CheckpointProposalJob {
|
|
401
484
|
return new CheckpointProposalJob(
|
|
402
|
-
epoch,
|
|
403
485
|
slot,
|
|
486
|
+
targetSlot,
|
|
487
|
+
targetEpoch,
|
|
404
488
|
checkpointNumber,
|
|
405
489
|
syncedToBlockNumber,
|
|
406
490
|
proposer,
|
|
@@ -416,19 +500,29 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
416
500
|
this.checkpointsBuilder,
|
|
417
501
|
this.l2BlockSource,
|
|
418
502
|
this.l1Constants,
|
|
503
|
+
this.signatureContext,
|
|
419
504
|
this.config,
|
|
420
505
|
this.timetable,
|
|
421
506
|
this.slasherClient,
|
|
422
507
|
this.epochCache,
|
|
423
508
|
this.dateProvider,
|
|
424
509
|
this.metrics,
|
|
510
|
+
this.checkpointProposalJobMetrics.createRecorder(),
|
|
425
511
|
this,
|
|
426
512
|
this.setState.bind(this),
|
|
427
513
|
this.tracer,
|
|
428
514
|
this.log.getBindings(),
|
|
515
|
+
proposedCheckpointData,
|
|
429
516
|
);
|
|
430
517
|
}
|
|
431
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Returns the current sequencer state.
|
|
521
|
+
*/
|
|
522
|
+
public getState(): SequencerState {
|
|
523
|
+
return this.state;
|
|
524
|
+
}
|
|
525
|
+
|
|
432
526
|
/**
|
|
433
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.
|
|
434
528
|
* @param proposedState - The new state to transition to.
|
|
@@ -454,19 +548,35 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
454
548
|
this.timetable.assertTimeLeft(proposedState, secondsIntoSlot);
|
|
455
549
|
}
|
|
456
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
|
+
|
|
457
557
|
const boringStates = [SequencerState.IDLE, SequencerState.SYNCHRONIZING];
|
|
458
558
|
const logLevel =
|
|
459
|
-
boringStates.includes(proposedState) && boringStates.includes(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
+
});
|
|
463
568
|
|
|
464
569
|
this.emit('state-changed', {
|
|
465
|
-
oldState
|
|
570
|
+
oldState,
|
|
466
571
|
newState: proposedState,
|
|
467
572
|
secondsIntoSlot,
|
|
468
573
|
slot: slotNumber,
|
|
469
574
|
});
|
|
575
|
+
if (stateChanged) {
|
|
576
|
+
this.metrics.recordStateDuration(stateDurationMs, oldState);
|
|
577
|
+
this.stateEnteredAtMs = transitionAtMs;
|
|
578
|
+
this.stateSlotNumber = slotNumber;
|
|
579
|
+
}
|
|
470
580
|
this.state = proposedState;
|
|
471
581
|
}
|
|
472
582
|
|
|
@@ -475,16 +585,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
475
585
|
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
476
586
|
*/
|
|
477
587
|
protected async checkSync(args: { ts: bigint; slot: SlotNumber }): Promise<SequencerSyncCheckResult | undefined> {
|
|
478
|
-
// Check that the archiver
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
const
|
|
482
|
-
const { slot
|
|
483
|
-
if (
|
|
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) {
|
|
484
594
|
this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
|
|
485
595
|
slot,
|
|
486
|
-
|
|
487
|
-
l1Timestamp,
|
|
596
|
+
syncedL2Slot,
|
|
488
597
|
});
|
|
489
598
|
return undefined;
|
|
490
599
|
}
|
|
@@ -494,25 +603,43 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
494
603
|
number: syncSummary.latestBlockNumber,
|
|
495
604
|
hash: syncSummary.latestBlockHash,
|
|
496
605
|
})),
|
|
497
|
-
this.l2BlockSource
|
|
606
|
+
this.l2BlockSource
|
|
607
|
+
.getL2Tips()
|
|
608
|
+
.then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed, proposedCheckpoint: t.proposedCheckpoint })),
|
|
498
609
|
this.p2pClient.getStatus().then(p2p => p2p.syncedToL2Block),
|
|
499
|
-
this.l1ToL2MessageSource.getL2Tips().then(t => t.proposed),
|
|
610
|
+
this.l1ToL2MessageSource.getL2Tips().then(t => ({ proposed: t.proposed, checkpointed: t.checkpointed })),
|
|
500
611
|
this.l2BlockSource.getPendingChainValidationStatus(),
|
|
612
|
+
this.l2BlockSource.getLastProposedCheckpoint(),
|
|
501
613
|
] as const);
|
|
502
614
|
|
|
503
|
-
const [worldState,
|
|
615
|
+
const [worldState, l2Tips, p2p, l1ToL2MessageSourceTips, pendingChainValidationStatus, proposedCheckpointData] =
|
|
616
|
+
syncedBlocks;
|
|
504
617
|
|
|
505
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,
|
|
506
619
|
// as the world state can compute the new genesis block hash, but other components use the hardcoded constant.
|
|
507
620
|
// TODO(palla/mbps): Fix the above. All components should be able to handle dynamic genesis block hashes.
|
|
508
621
|
const result =
|
|
509
|
-
(
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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);
|
|
513
635
|
|
|
514
636
|
if (!result) {
|
|
515
|
-
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
|
+
});
|
|
516
643
|
return undefined;
|
|
517
644
|
}
|
|
518
645
|
|
|
@@ -522,26 +649,33 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
522
649
|
const archive = new Fr((await this.worldState.getCommitted().getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
523
650
|
return {
|
|
524
651
|
checkpointNumber: CheckpointNumber.ZERO,
|
|
652
|
+
checkpointedCheckpointNumber: CheckpointNumber.ZERO,
|
|
525
653
|
blockNumber: BlockNumber.ZERO,
|
|
526
654
|
archive,
|
|
527
|
-
|
|
655
|
+
hasProposedCheckpoint: false,
|
|
656
|
+
syncedL2Slot,
|
|
528
657
|
pendingChainValidationStatus,
|
|
529
658
|
};
|
|
530
659
|
}
|
|
531
660
|
|
|
532
|
-
const
|
|
533
|
-
if (!
|
|
661
|
+
const blockData = await this.l2BlockSource.getBlockData(blockNumber);
|
|
662
|
+
if (!blockData) {
|
|
534
663
|
// this shouldn't really happen because a moment ago we checked that all components were in sync
|
|
535
|
-
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`);
|
|
536
665
|
return undefined;
|
|
537
666
|
}
|
|
538
667
|
|
|
668
|
+
const hasProposedCheckpoint = l2Tips.proposedCheckpoint.checkpoint.number > l2Tips.checkpointed.checkpoint.number;
|
|
669
|
+
|
|
539
670
|
return {
|
|
540
|
-
|
|
541
|
-
blockNumber:
|
|
542
|
-
checkpointNumber:
|
|
543
|
-
|
|
544
|
-
|
|
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,
|
|
545
679
|
pendingChainValidationStatus,
|
|
546
680
|
};
|
|
547
681
|
}
|
|
@@ -550,17 +684,20 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
550
684
|
* Checks if we are the proposer for the next slot.
|
|
551
685
|
* @returns True if we can propose, and the proposer address (undefined if anyone can propose)
|
|
552
686
|
*/
|
|
553
|
-
protected async checkCanPropose(
|
|
687
|
+
protected async checkCanPropose(targetSlot: SlotNumber): Promise<[boolean, EthAddress | undefined]> {
|
|
554
688
|
let proposer: EthAddress | undefined;
|
|
555
689
|
|
|
556
690
|
try {
|
|
557
|
-
proposer = await this.epochCache.getProposerAttesterAddressInSlot(
|
|
691
|
+
proposer = await this.epochCache.getProposerAttesterAddressInSlot(targetSlot);
|
|
558
692
|
} catch (e) {
|
|
559
693
|
if (e instanceof NoCommitteeError) {
|
|
560
|
-
this.
|
|
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`);
|
|
697
|
+
}
|
|
561
698
|
return [false, undefined];
|
|
562
699
|
}
|
|
563
|
-
this.log.error(`Error getting proposer for slot ${
|
|
700
|
+
this.log.error(`Error getting proposer for target slot ${targetSlot}`, e);
|
|
564
701
|
return [false, undefined];
|
|
565
702
|
}
|
|
566
703
|
|
|
@@ -577,10 +714,18 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
577
714
|
const weAreProposer = validatorAddresses.some(addr => addr.equals(proposer));
|
|
578
715
|
|
|
579
716
|
if (!weAreProposer) {
|
|
580
|
-
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
|
+
});
|
|
581
722
|
return [false, proposer];
|
|
582
723
|
}
|
|
583
724
|
|
|
725
|
+
this.log.info(`We are the proposer for pipeline slot ${targetSlot}`, {
|
|
726
|
+
targetSlot,
|
|
727
|
+
proposer,
|
|
728
|
+
});
|
|
584
729
|
return [true, proposer];
|
|
585
730
|
}
|
|
586
731
|
|
|
@@ -589,8 +734,8 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
589
734
|
* This allows the sequencer to participate in governance/slashing votes even when it cannot build blocks.
|
|
590
735
|
*/
|
|
591
736
|
@trackSpan('Seqeuencer.tryVoteWhenSyncFails', ({ slot }) => ({ [Attributes.SLOT_NUMBER]: slot }))
|
|
592
|
-
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; ts: bigint }): Promise<void> {
|
|
593
|
-
const { slot } = args;
|
|
737
|
+
protected async tryVoteWhenSyncFails(args: { slot: SlotNumber; targetSlot: SlotNumber; ts: bigint }): Promise<void> {
|
|
738
|
+
const { slot, targetSlot } = args;
|
|
594
739
|
|
|
595
740
|
// Prevent duplicate attempts in the same slot
|
|
596
741
|
if (this.lastSlotForFallbackVote === slot) {
|
|
@@ -618,7 +763,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
618
763
|
});
|
|
619
764
|
|
|
620
765
|
// Check if we're a proposer or proposal is open
|
|
621
|
-
const [canPropose, proposer] = await this.checkCanPropose(
|
|
766
|
+
const [canPropose, proposer] = await this.checkCanPropose(targetSlot);
|
|
622
767
|
if (!canPropose) {
|
|
623
768
|
this.log.trace(`Cannot vote in slot ${slot} since we are not a proposer`, { slot, proposer });
|
|
624
769
|
return;
|
|
@@ -635,9 +780,9 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
635
780
|
slot,
|
|
636
781
|
});
|
|
637
782
|
|
|
638
|
-
// Enqueue governance and slashing votes
|
|
783
|
+
// Enqueue governance and slashing votes (voter uses the target slot for L1 submission)
|
|
639
784
|
const voter = new CheckpointVoter(
|
|
640
|
-
|
|
785
|
+
targetSlot,
|
|
641
786
|
publisher,
|
|
642
787
|
attestorAddress,
|
|
643
788
|
this.validatorClient,
|
|
@@ -717,7 +862,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
717
862
|
syncedTo: SequencerSyncCheckResult,
|
|
718
863
|
currentSlot: SlotNumber,
|
|
719
864
|
): Promise<void> {
|
|
720
|
-
const { pendingChainValidationStatus,
|
|
865
|
+
const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
|
|
721
866
|
if (pendingChainValidationStatus.valid) {
|
|
722
867
|
return;
|
|
723
868
|
}
|
|
@@ -732,7 +877,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
732
877
|
|
|
733
878
|
const logData = {
|
|
734
879
|
invalidL1Timestamp: invalidCheckpointTimestamp,
|
|
735
|
-
|
|
880
|
+
syncedL2Slot,
|
|
736
881
|
invalidCheckpoint: pendingChainValidationStatus.checkpoint,
|
|
737
882
|
secondsBeforeInvalidatingBlockAsCommitteeMember,
|
|
738
883
|
secondsBeforeInvalidatingBlockAsNonCommitteeMember,
|
|
@@ -860,6 +1005,11 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
860
1005
|
return this.validatorClient?.getValidatorAddresses();
|
|
861
1006
|
}
|
|
862
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
|
+
|
|
863
1013
|
public getConfig() {
|
|
864
1014
|
return this.config;
|
|
865
1015
|
}
|
|
@@ -870,10 +1020,13 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
870
1020
|
}
|
|
871
1021
|
|
|
872
1022
|
type SequencerSyncCheckResult = {
|
|
873
|
-
|
|
1023
|
+
blockData?: BlockData;
|
|
874
1024
|
checkpointNumber: CheckpointNumber;
|
|
1025
|
+
checkpointedCheckpointNumber: CheckpointNumber;
|
|
875
1026
|
blockNumber: BlockNumber;
|
|
876
1027
|
archive: Fr;
|
|
877
|
-
|
|
1028
|
+
hasProposedCheckpoint: boolean;
|
|
1029
|
+
proposedCheckpointData?: ProposedCheckpointData;
|
|
1030
|
+
syncedL2Slot: SlotNumber;
|
|
878
1031
|
pendingChainValidationStatus: ValidateCheckpointResult;
|
|
879
1032
|
};
|