@aztec/sequencer-client 4.0.0-devnet.2-patch.3 → 4.0.0-devnet.3-patch.0
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 +38 -19
- package/dest/config.d.ts +24 -4
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +29 -16
- package/dest/global_variable_builder/global_builder.d.ts +13 -7
- 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/sequencer-publisher.d.ts +15 -8
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +65 -36
- package/dest/sequencer/checkpoint_proposal_job.d.ts +4 -4
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +98 -68
- 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 +12 -7
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +15 -17
- package/dest/sequencer/timetable.d.ts +4 -3
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +6 -7
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +7 -9
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +41 -30
- package/package.json +28 -28
- package/src/client/sequencer-client.ts +48 -14
- package/src/config.ts +35 -19
- package/src/global_variable_builder/global_builder.ts +22 -23
- package/src/global_variable_builder/index.ts +1 -1
- package/src/publisher/sequencer-publisher.ts +61 -44
- package/src/sequencer/checkpoint_proposal_job.ts +156 -98
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/sequencer.ts +16 -18
- package/src/sequencer/timetable.ts +7 -7
- package/src/sequencer/types.ts +1 -1
- package/src/test/mock_checkpoint_builder.ts +53 -48
|
@@ -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';
|
|
@@ -262,6 +270,23 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
262
270
|
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
|
|
263
271
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
264
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
|
+
|
|
265
290
|
// Record checkpoint-level build metrics
|
|
266
291
|
this.metrics.recordCheckpointBuild(
|
|
267
292
|
checkpointBuildTimer.ms(),
|
|
@@ -383,9 +408,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
383
408
|
const blocksInCheckpoint: L2Block[] = [];
|
|
384
409
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
385
410
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
386
|
-
|
|
387
|
-
// Remaining blob fields available for blocks (checkpoint end marker already subtracted)
|
|
388
|
-
let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
|
|
411
|
+
const slot = this.slot;
|
|
389
412
|
|
|
390
413
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
391
414
|
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
@@ -399,11 +422,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
399
422
|
const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
|
|
400
423
|
|
|
401
424
|
if (!timingInfo.canStart) {
|
|
402
|
-
this.log.debug(`Not enough time left in slot to start another block`, {
|
|
403
|
-
slot: this.slot,
|
|
404
|
-
blocksBuilt,
|
|
405
|
-
secondsIntoSlot,
|
|
406
|
-
});
|
|
425
|
+
this.log.debug(`Not enough time left in slot to start another block`, { slot, blocksBuilt, secondsIntoSlot });
|
|
407
426
|
break;
|
|
408
427
|
}
|
|
409
428
|
|
|
@@ -419,7 +438,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
419
438
|
blockNumber,
|
|
420
439
|
indexWithinCheckpoint,
|
|
421
440
|
txHashesAlreadyIncluded,
|
|
422
|
-
remainingBlobFields,
|
|
423
441
|
});
|
|
424
442
|
|
|
425
443
|
// TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
|
|
@@ -436,56 +454,37 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
436
454
|
} else if ('error' in buildResult) {
|
|
437
455
|
// If there was an error building the block, just exit the loop and give up the rest of the slot
|
|
438
456
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
439
|
-
this.log.warn(`Halting block building for slot ${
|
|
440
|
-
slot: this.slot,
|
|
441
|
-
blocksBuilt,
|
|
442
|
-
error: buildResult.error,
|
|
443
|
-
});
|
|
457
|
+
this.log.warn(`Halting block building for slot ${slot}`, { slot, blocksBuilt, error: buildResult.error });
|
|
444
458
|
}
|
|
445
459
|
break;
|
|
446
460
|
}
|
|
447
461
|
|
|
448
|
-
const { block, usedTxs
|
|
462
|
+
const { block, usedTxs } = buildResult;
|
|
449
463
|
blocksInCheckpoint.push(block);
|
|
450
|
-
|
|
451
|
-
// Update remaining blob fields for the next block
|
|
452
|
-
remainingBlobFields = newRemainingBlobFields;
|
|
453
|
-
|
|
454
|
-
// Sync the proposed block to the archiver to make it available
|
|
455
|
-
// Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
|
|
456
|
-
// Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
|
|
457
|
-
// Fire and forget - don't block the critical path, but log errors
|
|
458
|
-
this.syncProposedBlockToArchiver(block).catch(err => {
|
|
459
|
-
this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
|
|
460
|
-
});
|
|
461
|
-
|
|
462
464
|
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
463
465
|
|
|
464
|
-
// 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.
|
|
465
468
|
if (timingInfo.isLastBlock) {
|
|
466
|
-
this.
|
|
467
|
-
|
|
468
|
-
blockNumber,
|
|
469
|
-
blocksBuilt,
|
|
470
|
-
});
|
|
469
|
+
await this.syncProposedBlockToArchiver(block);
|
|
470
|
+
this.log.verbose(`Completed final block ${blockNumber} for slot ${slot}`, { slot, blockNumber, blocksBuilt });
|
|
471
471
|
blockPendingBroadcast = { block, txs: usedTxs };
|
|
472
472
|
break;
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
if
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
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));
|
|
489
488
|
|
|
490
489
|
// Wait until the next block's start time
|
|
491
490
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
@@ -499,6 +498,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
499
498
|
return { blocksInCheckpoint, blockPendingBroadcast };
|
|
500
499
|
}
|
|
501
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
|
+
|
|
502
523
|
/** Sleeps until it is time to produce the next block in the slot */
|
|
503
524
|
@trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
|
|
504
525
|
private async waitUntilNextSubslot(nextSubslotStart: number) {
|
|
@@ -518,18 +539,10 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
518
539
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
519
540
|
buildDeadline: Date | undefined;
|
|
520
541
|
txHashesAlreadyIncluded: Set<string>;
|
|
521
|
-
remainingBlobFields: number;
|
|
522
542
|
},
|
|
523
|
-
): Promise<{ block: L2Block; usedTxs: Tx[]
|
|
524
|
-
const {
|
|
525
|
-
|
|
526
|
-
forceCreate,
|
|
527
|
-
blockNumber,
|
|
528
|
-
indexWithinCheckpoint,
|
|
529
|
-
buildDeadline,
|
|
530
|
-
txHashesAlreadyIncluded,
|
|
531
|
-
remainingBlobFields,
|
|
532
|
-
} = opts;
|
|
543
|
+
): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
|
|
544
|
+
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
545
|
+
opts;
|
|
533
546
|
|
|
534
547
|
this.log.verbose(
|
|
535
548
|
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
|
|
@@ -538,8 +551,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
538
551
|
|
|
539
552
|
try {
|
|
540
553
|
// Wait until we have enough txs to build the block
|
|
541
|
-
const minTxs = this.
|
|
542
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
554
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
543
555
|
if (!canStartBuilding) {
|
|
544
556
|
this.log.warn(
|
|
545
557
|
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`,
|
|
@@ -563,19 +575,26 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
563
575
|
);
|
|
564
576
|
this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
|
|
565
577
|
|
|
566
|
-
//
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
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 = {
|
|
571
583
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
584
|
+
maxBlockGas:
|
|
585
|
+
this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
|
|
586
|
+
? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
|
|
587
|
+
: undefined,
|
|
575
588
|
deadline: buildDeadline,
|
|
589
|
+
isBuildingProposal: true,
|
|
590
|
+
minValidTxs,
|
|
591
|
+
maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
|
|
592
|
+
perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
|
|
576
593
|
};
|
|
577
594
|
|
|
578
|
-
// 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.
|
|
579
598
|
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
|
|
580
599
|
checkpointBuilder,
|
|
581
600
|
pendingTxs,
|
|
@@ -587,14 +606,16 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
587
606
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
588
607
|
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
589
608
|
|
|
590
|
-
|
|
591
|
-
// too long, then we may not get to minTxsPerBlock after executing public functions.
|
|
592
|
-
const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
|
|
593
|
-
const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
|
|
594
|
-
if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
|
|
609
|
+
if (buildResult.status === 'insufficient-valid-txs') {
|
|
595
610
|
this.log.warn(
|
|
596
611
|
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
|
|
597
|
-
{
|
|
612
|
+
{
|
|
613
|
+
slot: this.slot,
|
|
614
|
+
blockNumber,
|
|
615
|
+
numTxs: buildResult.processedCount,
|
|
616
|
+
indexWithinCheckpoint,
|
|
617
|
+
minValidTxs,
|
|
618
|
+
},
|
|
598
619
|
);
|
|
599
620
|
this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
|
|
600
621
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
@@ -602,7 +623,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
602
623
|
}
|
|
603
624
|
|
|
604
625
|
// Block creation succeeded, emit stats and metrics
|
|
605
|
-
const {
|
|
626
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
606
627
|
|
|
607
628
|
const blockStats = {
|
|
608
629
|
eventName: 'l2-block-built',
|
|
@@ -613,7 +634,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
613
634
|
|
|
614
635
|
const blockHash = await block.hash();
|
|
615
636
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
616
|
-
const manaPerSec =
|
|
637
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
617
638
|
|
|
618
639
|
this.log.info(
|
|
619
640
|
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
|
|
@@ -621,9 +642,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
621
642
|
);
|
|
622
643
|
|
|
623
644
|
this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
|
|
624
|
-
this.metrics.recordBuiltBlock(blockBuildDuration,
|
|
645
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
625
646
|
|
|
626
|
-
return { block, usedTxs
|
|
647
|
+
return { block, usedTxs };
|
|
627
648
|
} catch (err: any) {
|
|
628
649
|
this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
|
|
629
650
|
this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
|
|
@@ -633,13 +654,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
633
654
|
}
|
|
634
655
|
}
|
|
635
656
|
|
|
636
|
-
/** Uses the checkpoint builder to build a block, catching
|
|
657
|
+
/** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
|
|
637
658
|
private async buildSingleBlockWithCheckpointBuilder(
|
|
638
659
|
checkpointBuilder: CheckpointBuilder,
|
|
639
660
|
pendingTxs: AsyncIterable<Tx>,
|
|
640
661
|
blockNumber: BlockNumber,
|
|
641
662
|
blockTimestamp: bigint,
|
|
642
|
-
blockBuilderOptions:
|
|
663
|
+
blockBuilderOptions: BlockBuilderOptions,
|
|
643
664
|
) {
|
|
644
665
|
try {
|
|
645
666
|
const workTimer = new Timer();
|
|
@@ -647,8 +668,12 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
647
668
|
const blockBuildDuration = workTimer.ms();
|
|
648
669
|
return { ...result, blockBuildDuration, status: 'success' as const };
|
|
649
670
|
} catch (err: unknown) {
|
|
650
|
-
if (isErrorClass(err,
|
|
651
|
-
return {
|
|
671
|
+
if (isErrorClass(err, InsufficientValidTxsError)) {
|
|
672
|
+
return {
|
|
673
|
+
failedTxs: err.failedTxs,
|
|
674
|
+
processedCount: err.processedCount,
|
|
675
|
+
status: 'insufficient-valid-txs' as const,
|
|
676
|
+
};
|
|
652
677
|
}
|
|
653
678
|
throw err;
|
|
654
679
|
}
|
|
@@ -661,7 +686,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
661
686
|
blockNumber: BlockNumber;
|
|
662
687
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
663
688
|
buildDeadline: Date | undefined;
|
|
664
|
-
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
689
|
+
}): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
|
|
665
690
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
666
691
|
|
|
667
692
|
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
@@ -678,7 +703,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
678
703
|
// If we're past deadline, or we have no deadline, give up
|
|
679
704
|
const now = this.dateProvider.nowAsDate();
|
|
680
705
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
681
|
-
return { canStartBuilding: false, availableTxs
|
|
706
|
+
return { canStartBuilding: false, availableTxs, minTxs };
|
|
682
707
|
}
|
|
683
708
|
|
|
684
709
|
// Wait a bit before checking again
|
|
@@ -691,7 +716,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
691
716
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
692
717
|
}
|
|
693
718
|
|
|
694
|
-
return { canStartBuilding: true, availableTxs };
|
|
719
|
+
return { canStartBuilding: true, availableTxs, minTxs };
|
|
695
720
|
}
|
|
696
721
|
|
|
697
722
|
/**
|
|
@@ -743,11 +768,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
743
768
|
|
|
744
769
|
collectedAttestationsCount = attestations.length;
|
|
745
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
|
+
|
|
746
783
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
747
|
-
const sorted = orderAttestations(
|
|
784
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
748
785
|
|
|
749
786
|
// Manipulate the attestations if we've been configured to do so
|
|
750
|
-
if (
|
|
787
|
+
if (
|
|
788
|
+
this.config.injectFakeAttestation ||
|
|
789
|
+
this.config.injectHighSValueAttestation ||
|
|
790
|
+
this.config.injectUnrecoverableSignatureAttestation ||
|
|
791
|
+
this.config.shuffleAttestationOrdering
|
|
792
|
+
) {
|
|
751
793
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
752
794
|
}
|
|
753
795
|
|
|
@@ -776,7 +818,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
776
818
|
this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
|
|
777
819
|
);
|
|
778
820
|
|
|
779
|
-
if (
|
|
821
|
+
if (
|
|
822
|
+
this.config.injectFakeAttestation ||
|
|
823
|
+
this.config.injectHighSValueAttestation ||
|
|
824
|
+
this.config.injectUnrecoverableSignatureAttestation
|
|
825
|
+
) {
|
|
780
826
|
// Find non-empty attestations that are not from the proposer
|
|
781
827
|
const nonProposerIndices: number[] = [];
|
|
782
828
|
for (let i = 0; i < attestations.length; i++) {
|
|
@@ -786,8 +832,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
786
832
|
}
|
|
787
833
|
if (nonProposerIndices.length > 0) {
|
|
788
834
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
789
|
-
this.
|
|
790
|
-
|
|
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
|
+
}
|
|
791
849
|
}
|
|
792
850
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
793
851
|
}
|
|
@@ -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`, {
|
|
@@ -14,7 +14,7 @@ import type { P2P } from '@aztec/p2p';
|
|
|
14
14
|
import type { SlasherClientInterface } from '@aztec/slasher';
|
|
15
15
|
import type { BlockData, L2BlockSink, L2BlockSource, ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
16
16
|
import type { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
17
|
-
import {
|
|
17
|
+
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
18
18
|
import {
|
|
19
19
|
type ResolvedSequencerConfig,
|
|
20
20
|
type SequencerConfig,
|
|
@@ -110,7 +110,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
110
110
|
/** Updates sequencer config by the defined values and updates the timetable */
|
|
111
111
|
public updateConfig(config: Partial<SequencerConfig>) {
|
|
112
112
|
const filteredConfig = pickFromSchema(config, SequencerConfigSchema);
|
|
113
|
-
this.log.info(`Updated sequencer config`, omit(filteredConfig, '
|
|
113
|
+
this.log.info(`Updated sequencer config`, omit(filteredConfig, 'txPublicSetupAllowListExtend'));
|
|
114
114
|
this.config = merge(this.config, filteredConfig);
|
|
115
115
|
this.timetable = new SequencerTimetable(
|
|
116
116
|
{
|
|
@@ -281,8 +281,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
281
281
|
|
|
282
282
|
const logCtx = {
|
|
283
283
|
now,
|
|
284
|
-
|
|
285
|
-
syncedToL2Slot: getSlotAtTimestamp(syncedTo.l1Timestamp, this.l1Constants),
|
|
284
|
+
syncedToL2Slot: syncedTo.syncedL2Slot,
|
|
286
285
|
slot,
|
|
287
286
|
slotTs: ts,
|
|
288
287
|
checkpointNumber,
|
|
@@ -328,7 +327,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
328
327
|
|
|
329
328
|
// Check with the rollup contract if we can indeed propose at the next L2 slot. This check should not fail
|
|
330
329
|
// if all the previous checks are good, but we do it just in case.
|
|
331
|
-
const canProposeCheck = await publisher.
|
|
330
|
+
const canProposeCheck = await publisher.canProposeAt(
|
|
332
331
|
syncedTo.archive,
|
|
333
332
|
proposer ?? EthAddress.ZERO,
|
|
334
333
|
invalidateCheckpoint,
|
|
@@ -475,16 +474,15 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
475
474
|
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
476
475
|
*/
|
|
477
476
|
protected async checkSync(args: { ts: bigint; slot: SlotNumber }): Promise<SequencerSyncCheckResult | undefined> {
|
|
478
|
-
// Check that the archiver
|
|
479
|
-
//
|
|
480
|
-
//
|
|
481
|
-
const
|
|
482
|
-
const { slot
|
|
483
|
-
if (
|
|
477
|
+
// Check that the archiver has fully synced the L2 slot before the one we want to propose in.
|
|
478
|
+
// The archiver reports sync progress via L1 block timestamps and synced checkpoint slots.
|
|
479
|
+
// See getSyncedL2SlotNumber for how missed L1 blocks are handled.
|
|
480
|
+
const syncedL2Slot = await this.l2BlockSource.getSyncedL2SlotNumber();
|
|
481
|
+
const { slot } = args;
|
|
482
|
+
if (syncedL2Slot === undefined || syncedL2Slot + 1 < slot) {
|
|
484
483
|
this.log.debug(`Cannot propose block at next L2 slot ${slot} due to pending sync from L1`, {
|
|
485
484
|
slot,
|
|
486
|
-
|
|
487
|
-
l1Timestamp,
|
|
485
|
+
syncedL2Slot,
|
|
488
486
|
});
|
|
489
487
|
return undefined;
|
|
490
488
|
}
|
|
@@ -524,7 +522,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
524
522
|
checkpointNumber: CheckpointNumber.ZERO,
|
|
525
523
|
blockNumber: BlockNumber.ZERO,
|
|
526
524
|
archive,
|
|
527
|
-
|
|
525
|
+
syncedL2Slot,
|
|
528
526
|
pendingChainValidationStatus,
|
|
529
527
|
};
|
|
530
528
|
}
|
|
@@ -541,7 +539,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
541
539
|
blockNumber: blockData.header.getBlockNumber(),
|
|
542
540
|
checkpointNumber: blockData.checkpointNumber,
|
|
543
541
|
archive: blockData.archive.root,
|
|
544
|
-
|
|
542
|
+
syncedL2Slot,
|
|
545
543
|
pendingChainValidationStatus,
|
|
546
544
|
};
|
|
547
545
|
}
|
|
@@ -720,7 +718,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
720
718
|
syncedTo: SequencerSyncCheckResult,
|
|
721
719
|
currentSlot: SlotNumber,
|
|
722
720
|
): Promise<void> {
|
|
723
|
-
const { pendingChainValidationStatus,
|
|
721
|
+
const { pendingChainValidationStatus, syncedL2Slot } = syncedTo;
|
|
724
722
|
if (pendingChainValidationStatus.valid) {
|
|
725
723
|
return;
|
|
726
724
|
}
|
|
@@ -735,7 +733,7 @@ export class Sequencer extends (EventEmitter as new () => TypedEventEmitter<Sequ
|
|
|
735
733
|
|
|
736
734
|
const logData = {
|
|
737
735
|
invalidL1Timestamp: invalidCheckpointTimestamp,
|
|
738
|
-
|
|
736
|
+
syncedL2Slot,
|
|
739
737
|
invalidCheckpoint: pendingChainValidationStatus.checkpoint,
|
|
740
738
|
secondsBeforeInvalidatingBlockAsCommitteeMember,
|
|
741
739
|
secondsBeforeInvalidatingBlockAsNonCommitteeMember,
|
|
@@ -882,6 +880,6 @@ type SequencerSyncCheckResult = {
|
|
|
882
880
|
checkpointNumber: CheckpointNumber;
|
|
883
881
|
blockNumber: BlockNumber;
|
|
884
882
|
archive: Fr;
|
|
885
|
-
|
|
883
|
+
syncedL2Slot: SlotNumber;
|
|
886
884
|
pendingChainValidationStatus: ValidateCheckpointResult;
|
|
887
885
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Logger } from '@aztec/foundation/log';
|
|
2
2
|
import {
|
|
3
3
|
CHECKPOINT_ASSEMBLE_TIME,
|
|
4
4
|
CHECKPOINT_INITIALIZATION_TIME,
|
|
@@ -80,7 +80,7 @@ export class SequencerTimetable {
|
|
|
80
80
|
enforce: boolean;
|
|
81
81
|
},
|
|
82
82
|
private readonly metrics?: SequencerMetrics,
|
|
83
|
-
private readonly log
|
|
83
|
+
private readonly log?: Logger,
|
|
84
84
|
) {
|
|
85
85
|
this.ethereumSlotDuration = opts.ethereumSlotDuration;
|
|
86
86
|
this.aztecSlotDuration = opts.aztecSlotDuration;
|
|
@@ -132,7 +132,7 @@ export class SequencerTimetable {
|
|
|
132
132
|
const initializeDeadline = this.aztecSlotDuration - minWorkToDo;
|
|
133
133
|
this.initializeDeadline = initializeDeadline;
|
|
134
134
|
|
|
135
|
-
this.log
|
|
135
|
+
this.log?.info(
|
|
136
136
|
`Sequencer timetable initialized with ${this.maxNumberOfBlocks} blocks per slot (${this.enforce ? 'enforced' : 'not enforced'})`,
|
|
137
137
|
{
|
|
138
138
|
ethereumSlotDuration: this.ethereumSlotDuration,
|
|
@@ -206,7 +206,7 @@ export class SequencerTimetable {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
this.metrics?.recordStateTransitionBufferMs(Math.floor(bufferSeconds * 1000), newState);
|
|
209
|
-
this.log
|
|
209
|
+
this.log?.trace(`Enough time to transition to ${newState}`, { maxAllowedTime, secondsIntoSlot });
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/**
|
|
@@ -242,7 +242,7 @@ export class SequencerTimetable {
|
|
|
242
242
|
const canStart = available >= this.minExecutionTime;
|
|
243
243
|
const deadline = secondsIntoSlot + available;
|
|
244
244
|
|
|
245
|
-
this.log
|
|
245
|
+
this.log?.verbose(
|
|
246
246
|
`${canStart ? 'Can' : 'Cannot'} start single-block checkpoint at ${secondsIntoSlot}s into slot`,
|
|
247
247
|
{ secondsIntoSlot, maxAllowed, available, deadline },
|
|
248
248
|
);
|
|
@@ -262,7 +262,7 @@ export class SequencerTimetable {
|
|
|
262
262
|
// Found an available sub-slot! Is this the last one?
|
|
263
263
|
const isLastBlock = subSlot === this.maxNumberOfBlocks;
|
|
264
264
|
|
|
265
|
-
this.log
|
|
265
|
+
this.log?.verbose(
|
|
266
266
|
`Can start ${isLastBlock ? 'last block' : 'block'} in sub-slot ${subSlot} with deadline ${deadline}s`,
|
|
267
267
|
{ secondsIntoSlot, deadline, timeUntilDeadline, subSlot, maxBlocks: this.maxNumberOfBlocks },
|
|
268
268
|
);
|
|
@@ -272,7 +272,7 @@ export class SequencerTimetable {
|
|
|
272
272
|
}
|
|
273
273
|
|
|
274
274
|
// No sub-slots available with enough time
|
|
275
|
-
this.log
|
|
275
|
+
this.log?.verbose(`No time left to start any more blocks`, {
|
|
276
276
|
secondsIntoSlot,
|
|
277
277
|
maxBlocks: this.maxNumberOfBlocks,
|
|
278
278
|
initializationOffset: this.initializationOffset,
|
package/src/sequencer/types.ts
CHANGED
|
@@ -2,5 +2,5 @@ import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
|
|
|
2
2
|
|
|
3
3
|
export type SequencerRollupConstants = Pick<
|
|
4
4
|
L1RollupConstants,
|
|
5
|
-
'ethereumSlotDuration' | 'l1GenesisTime' | 'slotDuration'
|
|
5
|
+
'ethereumSlotDuration' | 'l1GenesisTime' | 'slotDuration' | 'rollupManaLimit'
|
|
6
6
|
>;
|