@aztec/sequencer-client 0.0.1-commit.23b0eb0 → 0.0.1-commit.2448fdb
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 +16 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +56 -23
- package/dest/config.d.ts +25 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +46 -28
- package/dest/global_variable_builder/global_builder.d.ts +14 -10
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +22 -21
- package/dest/global_variable_builder/index.d.ts +2 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/publisher/config.d.ts +31 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +101 -42
- package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +13 -2
- package/dest/publisher/sequencer-publisher.d.ts +29 -13
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +106 -57
- package/dest/sequencer/checkpoint_proposal_job.d.ts +6 -4
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +139 -80
- 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/metrics.d.ts +17 -5
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +86 -15
- package/dest/sequencer/sequencer.d.ts +27 -13
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +42 -42
- package/dest/sequencer/timetable.d.ts +4 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +7 -11
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +3 -5
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +11 -11
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +47 -34
- package/dest/test/utils.d.ts +3 -3
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +5 -4
- package/package.json +28 -28
- package/src/client/sequencer-client.ts +78 -21
- package/src/config.ts +61 -38
- package/src/global_variable_builder/global_builder.ts +23 -24
- package/src/global_variable_builder/index.ts +1 -1
- package/src/publisher/config.ts +112 -43
- package/src/publisher/sequencer-publisher-factory.ts +23 -6
- package/src/publisher/sequencer-publisher.ts +123 -71
- package/src/sequencer/checkpoint_proposal_job.ts +213 -109
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/metrics.ts +92 -18
- package/src/sequencer/sequencer.ts +53 -48
- package/src/sequencer/timetable.ts +13 -12
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +65 -49
- package/src/test/utils.ts +5 -2
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
|
|
2
|
-
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
|
|
3
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
2
|
import {
|
|
5
3
|
BlockNumber,
|
|
@@ -9,6 +7,11 @@ import {
|
|
|
9
7
|
SlotNumber,
|
|
10
8
|
} from '@aztec/foundation/branded-types';
|
|
11
9
|
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
10
|
+
import {
|
|
11
|
+
flipSignature,
|
|
12
|
+
generateRecoverableSignature,
|
|
13
|
+
generateUnrecoverableSignature,
|
|
14
|
+
} from '@aztec/foundation/crypto/secp256k1-signer';
|
|
12
15
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
13
16
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
14
17
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
@@ -27,18 +30,23 @@ import {
|
|
|
27
30
|
type L2BlockSource,
|
|
28
31
|
MaliciousCommitteeAttestationsAndSigners,
|
|
29
32
|
} from '@aztec/stdlib/block';
|
|
30
|
-
import type
|
|
33
|
+
import { type Checkpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
31
34
|
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
32
35
|
import { Gas } from '@aztec/stdlib/gas';
|
|
33
36
|
import {
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
type BlockBuilderOptions,
|
|
38
|
+
InsufficientValidTxsError,
|
|
36
39
|
type ResolvedSequencerConfig,
|
|
37
40
|
type WorldStateSynchronizer,
|
|
38
41
|
} from '@aztec/stdlib/interfaces/server';
|
|
39
42
|
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
40
|
-
import type {
|
|
41
|
-
|
|
43
|
+
import type {
|
|
44
|
+
BlockProposal,
|
|
45
|
+
BlockProposalOptions,
|
|
46
|
+
CheckpointProposal,
|
|
47
|
+
CheckpointProposalOptions,
|
|
48
|
+
} from '@aztec/stdlib/p2p';
|
|
49
|
+
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
42
50
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
43
51
|
import { type FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
44
52
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
@@ -129,7 +137,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
129
137
|
await Promise.all(votesPromises);
|
|
130
138
|
|
|
131
139
|
if (checkpoint) {
|
|
132
|
-
this.metrics.
|
|
140
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
@@ -186,18 +194,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
186
194
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
187
195
|
|
|
188
196
|
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
189
|
-
const
|
|
190
|
-
c => c.
|
|
191
|
-
|
|
192
|
-
|
|
197
|
+
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.epoch))
|
|
198
|
+
.filter(c => c.checkpointNumber < this.checkpointNumber)
|
|
199
|
+
.map(c => c.checkpointOutHash);
|
|
200
|
+
|
|
201
|
+
// Get the fee asset price modifier from the oracle
|
|
202
|
+
const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
|
|
193
203
|
|
|
194
204
|
// Create a long-lived forked world state for the checkpoint builder
|
|
195
|
-
using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
205
|
+
await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
196
206
|
|
|
197
207
|
// Create checkpoint builder for the entire slot
|
|
198
208
|
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
|
|
199
209
|
this.checkpointNumber,
|
|
200
210
|
checkpointGlobalVariables,
|
|
211
|
+
feeAssetPriceModifier,
|
|
201
212
|
l1ToL2Messages,
|
|
202
213
|
previousCheckpointOutHashes,
|
|
203
214
|
fork,
|
|
@@ -217,6 +228,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
217
228
|
|
|
218
229
|
let blocksInCheckpoint: L2Block[] = [];
|
|
219
230
|
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
231
|
+
const checkpointBuildTimer = new Timer();
|
|
220
232
|
|
|
221
233
|
try {
|
|
222
234
|
// Main loop: build blocks for the checkpoint
|
|
@@ -244,11 +256,45 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
244
256
|
return undefined;
|
|
245
257
|
}
|
|
246
258
|
|
|
259
|
+
const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
|
|
260
|
+
if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
|
|
261
|
+
this.log.warn(
|
|
262
|
+
`Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
|
|
263
|
+
{ slot: this.slot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
|
|
264
|
+
);
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
247
268
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
248
269
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
249
270
|
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
|
|
250
271
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
251
272
|
|
|
273
|
+
// Final validation: per-block limits are only checked if the operator set them explicitly.
|
|
274
|
+
// Otherwise, checkpoint-level budgets were already enforced by the redistribution logic.
|
|
275
|
+
try {
|
|
276
|
+
validateCheckpoint(checkpoint, {
|
|
277
|
+
rollupManaLimit: this.l1Constants.rollupManaLimit,
|
|
278
|
+
maxL2BlockGas: this.config.maxL2BlockGas,
|
|
279
|
+
maxDABlockGas: this.config.maxDABlockGas,
|
|
280
|
+
maxTxsPerBlock: this.config.maxTxsPerBlock,
|
|
281
|
+
maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
|
|
282
|
+
});
|
|
283
|
+
} catch (err) {
|
|
284
|
+
this.log.error(`Built an invalid checkpoint at slot ${this.slot} (skipping proposal)`, err, {
|
|
285
|
+
checkpoint: checkpoint.header.toInspect(),
|
|
286
|
+
});
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Record checkpoint-level build metrics
|
|
291
|
+
this.metrics.recordCheckpointBuild(
|
|
292
|
+
checkpointBuildTimer.ms(),
|
|
293
|
+
blocksInCheckpoint.length,
|
|
294
|
+
checkpoint.getStats().txCount,
|
|
295
|
+
Number(checkpoint.header.totalManaUsed.toBigInt()),
|
|
296
|
+
);
|
|
297
|
+
|
|
252
298
|
// Do not collect attestations nor publish to L1 in fisherman mode
|
|
253
299
|
if (this.config.fishermanMode) {
|
|
254
300
|
this.log.info(
|
|
@@ -275,6 +321,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
275
321
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
276
322
|
checkpoint.header,
|
|
277
323
|
checkpoint.archive.root,
|
|
324
|
+
feeAssetPriceModifier,
|
|
278
325
|
lastBlock,
|
|
279
326
|
this.proposer,
|
|
280
327
|
checkpointProposalOptions,
|
|
@@ -313,6 +360,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
313
360
|
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
314
361
|
const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
|
|
315
362
|
const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
|
|
363
|
+
|
|
364
|
+
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
365
|
+
if (
|
|
366
|
+
this.config.skipPublishingCheckpointsPercent !== undefined &&
|
|
367
|
+
this.config.skipPublishingCheckpointsPercent > 0
|
|
368
|
+
) {
|
|
369
|
+
const result = Math.max(0, randomInt(100));
|
|
370
|
+
if (result < this.config.skipPublishingCheckpointsPercent) {
|
|
371
|
+
this.log.warn(
|
|
372
|
+
`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`,
|
|
373
|
+
);
|
|
374
|
+
return checkpoint;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
316
378
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
317
379
|
txTimeoutAt,
|
|
318
380
|
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
@@ -346,9 +408,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
346
408
|
const blocksInCheckpoint: L2Block[] = [];
|
|
347
409
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
348
410
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
349
|
-
|
|
350
|
-
// Remaining blob fields available for blocks (checkpoint end marker already subtracted)
|
|
351
|
-
let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
|
|
411
|
+
const slot = this.slot;
|
|
352
412
|
|
|
353
413
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
354
414
|
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
@@ -362,11 +422,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
362
422
|
const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
|
|
363
423
|
|
|
364
424
|
if (!timingInfo.canStart) {
|
|
365
|
-
this.log.debug(`Not enough time left in slot to start another block`, {
|
|
366
|
-
slot: this.slot,
|
|
367
|
-
blocksBuilt,
|
|
368
|
-
secondsIntoSlot,
|
|
369
|
-
});
|
|
425
|
+
this.log.debug(`Not enough time left in slot to start another block`, { slot, blocksBuilt, secondsIntoSlot });
|
|
370
426
|
break;
|
|
371
427
|
}
|
|
372
428
|
|
|
@@ -382,7 +438,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
382
438
|
blockNumber,
|
|
383
439
|
indexWithinCheckpoint,
|
|
384
440
|
txHashesAlreadyIncluded,
|
|
385
|
-
remainingBlobFields,
|
|
386
441
|
});
|
|
387
442
|
|
|
388
443
|
// TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
|
|
@@ -399,56 +454,37 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
399
454
|
} else if ('error' in buildResult) {
|
|
400
455
|
// If there was an error building the block, just exit the loop and give up the rest of the slot
|
|
401
456
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
402
|
-
this.log.warn(`Halting block building for slot ${
|
|
403
|
-
slot: this.slot,
|
|
404
|
-
blocksBuilt,
|
|
405
|
-
error: buildResult.error,
|
|
406
|
-
});
|
|
457
|
+
this.log.warn(`Halting block building for slot ${slot}`, { slot, blocksBuilt, error: buildResult.error });
|
|
407
458
|
}
|
|
408
459
|
break;
|
|
409
460
|
}
|
|
410
461
|
|
|
411
|
-
const { block, usedTxs
|
|
462
|
+
const { block, usedTxs } = buildResult;
|
|
412
463
|
blocksInCheckpoint.push(block);
|
|
413
|
-
|
|
414
|
-
// Update remaining blob fields for the next block
|
|
415
|
-
remainingBlobFields = newRemainingBlobFields;
|
|
416
|
-
|
|
417
|
-
// Sync the proposed block to the archiver to make it available
|
|
418
|
-
// Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
|
|
419
|
-
// Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
|
|
420
|
-
// Fire and forget - don't block the critical path, but log errors
|
|
421
|
-
this.syncProposedBlockToArchiver(block).catch(err => {
|
|
422
|
-
this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
|
|
423
|
-
});
|
|
424
|
-
|
|
425
464
|
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
426
465
|
|
|
427
|
-
// If this is the last block,
|
|
466
|
+
// If this is the last block, send the proposed block to the archiver,
|
|
467
|
+
// and exit the loop now so we can build the checkpoint and start collecting attestations.
|
|
428
468
|
if (timingInfo.isLastBlock) {
|
|
429
|
-
this.
|
|
430
|
-
|
|
431
|
-
blockNumber,
|
|
432
|
-
blocksBuilt,
|
|
433
|
-
});
|
|
469
|
+
await this.syncProposedBlockToArchiver(block);
|
|
470
|
+
this.log.verbose(`Completed final block ${blockNumber} for slot ${slot}`, { slot, blockNumber, blocksBuilt });
|
|
434
471
|
blockPendingBroadcast = { block, txs: usedTxs };
|
|
435
472
|
break;
|
|
436
473
|
}
|
|
437
474
|
|
|
438
|
-
//
|
|
439
|
-
//
|
|
440
|
-
if
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
475
|
+
// Broadcast the block proposal (unless we're in fisherman mode) unless the block is the last one,
|
|
476
|
+
// in which case we'll broadcast it along with the checkpoint at the end of the loop.
|
|
477
|
+
// Note that we only send the block to the archiver if we manage to create the proposal, so if there's
|
|
478
|
+
// a HA error we don't pollute our archiver with a block that won't make it to the chain.
|
|
479
|
+
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
480
|
+
|
|
481
|
+
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal.
|
|
482
|
+
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
483
|
+
// If this throws, we abort the entire checkpoint.
|
|
484
|
+
await this.syncProposedBlockToArchiver(block);
|
|
485
|
+
|
|
486
|
+
// Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
|
|
487
|
+
proposal && (await this.p2pClient.broadcastProposal(proposal));
|
|
452
488
|
|
|
453
489
|
// Wait until the next block's start time
|
|
454
490
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
@@ -462,6 +498,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
462
498
|
return { blocksInCheckpoint, blockPendingBroadcast };
|
|
463
499
|
}
|
|
464
500
|
|
|
501
|
+
/** Creates a block proposal for a given block via the validator client (unless in fisherman mode) */
|
|
502
|
+
private createBlockProposal(
|
|
503
|
+
block: L2Block,
|
|
504
|
+
inHash: Fr,
|
|
505
|
+
usedTxs: Tx[],
|
|
506
|
+
blockProposalOptions: BlockProposalOptions,
|
|
507
|
+
): Promise<BlockProposal | undefined> {
|
|
508
|
+
if (this.config.fishermanMode) {
|
|
509
|
+
this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`);
|
|
510
|
+
return Promise.resolve(undefined);
|
|
511
|
+
}
|
|
512
|
+
return this.validatorClient.createBlockProposal(
|
|
513
|
+
block.header,
|
|
514
|
+
block.indexWithinCheckpoint,
|
|
515
|
+
inHash,
|
|
516
|
+
block.archive.root,
|
|
517
|
+
usedTxs,
|
|
518
|
+
this.proposer,
|
|
519
|
+
blockProposalOptions,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
465
523
|
/** Sleeps until it is time to produce the next block in the slot */
|
|
466
524
|
@trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
|
|
467
525
|
private async waitUntilNextSubslot(nextSubslotStart: number) {
|
|
@@ -481,18 +539,10 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
481
539
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
482
540
|
buildDeadline: Date | undefined;
|
|
483
541
|
txHashesAlreadyIncluded: Set<string>;
|
|
484
|
-
remainingBlobFields: number;
|
|
485
542
|
},
|
|
486
|
-
): Promise<{ block: L2Block; usedTxs: Tx[]
|
|
487
|
-
const {
|
|
488
|
-
|
|
489
|
-
forceCreate,
|
|
490
|
-
blockNumber,
|
|
491
|
-
indexWithinCheckpoint,
|
|
492
|
-
buildDeadline,
|
|
493
|
-
txHashesAlreadyIncluded,
|
|
494
|
-
remainingBlobFields,
|
|
495
|
-
} = opts;
|
|
543
|
+
): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
|
|
544
|
+
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
545
|
+
opts;
|
|
496
546
|
|
|
497
547
|
this.log.verbose(
|
|
498
548
|
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
|
|
@@ -501,8 +551,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
501
551
|
|
|
502
552
|
try {
|
|
503
553
|
// Wait until we have enough txs to build the block
|
|
504
|
-
const minTxs = this.
|
|
505
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
554
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
506
555
|
if (!canStartBuilding) {
|
|
507
556
|
this.log.warn(
|
|
508
557
|
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`,
|
|
@@ -516,7 +565,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
516
565
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
517
566
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
518
567
|
const pendingTxs = filter(
|
|
519
|
-
this.p2pClient.
|
|
568
|
+
this.p2pClient.iterateEligiblePendingTxs(),
|
|
520
569
|
tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
|
|
521
570
|
);
|
|
522
571
|
|
|
@@ -526,19 +575,26 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
526
575
|
);
|
|
527
576
|
this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
|
|
528
577
|
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const blockBuilderOptions:
|
|
578
|
+
// Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
|
|
579
|
+
// by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
|
|
580
|
+
// minValidTxs is passed into the builder so it can reject the block *before* updating state.
|
|
581
|
+
const minValidTxs = forceCreate ? 0 : (this.config.minValidTxsPerBlock ?? minTxs);
|
|
582
|
+
const blockBuilderOptions: BlockBuilderOptions = {
|
|
534
583
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
584
|
+
maxBlockGas:
|
|
585
|
+
this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
|
|
586
|
+
? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
|
|
587
|
+
: undefined,
|
|
538
588
|
deadline: buildDeadline,
|
|
589
|
+
isBuildingProposal: true,
|
|
590
|
+
minValidTxs,
|
|
591
|
+
maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
|
|
592
|
+
perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
|
|
539
593
|
};
|
|
540
594
|
|
|
541
|
-
// Actually build the block by executing txs
|
|
595
|
+
// Actually build the block by executing txs. The builder throws InsufficientValidTxsError
|
|
596
|
+
// if the number of successfully processed txs is below minValidTxs, ensuring state is not
|
|
597
|
+
// updated for blocks that will be discarded.
|
|
542
598
|
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
|
|
543
599
|
checkpointBuilder,
|
|
544
600
|
pendingTxs,
|
|
@@ -550,14 +606,16 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
550
606
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
551
607
|
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
552
608
|
|
|
553
|
-
|
|
554
|
-
// too long, then we may not get to minTxsPerBlock after executing public functions.
|
|
555
|
-
const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
|
|
556
|
-
const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
|
|
557
|
-
if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
|
|
609
|
+
if (buildResult.status === 'insufficient-valid-txs') {
|
|
558
610
|
this.log.warn(
|
|
559
611
|
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
|
|
560
|
-
{
|
|
612
|
+
{
|
|
613
|
+
slot: this.slot,
|
|
614
|
+
blockNumber,
|
|
615
|
+
numTxs: buildResult.processedCount,
|
|
616
|
+
indexWithinCheckpoint,
|
|
617
|
+
minValidTxs,
|
|
618
|
+
},
|
|
561
619
|
);
|
|
562
620
|
this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
|
|
563
621
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
@@ -565,7 +623,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
565
623
|
}
|
|
566
624
|
|
|
567
625
|
// Block creation succeeded, emit stats and metrics
|
|
568
|
-
const {
|
|
626
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
569
627
|
|
|
570
628
|
const blockStats = {
|
|
571
629
|
eventName: 'l2-block-built',
|
|
@@ -576,7 +634,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
576
634
|
|
|
577
635
|
const blockHash = await block.hash();
|
|
578
636
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
579
|
-
const manaPerSec =
|
|
637
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
580
638
|
|
|
581
639
|
this.log.info(
|
|
582
640
|
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
|
|
@@ -584,9 +642,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
584
642
|
);
|
|
585
643
|
|
|
586
644
|
this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
|
|
587
|
-
this.metrics.recordBuiltBlock(blockBuildDuration,
|
|
645
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
588
646
|
|
|
589
|
-
return { block, usedTxs
|
|
647
|
+
return { block, usedTxs };
|
|
590
648
|
} catch (err: any) {
|
|
591
649
|
this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
|
|
592
650
|
this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
|
|
@@ -596,13 +654,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
596
654
|
}
|
|
597
655
|
}
|
|
598
656
|
|
|
599
|
-
/** Uses the checkpoint builder to build a block, catching
|
|
657
|
+
/** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
|
|
600
658
|
private async buildSingleBlockWithCheckpointBuilder(
|
|
601
659
|
checkpointBuilder: CheckpointBuilder,
|
|
602
660
|
pendingTxs: AsyncIterable<Tx>,
|
|
603
661
|
blockNumber: BlockNumber,
|
|
604
662
|
blockTimestamp: bigint,
|
|
605
|
-
blockBuilderOptions:
|
|
663
|
+
blockBuilderOptions: BlockBuilderOptions,
|
|
606
664
|
) {
|
|
607
665
|
try {
|
|
608
666
|
const workTimer = new Timer();
|
|
@@ -610,8 +668,12 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
610
668
|
const blockBuildDuration = workTimer.ms();
|
|
611
669
|
return { ...result, blockBuildDuration, status: 'success' as const };
|
|
612
670
|
} catch (err: unknown) {
|
|
613
|
-
if (isErrorClass(err,
|
|
614
|
-
return {
|
|
671
|
+
if (isErrorClass(err, InsufficientValidTxsError)) {
|
|
672
|
+
return {
|
|
673
|
+
failedTxs: err.failedTxs,
|
|
674
|
+
processedCount: err.processedCount,
|
|
675
|
+
status: 'insufficient-valid-txs' as const,
|
|
676
|
+
};
|
|
615
677
|
}
|
|
616
678
|
throw err;
|
|
617
679
|
}
|
|
@@ -624,7 +686,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
624
686
|
blockNumber: BlockNumber;
|
|
625
687
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
626
688
|
buildDeadline: Date | undefined;
|
|
627
|
-
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
689
|
+
}): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
|
|
628
690
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
629
691
|
|
|
630
692
|
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
@@ -641,7 +703,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
641
703
|
// If we're past deadline, or we have no deadline, give up
|
|
642
704
|
const now = this.dateProvider.nowAsDate();
|
|
643
705
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
644
|
-
return { canStartBuilding: false, availableTxs
|
|
706
|
+
return { canStartBuilding: false, availableTxs, minTxs };
|
|
645
707
|
}
|
|
646
708
|
|
|
647
709
|
// Wait a bit before checking again
|
|
@@ -650,11 +712,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
650
712
|
`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (have ${availableTxs} but need ${minTxs})`,
|
|
651
713
|
{ blockNumber, slot: this.slot, indexWithinCheckpoint },
|
|
652
714
|
);
|
|
653
|
-
await
|
|
715
|
+
await this.waitForTxsPollingInterval();
|
|
654
716
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
655
717
|
}
|
|
656
718
|
|
|
657
|
-
return { canStartBuilding: true, availableTxs };
|
|
719
|
+
return { canStartBuilding: true, availableTxs, minTxs };
|
|
658
720
|
}
|
|
659
721
|
|
|
660
722
|
/**
|
|
@@ -706,11 +768,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
706
768
|
|
|
707
769
|
collectedAttestationsCount = attestations.length;
|
|
708
770
|
|
|
771
|
+
// Trim attestations to minimum required to save L1 calldata gas
|
|
772
|
+
const localAddresses = this.validatorClient.getValidatorAddresses();
|
|
773
|
+
const trimmed = trimAttestations(
|
|
774
|
+
attestations,
|
|
775
|
+
numberOfRequiredAttestations,
|
|
776
|
+
this.attestorAddress,
|
|
777
|
+
localAddresses,
|
|
778
|
+
);
|
|
779
|
+
if (trimmed.length < attestations.length) {
|
|
780
|
+
this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
|
|
781
|
+
}
|
|
782
|
+
|
|
709
783
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
710
|
-
const sorted = orderAttestations(
|
|
784
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
711
785
|
|
|
712
786
|
// Manipulate the attestations if we've been configured to do so
|
|
713
|
-
if (
|
|
787
|
+
if (
|
|
788
|
+
this.config.injectFakeAttestation ||
|
|
789
|
+
this.config.injectHighSValueAttestation ||
|
|
790
|
+
this.config.injectUnrecoverableSignatureAttestation ||
|
|
791
|
+
this.config.shuffleAttestationOrdering
|
|
792
|
+
) {
|
|
714
793
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
715
794
|
}
|
|
716
795
|
|
|
@@ -739,7 +818,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
739
818
|
this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
|
|
740
819
|
);
|
|
741
820
|
|
|
742
|
-
if (
|
|
821
|
+
if (
|
|
822
|
+
this.config.injectFakeAttestation ||
|
|
823
|
+
this.config.injectHighSValueAttestation ||
|
|
824
|
+
this.config.injectUnrecoverableSignatureAttestation
|
|
825
|
+
) {
|
|
743
826
|
// Find non-empty attestations that are not from the proposer
|
|
744
827
|
const nonProposerIndices: number[] = [];
|
|
745
828
|
for (let i = 0; i < attestations.length; i++) {
|
|
@@ -749,8 +832,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
749
832
|
}
|
|
750
833
|
if (nonProposerIndices.length > 0) {
|
|
751
834
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
752
|
-
this.
|
|
753
|
-
|
|
835
|
+
if (this.config.injectHighSValueAttestation) {
|
|
836
|
+
this.log.warn(
|
|
837
|
+
`Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
838
|
+
);
|
|
839
|
+
unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
|
|
840
|
+
} else if (this.config.injectUnrecoverableSignatureAttestation) {
|
|
841
|
+
this.log.warn(
|
|
842
|
+
`Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
843
|
+
);
|
|
844
|
+
unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
|
|
845
|
+
} else {
|
|
846
|
+
this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
847
|
+
unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
|
|
848
|
+
}
|
|
754
849
|
}
|
|
755
850
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
756
851
|
}
|
|
@@ -779,16 +874,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
779
874
|
const failedTxData = failedTxs.map(fail => fail.tx);
|
|
780
875
|
const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
|
|
781
876
|
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
782
|
-
await this.p2pClient.
|
|
877
|
+
await this.p2pClient.handleFailedExecution(failedTxHashes);
|
|
783
878
|
}
|
|
784
879
|
|
|
785
880
|
/**
|
|
786
881
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
787
882
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
788
883
|
* would never receive its own block without this explicit sync.
|
|
884
|
+
*
|
|
885
|
+
* In fisherman mode we skip this push: the fisherman builds blocks locally for validation
|
|
886
|
+
* and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
|
|
887
|
+
* whenever the real proposer's block arrives from L1.
|
|
789
888
|
*/
|
|
790
889
|
private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
|
|
791
|
-
if (this.config.skipPushProposedBlocksToArchiver !== false) {
|
|
890
|
+
if (this.config.skipPushProposedBlocksToArchiver !== false || this.config.fishermanMode) {
|
|
792
891
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
793
892
|
blockNumber: block.number,
|
|
794
893
|
slot: block.header.globalVariables.slotNumber,
|
|
@@ -821,7 +920,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
821
920
|
slot: this.slot,
|
|
822
921
|
feeAnalysisId: feeAnalysis?.id,
|
|
823
922
|
});
|
|
824
|
-
this.metrics.
|
|
923
|
+
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
825
924
|
}
|
|
826
925
|
|
|
827
926
|
this.publisher.clearPendingRequests();
|
|
@@ -857,6 +956,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
857
956
|
await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
|
|
858
957
|
}
|
|
859
958
|
|
|
959
|
+
/** Waits the polling interval for transactions. Extracted for test overriding. */
|
|
960
|
+
protected async waitForTxsPollingInterval(): Promise<void> {
|
|
961
|
+
await sleep(TXS_POLLING_MS);
|
|
962
|
+
}
|
|
963
|
+
|
|
860
964
|
private getSlotStartBuildTimestamp(): number {
|
|
861
965
|
return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
|
|
862
966
|
}
|
|
@@ -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`, {
|