@aztec/sequencer-client 0.0.1-commit.b2a5d0dd1 → 0.0.1-commit.b3d3157a
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +281 -21
- package/dest/client/sequencer-client.d.ts +3 -3
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +6 -6
- package/dest/config.d.ts +6 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +33 -10
- package/dest/global_variable_builder/fee_provider.js +1 -1
- package/dest/global_variable_builder/global_builder.d.ts +2 -3
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +1 -1
- 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 +3 -3
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/sequencer-bundle-simulator.d.ts +96 -0
- package/dest/publisher/sequencer-bundle-simulator.d.ts.map +1 -0
- package/dest/publisher/sequencer-bundle-simulator.js +198 -0
- package/dest/publisher/sequencer-publisher.d.ts +36 -36
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +271 -500
- package/dest/sequencer/automine/automine_factory.d.ts +54 -0
- package/dest/sequencer/automine/automine_factory.d.ts.map +1 -0
- package/dest/sequencer/automine/automine_factory.js +84 -0
- package/dest/sequencer/automine/automine_sequencer.d.ts +151 -0
- package/dest/sequencer/automine/automine_sequencer.d.ts.map +1 -0
- package/dest/sequencer/automine/automine_sequencer.js +509 -0
- package/dest/sequencer/chain_state_overrides.d.ts +52 -16
- package/dest/sequencer/chain_state_overrides.d.ts.map +1 -1
- package/dest/sequencer/chain_state_overrides.js +86 -27
- package/dest/sequencer/checkpoint_proposal_job.d.ts +24 -6
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +373 -83
- package/dest/sequencer/events.d.ts +42 -2
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/index.d.ts +3 -1
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +2 -0
- package/dest/sequencer/metrics.d.ts +3 -1
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +7 -0
- package/dest/sequencer/sequencer.d.ts +44 -9
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +256 -79
- package/dest/sequencer/timetable.d.ts +3 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +3 -7
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/utils.d.ts +1 -1
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +7 -6
- package/package.json +27 -27
- package/src/client/sequencer-client.ts +12 -7
- package/src/config.ts +38 -10
- package/src/global_variable_builder/fee_provider.ts +1 -1
- package/src/global_variable_builder/global_builder.ts +2 -3
- package/src/index.ts +10 -1
- package/src/publisher/config.ts +2 -2
- package/src/publisher/sequencer-bundle-simulator.ts +254 -0
- package/src/publisher/sequencer-publisher.ts +286 -521
- package/src/sequencer/README.md +162 -520
- package/src/sequencer/automine/README.md +46 -0
- package/src/sequencer/automine/automine_factory.ts +145 -0
- package/src/sequencer/automine/automine_sequencer.ts +595 -0
- package/src/sequencer/chain_state_overrides.ts +133 -58
- package/src/sequencer/checkpoint_proposal_job.ts +420 -100
- package/src/sequencer/events.ts +46 -2
- package/src/sequencer/index.ts +2 -0
- package/src/sequencer/metrics.ts +9 -0
- package/src/sequencer/sequencer.ts +284 -95
- package/src/sequencer/timetable.ts +2 -9
- package/src/sequencer/types.ts +1 -1
- package/src/test/utils.ts +28 -10
|
@@ -14,12 +14,12 @@ import {
|
|
|
14
14
|
generateUnrecoverableSignature,
|
|
15
15
|
} from '@aztec/foundation/crypto/secp256k1-signer';
|
|
16
16
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
17
|
+
import { InterruptError, TimeoutError } from '@aztec/foundation/error';
|
|
17
18
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
18
19
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
19
20
|
import { filter } from '@aztec/foundation/iterator';
|
|
20
21
|
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
21
|
-
import {
|
|
22
|
-
import { sleep, sleepUntil } from '@aztec/foundation/sleep';
|
|
22
|
+
import { InterruptibleSleep } from '@aztec/foundation/sleep';
|
|
23
23
|
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
24
24
|
import { type TypedEventEmitter, isErrorClass, unfreeze } from '@aztec/foundation/types';
|
|
25
25
|
import type { P2P } from '@aztec/p2p';
|
|
@@ -31,9 +31,15 @@ import {
|
|
|
31
31
|
type L2BlockSink,
|
|
32
32
|
type L2BlockSource,
|
|
33
33
|
MaliciousCommitteeAttestationsAndSigners,
|
|
34
|
+
type ProposedCheckpointSink,
|
|
34
35
|
type ValidateCheckpointResult,
|
|
35
36
|
} from '@aztec/stdlib/block';
|
|
36
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
type Checkpoint,
|
|
39
|
+
type ProposedCheckpointData,
|
|
40
|
+
getPreviousCheckpointOutHashes,
|
|
41
|
+
validateCheckpoint,
|
|
42
|
+
} from '@aztec/stdlib/checkpoint';
|
|
37
43
|
import { computeQuorum, getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
38
44
|
import { Gas } from '@aztec/stdlib/gas';
|
|
39
45
|
import {
|
|
@@ -46,8 +52,10 @@ import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@azte
|
|
|
46
52
|
import type {
|
|
47
53
|
BlockProposal,
|
|
48
54
|
BlockProposalOptions,
|
|
55
|
+
CheckpointAttestation,
|
|
49
56
|
CheckpointProposal,
|
|
50
57
|
CheckpointProposalOptions,
|
|
58
|
+
CoordinationSignatureContext,
|
|
51
59
|
} from '@aztec/stdlib/p2p';
|
|
52
60
|
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
53
61
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
@@ -59,10 +67,7 @@ import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validato
|
|
|
59
67
|
|
|
60
68
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
61
69
|
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
62
|
-
import {
|
|
63
|
-
buildPipelinedParentSimulationOverridesPlan,
|
|
64
|
-
buildSubmissionSimulationOverridesPlan,
|
|
65
|
-
} from './chain_state_overrides.js';
|
|
70
|
+
import { buildCheckpointSimulationOverridesPlan } from './chain_state_overrides.js';
|
|
66
71
|
import type { CheckpointProposalJobMetricsRecorder } from './checkpoint_proposal_job_metrics.js';
|
|
67
72
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
68
73
|
import { SequencerInterruptedError } from './errors.js';
|
|
@@ -74,6 +79,7 @@ import { SequencerState } from './utils.js';
|
|
|
74
79
|
|
|
75
80
|
/** How much time to sleep while waiting for min transactions to accumulate for a block */
|
|
76
81
|
const TXS_POLLING_MS = 500;
|
|
82
|
+
const ARCHIVER_SYNC_POLLING_MS = 200;
|
|
77
83
|
|
|
78
84
|
/** Result from proposeCheckpoint when a checkpoint was successfully built and broadcast. */
|
|
79
85
|
type CheckpointProposalBroadcast = {
|
|
@@ -97,12 +103,24 @@ type CheckpointProposalResult = {
|
|
|
97
103
|
*/
|
|
98
104
|
export class CheckpointProposalJob implements Traceable {
|
|
99
105
|
protected readonly log: Logger;
|
|
106
|
+
private readonly checkpointEventLog: Logger;
|
|
100
107
|
|
|
101
108
|
/** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */
|
|
102
109
|
private pendingL1Submission: Promise<void> | undefined;
|
|
110
|
+
private readonly interruptibleSleep = new InterruptibleSleep();
|
|
111
|
+
private interrupted = false;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Chain state overrides built once per slot in proposeCheckpoint after the checkpoint is
|
|
115
|
+
* complete. Carries the pending parent override (archive + slot + fee header) for pipelining,
|
|
116
|
+
* or the invalidation pending override when rolling back. Consumed by
|
|
117
|
+
* publisher.validateBlockHeader before broadcast.
|
|
118
|
+
*/
|
|
119
|
+
private checkpointSimulationOverridesPlan?: SimulationOverridesPlan;
|
|
103
120
|
|
|
104
|
-
|
|
105
|
-
|
|
121
|
+
private getSignatureContext(): CoordinationSignatureContext {
|
|
122
|
+
return this.signatureContext;
|
|
123
|
+
}
|
|
106
124
|
|
|
107
125
|
constructor(
|
|
108
126
|
private readonly slotNow: SlotNumber,
|
|
@@ -110,6 +128,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
110
128
|
private readonly targetEpoch: EpochNumber,
|
|
111
129
|
private readonly checkpointNumber: CheckpointNumber,
|
|
112
130
|
private readonly syncedToBlockNumber: BlockNumber,
|
|
131
|
+
private readonly checkpointedCheckpointNumber: CheckpointNumber,
|
|
113
132
|
// TODO(palla/mbps): Can we remove the proposer in favor of attestorAddress? Need to check fisherman-node flows.
|
|
114
133
|
private readonly proposer: EthAddress | undefined,
|
|
115
134
|
private readonly publisher: SequencerPublisher,
|
|
@@ -122,8 +141,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
122
141
|
private readonly l1ToL2MessageSource: L1ToL2MessageSource,
|
|
123
142
|
private readonly l2BlockSource: L2BlockSource,
|
|
124
143
|
private readonly checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
125
|
-
private readonly blockSink: L2BlockSink,
|
|
144
|
+
private readonly blockSink: L2BlockSink & ProposedCheckpointSink,
|
|
126
145
|
private readonly l1Constants: SequencerRollupConstants,
|
|
146
|
+
private readonly signatureContext: CoordinationSignatureContext,
|
|
127
147
|
protected config: ResolvedSequencerConfig,
|
|
128
148
|
protected timetable: SequencerTimetable,
|
|
129
149
|
private readonly slasherClient: SlasherClientInterface | undefined,
|
|
@@ -132,7 +152,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
132
152
|
private readonly metrics: SequencerMetrics,
|
|
133
153
|
private readonly checkpointMetrics: CheckpointProposalJobMetricsRecorder,
|
|
134
154
|
protected readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
|
|
135
|
-
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
|
|
155
|
+
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber, timeReferenceSlot?: SlotNumber) => void,
|
|
136
156
|
public readonly tracer: Tracer,
|
|
137
157
|
bindings?: LoggerBindings,
|
|
138
158
|
private readonly proposedCheckpointData?: ProposedCheckpointData,
|
|
@@ -141,6 +161,10 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
141
161
|
...bindings,
|
|
142
162
|
instanceId: `slot-${this.slotNow}`,
|
|
143
163
|
});
|
|
164
|
+
this.checkpointEventLog = createLogger('sequencer:checkpoint-events', {
|
|
165
|
+
...bindings,
|
|
166
|
+
instanceId: `slot-${this.slotNow}`,
|
|
167
|
+
});
|
|
144
168
|
}
|
|
145
169
|
|
|
146
170
|
/** Awaits the pending L1 submission if one is in progress. Call during shutdown. */
|
|
@@ -149,6 +173,37 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
149
173
|
await this.pendingL1Submission;
|
|
150
174
|
}
|
|
151
175
|
|
|
176
|
+
/** Interrupts job-owned waits, including the publisher's send-at-slot sleep, so shutdown can finish. */
|
|
177
|
+
public interrupt(): void {
|
|
178
|
+
this.interrupted = true;
|
|
179
|
+
this.interruptibleSleep.interrupt(true);
|
|
180
|
+
this.publisher.interrupt();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private async awaitInterruptibleSleep(ms: number): Promise<void> {
|
|
184
|
+
if (this.interrupted) {
|
|
185
|
+
throw new SequencerInterruptedError();
|
|
186
|
+
}
|
|
187
|
+
if (ms <= 0) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
await this.interruptibleSleep.sleep(ms);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (err instanceof InterruptError) {
|
|
194
|
+
throw new SequencerInterruptedError();
|
|
195
|
+
}
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private logCheckpointEvent(eventName: string, message: string, fields: Record<string, unknown>): void {
|
|
201
|
+
this.checkpointEventLog.debug(message, {
|
|
202
|
+
eventName: `sequencer-checkpoint-${eventName}`,
|
|
203
|
+
...fields,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
152
207
|
/**
|
|
153
208
|
* Executes the checkpoint proposal job.
|
|
154
209
|
* Builds blocks, assembles checkpoint, and broadcasts the proposal (blocking).
|
|
@@ -179,9 +234,14 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
179
234
|
|
|
180
235
|
if (!broadcast) {
|
|
181
236
|
await Promise.all(votesPromises);
|
|
182
|
-
// Still submit votes even without a checkpoint
|
|
237
|
+
// Still submit votes even without a checkpoint.
|
|
238
|
+
// Under proposer pipelining, vote-offenses signatures are EIP-712-bound to `targetSlot`
|
|
239
|
+
// (the pipelined slot in which the multicall is expected to mine). Submitting at the
|
|
240
|
+
// wall-clock time would let the multicall mine in a different L2 slot, causing
|
|
241
|
+
// signature verification to fail silently inside Multicall3. Delay submission to the
|
|
242
|
+
// start of `targetSlot` so the tx mines in the slot the vote was signed for.
|
|
183
243
|
if (!this.config.fishermanMode) {
|
|
184
|
-
this.pendingL1Submission = this.publisher.sendRequestsAt(this.
|
|
244
|
+
this.pendingL1Submission = this.publisher.sendRequestsAt(this.targetSlot).then(() => {});
|
|
185
245
|
}
|
|
186
246
|
return undefined;
|
|
187
247
|
}
|
|
@@ -211,7 +271,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
211
271
|
votesPromises: Promise<unknown>[],
|
|
212
272
|
): Promise<void> {
|
|
213
273
|
const { checkpoint } = broadcast;
|
|
214
|
-
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
215
274
|
|
|
216
275
|
try {
|
|
217
276
|
// Wait for all votes actions, enqueued at the beginning, to resolve
|
|
@@ -220,15 +279,14 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
220
279
|
// Try to collect attestations from the committee
|
|
221
280
|
const signedAttestations = await this.getSignedCommitteeAttestations(broadcast);
|
|
222
281
|
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
if (signedAttestations && (
|
|
282
|
+
// Wait for the previous checkpoint to land on L1 before submitting, so we can check it
|
|
283
|
+
// matches the proposed checkpoint we used as parent, and has valid attestations.
|
|
284
|
+
if (signedAttestations && (await this.waitForValidParentCheckpointOnL1())) {
|
|
226
285
|
await this.enqueueCheckpointForSubmission({ checkpoint, ...signedAttestations });
|
|
227
286
|
}
|
|
228
287
|
|
|
229
288
|
// If we failed to collect attestations, at least check if we need to issue an invalidation
|
|
230
|
-
|
|
231
|
-
if (!signedAttestations && isPipelining && (await this.waitForSyncedL2SlotNumber(this.slotNow))) {
|
|
289
|
+
if (!signedAttestations && (await this.waitForSyncedL2SlotNumber(this.slotNow))) {
|
|
232
290
|
const validationStatus = await this.l2BlockSource.getPendingChainValidationStatus();
|
|
233
291
|
if (!validationStatus.valid) {
|
|
234
292
|
this.log.warn(
|
|
@@ -240,32 +298,56 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
240
298
|
}
|
|
241
299
|
|
|
242
300
|
// Send whatever was enqueued: votes + (propose | invalidation | nothing).
|
|
243
|
-
|
|
244
|
-
const submitAfter = isPipelining
|
|
245
|
-
? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000)
|
|
246
|
-
: new Date(this.dateProvider.now());
|
|
247
|
-
|
|
248
|
-
const l1Response = await this.publisher.sendRequestsAt(submitAfter);
|
|
301
|
+
const l1Response = await this.publisher.sendRequestsAt(this.targetSlot);
|
|
249
302
|
const proposedAction = l1Response?.successfulActions.find(a => a === 'propose');
|
|
250
303
|
if (proposedAction) {
|
|
304
|
+
this.logCheckpointEvent('published', `Checkpoint published for slot ${this.targetSlot}`, {
|
|
305
|
+
slot: this.targetSlot,
|
|
306
|
+
checkpointNumber: this.checkpointNumber,
|
|
307
|
+
successfulActions: l1Response?.successfulActions,
|
|
308
|
+
sentActions: l1Response?.sentActions,
|
|
309
|
+
});
|
|
251
310
|
this.eventEmitter.emit('checkpoint-published', { checkpoint: this.checkpointNumber, slot: this.targetSlot });
|
|
252
311
|
const coinbase = checkpoint.header.coinbase;
|
|
253
312
|
await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
|
|
254
313
|
} else {
|
|
314
|
+
this.logCheckpointEvent('publish-failed', `Checkpoint publish failed for slot ${this.targetSlot}`, {
|
|
315
|
+
slot: this.targetSlot,
|
|
316
|
+
checkpointNumber: this.checkpointNumber,
|
|
317
|
+
successfulActions: l1Response?.successfulActions,
|
|
318
|
+
failedActions: l1Response?.failedActions,
|
|
319
|
+
sentActions: l1Response?.sentActions,
|
|
320
|
+
expiredActions: l1Response?.expiredActions,
|
|
321
|
+
reason: 'propose_action_not_successful',
|
|
322
|
+
});
|
|
323
|
+
this.log.warn(`Checkpoint publish failed for slot ${this.targetSlot}`, {
|
|
324
|
+
slot: this.targetSlot,
|
|
325
|
+
checkpointNumber: this.checkpointNumber,
|
|
326
|
+
successfulActions: l1Response?.successfulActions,
|
|
327
|
+
failedActions: l1Response?.failedActions,
|
|
328
|
+
sentActions: l1Response?.sentActions,
|
|
329
|
+
expiredActions: l1Response?.expiredActions,
|
|
330
|
+
reason: 'propose_action_not_successful',
|
|
331
|
+
});
|
|
255
332
|
this.eventEmitter.emit('checkpoint-publish-failed', { ...l1Response, slot: this.targetSlot });
|
|
256
|
-
|
|
257
|
-
this.metrics.recordPipelineDiscard();
|
|
258
|
-
}
|
|
333
|
+
this.metrics.recordPipelineDiscard();
|
|
259
334
|
}
|
|
260
335
|
} catch (err) {
|
|
261
336
|
if (err instanceof SequencerInterruptedError) {
|
|
262
337
|
return;
|
|
263
338
|
}
|
|
264
|
-
this.
|
|
339
|
+
this.logCheckpointEvent('publish-failed', `Checkpoint publish failed for slot ${this.targetSlot}`, {
|
|
340
|
+
slot: this.targetSlot,
|
|
341
|
+
checkpointNumber: this.checkpointNumber,
|
|
342
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
343
|
+
});
|
|
344
|
+
this.log.error(`Background attestation/L1 pipeline failed for slot ${this.targetSlot}`, err, {
|
|
345
|
+
slot: this.targetSlot,
|
|
346
|
+
checkpointNumber: this.checkpointNumber,
|
|
347
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
348
|
+
});
|
|
265
349
|
this.eventEmitter.emit('checkpoint-publish-failed', { slot: this.targetSlot });
|
|
266
|
-
|
|
267
|
-
this.metrics.recordPipelineDiscard();
|
|
268
|
-
}
|
|
350
|
+
this.metrics.recordPipelineDiscard();
|
|
269
351
|
}
|
|
270
352
|
}
|
|
271
353
|
|
|
@@ -273,7 +355,8 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
273
355
|
private async enqueueCheckpointForSubmission(result: CheckpointProposalResult): Promise<void> {
|
|
274
356
|
const { checkpoint, attestations, attestationsSignature } = result;
|
|
275
357
|
|
|
276
|
-
|
|
358
|
+
// Build-frame deadlines are measured against `slotNow`; observers still see `targetSlot`.
|
|
359
|
+
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot, this.slotNow);
|
|
277
360
|
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
278
361
|
const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
|
|
279
362
|
const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
|
|
@@ -292,17 +375,8 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
292
375
|
}
|
|
293
376
|
}
|
|
294
377
|
|
|
295
|
-
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
296
|
-
const submissionSimulationOverridesPlan = buildSubmissionSimulationOverridesPlan({
|
|
297
|
-
pipelinedParentPlan: this.pipelinedParentSimulationOverridesPlan,
|
|
298
|
-
invalidateToPendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
299
|
-
lastArchiveRoot: checkpoint.header.lastArchiveRoot,
|
|
300
|
-
pipeliningEnabled: isPipelining,
|
|
301
|
-
});
|
|
302
|
-
|
|
303
378
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
304
379
|
txTimeoutAt,
|
|
305
|
-
...(submissionSimulationOverridesPlan ? { simulationOverridesPlan: submissionSimulationOverridesPlan } : {}),
|
|
306
380
|
});
|
|
307
381
|
}
|
|
308
382
|
|
|
@@ -318,16 +392,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
318
392
|
const timeoutSeconds = Math.max(0.1, (targetSlotEndMs + syncDelayTolerance - this.dateProvider.now()) / 1000);
|
|
319
393
|
|
|
320
394
|
try {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
timeoutSeconds
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
395
|
+
const timer = new Timer();
|
|
396
|
+
while (true) {
|
|
397
|
+
const syncedSlot = await this.l2BlockSource.getSyncedL2SlotNumber();
|
|
398
|
+
if (syncedSlot !== undefined && syncedSlot >= waitForSlot) {
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
if (timeoutSeconds && timer.s() > timeoutSeconds) {
|
|
402
|
+
throw new TimeoutError(`Timeout awaiting archiver sync past slot ${waitForSlot}`);
|
|
403
|
+
}
|
|
404
|
+
await this.awaitInterruptibleSleep(ARCHIVER_SYNC_POLLING_MS);
|
|
405
|
+
}
|
|
406
|
+
} catch (err) {
|
|
407
|
+
if (err instanceof SequencerInterruptedError) {
|
|
408
|
+
throw err;
|
|
409
|
+
}
|
|
331
410
|
this.log.warn(
|
|
332
411
|
`Archiver did not sync L1 past slot ${waitForSlot} before slot ${this.targetSlot} expired, discarding pipelined work`,
|
|
333
412
|
{ checkpointNumber: this.checkpointNumber },
|
|
@@ -345,6 +424,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
345
424
|
* If the parent has invalid attestations, enqueues an invalidation. Returns whether to proceed with the proposal.
|
|
346
425
|
*/
|
|
347
426
|
protected async waitForValidParentCheckpointOnL1(): Promise<boolean> {
|
|
427
|
+
if (this.config.skipWaitForValidParentCheckpointOnL1) {
|
|
428
|
+
this.log.warn(`Skipping waitForValidParentCheckpointOnL1 due to test configuration`, {
|
|
429
|
+
checkpointNumber: this.checkpointNumber,
|
|
430
|
+
});
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
|
|
348
434
|
const parentCheckpointNumber = CheckpointNumber(this.checkpointNumber - 1);
|
|
349
435
|
|
|
350
436
|
// Wait until archiver has synced L1 past the parent's slot (slotNow)
|
|
@@ -451,7 +537,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
451
537
|
private async proposeCheckpoint(): Promise<CheckpointProposalBroadcast | undefined> {
|
|
452
538
|
try {
|
|
453
539
|
const now = this.dateProvider.now();
|
|
454
|
-
if (this.
|
|
540
|
+
if (this.proposedCheckpointData) {
|
|
455
541
|
// Measure against the wall-clock slot whose build window we are currently using.
|
|
456
542
|
// In pipelining mode `targetSlot` is intentionally one slot ahead, which makes the
|
|
457
543
|
// target-slot boundary a full slot away from the actual build start time.
|
|
@@ -465,12 +551,15 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
465
551
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
|
|
466
552
|
|
|
467
553
|
// Start the checkpoint
|
|
468
|
-
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot);
|
|
469
|
-
this.
|
|
554
|
+
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot, this.slotNow);
|
|
555
|
+
this.logCheckpointEvent('slot-started', `Starting checkpoint proposal for slot ${this.targetSlot}`, {
|
|
470
556
|
buildSlot: this.slotNow,
|
|
471
557
|
submissionSlot: this.targetSlot,
|
|
472
|
-
|
|
558
|
+
slot: this.targetSlot,
|
|
559
|
+
checkpointNumber: this.checkpointNumber,
|
|
473
560
|
proposer: this.proposer?.toString(),
|
|
561
|
+
attestorAddress: this.attestorAddress.toString(),
|
|
562
|
+
publisherAddress: this.publisher.getSenderAddress().toString(),
|
|
474
563
|
coinbase: coinbase.toString(),
|
|
475
564
|
});
|
|
476
565
|
this.metrics.incOpenSlot(this.targetSlot, this.proposer?.toString() ?? 'unknown');
|
|
@@ -480,37 +569,51 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
480
569
|
this.publisher.enqueueInvalidateCheckpoint(this.invalidateCheckpoint);
|
|
481
570
|
}
|
|
482
571
|
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
// fee
|
|
486
|
-
|
|
487
|
-
this.
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
:
|
|
572
|
+
// Build the simulation plan for this slot. When pipelining, this overrides L1's view of
|
|
573
|
+
// pending/archive/fee-header to "as if the proposed parent had landed", so both the
|
|
574
|
+
// mana-min-fee simulation (in the globals builder) and the pre-broadcast
|
|
575
|
+
// validateBlockHeader see the chain tip the eventual L1 send will see.
|
|
576
|
+
this.checkpointSimulationOverridesPlan = await buildCheckpointSimulationOverridesPlan({
|
|
577
|
+
checkpointNumber: this.checkpointNumber,
|
|
578
|
+
proposedCheckpointData: this.proposedCheckpointData,
|
|
579
|
+
invalidateToPendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
580
|
+
checkpointedCheckpointNumber: this.checkpointedCheckpointNumber,
|
|
581
|
+
rollup: this.publisher.rollupContract,
|
|
582
|
+
signatureContext: this.signatureContext,
|
|
583
|
+
log: this.log,
|
|
584
|
+
});
|
|
495
585
|
|
|
496
586
|
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(
|
|
497
587
|
coinbase,
|
|
498
588
|
feeRecipient,
|
|
499
589
|
this.targetSlot,
|
|
500
|
-
this.
|
|
590
|
+
this.checkpointSimulationOverridesPlan,
|
|
501
591
|
);
|
|
502
592
|
|
|
503
593
|
// Collect L1 to L2 messages for the checkpoint and compute their hash
|
|
504
594
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(this.checkpointNumber);
|
|
505
595
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
506
596
|
|
|
507
|
-
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
597
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch.
|
|
598
|
+
// Under pipelining the parent checkpoint may not be on L1 yet at build time, so the helper
|
|
599
|
+
// splices in the parent's checkpointOutHash from the locally-known proposed checkpoint so
|
|
600
|
+
// the resulting `epochOutHash` matches what validators (and L1) compute once the parent
|
|
601
|
+
// lands on L1.
|
|
602
|
+
const previousCheckpointOutHashes = await getPreviousCheckpointOutHashes({
|
|
603
|
+
blockSource: this.l2BlockSource,
|
|
604
|
+
epoch: this.targetEpoch,
|
|
605
|
+
checkpointNumber: this.checkpointNumber,
|
|
606
|
+
l1Constants: this.epochCache.getL1Constants(),
|
|
607
|
+
pipeliningEnabled: true,
|
|
608
|
+
proposedCheckpointData: this.proposedCheckpointData,
|
|
609
|
+
log: this.log,
|
|
610
|
+
});
|
|
511
611
|
|
|
512
|
-
//
|
|
513
|
-
|
|
612
|
+
// Anchor the modifier to the predicted parent fee header: L1 will apply it against
|
|
613
|
+
// that, not against the latest published checkpoint (which lags by one under pipelining).
|
|
614
|
+
const predictedParentEthPerFeeAssetE12 =
|
|
615
|
+
this.checkpointSimulationOverridesPlan?.pendingCheckpointState?.feeHeader?.ethPerFeeAsset;
|
|
616
|
+
const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier(predictedParentEthPerFeeAssetE12);
|
|
514
617
|
|
|
515
618
|
// Create a long-lived forked world state for the checkpoint builder
|
|
516
619
|
await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
@@ -534,7 +637,8 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
534
637
|
|
|
535
638
|
const checkpointProposalOptions: CheckpointProposalOptions = {
|
|
536
639
|
publishFullTxs: !!this.config.publishTxsWithProposals,
|
|
537
|
-
broadcastInvalidCheckpointProposal:
|
|
640
|
+
broadcastInvalidCheckpointProposal:
|
|
641
|
+
this.config.broadcastInvalidCheckpointProposalOnly || this.config.broadcastInvalidBlockProposal,
|
|
538
642
|
};
|
|
539
643
|
|
|
540
644
|
let blocksInCheckpoint: L2Block[] = [];
|
|
@@ -562,23 +666,45 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
562
666
|
}
|
|
563
667
|
|
|
564
668
|
if (blocksInCheckpoint.length === 0) {
|
|
565
|
-
this.
|
|
669
|
+
this.logCheckpointEvent('build-failed', `Checkpoint build failed for slot ${this.targetSlot}`, {
|
|
670
|
+
slot: this.targetSlot,
|
|
671
|
+
checkpointNumber: this.checkpointNumber,
|
|
672
|
+
reason: 'no_blocks_built',
|
|
673
|
+
});
|
|
674
|
+
this.log.warn(`No blocks were built for slot ${this.targetSlot}`, {
|
|
675
|
+
slot: this.targetSlot,
|
|
676
|
+
checkpointNumber: this.checkpointNumber,
|
|
677
|
+
reason: 'no_blocks_built',
|
|
678
|
+
});
|
|
566
679
|
this.eventEmitter.emit('checkpoint-empty', { slot: this.targetSlot });
|
|
567
680
|
return undefined;
|
|
568
681
|
}
|
|
569
682
|
|
|
570
683
|
const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
|
|
571
684
|
if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
|
|
685
|
+
this.logCheckpointEvent('build-failed', `Checkpoint build failed for slot ${this.targetSlot}`, {
|
|
686
|
+
slot: this.targetSlot,
|
|
687
|
+
checkpointNumber: this.checkpointNumber,
|
|
688
|
+
blocksBuilt: blocksInCheckpoint.length,
|
|
689
|
+
minBlocksForCheckpoint,
|
|
690
|
+
reason: 'min_blocks_not_met',
|
|
691
|
+
});
|
|
572
692
|
this.log.warn(
|
|
573
693
|
`Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
|
|
574
|
-
{
|
|
694
|
+
{
|
|
695
|
+
slot: this.targetSlot,
|
|
696
|
+
checkpointNumber: this.checkpointNumber,
|
|
697
|
+
blocksBuilt: blocksInCheckpoint.length,
|
|
698
|
+
minBlocksForCheckpoint,
|
|
699
|
+
reason: 'min_blocks_not_met',
|
|
700
|
+
},
|
|
575
701
|
);
|
|
576
702
|
return undefined;
|
|
577
703
|
}
|
|
578
704
|
|
|
579
705
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
580
706
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
581
|
-
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.targetSlot);
|
|
707
|
+
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.targetSlot, this.slotNow);
|
|
582
708
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
583
709
|
|
|
584
710
|
// Final validation: per-block limits are only checked if the operator set them explicitly.
|
|
@@ -592,7 +718,18 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
592
718
|
maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
|
|
593
719
|
});
|
|
594
720
|
} catch (err) {
|
|
721
|
+
this.logCheckpointEvent('build-failed', `Checkpoint build failed for slot ${this.targetSlot}`, {
|
|
722
|
+
slot: this.targetSlot,
|
|
723
|
+
checkpointNumber: this.checkpointNumber,
|
|
724
|
+
blocksBuilt: blocksInCheckpoint.length,
|
|
725
|
+
reason: 'invalid_checkpoint',
|
|
726
|
+
checkpoint: checkpoint.header.toInspect(),
|
|
727
|
+
});
|
|
595
728
|
this.log.error(`Built an invalid checkpoint at slot ${this.slotNow} (skipping proposal)`, err, {
|
|
729
|
+
slot: this.targetSlot,
|
|
730
|
+
checkpointNumber: this.checkpointNumber,
|
|
731
|
+
blocksBuilt: blocksInCheckpoint.length,
|
|
732
|
+
reason: 'invalid_checkpoint',
|
|
596
733
|
checkpoint: checkpoint.header.toInspect(),
|
|
597
734
|
});
|
|
598
735
|
return undefined;
|
|
@@ -605,6 +742,17 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
605
742
|
checkpoint.getStats().txCount,
|
|
606
743
|
Number(checkpoint.header.totalManaUsed.toBigInt()),
|
|
607
744
|
);
|
|
745
|
+
this.logCheckpointEvent('built', `Checkpoint built for slot ${this.targetSlot}`, {
|
|
746
|
+
slot: this.targetSlot,
|
|
747
|
+
buildSlot: this.slotNow,
|
|
748
|
+
checkpointNumber: this.checkpointNumber,
|
|
749
|
+
proposer: this.proposer?.toString(),
|
|
750
|
+
attestorAddress: this.attestorAddress.toString(),
|
|
751
|
+
publisherAddress: this.publisher.getSenderAddress().toString(),
|
|
752
|
+
blocksBuilt: blocksInCheckpoint.length,
|
|
753
|
+
txCount: checkpoint.getStats().txCount,
|
|
754
|
+
totalMana: Number(checkpoint.header.totalManaUsed.toBigInt()),
|
|
755
|
+
});
|
|
608
756
|
|
|
609
757
|
// In fisherman mode, return the checkpoint without broadcasting or collecting attestations
|
|
610
758
|
if (this.config.fishermanMode) {
|
|
@@ -622,6 +770,25 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
622
770
|
return { checkpoint, proposal: undefined!, blockProposedAt: this.dateProvider.now() };
|
|
623
771
|
}
|
|
624
772
|
|
|
773
|
+
// Validate the header against L1 state before broadcasting.
|
|
774
|
+
// If this fails the slot is aborted before any gossip work; state drift between here
|
|
775
|
+
// and the eventual L1 send is caught by the bundle simulate at send time.
|
|
776
|
+
try {
|
|
777
|
+
await this.publisher.validateBlockHeader(checkpoint.header, this.checkpointSimulationOverridesPlan);
|
|
778
|
+
} catch (err) {
|
|
779
|
+
this.log.error(`Pre-broadcast header validation failed for slot ${this.targetSlot}; aborting`, err, {
|
|
780
|
+
slot: this.targetSlot,
|
|
781
|
+
checkpointNumber: this.checkpointNumber,
|
|
782
|
+
});
|
|
783
|
+
this.metrics.recordCheckpointProposalFailed('header_validation_failed');
|
|
784
|
+
this.eventEmitter.emit('header-validation-failed', {
|
|
785
|
+
slot: this.targetSlot,
|
|
786
|
+
checkpointNumber: this.checkpointNumber,
|
|
787
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
788
|
+
});
|
|
789
|
+
return undefined;
|
|
790
|
+
}
|
|
791
|
+
|
|
625
792
|
// Create the checkpoint proposal and broadcast it
|
|
626
793
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
627
794
|
checkpoint.header,
|
|
@@ -633,9 +800,26 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
633
800
|
checkpointProposalOptions,
|
|
634
801
|
);
|
|
635
802
|
|
|
803
|
+
// Advance our own optimistic proposed-checkpoint tip locally before gossiping. Gossipsub
|
|
804
|
+
// doesn't echo our own messages back, so this is how the proposer makes its own proposed
|
|
805
|
+
// checkpoint visible for pipelining the next slot. Built from local checkpoint data — never
|
|
806
|
+
// from the broadcast proposal archive, which may be deliberately corrupted under test flags.
|
|
807
|
+
// Fail closed: if this throws, the outer catch aborts the slot before gossiping.
|
|
808
|
+
await this.syncProposedCheckpointToArchiver(checkpoint, blocksInCheckpoint.length, feeAssetPriceModifier);
|
|
809
|
+
|
|
636
810
|
const blockProposedAt = this.dateProvider.now();
|
|
637
|
-
|
|
638
|
-
|
|
811
|
+
if (this.config.skipBroadcastCheckpointProposal) {
|
|
812
|
+
// Test-only: suppress the CheckpointProposal so peers never see a proposed checkpoint for
|
|
813
|
+
// this slot, but still broadcast the held last block standalone so peers' archivers ingest
|
|
814
|
+
// it as a proposed-but-uncheckpointed tip — the exact orphan-block state that
|
|
815
|
+
// pruneOrphanProposedBlocks / checkSync must handle.
|
|
816
|
+
if (blockPendingBroadcast && !this.config.skipBroadcastProposals) {
|
|
817
|
+
await this.p2pClient.broadcastProposal(blockPendingBroadcast);
|
|
818
|
+
}
|
|
819
|
+
} else if (!this.config.skipBroadcastProposals) {
|
|
820
|
+
await this.p2pClient.broadcastCheckpointProposal(proposal);
|
|
821
|
+
this.checkpointMetrics.noteCheckpointBroadcast(this.dateProvider.now());
|
|
822
|
+
}
|
|
639
823
|
|
|
640
824
|
// Return immediately after broadcast — attestation collection happens in the background
|
|
641
825
|
return { checkpoint, proposal, blockProposedAt };
|
|
@@ -675,6 +859,15 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
675
859
|
const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
|
|
676
860
|
const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
|
|
677
861
|
|
|
862
|
+
if (blocksBuilt >= this.config.maxBlocksPerCheckpoint) {
|
|
863
|
+
this.log.debug(`Reached max blocks per checkpoint`, {
|
|
864
|
+
slot: this.targetSlot,
|
|
865
|
+
blocksBuilt,
|
|
866
|
+
maxBlocksPerCheckpoint: this.config.maxBlocksPerCheckpoint,
|
|
867
|
+
});
|
|
868
|
+
break;
|
|
869
|
+
}
|
|
870
|
+
|
|
678
871
|
const secondsIntoSlot = this.getSecondsIntoSlot();
|
|
679
872
|
const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
|
|
680
873
|
|
|
@@ -735,7 +928,12 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
735
928
|
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
736
929
|
|
|
737
930
|
// Sign the block proposal. This will throw if HA signing fails.
|
|
738
|
-
const proposal = await this.createBlockProposal(block, inHash, usedTxs,
|
|
931
|
+
const proposal = await this.createBlockProposal(block, inHash, usedTxs, {
|
|
932
|
+
...blockProposalOptions,
|
|
933
|
+
broadcastInvalidBlockProposal:
|
|
934
|
+
blockProposalOptions.broadcastInvalidBlockProposal ||
|
|
935
|
+
block.indexWithinCheckpoint === this.config.invalidBlockProposalIndexWithinCheckpoint,
|
|
936
|
+
});
|
|
739
937
|
|
|
740
938
|
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal,
|
|
741
939
|
// so we avoid polluting our archive with a block that would fail.
|
|
@@ -756,7 +954,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
756
954
|
}
|
|
757
955
|
|
|
758
956
|
// Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
|
|
759
|
-
proposal &&
|
|
957
|
+
if (proposal && !this.config.skipBroadcastProposals) {
|
|
958
|
+
await this.p2pClient.broadcastProposal(proposal);
|
|
959
|
+
}
|
|
760
960
|
|
|
761
961
|
// Wait until the next block's start time
|
|
762
962
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
@@ -796,7 +996,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
796
996
|
/** Sleeps until it is time to produce the next block in the slot */
|
|
797
997
|
@trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
|
|
798
998
|
private async waitUntilNextSubslot(nextSubslotStart: number) {
|
|
799
|
-
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot);
|
|
999
|
+
this.setStateFn(SequencerState.WAITING_UNTIL_NEXT_BLOCK, this.targetSlot, this.slotNow);
|
|
800
1000
|
this.log.verbose(`Waiting until time for the next block at ${nextSubslotStart}s into slot`, {
|
|
801
1001
|
slot: this.targetSlot,
|
|
802
1002
|
});
|
|
@@ -830,9 +1030,26 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
830
1030
|
// Wait until we have enough txs to build the block
|
|
831
1031
|
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
832
1032
|
if (!canStartBuilding) {
|
|
1033
|
+
this.logCheckpointEvent('block-build-failed', `Block build failed for slot ${this.targetSlot}`, {
|
|
1034
|
+
reason: 'insufficient_txs',
|
|
1035
|
+
blockNumber,
|
|
1036
|
+
slot: this.targetSlot,
|
|
1037
|
+
checkpointNumber: this.checkpointNumber,
|
|
1038
|
+
indexWithinCheckpoint,
|
|
1039
|
+
availableTxs,
|
|
1040
|
+
minTxs,
|
|
1041
|
+
});
|
|
833
1042
|
this.log.warn(
|
|
834
1043
|
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (got ${availableTxs} txs but needs ${minTxs})`,
|
|
835
|
-
{
|
|
1044
|
+
{
|
|
1045
|
+
reason: 'insufficient_txs',
|
|
1046
|
+
blockNumber,
|
|
1047
|
+
slot: this.targetSlot,
|
|
1048
|
+
checkpointNumber: this.checkpointNumber,
|
|
1049
|
+
indexWithinCheckpoint,
|
|
1050
|
+
availableTxs,
|
|
1051
|
+
minTxs,
|
|
1052
|
+
},
|
|
836
1053
|
);
|
|
837
1054
|
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.targetSlot });
|
|
838
1055
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
@@ -850,7 +1067,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
850
1067
|
`Building block ${blockNumber} at index ${indexWithinCheckpoint} for slot ${this.targetSlot} with ${availableTxs} available txs`,
|
|
851
1068
|
{ slot: this.targetSlot, blockNumber, indexWithinCheckpoint },
|
|
852
1069
|
);
|
|
853
|
-
this.setStateFn(SequencerState.CREATING_BLOCK, this.targetSlot);
|
|
1070
|
+
this.setStateFn(SequencerState.CREATING_BLOCK, this.targetSlot, this.slotNow);
|
|
854
1071
|
|
|
855
1072
|
// Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
|
|
856
1073
|
// by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
|
|
@@ -884,10 +1101,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
884
1101
|
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
885
1102
|
|
|
886
1103
|
if (buildResult.status === 'insufficient-valid-txs') {
|
|
1104
|
+
this.logCheckpointEvent('block-build-failed', `Block build failed for slot ${this.targetSlot}`, {
|
|
1105
|
+
reason: 'insufficient_valid_txs',
|
|
1106
|
+
slot: this.targetSlot,
|
|
1107
|
+
checkpointNumber: this.checkpointNumber,
|
|
1108
|
+
blockNumber,
|
|
1109
|
+
numTxs: buildResult.processedCount,
|
|
1110
|
+
indexWithinCheckpoint,
|
|
1111
|
+
minValidTxs,
|
|
1112
|
+
});
|
|
887
1113
|
this.log.warn(
|
|
888
1114
|
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.targetSlot} has too few valid txs to be proposed`,
|
|
889
1115
|
{
|
|
1116
|
+
reason: 'insufficient_valid_txs',
|
|
890
1117
|
slot: this.targetSlot,
|
|
1118
|
+
checkpointNumber: this.checkpointNumber,
|
|
891
1119
|
blockNumber,
|
|
892
1120
|
numTxs: buildResult.processedCount,
|
|
893
1121
|
indexWithinCheckpoint,
|
|
@@ -925,6 +1153,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
925
1153
|
// `buildSlot` is the wall-clock slot during which the block was actually built.
|
|
926
1154
|
this.eventEmitter.emit('block-proposed', {
|
|
927
1155
|
blockNumber: block.number,
|
|
1156
|
+
blockHash,
|
|
1157
|
+
checkpointNumber: this.checkpointNumber,
|
|
1158
|
+
indexWithinCheckpoint: block.indexWithinCheckpoint,
|
|
928
1159
|
slot: this.targetSlot,
|
|
929
1160
|
buildSlot: this.slotNow,
|
|
930
1161
|
});
|
|
@@ -936,7 +1167,18 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
936
1167
|
reason: err.message,
|
|
937
1168
|
slot: this.targetSlot,
|
|
938
1169
|
});
|
|
939
|
-
this.
|
|
1170
|
+
this.logCheckpointEvent('block-build-failed', `Block build failed for slot ${this.targetSlot}`, {
|
|
1171
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1172
|
+
slot: this.targetSlot,
|
|
1173
|
+
checkpointNumber: this.checkpointNumber,
|
|
1174
|
+
blockNumber,
|
|
1175
|
+
});
|
|
1176
|
+
this.log.error(`Error building block`, err, {
|
|
1177
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1178
|
+
slot: this.targetSlot,
|
|
1179
|
+
checkpointNumber: this.checkpointNumber,
|
|
1180
|
+
blockNumber,
|
|
1181
|
+
});
|
|
940
1182
|
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
941
1183
|
this.metrics.recordFailedBlock();
|
|
942
1184
|
return { error: err };
|
|
@@ -996,7 +1238,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
996
1238
|
}
|
|
997
1239
|
|
|
998
1240
|
// Wait a bit before checking again
|
|
999
|
-
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.targetSlot);
|
|
1241
|
+
this.setStateFn(SequencerState.WAITING_FOR_TXS, this.targetSlot, this.slotNow);
|
|
1000
1242
|
this.log.verbose(
|
|
1001
1243
|
`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (have ${availableTxs} but need ${minTxs})`,
|
|
1002
1244
|
{ blockNumber, slot: this.targetSlot, indexWithinCheckpoint },
|
|
@@ -1012,7 +1254,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1012
1254
|
broadcast: CheckpointProposalBroadcast,
|
|
1013
1255
|
): Promise<{ attestations: CommitteeAttestationsAndSigners; attestationsSignature: Signature } | undefined> {
|
|
1014
1256
|
const { proposal, blockProposedAt } = broadcast;
|
|
1015
|
-
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
|
|
1257
|
+
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot, this.slotNow);
|
|
1016
1258
|
const attestations = await this.waitForAttestations(proposal);
|
|
1017
1259
|
if (!attestations) {
|
|
1018
1260
|
return undefined;
|
|
@@ -1048,7 +1290,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1048
1290
|
): Promise<CommitteeAttestationsAndSigners | undefined> {
|
|
1049
1291
|
if (this.config.fishermanMode) {
|
|
1050
1292
|
this.log.debug('Skipping attestation collection in fisherman mode');
|
|
1051
|
-
return CommitteeAttestationsAndSigners.empty();
|
|
1293
|
+
return CommitteeAttestationsAndSigners.empty(this.getSignatureContext());
|
|
1052
1294
|
}
|
|
1053
1295
|
|
|
1054
1296
|
const slotNumber = proposal.slotNumber;
|
|
@@ -1058,7 +1300,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1058
1300
|
throw new Error('No committee when collecting attestations');
|
|
1059
1301
|
} else if (committee.length === 0) {
|
|
1060
1302
|
this.log.verbose(`Attesting committee is empty`);
|
|
1061
|
-
return CommitteeAttestationsAndSigners.empty();
|
|
1303
|
+
return CommitteeAttestationsAndSigners.empty(this.getSignatureContext());
|
|
1062
1304
|
} else {
|
|
1063
1305
|
this.log.debug(`Attesting committee length is ${committee.length}`, { committee });
|
|
1064
1306
|
}
|
|
@@ -1068,7 +1310,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1068
1310
|
if (this.config.skipCollectingAttestations) {
|
|
1069
1311
|
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
1070
1312
|
const attestations = await this.validatorClient?.collectOwnAttestations(proposal, this.checkpointNumber);
|
|
1071
|
-
|
|
1313
|
+
this.logCheckpointAttestations('collected', committee, attestations ?? [], numberOfRequiredAttestations, {
|
|
1314
|
+
reason: 'collect_own_only',
|
|
1315
|
+
});
|
|
1316
|
+
return new CommitteeAttestationsAndSigners(
|
|
1317
|
+
orderAttestations(attestations ?? [], committee),
|
|
1318
|
+
this.getSignatureContext(),
|
|
1319
|
+
);
|
|
1072
1320
|
}
|
|
1073
1321
|
|
|
1074
1322
|
const attestationTimeAllowed = this.config.enforceTimeTable
|
|
@@ -1104,6 +1352,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1104
1352
|
|
|
1105
1353
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
1106
1354
|
const sorted = orderAttestations(trimmed, committee);
|
|
1355
|
+
this.logCheckpointAttestations('collected', committee, attestations, numberOfRequiredAttestations, {
|
|
1356
|
+
submittedCount: trimmed.length,
|
|
1357
|
+
});
|
|
1107
1358
|
|
|
1108
1359
|
// Manipulate the attestations if we've been configured to do so
|
|
1109
1360
|
if (
|
|
@@ -1115,15 +1366,23 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1115
1366
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
1116
1367
|
}
|
|
1117
1368
|
|
|
1118
|
-
return new CommitteeAttestationsAndSigners(sorted);
|
|
1369
|
+
return new CommitteeAttestationsAndSigners(sorted, this.getSignatureContext());
|
|
1119
1370
|
} catch (err) {
|
|
1120
1371
|
if (err && err instanceof AttestationTimeoutError) {
|
|
1121
1372
|
collectedAttestationsCount = err.collectedCount;
|
|
1373
|
+
this.logCheckpointAttestations('failed', committee, undefined, numberOfRequiredAttestations, {
|
|
1374
|
+
collectedCount: collectedAttestationsCount,
|
|
1375
|
+
reason: 'timeout',
|
|
1376
|
+
});
|
|
1122
1377
|
this.log.error(
|
|
1123
1378
|
`Timeout while waiting for attestations for checkpoint proposal at slot ${proposal.slotNumber} (collected ${collectedAttestationsCount}/${numberOfRequiredAttestations})`,
|
|
1124
1379
|
err,
|
|
1125
1380
|
);
|
|
1126
1381
|
} else {
|
|
1382
|
+
this.logCheckpointAttestations('failed', committee, undefined, numberOfRequiredAttestations, {
|
|
1383
|
+
collectedCount: collectedAttestationsCount,
|
|
1384
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1385
|
+
});
|
|
1127
1386
|
this.log.error(`Error collecting attestations for checkpoint proposal at slot ${proposal.slotNumber}`, err);
|
|
1128
1387
|
}
|
|
1129
1388
|
return undefined;
|
|
@@ -1132,6 +1391,31 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1132
1391
|
}
|
|
1133
1392
|
}
|
|
1134
1393
|
|
|
1394
|
+
private logCheckpointAttestations(
|
|
1395
|
+
status: 'collected' | 'failed',
|
|
1396
|
+
committee: EthAddress[],
|
|
1397
|
+
attestations: CheckpointAttestation[] | undefined,
|
|
1398
|
+
requiredAttestations: number,
|
|
1399
|
+
opts: { collectedCount?: number; submittedCount?: number; reason?: string } = {},
|
|
1400
|
+
) {
|
|
1401
|
+
const signedValidators =
|
|
1402
|
+
attestations
|
|
1403
|
+
?.map(attestation => attestation.getSender()?.toString())
|
|
1404
|
+
.filter((address): address is `0x${string}` => address !== undefined) ?? [];
|
|
1405
|
+
const collectedCount = opts.collectedCount ?? new Set(signedValidators).size;
|
|
1406
|
+
const missingValidatorCount = status === 'failed' ? Math.max(0, requiredAttestations - collectedCount) : undefined;
|
|
1407
|
+
this.logCheckpointEvent(`attestations-${status}`, `Checkpoint attestations ${status} for slot ${this.targetSlot}`, {
|
|
1408
|
+
slot: this.targetSlot,
|
|
1409
|
+
checkpointNumber: this.checkpointNumber,
|
|
1410
|
+
committeeSize: committee.length,
|
|
1411
|
+
requiredAttestations,
|
|
1412
|
+
collectedAttestations: collectedCount,
|
|
1413
|
+
...(opts.submittedCount !== undefined && { submittedAttestations: opts.submittedCount }),
|
|
1414
|
+
...(missingValidatorCount !== undefined && { missingValidatorCount }),
|
|
1415
|
+
...(opts.reason !== undefined && { reason: opts.reason }),
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1135
1419
|
/** Breaks the attestations before publishing based on attack configs */
|
|
1136
1420
|
private manipulateAttestations(
|
|
1137
1421
|
slotNumber: SlotNumber,
|
|
@@ -1175,7 +1459,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1175
1459
|
unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
|
|
1176
1460
|
}
|
|
1177
1461
|
}
|
|
1178
|
-
return new CommitteeAttestationsAndSigners(attestations);
|
|
1462
|
+
return new CommitteeAttestationsAndSigners(attestations, this.getSignatureContext());
|
|
1179
1463
|
}
|
|
1180
1464
|
|
|
1181
1465
|
if (this.config.shuffleAttestationOrdering) {
|
|
@@ -1197,11 +1481,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1197
1481
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
1198
1482
|
}
|
|
1199
1483
|
|
|
1200
|
-
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
1201
|
-
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
1484
|
+
const signers = new CommitteeAttestationsAndSigners(attestations, this.getSignatureContext()).getSigners();
|
|
1485
|
+
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers, this.getSignatureContext());
|
|
1202
1486
|
}
|
|
1203
1487
|
|
|
1204
|
-
return new CommitteeAttestationsAndSigners(attestations);
|
|
1488
|
+
return new CommitteeAttestationsAndSigners(attestations, this.getSignatureContext());
|
|
1205
1489
|
}
|
|
1206
1490
|
|
|
1207
1491
|
private async dropFailedTxsFromP2P(failedTxs: FailedTx[]) {
|
|
@@ -1238,6 +1522,41 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1238
1522
|
await this.blockSink.addBlock(block);
|
|
1239
1523
|
}
|
|
1240
1524
|
|
|
1525
|
+
/**
|
|
1526
|
+
* Adds the proposed checkpoint to the archiver so the proposer's optimistic proposed-checkpoint
|
|
1527
|
+
* tip advances locally. Gossip doesn't echo our own messages back, so without this the proposer
|
|
1528
|
+
* would never see its own proposed checkpoint and couldn't pipeline the next slot.
|
|
1529
|
+
*
|
|
1530
|
+
* Skipped whenever proposed blocks aren't pushed (`skipPushProposedBlocksToArchiver`, fisherman
|
|
1531
|
+
* mode): the archiver derives the checkpoint archive from its stored blocks, so without them the
|
|
1532
|
+
* push would fail. All blocks were already added (and awaited) during block building, so this
|
|
1533
|
+
* needs no retry — they are guaranteed present by the time we get here.
|
|
1534
|
+
*/
|
|
1535
|
+
private async syncProposedCheckpointToArchiver(
|
|
1536
|
+
checkpoint: Checkpoint,
|
|
1537
|
+
blockCount: number,
|
|
1538
|
+
feeAssetPriceModifier: bigint,
|
|
1539
|
+
): Promise<void> {
|
|
1540
|
+
if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
const startBlock = BlockNumber(this.syncedToBlockNumber + 1);
|
|
1544
|
+
this.log.debug(`Syncing proposed checkpoint ${this.checkpointNumber} to archiver`, {
|
|
1545
|
+
checkpointNumber: this.checkpointNumber,
|
|
1546
|
+
slot: this.targetSlot,
|
|
1547
|
+
startBlock,
|
|
1548
|
+
blockCount,
|
|
1549
|
+
});
|
|
1550
|
+
await this.blockSink.addProposedCheckpoint({
|
|
1551
|
+
header: checkpoint.header,
|
|
1552
|
+
checkpointNumber: this.checkpointNumber,
|
|
1553
|
+
startBlock,
|
|
1554
|
+
blockCount,
|
|
1555
|
+
totalManaUsed: checkpoint.header.totalManaUsed.toBigInt(),
|
|
1556
|
+
feeAssetPriceModifier,
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1241
1560
|
/** Runs fee analysis and logs checkpoint outcome as fisherman */
|
|
1242
1561
|
private async handleCheckpointEndAsFisherman(checkpoint: Checkpoint | undefined) {
|
|
1243
1562
|
// Perform L1 fee analysis before clearing requests
|
|
@@ -1290,12 +1609,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1290
1609
|
protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
|
|
1291
1610
|
const slotStartTimestamp = this.getSlotStartBuildTimestamp();
|
|
1292
1611
|
const targetTimestamp = slotStartTimestamp + targetSecondsIntoSlot;
|
|
1293
|
-
|
|
1612
|
+
const delayMs = targetTimestamp * 1000 - this.dateProvider.nowAsDate().getTime();
|
|
1613
|
+
await this.awaitInterruptibleSleep(delayMs);
|
|
1294
1614
|
}
|
|
1295
1615
|
|
|
1296
|
-
/**
|
|
1616
|
+
/** Pause between mempool polls in waitForMinTxs. Extracted for test overriding. */
|
|
1297
1617
|
protected async waitForTxsPollingInterval(): Promise<void> {
|
|
1298
|
-
await
|
|
1618
|
+
await this.awaitInterruptibleSleep(TXS_POLLING_MS);
|
|
1299
1619
|
}
|
|
1300
1620
|
|
|
1301
1621
|
private getSlotStartBuildTimestamp(): number {
|