@aztec/sequencer-client 0.0.1-commit.2d9cb6034 → 0.0.1-commit.2e20a94
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 +3 -1
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +3 -4
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +8 -11
- 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 +4 -9
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +3 -41
- package/dest/global_variable_builder/index.d.ts +3 -1
- 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 +1 -1
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +2 -2
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +3 -4
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.d.ts +1 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +0 -1
- package/dest/publisher/sequencer-publisher.d.ts +16 -35
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +60 -92
- 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 +13 -21
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +143 -152
- 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/metrics.d.ts +5 -10
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +12 -20
- package/dest/sequencer/sequencer.d.ts +5 -2
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +31 -23
- package/dest/sequencer/timetable.d.ts +14 -1
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +45 -36
- package/package.json +27 -27
- package/src/client/sequencer-client.ts +5 -7
- package/src/config.ts +8 -11
- 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 +7 -54
- package/src/global_variable_builder/index.ts +2 -0
- package/src/publisher/config.ts +6 -4
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +3 -1
- package/src/publisher/sequencer-publisher-factory.ts +0 -3
- package/src/publisher/sequencer-publisher.ts +106 -152
- package/src/sequencer/README.md +83 -13
- package/src/sequencer/chain_state_overrides.ts +87 -0
- package/src/sequencer/checkpoint_proposal_job.ts +185 -201
- package/src/sequencer/checkpoint_proposal_job_metrics.ts +128 -0
- package/src/sequencer/metrics.ts +15 -25
- package/src/sequencer/sequencer.ts +44 -26
- package/src/sequencer/timetable.ts +57 -45
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import
|
|
2
|
+
import type { SimulationOverridesPlan } from '@aztec/ethereum/contracts';
|
|
3
3
|
import {
|
|
4
4
|
BlockNumber,
|
|
5
5
|
CheckpointNumber,
|
|
@@ -57,6 +57,11 @@ import { DutyAlreadySignedError, SlashingProtectionError } from '@aztec/validato
|
|
|
57
57
|
|
|
58
58
|
import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
59
59
|
import type { InvalidateCheckpointRequest, SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
60
|
+
import {
|
|
61
|
+
buildPipelinedParentSimulationOverridesPlan,
|
|
62
|
+
buildSubmissionSimulationOverridesPlan,
|
|
63
|
+
} from './chain_state_overrides.js';
|
|
64
|
+
import type { CheckpointProposalJobMetricsRecorder } from './checkpoint_proposal_job_metrics.js';
|
|
60
65
|
import { CheckpointVoter } from './checkpoint_voter.js';
|
|
61
66
|
import { SequencerInterruptedError } from './errors.js';
|
|
62
67
|
import type { SequencerEvents } from './events.js';
|
|
@@ -68,7 +73,14 @@ import { SequencerState } from './utils.js';
|
|
|
68
73
|
/** How much time to sleep while waiting for min transactions to accumulate for a block */
|
|
69
74
|
const TXS_POLLING_MS = 500;
|
|
70
75
|
|
|
71
|
-
/** Result from proposeCheckpoint when a checkpoint was successfully built and
|
|
76
|
+
/** Result from proposeCheckpoint when a checkpoint was successfully built and broadcast. */
|
|
77
|
+
type CheckpointProposalBroadcast = {
|
|
78
|
+
checkpoint: Checkpoint;
|
|
79
|
+
proposal: CheckpointProposal;
|
|
80
|
+
blockProposedAt: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Result after attestation collection and signing, ready for L1 submission. */
|
|
72
84
|
type CheckpointProposalResult = {
|
|
73
85
|
checkpoint: Checkpoint;
|
|
74
86
|
attestations: CommitteeAttestationsAndSigners;
|
|
@@ -87,8 +99,8 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
87
99
|
/** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */
|
|
88
100
|
private pendingL1Submission: Promise<void> | undefined;
|
|
89
101
|
|
|
90
|
-
/**
|
|
91
|
-
private
|
|
102
|
+
/** Pipelined parent chain state used while building and later submitting this checkpoint. */
|
|
103
|
+
private pipelinedParentSimulationOverridesPlan?: SimulationOverridesPlan;
|
|
92
104
|
|
|
93
105
|
constructor(
|
|
94
106
|
private readonly slotNow: SlotNumber,
|
|
@@ -116,6 +128,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
116
128
|
private readonly epochCache: EpochCache,
|
|
117
129
|
private readonly dateProvider: DateProvider,
|
|
118
130
|
private readonly metrics: SequencerMetrics,
|
|
131
|
+
private readonly checkpointMetrics: CheckpointProposalJobMetricsRecorder,
|
|
119
132
|
private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
|
|
120
133
|
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
|
|
121
134
|
public readonly tracer: Tracer,
|
|
@@ -136,8 +149,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
136
149
|
|
|
137
150
|
/**
|
|
138
151
|
* Executes the checkpoint proposal job.
|
|
139
|
-
* Builds blocks,
|
|
140
|
-
*
|
|
152
|
+
* Builds blocks, assembles checkpoint, and broadcasts the proposal (blocking).
|
|
153
|
+
* Attestation collection, signing, and L1 submission are backgrounded so the
|
|
154
|
+
* work loop can return to IDLE immediately for consecutive slot proposals.
|
|
141
155
|
* Returns the built checkpoint if successful, undefined otherwise.
|
|
142
156
|
*/
|
|
143
157
|
@trackSpan('CheckpointProposalJob.execute')
|
|
@@ -157,73 +171,99 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
157
171
|
this.log,
|
|
158
172
|
).enqueueVotes();
|
|
159
173
|
|
|
160
|
-
// Build
|
|
161
|
-
//
|
|
162
|
-
const
|
|
163
|
-
const checkpoint = proposalResult?.checkpoint;
|
|
174
|
+
// Build blocks, assemble checkpoint, and broadcast proposal (BLOCKING).
|
|
175
|
+
// Returns after broadcast — attestation collection is deferred.
|
|
176
|
+
const broadcast = await this.proposeCheckpoint();
|
|
164
177
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
if (!broadcast) {
|
|
179
|
+
await Promise.all(votesPromises);
|
|
180
|
+
// Still submit votes even without a checkpoint
|
|
181
|
+
if (!this.config.fishermanMode) {
|
|
182
|
+
this.pendingL1Submission = this.publisher.sendRequestsAt(this.dateProvider.nowAsDate()).then(() => {});
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
170
185
|
}
|
|
171
186
|
|
|
187
|
+
const { checkpoint } = broadcast;
|
|
188
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
189
|
+
|
|
172
190
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
173
191
|
if (this.config.fishermanMode) {
|
|
174
192
|
await this.handleCheckpointEndAsFisherman(checkpoint);
|
|
175
|
-
return;
|
|
193
|
+
return checkpoint;
|
|
176
194
|
}
|
|
177
195
|
|
|
178
|
-
//
|
|
179
|
-
|
|
196
|
+
// Background the attestation → signing → L1 pipeline so the work loop is unblocked
|
|
197
|
+
this.pendingL1Submission = this.waitForAttestationsAndEnqueueSubmissionAsync(broadcast, votesPromises);
|
|
198
|
+
|
|
199
|
+
// Return the built checkpoint immediately — the work loop is now unblocked
|
|
200
|
+
return checkpoint;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Background pipeline: collects attestations, signs them, enqueues the checkpoint, and submits to L1.
|
|
205
|
+
* Runs as a fire-and-forget task stored in `pendingL1Submission` so the work loop is unblocked.
|
|
206
|
+
*/
|
|
207
|
+
private async waitForAttestationsAndEnqueueSubmissionAsync(
|
|
208
|
+
broadcast: CheckpointProposalBroadcast,
|
|
209
|
+
votesPromises: Promise<unknown>[],
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
const { checkpoint, proposal, blockProposedAt } = broadcast;
|
|
212
|
+
try {
|
|
213
|
+
await Promise.all(votesPromises);
|
|
214
|
+
|
|
215
|
+
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
|
|
216
|
+
const attestations = await this.waitForAttestations(proposal);
|
|
217
|
+
|
|
218
|
+
this.checkpointMetrics.recordCheckpointAttestationDelay(this.dateProvider.now() - blockProposedAt);
|
|
219
|
+
|
|
220
|
+
// Proposer must sign over the attestations before pushing them to L1
|
|
221
|
+
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
222
|
+
let attestationsSignature: Signature;
|
|
180
223
|
try {
|
|
181
|
-
await this.
|
|
224
|
+
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
|
|
225
|
+
attestations,
|
|
226
|
+
signer,
|
|
227
|
+
this.targetSlot,
|
|
228
|
+
this.checkpointNumber,
|
|
229
|
+
);
|
|
182
230
|
} catch (err) {
|
|
183
|
-
this.
|
|
184
|
-
|
|
231
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
throw err;
|
|
185
235
|
}
|
|
186
|
-
}
|
|
187
236
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000)
|
|
191
|
-
: new Date(this.dateProvider.now());
|
|
192
|
-
|
|
193
|
-
// TODO(https://github.com/AztecProtocol/aztec-packages/pull/21250): should discard the pending submission if a reorg occurs underneath
|
|
194
|
-
|
|
195
|
-
// Schedule L1 submission in the background so the work loop returns immediately.
|
|
196
|
-
// The publisher will sleep until submitAfter, then send the bundled requests.
|
|
197
|
-
// The promise is stored so it can be awaited during shutdown.
|
|
198
|
-
this.pendingL1Submission = this.publisher
|
|
199
|
-
.sendRequestsAt(submitAfter)
|
|
200
|
-
.then(async l1Response => {
|
|
201
|
-
const proposedAction = l1Response?.successfulActions.find(a => a === 'propose');
|
|
202
|
-
if (proposedAction) {
|
|
203
|
-
this.eventEmitter.emit('checkpoint-published', { checkpoint: this.checkpointNumber, slot: this.targetSlot });
|
|
204
|
-
const coinbase = checkpoint?.header.coinbase;
|
|
205
|
-
await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
|
|
206
|
-
} else if (checkpoint) {
|
|
207
|
-
this.eventEmitter.emit('checkpoint-publish-failed', { ...l1Response, slot: this.targetSlot });
|
|
208
|
-
|
|
209
|
-
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
210
|
-
this.metrics.recordPipelineDiscard();
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
})
|
|
214
|
-
.catch(err => {
|
|
215
|
-
this.log.error(`Background L1 submission failed for slot ${this.targetSlot}`, err);
|
|
216
|
-
if (checkpoint) {
|
|
217
|
-
this.eventEmitter.emit('checkpoint-publish-failed', { slot: this.targetSlot });
|
|
218
|
-
|
|
219
|
-
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
220
|
-
this.metrics.recordPipelineDiscard();
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
});
|
|
237
|
+
// Enqueue the checkpoint for L1 submission
|
|
238
|
+
await this.enqueueCheckpointForSubmission({ checkpoint, attestations, attestationsSignature });
|
|
224
239
|
|
|
225
|
-
|
|
226
|
-
|
|
240
|
+
// Compute the earliest time to submit: pipeline slot start when pipelining, now otherwise.
|
|
241
|
+
const submitAfter = this.epochCache.isProposerPipeliningEnabled()
|
|
242
|
+
? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000)
|
|
243
|
+
: new Date(this.dateProvider.now());
|
|
244
|
+
|
|
245
|
+
const l1Response = await this.publisher.sendRequestsAt(submitAfter);
|
|
246
|
+
const proposedAction = l1Response?.successfulActions.find(a => a === 'propose');
|
|
247
|
+
if (proposedAction) {
|
|
248
|
+
this.eventEmitter.emit('checkpoint-published', { checkpoint: this.checkpointNumber, slot: this.targetSlot });
|
|
249
|
+
const coinbase = checkpoint.header.coinbase;
|
|
250
|
+
await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
|
|
251
|
+
} else {
|
|
252
|
+
this.eventEmitter.emit('checkpoint-publish-failed', { ...l1Response, slot: this.targetSlot });
|
|
253
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
254
|
+
this.metrics.recordPipelineDiscard();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch (err) {
|
|
258
|
+
if (err instanceof SequencerInterruptedError) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.log.error(`Background attestation/L1 pipeline failed for slot ${this.targetSlot}`, err);
|
|
262
|
+
this.eventEmitter.emit('checkpoint-publish-failed', { slot: this.targetSlot });
|
|
263
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
264
|
+
this.metrics.recordPipelineDiscard();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
227
267
|
}
|
|
228
268
|
|
|
229
269
|
/** Enqueues the checkpoint for L1 submission. Called after pipeline sleep in execute(). */
|
|
@@ -249,10 +289,17 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
249
289
|
}
|
|
250
290
|
}
|
|
251
291
|
|
|
292
|
+
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
293
|
+
const submissionSimulationOverridesPlan = buildSubmissionSimulationOverridesPlan({
|
|
294
|
+
pipelinedParentPlan: this.pipelinedParentSimulationOverridesPlan,
|
|
295
|
+
invalidateToPendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
296
|
+
lastArchiveRoot: checkpoint.header.lastArchiveRoot,
|
|
297
|
+
pipeliningEnabled: isPipelining,
|
|
298
|
+
});
|
|
299
|
+
|
|
252
300
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
253
301
|
txTimeoutAt,
|
|
254
|
-
|
|
255
|
-
forceProposedFeeHeader: this.computedForceProposedFeeHeader,
|
|
302
|
+
...(submissionSimulationOverridesPlan ? { simulationOverridesPlan: submissionSimulationOverridesPlan } : {}),
|
|
256
303
|
});
|
|
257
304
|
}
|
|
258
305
|
|
|
@@ -263,8 +310,18 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
263
310
|
[Attributes.SLOT_NUMBER]: this.targetSlot,
|
|
264
311
|
};
|
|
265
312
|
})
|
|
266
|
-
private async proposeCheckpoint(): Promise<
|
|
313
|
+
private async proposeCheckpoint(): Promise<CheckpointProposalBroadcast | undefined> {
|
|
267
314
|
try {
|
|
315
|
+
const now = this.dateProvider.now();
|
|
316
|
+
if (this.epochCache.isProposerPipeliningEnabled() && this.proposedCheckpointData) {
|
|
317
|
+
// Measure against the wall-clock slot whose build window we are currently using.
|
|
318
|
+
// In pipelining mode `targetSlot` is intentionally one slot ahead, which makes the
|
|
319
|
+
// target-slot boundary a full slot away from the actual build start time.
|
|
320
|
+
const slotBoundaryMs = Number(getTimestampForSlot(this.slotNow, this.l1Constants)) * 1000;
|
|
321
|
+
this.checkpointMetrics.recordPipelinedCheckpointBuildStartOffsetFromSlotBoundary(now - slotBoundaryMs);
|
|
322
|
+
}
|
|
323
|
+
this.checkpointMetrics.startCheckpointTiming(now);
|
|
324
|
+
|
|
268
325
|
// Get operator configured coinbase and fee recipient for this attestor
|
|
269
326
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
|
|
270
327
|
const feeRecipient = this.validatorClient.getFeeRecipientForAttestor(this.attestorAddress);
|
|
@@ -289,21 +346,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
289
346
|
// When pipelining, force the proposed checkpoint number and fee header to our parent so the
|
|
290
347
|
// fee computation sees the same chain tip that L1 will see once the previous pipelined checkpoint lands.
|
|
291
348
|
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
349
|
+
this.pipelinedParentSimulationOverridesPlan = isPipelining
|
|
350
|
+
? await buildPipelinedParentSimulationOverridesPlan({
|
|
351
|
+
checkpointNumber: this.checkpointNumber,
|
|
352
|
+
proposedCheckpointData: this.proposedCheckpointData,
|
|
353
|
+
rollup: this.publisher.rollupContract,
|
|
354
|
+
log: this.log,
|
|
355
|
+
})
|
|
356
|
+
: undefined;
|
|
298
357
|
|
|
299
358
|
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(
|
|
300
359
|
coinbase,
|
|
301
360
|
feeRecipient,
|
|
302
361
|
this.targetSlot,
|
|
303
|
-
|
|
304
|
-
forcePendingCheckpointNumber: parentCheckpointNumber,
|
|
305
|
-
forceProposedFeeHeader: this.computedForceProposedFeeHeader,
|
|
306
|
-
},
|
|
362
|
+
this.pipelinedParentSimulationOverridesPlan,
|
|
307
363
|
);
|
|
308
364
|
|
|
309
365
|
// Collect L1 to L2 messages for the checkpoint and compute their hash
|
|
@@ -344,7 +400,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
344
400
|
};
|
|
345
401
|
|
|
346
402
|
let blocksInCheckpoint: L2Block[] = [];
|
|
347
|
-
let blockPendingBroadcast:
|
|
403
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
348
404
|
const checkpointBuildTimer = new Timer();
|
|
349
405
|
|
|
350
406
|
try {
|
|
@@ -405,14 +461,14 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
405
461
|
}
|
|
406
462
|
|
|
407
463
|
// Record checkpoint-level build metrics
|
|
408
|
-
this.
|
|
464
|
+
this.checkpointMetrics.recordCheckpointBuild(
|
|
409
465
|
checkpointBuildTimer.ms(),
|
|
410
466
|
blocksInCheckpoint.length,
|
|
411
467
|
checkpoint.getStats().txCount,
|
|
412
468
|
Number(checkpoint.header.totalManaUsed.toBigInt()),
|
|
413
469
|
);
|
|
414
470
|
|
|
415
|
-
//
|
|
471
|
+
// In fisherman mode, return the checkpoint without broadcasting or collecting attestations
|
|
416
472
|
if (this.config.fishermanMode) {
|
|
417
473
|
this.log.info(
|
|
418
474
|
`Built checkpoint for slot ${this.targetSlot} with ${blocksInCheckpoint.length} blocks. ` +
|
|
@@ -424,60 +480,27 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
424
480
|
},
|
|
425
481
|
);
|
|
426
482
|
this.metrics.recordCheckpointSuccess();
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
attestations: CommitteeAttestationsAndSigners.empty(),
|
|
430
|
-
attestationsSignature: Signature.empty(),
|
|
431
|
-
};
|
|
483
|
+
// Return a broadcast result with a dummy proposal — fisherman mode skips attestation collection
|
|
484
|
+
return { checkpoint, proposal: undefined!, blockProposedAt: this.dateProvider.now() };
|
|
432
485
|
}
|
|
433
486
|
|
|
434
|
-
// Include the block pending broadcast in the checkpoint proposal if any
|
|
435
|
-
const lastBlock = blockPendingBroadcast && {
|
|
436
|
-
blockHeader: blockPendingBroadcast.block.header,
|
|
437
|
-
indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
|
|
438
|
-
txs: blockPendingBroadcast.txs,
|
|
439
|
-
};
|
|
440
|
-
|
|
441
487
|
// Create the checkpoint proposal and broadcast it
|
|
442
488
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
443
489
|
checkpoint.header,
|
|
444
490
|
checkpoint.archive.root,
|
|
491
|
+
this.checkpointNumber,
|
|
445
492
|
feeAssetPriceModifier,
|
|
446
|
-
|
|
493
|
+
blockPendingBroadcast,
|
|
447
494
|
this.proposer,
|
|
448
495
|
checkpointProposalOptions,
|
|
449
496
|
);
|
|
450
497
|
|
|
451
498
|
const blockProposedAt = this.dateProvider.now();
|
|
452
499
|
await this.p2pClient.broadcastCheckpointProposal(proposal);
|
|
500
|
+
this.checkpointMetrics.noteCheckpointBroadcast(this.dateProvider.now());
|
|
453
501
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const blockAttestedAt = this.dateProvider.now();
|
|
457
|
-
|
|
458
|
-
this.metrics.recordCheckpointAttestationDelay(blockAttestedAt - blockProposedAt);
|
|
459
|
-
|
|
460
|
-
// Proposer must sign over the attestations before pushing them to L1
|
|
461
|
-
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
462
|
-
let attestationsSignature: Signature;
|
|
463
|
-
try {
|
|
464
|
-
attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
|
|
465
|
-
attestations,
|
|
466
|
-
signer,
|
|
467
|
-
this.targetSlot,
|
|
468
|
-
this.checkpointNumber,
|
|
469
|
-
);
|
|
470
|
-
} catch (err) {
|
|
471
|
-
// We shouldn't really get here since we yield to another HA node
|
|
472
|
-
// as soon as we see these errors when creating block or checkpoint proposals.
|
|
473
|
-
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
474
|
-
return undefined;
|
|
475
|
-
}
|
|
476
|
-
throw err;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Return the result for the caller to enqueue after the pipeline sleep
|
|
480
|
-
return { checkpoint, attestations, attestationsSignature };
|
|
502
|
+
// Return immediately after broadcast — attestation collection happens in the background
|
|
503
|
+
return { checkpoint, proposal, blockProposedAt };
|
|
481
504
|
} catch (err) {
|
|
482
505
|
if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
|
|
483
506
|
// swallow this error. It's already been logged by a function deeper in the stack
|
|
@@ -500,14 +523,14 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
500
523
|
blockProposalOptions: BlockProposalOptions,
|
|
501
524
|
): Promise<{
|
|
502
525
|
blocksInCheckpoint: L2Block[];
|
|
503
|
-
blockPendingBroadcast:
|
|
526
|
+
blockPendingBroadcast: BlockProposal | undefined;
|
|
504
527
|
}> {
|
|
505
528
|
const blocksInCheckpoint: L2Block[] = [];
|
|
506
529
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
507
530
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
508
531
|
|
|
509
532
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
510
|
-
let blockPendingBroadcast:
|
|
533
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
511
534
|
|
|
512
535
|
while (true) {
|
|
513
536
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
@@ -540,19 +563,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
540
563
|
txHashesAlreadyIncluded,
|
|
541
564
|
});
|
|
542
565
|
|
|
543
|
-
//
|
|
544
|
-
if (
|
|
545
|
-
// If
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
566
|
+
// If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
|
|
567
|
+
if ('failure' in buildResult) {
|
|
568
|
+
// If this was the last subslot, or we're running with a single block per slot, we're done
|
|
569
|
+
if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
// Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
|
|
549
573
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
550
574
|
continue;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// If there was an error building the block, we just exit the loop and give up the rest of the slot.
|
|
578
|
+
// We don't want to risk building more blocks if something went wrong.
|
|
579
|
+
if ('error' in buildResult) {
|
|
556
580
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
557
581
|
this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
|
|
558
582
|
slot: this.targetSlot,
|
|
@@ -564,33 +588,35 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
564
588
|
}
|
|
565
589
|
|
|
566
590
|
const { block, usedTxs } = buildResult;
|
|
591
|
+
this.checkpointMetrics.noteCheckpointBlockBuilt(this.dateProvider.now(), {
|
|
592
|
+
isFirstBlock: blocksBuilt === 0,
|
|
593
|
+
isLastBlock: timingInfo.isLastBlock,
|
|
594
|
+
});
|
|
595
|
+
|
|
567
596
|
blocksInCheckpoint.push(block);
|
|
568
597
|
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
569
598
|
|
|
570
|
-
//
|
|
571
|
-
|
|
599
|
+
// Sign the block proposal. This will throw if HA signing fails.
|
|
600
|
+
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
601
|
+
|
|
602
|
+
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal,
|
|
603
|
+
// so we avoid polluting our archive with a block that would fail.
|
|
604
|
+
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
605
|
+
// If this throws, we abort the entire checkpoint.
|
|
606
|
+
await this.syncProposedBlockToArchiver(block);
|
|
607
|
+
|
|
608
|
+
// If this is the last block, do not broadcast it, since it will be included in the checkpoint proposal.
|
|
572
609
|
if (timingInfo.isLastBlock) {
|
|
573
|
-
await this.syncProposedBlockToArchiver(block);
|
|
574
610
|
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
|
|
575
611
|
slot: this.targetSlot,
|
|
576
612
|
blockNumber,
|
|
577
613
|
blocksBuilt,
|
|
578
614
|
});
|
|
579
|
-
|
|
615
|
+
|
|
616
|
+
blockPendingBroadcast = proposal;
|
|
580
617
|
break;
|
|
581
618
|
}
|
|
582
619
|
|
|
583
|
-
// Broadcast the block proposal (unless we're in fisherman mode) unless the block is the last one,
|
|
584
|
-
// in which case we'll broadcast it along with the checkpoint at the end of the loop.
|
|
585
|
-
// Note that we only send the block to the archiver if we manage to create the proposal, so if there's
|
|
586
|
-
// a HA error we don't pollute our archiver with a block that won't make it to the chain.
|
|
587
|
-
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
588
|
-
|
|
589
|
-
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal.
|
|
590
|
-
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
591
|
-
// If this throws, we abort the entire checkpoint.
|
|
592
|
-
await this.syncProposedBlockToArchiver(block);
|
|
593
|
-
|
|
594
620
|
// Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
|
|
595
621
|
proposal && (await this.p2pClient.broadcastProposal(proposal));
|
|
596
622
|
|
|
@@ -619,6 +645,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
619
645
|
}
|
|
620
646
|
return this.validatorClient.createBlockProposal(
|
|
621
647
|
block.header,
|
|
648
|
+
this.checkpointNumber,
|
|
622
649
|
block.indexWithinCheckpoint,
|
|
623
650
|
inHash,
|
|
624
651
|
block.archive.root,
|
|
@@ -650,7 +677,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
650
677
|
buildDeadline: Date | undefined;
|
|
651
678
|
txHashesAlreadyIncluded: Set<string>;
|
|
652
679
|
},
|
|
653
|
-
): Promise<
|
|
680
|
+
): Promise<
|
|
681
|
+
{ block: L2Block; usedTxs: Tx[] } | { failure: 'insufficient-txs' | 'insufficient-valid-txs' } | { error: Error }
|
|
682
|
+
> {
|
|
654
683
|
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
655
684
|
opts;
|
|
656
685
|
|
|
@@ -669,7 +698,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
669
698
|
);
|
|
670
699
|
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.targetSlot });
|
|
671
700
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
672
|
-
return
|
|
701
|
+
return { failure: 'insufficient-txs' };
|
|
673
702
|
}
|
|
674
703
|
|
|
675
704
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
@@ -732,7 +761,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
732
761
|
slot: this.targetSlot,
|
|
733
762
|
});
|
|
734
763
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
735
|
-
return
|
|
764
|
+
return { failure: 'insufficient-valid-txs' };
|
|
736
765
|
}
|
|
737
766
|
|
|
738
767
|
// Block creation succeeded, emit stats and metrics
|
|
@@ -761,7 +790,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
761
790
|
slot: this.targetSlot,
|
|
762
791
|
buildSlot: this.slotNow,
|
|
763
792
|
});
|
|
764
|
-
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
793
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe(), this.targetSlot);
|
|
765
794
|
|
|
766
795
|
return { block, usedTxs };
|
|
767
796
|
} catch (err: any) {
|
|
@@ -868,12 +897,12 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
868
897
|
|
|
869
898
|
if (this.config.skipCollectingAttestations) {
|
|
870
899
|
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
871
|
-
const attestations = await this.validatorClient?.collectOwnAttestations(proposal);
|
|
900
|
+
const attestations = await this.validatorClient?.collectOwnAttestations(proposal, this.checkpointNumber);
|
|
872
901
|
return new CommitteeAttestationsAndSigners(orderAttestations(attestations ?? [], committee));
|
|
873
902
|
}
|
|
874
903
|
|
|
875
904
|
const attestationTimeAllowed = this.config.enforceTimeTable
|
|
876
|
-
? this.timetable.
|
|
905
|
+
? this.timetable.getCheckpointAttestationDeadline()
|
|
877
906
|
: this.l1Constants.slotDuration;
|
|
878
907
|
const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
|
|
879
908
|
|
|
@@ -886,6 +915,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
886
915
|
proposal,
|
|
887
916
|
numberOfRequiredAttestations,
|
|
888
917
|
attestationDeadline,
|
|
918
|
+
this.checkpointNumber,
|
|
889
919
|
);
|
|
890
920
|
|
|
891
921
|
collectedAttestationsCount = attestations.length;
|
|
@@ -1012,9 +1042,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1012
1042
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
1013
1043
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
1014
1044
|
* would never receive its own block without this explicit sync.
|
|
1045
|
+
*
|
|
1046
|
+
* In fisherman mode we skip this push: the fisherman builds blocks locally for validation
|
|
1047
|
+
* and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
|
|
1048
|
+
* whenever the real proposer's block arrives from L1.
|
|
1015
1049
|
*/
|
|
1016
1050
|
private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
|
|
1017
|
-
if (this.config.skipPushProposedBlocksToArchiver) {
|
|
1051
|
+
if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
|
|
1018
1052
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
1019
1053
|
blockNumber: block.number,
|
|
1020
1054
|
slot: block.header.globalVariables.slotNumber,
|
|
@@ -1075,56 +1109,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1075
1109
|
return false;
|
|
1076
1110
|
}
|
|
1077
1111
|
|
|
1078
|
-
/**
|
|
1079
|
-
* In times of congestion we need to simulate using the correct fee header override for the previous block
|
|
1080
|
-
* We calculate the correct fee header values.
|
|
1081
|
-
*
|
|
1082
|
-
* If we are in block 1, or the checkpoint we are querying does not exist, we return undefined. However
|
|
1083
|
-
* If we are pipelining - where this function is called, the grandparentCheckpointNumber should always exist
|
|
1084
|
-
* @param parentCheckpointNumber
|
|
1085
|
-
* @returns
|
|
1086
|
-
*/
|
|
1087
|
-
protected async computeForceProposedFeeHeader(parentCheckpointNumber: CheckpointNumber): Promise<
|
|
1088
|
-
| {
|
|
1089
|
-
checkpointNumber: CheckpointNumber;
|
|
1090
|
-
feeHeader: FeeHeader;
|
|
1091
|
-
}
|
|
1092
|
-
| undefined
|
|
1093
|
-
> {
|
|
1094
|
-
if (!this.proposedCheckpointData) {
|
|
1095
|
-
return undefined;
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
|
-
const rollup = this.publisher.rollupContract;
|
|
1099
|
-
const grandparentCheckpointNumber = CheckpointNumber(this.checkpointNumber - 2);
|
|
1100
|
-
try {
|
|
1101
|
-
const [grandparentCheckpoint, manaTarget] = await Promise.all([
|
|
1102
|
-
rollup.getCheckpoint(grandparentCheckpointNumber),
|
|
1103
|
-
rollup.getManaTarget(),
|
|
1104
|
-
]);
|
|
1105
|
-
|
|
1106
|
-
if (!grandparentCheckpoint || !grandparentCheckpoint.feeHeader) {
|
|
1107
|
-
this.log.error(
|
|
1108
|
-
`Grandparent checkpoint or its feeHeader is undefined for checkpointNumber=${grandparentCheckpointNumber.toString()}`,
|
|
1109
|
-
);
|
|
1110
|
-
return undefined;
|
|
1111
|
-
} else {
|
|
1112
|
-
const parentFeeHeader = RollupContract.computeChildFeeHeader(
|
|
1113
|
-
grandparentCheckpoint.feeHeader,
|
|
1114
|
-
this.proposedCheckpointData.totalManaUsed,
|
|
1115
|
-
this.proposedCheckpointData.feeAssetPriceModifier,
|
|
1116
|
-
manaTarget,
|
|
1117
|
-
);
|
|
1118
|
-
return { checkpointNumber: parentCheckpointNumber, feeHeader: parentFeeHeader };
|
|
1119
|
-
}
|
|
1120
|
-
} catch (err) {
|
|
1121
|
-
this.log.error(
|
|
1122
|
-
`Failed to fetch grandparent checkpoint or mana target for checkpointNumber=${grandparentCheckpointNumber.toString()}: ${err}`,
|
|
1123
|
-
);
|
|
1124
|
-
return undefined;
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
1112
|
/** Waits until a specific time within the current slot */
|
|
1129
1113
|
@trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
|
|
1130
1114
|
protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
|