@aztec/sequencer-client 0.0.1-commit.e0f15ab9b → 0.0.1-commit.e304674f1
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 +1 -1
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +0 -4
- package/dest/global_variable_builder/global_builder.d.ts +3 -3
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +7 -4
- 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 +52 -31
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +106 -87
- package/dest/sequencer/checkpoint_proposal_job.d.ts +31 -10
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +179 -108
- 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/sequencer.d.ts +14 -4
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +67 -18
- package/dest/sequencer/timetable.d.ts +4 -1
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +15 -5
- package/package.json +27 -27
- package/src/client/sequencer-client.ts +0 -7
- package/src/global_variable_builder/global_builder.ts +15 -3
- package/src/publisher/sequencer-publisher-factory.ts +0 -3
- package/src/publisher/sequencer-publisher.ts +174 -124
- package/src/sequencer/README.md +81 -12
- package/src/sequencer/checkpoint_proposal_job.ts +215 -117
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/sequencer.ts +97 -20
- package/src/sequencer/timetable.ts +19 -8
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
+
import { type FeeHeader, RollupContract } from '@aztec/ethereum/contracts';
|
|
2
3
|
import {
|
|
3
4
|
BlockNumber,
|
|
4
5
|
CheckpointNumber,
|
|
@@ -30,8 +31,8 @@ import {
|
|
|
30
31
|
type L2BlockSource,
|
|
31
32
|
MaliciousCommitteeAttestationsAndSigners,
|
|
32
33
|
} from '@aztec/stdlib/block';
|
|
33
|
-
import { type Checkpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
34
|
-
import { getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
34
|
+
import { type Checkpoint, type ProposedCheckpointData, validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
35
|
+
import { computeQuorum, getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
35
36
|
import { Gas } from '@aztec/stdlib/gas';
|
|
36
37
|
import {
|
|
37
38
|
type BlockBuilderOptions,
|
|
@@ -67,6 +68,13 @@ import { SequencerState } from './utils.js';
|
|
|
67
68
|
/** How much time to sleep while waiting for min transactions to accumulate for a block */
|
|
68
69
|
const TXS_POLLING_MS = 500;
|
|
69
70
|
|
|
71
|
+
/** Result from proposeCheckpoint when a checkpoint was successfully built and attested. */
|
|
72
|
+
type CheckpointProposalResult = {
|
|
73
|
+
checkpoint: Checkpoint;
|
|
74
|
+
attestations: CommitteeAttestationsAndSigners;
|
|
75
|
+
attestationsSignature: Signature;
|
|
76
|
+
};
|
|
77
|
+
|
|
70
78
|
/**
|
|
71
79
|
* Handles the execution of a checkpoint proposal after the initial preparation phase.
|
|
72
80
|
* This includes building blocks, collecting attestations, and publishing the checkpoint to L1,
|
|
@@ -76,10 +84,15 @@ const TXS_POLLING_MS = 500;
|
|
|
76
84
|
export class CheckpointProposalJob implements Traceable {
|
|
77
85
|
protected readonly log: Logger;
|
|
78
86
|
|
|
87
|
+
/** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */
|
|
88
|
+
private pendingL1Submission: Promise<void> | undefined;
|
|
89
|
+
|
|
90
|
+
/** Fee header override computed during proposeCheckpoint, reused in enqueueCheckpointForSubmission. */
|
|
91
|
+
private computedForceProposedFeeHeader?: { checkpointNumber: CheckpointNumber; feeHeader: FeeHeader };
|
|
92
|
+
|
|
79
93
|
constructor(
|
|
80
94
|
private readonly slotNow: SlotNumber,
|
|
81
95
|
private readonly targetSlot: SlotNumber,
|
|
82
|
-
private readonly epochNow: EpochNumber,
|
|
83
96
|
private readonly targetEpoch: EpochNumber,
|
|
84
97
|
private readonly checkpointNumber: CheckpointNumber,
|
|
85
98
|
private readonly syncedToBlockNumber: BlockNumber,
|
|
@@ -107,6 +120,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
107
120
|
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
|
|
108
121
|
public readonly tracer: Tracer,
|
|
109
122
|
bindings?: LoggerBindings,
|
|
123
|
+
private readonly proposedCheckpointData?: ProposedCheckpointData,
|
|
110
124
|
) {
|
|
111
125
|
this.log = createLogger('sequencer:checkpoint-proposal', {
|
|
112
126
|
...bindings,
|
|
@@ -114,19 +128,17 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
114
128
|
});
|
|
115
129
|
}
|
|
116
130
|
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
/** The wall-clock epoch. */
|
|
123
|
-
private get epoch(): EpochNumber {
|
|
124
|
-
return this.epochNow;
|
|
131
|
+
/** Awaits the pending L1 submission if one is in progress. Call during shutdown. */
|
|
132
|
+
public async awaitPendingSubmission(): Promise<void> {
|
|
133
|
+
this.log.info('Awaiting pending L1 payload submission');
|
|
134
|
+
await this.pendingL1Submission;
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
/**
|
|
128
138
|
* Executes the checkpoint proposal job.
|
|
129
|
-
*
|
|
139
|
+
* Builds blocks, collects attestations, enqueues requests, and schedules L1 submission as a
|
|
140
|
+
* background task so the work loop can return to IDLE immediately.
|
|
141
|
+
* Returns the built checkpoint if successful, undefined otherwise.
|
|
130
142
|
*/
|
|
131
143
|
@trackSpan('CheckpointProposalJob.execute')
|
|
132
144
|
public async execute(): Promise<Checkpoint | undefined> {
|
|
@@ -145,8 +157,10 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
145
157
|
this.log,
|
|
146
158
|
).enqueueVotes();
|
|
147
159
|
|
|
148
|
-
// Build and propose the checkpoint.
|
|
149
|
-
|
|
160
|
+
// Build and propose the checkpoint. Builds blocks, broadcasts, collects attestations, and signs.
|
|
161
|
+
// Does NOT enqueue to L1 yet — that happens after the pipeline sleep.
|
|
162
|
+
const proposalResult = await this.proposeCheckpoint();
|
|
163
|
+
const checkpoint = proposalResult?.checkpoint;
|
|
150
164
|
|
|
151
165
|
// Wait until the voting promises have resolved, so all requests are enqueued (not sent)
|
|
152
166
|
await Promise.all(votesPromises);
|
|
@@ -161,41 +175,83 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
161
175
|
return;
|
|
162
176
|
}
|
|
163
177
|
|
|
164
|
-
//
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
slot
|
|
170
|
-
|
|
171
|
-
|
|
178
|
+
// Enqueue the checkpoint for L1 submission
|
|
179
|
+
if (proposalResult) {
|
|
180
|
+
try {
|
|
181
|
+
await this.enqueueCheckpointForSubmission(proposalResult);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
this.log.error(`Failed to enqueue checkpoint for L1 submission at slot ${this.targetSlot}`, err);
|
|
184
|
+
// Continue to sendRequestsAt so votes are still sent
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Compute the earliest time to submit: pipeline slot start when pipelining, now otherwise.
|
|
189
|
+
const submitAfter = this.epochCache.isProposerPipeliningEnabled()
|
|
190
|
+
? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000)
|
|
191
|
+
: new Date(this.dateProvider.now());
|
|
192
|
+
|
|
193
|
+
// Schedule L1 submission in the background so the work loop returns immediately.
|
|
194
|
+
// The publisher will sleep until submitAfter, then send the bundled requests.
|
|
195
|
+
// The promise is stored so it can be awaited during shutdown.
|
|
196
|
+
this.pendingL1Submission = this.publisher
|
|
197
|
+
.sendRequestsAt(submitAfter)
|
|
198
|
+
.then(async l1Response => {
|
|
199
|
+
const proposedAction = l1Response?.successfulActions.find(a => a === 'propose');
|
|
200
|
+
if (proposedAction) {
|
|
201
|
+
this.eventEmitter.emit('checkpoint-published', { checkpoint: this.checkpointNumber, slot: this.targetSlot });
|
|
202
|
+
const coinbase = checkpoint?.header.coinbase;
|
|
203
|
+
await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
|
|
204
|
+
} else if (checkpoint) {
|
|
205
|
+
this.eventEmitter.emit('checkpoint-publish-failed', { ...l1Response, slot: this.targetSlot });
|
|
206
|
+
|
|
207
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
208
|
+
this.metrics.recordPipelineDiscard();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
.catch(err => {
|
|
213
|
+
this.log.error(`Background L1 submission failed for slot ${this.targetSlot}`, err);
|
|
214
|
+
if (checkpoint) {
|
|
215
|
+
this.eventEmitter.emit('checkpoint-publish-failed', { slot: this.targetSlot });
|
|
216
|
+
|
|
217
|
+
if (this.epochCache.isProposerPipeliningEnabled()) {
|
|
218
|
+
this.metrics.recordPipelineDiscard();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
172
221
|
});
|
|
173
|
-
await sleepUntil(new Date(Number(submissionSlotTimestamp) * 1000), this.dateProvider.nowAsDate());
|
|
174
222
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
223
|
+
// Return the built checkpoint immediately — the work loop is now unblocked
|
|
224
|
+
return checkpoint;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Enqueues the checkpoint for L1 submission. Called after pipeline sleep in execute(). */
|
|
228
|
+
private async enqueueCheckpointForSubmission(result: CheckpointProposalResult): Promise<void> {
|
|
229
|
+
const { checkpoint, attestations, attestationsSignature } = result;
|
|
230
|
+
|
|
231
|
+
this.setStateFn(SequencerState.PUBLISHING_CHECKPOINT, this.targetSlot);
|
|
232
|
+
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
233
|
+
const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
|
|
234
|
+
const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
|
|
235
|
+
|
|
236
|
+
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
237
|
+
if (
|
|
238
|
+
this.config.skipPublishingCheckpointsPercent !== undefined &&
|
|
239
|
+
this.config.skipPublishingCheckpointsPercent > 0
|
|
240
|
+
) {
|
|
241
|
+
const roll = Math.max(0, randomInt(100));
|
|
242
|
+
if (roll < this.config.skipPublishingCheckpointsPercent) {
|
|
180
243
|
this.log.warn(
|
|
181
|
-
`
|
|
244
|
+
`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${roll}`,
|
|
182
245
|
);
|
|
183
|
-
return
|
|
246
|
+
return;
|
|
184
247
|
}
|
|
185
248
|
}
|
|
186
249
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const coinbase = checkpoint?.header.coinbase;
|
|
193
|
-
await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
|
|
194
|
-
return checkpoint;
|
|
195
|
-
} else if (checkpoint) {
|
|
196
|
-
this.eventEmitter.emit('checkpoint-publish-failed', { ...l1Response, slot: this.slot });
|
|
197
|
-
return undefined;
|
|
198
|
-
}
|
|
250
|
+
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
251
|
+
txTimeoutAt,
|
|
252
|
+
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
253
|
+
forceProposedFeeHeader: this.computedForceProposedFeeHeader,
|
|
254
|
+
});
|
|
199
255
|
}
|
|
200
256
|
|
|
201
257
|
@trackSpan('CheckpointProposalJob.proposeCheckpoint', function () {
|
|
@@ -205,7 +261,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
205
261
|
[Attributes.SLOT_NUMBER]: this.targetSlot,
|
|
206
262
|
};
|
|
207
263
|
})
|
|
208
|
-
private async proposeCheckpoint(): Promise<
|
|
264
|
+
private async proposeCheckpoint(): Promise<CheckpointProposalResult | undefined> {
|
|
209
265
|
try {
|
|
210
266
|
// Get operator configured coinbase and fee recipient for this attestor
|
|
211
267
|
const coinbase = this.validatorClient.getCoinbaseForAttestor(this.attestorAddress);
|
|
@@ -214,7 +270,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
214
270
|
// Start the checkpoint
|
|
215
271
|
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot);
|
|
216
272
|
this.log.info(`Starting checkpoint proposal`, {
|
|
217
|
-
buildSlot: this.
|
|
273
|
+
buildSlot: this.slotNow,
|
|
218
274
|
submissionSlot: this.targetSlot,
|
|
219
275
|
pipelining: this.epochCache.isProposerPipeliningEnabled(),
|
|
220
276
|
proposer: this.proposer?.toString(),
|
|
@@ -227,11 +283,25 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
227
283
|
this.publisher.enqueueInvalidateCheckpoint(this.invalidateCheckpoint);
|
|
228
284
|
}
|
|
229
285
|
|
|
230
|
-
// Create checkpoint builder for the slot
|
|
286
|
+
// Create checkpoint builder for the slot.
|
|
287
|
+
// When pipelining, force the proposed checkpoint number and fee header to our parent so the
|
|
288
|
+
// fee computation sees the same chain tip that L1 will see once the previous pipelined checkpoint lands.
|
|
289
|
+
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
290
|
+
const parentCheckpointNumber = isPipelining ? CheckpointNumber(this.checkpointNumber - 1) : undefined;
|
|
291
|
+
|
|
292
|
+
// Compute the parent's fee header override when pipelining
|
|
293
|
+
if (isPipelining && this.proposedCheckpointData) {
|
|
294
|
+
this.computedForceProposedFeeHeader = await this.computeForceProposedFeeHeader(parentCheckpointNumber!);
|
|
295
|
+
}
|
|
296
|
+
|
|
231
297
|
const checkpointGlobalVariables = await this.globalsBuilder.buildCheckpointGlobalVariables(
|
|
232
298
|
coinbase,
|
|
233
299
|
feeRecipient,
|
|
234
300
|
this.targetSlot,
|
|
301
|
+
{
|
|
302
|
+
forcePendingCheckpointNumber: parentCheckpointNumber,
|
|
303
|
+
forceProposedFeeHeader: this.computedForceProposedFeeHeader,
|
|
304
|
+
},
|
|
235
305
|
);
|
|
236
306
|
|
|
237
307
|
// Collect L1 to L2 messages for the checkpoint and compute their hash
|
|
@@ -272,7 +342,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
272
342
|
};
|
|
273
343
|
|
|
274
344
|
let blocksInCheckpoint: L2Block[] = [];
|
|
275
|
-
let blockPendingBroadcast:
|
|
345
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
276
346
|
const checkpointBuildTimer = new Timer();
|
|
277
347
|
|
|
278
348
|
try {
|
|
@@ -326,7 +396,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
326
396
|
maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
|
|
327
397
|
});
|
|
328
398
|
} catch (err) {
|
|
329
|
-
this.log.error(`Built an invalid checkpoint at slot ${this.
|
|
399
|
+
this.log.error(`Built an invalid checkpoint at slot ${this.slotNow} (skipping proposal)`, err, {
|
|
330
400
|
checkpoint: checkpoint.header.toInspect(),
|
|
331
401
|
});
|
|
332
402
|
return undefined;
|
|
@@ -352,22 +422,19 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
352
422
|
},
|
|
353
423
|
);
|
|
354
424
|
this.metrics.recordCheckpointSuccess();
|
|
355
|
-
return
|
|
425
|
+
return {
|
|
426
|
+
checkpoint,
|
|
427
|
+
attestations: CommitteeAttestationsAndSigners.empty(),
|
|
428
|
+
attestationsSignature: Signature.empty(),
|
|
429
|
+
};
|
|
356
430
|
}
|
|
357
431
|
|
|
358
|
-
// Include the block pending broadcast in the checkpoint proposal if any
|
|
359
|
-
const lastBlock = blockPendingBroadcast && {
|
|
360
|
-
blockHeader: blockPendingBroadcast.block.header,
|
|
361
|
-
indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
|
|
362
|
-
txs: blockPendingBroadcast.txs,
|
|
363
|
-
};
|
|
364
|
-
|
|
365
432
|
// Create the checkpoint proposal and broadcast it
|
|
366
433
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
367
434
|
checkpoint.header,
|
|
368
435
|
checkpoint.archive.root,
|
|
369
436
|
feeAssetPriceModifier,
|
|
370
|
-
|
|
437
|
+
blockPendingBroadcast,
|
|
371
438
|
this.proposer,
|
|
372
439
|
checkpointProposalOptions,
|
|
373
440
|
);
|
|
@@ -400,39 +467,15 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
400
467
|
throw err;
|
|
401
468
|
}
|
|
402
469
|
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
406
|
-
const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
|
|
407
|
-
const txTimeoutAt = new Date((submissionSlotStart + aztecSlotDuration) * 1000);
|
|
408
|
-
|
|
409
|
-
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
410
|
-
if (
|
|
411
|
-
this.config.skipPublishingCheckpointsPercent !== undefined &&
|
|
412
|
-
this.config.skipPublishingCheckpointsPercent > 0
|
|
413
|
-
) {
|
|
414
|
-
const result = Math.max(0, randomInt(100));
|
|
415
|
-
if (result < this.config.skipPublishingCheckpointsPercent) {
|
|
416
|
-
this.log.warn(
|
|
417
|
-
`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`,
|
|
418
|
-
);
|
|
419
|
-
return checkpoint;
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
424
|
-
txTimeoutAt,
|
|
425
|
-
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
return checkpoint;
|
|
470
|
+
// Return the result for the caller to enqueue after the pipeline sleep
|
|
471
|
+
return { checkpoint, attestations, attestationsSignature };
|
|
429
472
|
} catch (err) {
|
|
430
473
|
if (err && (err instanceof DutyAlreadySignedError || err instanceof SlashingProtectionError)) {
|
|
431
474
|
// swallow this error. It's already been logged by a function deeper in the stack
|
|
432
475
|
return undefined;
|
|
433
476
|
}
|
|
434
477
|
|
|
435
|
-
this.log.error(`Error building checkpoint at slot ${this.
|
|
478
|
+
this.log.error(`Error building checkpoint at slot ${this.targetSlot}`, err);
|
|
436
479
|
return undefined;
|
|
437
480
|
}
|
|
438
481
|
}
|
|
@@ -448,14 +491,14 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
448
491
|
blockProposalOptions: BlockProposalOptions,
|
|
449
492
|
): Promise<{
|
|
450
493
|
blocksInCheckpoint: L2Block[];
|
|
451
|
-
blockPendingBroadcast:
|
|
494
|
+
blockPendingBroadcast: BlockProposal | undefined;
|
|
452
495
|
}> {
|
|
453
496
|
const blocksInCheckpoint: L2Block[] = [];
|
|
454
497
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
455
498
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
456
499
|
|
|
457
500
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
458
|
-
let blockPendingBroadcast:
|
|
501
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
459
502
|
|
|
460
503
|
while (true) {
|
|
461
504
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
@@ -488,19 +531,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
488
531
|
txHashesAlreadyIncluded,
|
|
489
532
|
});
|
|
490
533
|
|
|
491
|
-
//
|
|
492
|
-
if (
|
|
493
|
-
// If
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
534
|
+
// If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
|
|
535
|
+
if ('failure' in buildResult) {
|
|
536
|
+
// If this was the last subslot, or we're running with a single block per slot, we're done
|
|
537
|
+
if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
// Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
|
|
497
541
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
498
542
|
continue;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// If there was an error building the block, we just exit the loop and give up the rest of the slot.
|
|
546
|
+
// We don't want to risk building more blocks if something went wrong.
|
|
547
|
+
if ('error' in buildResult) {
|
|
504
548
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
505
549
|
this.log.warn(`Halting block building for slot ${this.targetSlot}`, {
|
|
506
550
|
slot: this.targetSlot,
|
|
@@ -515,30 +559,26 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
515
559
|
blocksInCheckpoint.push(block);
|
|
516
560
|
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
517
561
|
|
|
518
|
-
//
|
|
519
|
-
|
|
562
|
+
// Sign the block proposal. This will throw if HA signing fails.
|
|
563
|
+
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
564
|
+
|
|
565
|
+
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal,
|
|
566
|
+
// so we avoid polluting our archive with a block that would fail.
|
|
567
|
+
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
568
|
+
// If this throws, we abort the entire checkpoint.
|
|
569
|
+
await this.syncProposedBlockToArchiver(block);
|
|
570
|
+
|
|
571
|
+
// If this is the last block, do not broadcast it, since it will be included in the checkpoint proposal.
|
|
520
572
|
if (timingInfo.isLastBlock) {
|
|
521
|
-
await this.syncProposedBlockToArchiver(block);
|
|
522
573
|
this.log.verbose(`Completed final block ${blockNumber} for slot ${this.targetSlot}`, {
|
|
523
574
|
slot: this.targetSlot,
|
|
524
575
|
blockNumber,
|
|
525
576
|
blocksBuilt,
|
|
526
577
|
});
|
|
527
|
-
blockPendingBroadcast =
|
|
578
|
+
blockPendingBroadcast = proposal;
|
|
528
579
|
break;
|
|
529
580
|
}
|
|
530
581
|
|
|
531
|
-
// Broadcast the block proposal (unless we're in fisherman mode) unless the block is the last one,
|
|
532
|
-
// in which case we'll broadcast it along with the checkpoint at the end of the loop.
|
|
533
|
-
// Note that we only send the block to the archiver if we manage to create the proposal, so if there's
|
|
534
|
-
// a HA error we don't pollute our archiver with a block that won't make it to the chain.
|
|
535
|
-
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
536
|
-
|
|
537
|
-
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal.
|
|
538
|
-
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
539
|
-
// If this throws, we abort the entire checkpoint.
|
|
540
|
-
await this.syncProposedBlockToArchiver(block);
|
|
541
|
-
|
|
542
582
|
// Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
|
|
543
583
|
proposal && (await this.p2pClient.broadcastProposal(proposal));
|
|
544
584
|
|
|
@@ -598,7 +638,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
598
638
|
buildDeadline: Date | undefined;
|
|
599
639
|
txHashesAlreadyIncluded: Set<string>;
|
|
600
640
|
},
|
|
601
|
-
): Promise<
|
|
641
|
+
): Promise<
|
|
642
|
+
{ block: L2Block; usedTxs: Tx[] } | { failure: 'insufficient-txs' | 'insufficient-valid-txs' } | { error: Error }
|
|
643
|
+
> {
|
|
602
644
|
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
603
645
|
opts;
|
|
604
646
|
|
|
@@ -617,7 +659,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
617
659
|
);
|
|
618
660
|
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.targetSlot });
|
|
619
661
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
620
|
-
return
|
|
662
|
+
return { failure: 'insufficient-txs' };
|
|
621
663
|
}
|
|
622
664
|
|
|
623
665
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
@@ -680,7 +722,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
680
722
|
slot: this.targetSlot,
|
|
681
723
|
});
|
|
682
724
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
683
|
-
return
|
|
725
|
+
return { failure: 'insufficient-valid-txs' };
|
|
684
726
|
}
|
|
685
727
|
|
|
686
728
|
// Block creation succeeded, emit stats and metrics
|
|
@@ -702,6 +744,8 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
702
744
|
{ blockHash, txHashes, manaPerSec, ...blockStats },
|
|
703
745
|
);
|
|
704
746
|
|
|
747
|
+
// `slot` is the target/submission slot (may be one ahead when pipelining),
|
|
748
|
+
// `buildSlot` is the wall-clock slot during which the block was actually built.
|
|
705
749
|
this.eventEmitter.emit('block-proposed', {
|
|
706
750
|
blockNumber: block.number,
|
|
707
751
|
slot: this.targetSlot,
|
|
@@ -810,7 +854,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
810
854
|
this.log.debug(`Attesting committee length is ${committee.length}`, { committee });
|
|
811
855
|
}
|
|
812
856
|
|
|
813
|
-
const numberOfRequiredAttestations =
|
|
857
|
+
const numberOfRequiredAttestations = computeQuorum(committee.length);
|
|
814
858
|
|
|
815
859
|
if (this.config.skipCollectingAttestations) {
|
|
816
860
|
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
@@ -958,9 +1002,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
958
1002
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
959
1003
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
960
1004
|
* would never receive its own block without this explicit sync.
|
|
1005
|
+
*
|
|
1006
|
+
* In fisherman mode we skip this push: the fisherman builds blocks locally for validation
|
|
1007
|
+
* and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
|
|
1008
|
+
* whenever the real proposer's block arrives from L1.
|
|
961
1009
|
*/
|
|
962
1010
|
private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
|
|
963
|
-
if (this.config.skipPushProposedBlocksToArchiver
|
|
1011
|
+
if (this.config.skipPushProposedBlocksToArchiver || this.config.fishermanMode) {
|
|
964
1012
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
965
1013
|
blockNumber: block.number,
|
|
966
1014
|
slot: block.header.globalVariables.slotNumber,
|
|
@@ -1021,6 +1069,56 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1021
1069
|
return false;
|
|
1022
1070
|
}
|
|
1023
1071
|
|
|
1072
|
+
/**
|
|
1073
|
+
* In times of congestion we need to simulate using the correct fee header override for the previous block
|
|
1074
|
+
* We calculate the correct fee header values.
|
|
1075
|
+
*
|
|
1076
|
+
* If we are in block 1, or the checkpoint we are querying does not exist, we return undefined. However
|
|
1077
|
+
* If we are pipelining - where this function is called, the grandparentCheckpointNumber should always exist
|
|
1078
|
+
* @param parentCheckpointNumber
|
|
1079
|
+
* @returns
|
|
1080
|
+
*/
|
|
1081
|
+
protected async computeForceProposedFeeHeader(parentCheckpointNumber: CheckpointNumber): Promise<
|
|
1082
|
+
| {
|
|
1083
|
+
checkpointNumber: CheckpointNumber;
|
|
1084
|
+
feeHeader: FeeHeader;
|
|
1085
|
+
}
|
|
1086
|
+
| undefined
|
|
1087
|
+
> {
|
|
1088
|
+
if (!this.proposedCheckpointData) {
|
|
1089
|
+
return undefined;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const rollup = this.publisher.rollupContract;
|
|
1093
|
+
const grandparentCheckpointNumber = CheckpointNumber(this.checkpointNumber - 2);
|
|
1094
|
+
try {
|
|
1095
|
+
const [grandparentCheckpoint, manaTarget] = await Promise.all([
|
|
1096
|
+
rollup.getCheckpoint(grandparentCheckpointNumber),
|
|
1097
|
+
rollup.getManaTarget(),
|
|
1098
|
+
]);
|
|
1099
|
+
|
|
1100
|
+
if (!grandparentCheckpoint || !grandparentCheckpoint.feeHeader) {
|
|
1101
|
+
this.log.error(
|
|
1102
|
+
`Grandparent checkpoint or its feeHeader is undefined for checkpointNumber=${grandparentCheckpointNumber.toString()}`,
|
|
1103
|
+
);
|
|
1104
|
+
return undefined;
|
|
1105
|
+
} else {
|
|
1106
|
+
const parentFeeHeader = RollupContract.computeChildFeeHeader(
|
|
1107
|
+
grandparentCheckpoint.feeHeader,
|
|
1108
|
+
this.proposedCheckpointData.totalManaUsed,
|
|
1109
|
+
this.proposedCheckpointData.feeAssetPriceModifier,
|
|
1110
|
+
manaTarget,
|
|
1111
|
+
);
|
|
1112
|
+
return { checkpointNumber: parentCheckpointNumber, feeHeader: parentFeeHeader };
|
|
1113
|
+
}
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
this.log.error(
|
|
1116
|
+
`Failed to fetch grandparent checkpoint or mana target for checkpointNumber=${grandparentCheckpointNumber.toString()}: ${err}`,
|
|
1117
|
+
);
|
|
1118
|
+
return undefined;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1024
1122
|
/** Waits until a specific time within the current slot */
|
|
1025
1123
|
@trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
|
|
1026
1124
|
protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
|
|
@@ -1035,7 +1133,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1035
1133
|
}
|
|
1036
1134
|
|
|
1037
1135
|
private getSlotStartBuildTimestamp(): number {
|
|
1038
|
-
return getSlotStartBuildTimestamp(this.
|
|
1136
|
+
return getSlotStartBuildTimestamp(this.slotNow, this.l1Constants);
|
|
1039
1137
|
}
|
|
1040
1138
|
|
|
1041
1139
|
private getSecondsIntoSlot(): number {
|
|
@@ -2,7 +2,6 @@ import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
|
2
2
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
3
|
import type { Logger } from '@aztec/foundation/log';
|
|
4
4
|
import type { SlasherClientInterface } from '@aztec/slasher';
|
|
5
|
-
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
6
5
|
import type { ResolvedSequencerConfig } from '@aztec/stdlib/interfaces/server';
|
|
7
6
|
import type { ValidatorClient } from '@aztec/validator-client';
|
|
8
7
|
import { DutyAlreadySignedError } from '@aztec/validator-ha-signer/errors';
|
|
@@ -18,7 +17,6 @@ import type { SequencerRollupConstants } from './types.js';
|
|
|
18
17
|
* Handles governance and slashing voting for a given slot.
|
|
19
18
|
*/
|
|
20
19
|
export class CheckpointVoter {
|
|
21
|
-
private slotTimestamp: bigint;
|
|
22
20
|
private governanceSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
|
|
23
21
|
private slashingSigner: (msg: TypedDataDefinition) => Promise<`0x${string}`>;
|
|
24
22
|
|
|
@@ -33,8 +31,6 @@ export class CheckpointVoter {
|
|
|
33
31
|
private readonly metrics: SequencerMetrics,
|
|
34
32
|
private readonly log: Logger,
|
|
35
33
|
) {
|
|
36
|
-
this.slotTimestamp = getTimestampForSlot(this.slot, this.l1Constants);
|
|
37
|
-
|
|
38
34
|
// Create separate signers with appropriate duty contexts for governance and slashing votes
|
|
39
35
|
// These use HA protection to ensure only one node signs per slot/duty
|
|
40
36
|
const governanceContext: SigningContext = { slot: this.slot, dutyType: DutyType.GOVERNANCE_VOTE };
|
|
@@ -77,7 +73,6 @@ export class CheckpointVoter {
|
|
|
77
73
|
return await this.publisher.enqueueGovernanceCastSignal(
|
|
78
74
|
governanceProposerPayload,
|
|
79
75
|
this.slot,
|
|
80
|
-
this.slotTimestamp,
|
|
81
76
|
this.attestorAddress,
|
|
82
77
|
this.governanceSigner,
|
|
83
78
|
);
|
|
@@ -108,13 +103,7 @@ export class CheckpointVoter {
|
|
|
108
103
|
|
|
109
104
|
this.metrics.recordSlashingAttempt(actions.length);
|
|
110
105
|
|
|
111
|
-
return await this.publisher.enqueueSlashingActions(
|
|
112
|
-
actions,
|
|
113
|
-
this.slot,
|
|
114
|
-
this.slotTimestamp,
|
|
115
|
-
this.attestorAddress,
|
|
116
|
-
this.slashingSigner,
|
|
117
|
-
);
|
|
106
|
+
return await this.publisher.enqueueSlashingActions(actions, this.slot, this.attestorAddress, this.slashingSigner);
|
|
118
107
|
} catch (err) {
|
|
119
108
|
if (err instanceof DutyAlreadySignedError) {
|
|
120
109
|
this.log.info(`Slashing vote already signed by another node`, {
|