@aztec/sequencer-client 0.0.1-commit.21ecf947b → 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 -5
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +42 -20
- 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 +22 -13
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +92 -52
- 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 +129 -75
- 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 +13 -5
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +32 -10
- package/dest/sequencer/sequencer.d.ts +27 -13
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +41 -42
- 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/index.d.ts +3 -5
- package/dest/test/index.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 +78 -21
- package/src/config.ts +52 -27
- 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 +96 -66
- package/src/sequencer/checkpoint_proposal_job.ts +200 -106
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/metrics.ts +39 -13
- package/src/sequencer/sequencer.ts +52 -48
- package/src/sequencer/timetable.ts +7 -7
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- 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';
|
|
@@ -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,16 +194,15 @@ 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
|
-
const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
|
|
197
|
+
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.epoch))
|
|
198
|
+
.filter(c => c.checkpointNumber < this.checkpointNumber)
|
|
199
|
+
.map(c => c.checkpointOutHash);
|
|
193
200
|
|
|
194
201
|
// Get the fee asset price modifier from the oracle
|
|
195
202
|
const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
|
|
196
203
|
|
|
197
204
|
// Create a long-lived forked world state for the checkpoint builder
|
|
198
|
-
using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
205
|
+
await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
199
206
|
|
|
200
207
|
// Create checkpoint builder for the entire slot
|
|
201
208
|
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
|
|
@@ -221,6 +228,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
221
228
|
|
|
222
229
|
let blocksInCheckpoint: L2Block[] = [];
|
|
223
230
|
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
231
|
+
const checkpointBuildTimer = new Timer();
|
|
224
232
|
|
|
225
233
|
try {
|
|
226
234
|
// Main loop: build blocks for the checkpoint
|
|
@@ -248,11 +256,45 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
248
256
|
return undefined;
|
|
249
257
|
}
|
|
250
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
|
+
|
|
251
268
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
252
269
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
253
270
|
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
|
|
254
271
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
255
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
|
+
|
|
256
298
|
// Do not collect attestations nor publish to L1 in fisherman mode
|
|
257
299
|
if (this.config.fishermanMode) {
|
|
258
300
|
this.log.info(
|
|
@@ -318,6 +360,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
318
360
|
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
319
361
|
const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
|
|
320
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
|
+
|
|
321
378
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
322
379
|
txTimeoutAt,
|
|
323
380
|
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
@@ -351,9 +408,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
351
408
|
const blocksInCheckpoint: L2Block[] = [];
|
|
352
409
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
353
410
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
354
|
-
|
|
355
|
-
// Remaining blob fields available for blocks (checkpoint end marker already subtracted)
|
|
356
|
-
let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
|
|
411
|
+
const slot = this.slot;
|
|
357
412
|
|
|
358
413
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
359
414
|
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
@@ -367,11 +422,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
367
422
|
const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
|
|
368
423
|
|
|
369
424
|
if (!timingInfo.canStart) {
|
|
370
|
-
this.log.debug(`Not enough time left in slot to start another block`, {
|
|
371
|
-
slot: this.slot,
|
|
372
|
-
blocksBuilt,
|
|
373
|
-
secondsIntoSlot,
|
|
374
|
-
});
|
|
425
|
+
this.log.debug(`Not enough time left in slot to start another block`, { slot, blocksBuilt, secondsIntoSlot });
|
|
375
426
|
break;
|
|
376
427
|
}
|
|
377
428
|
|
|
@@ -387,7 +438,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
387
438
|
blockNumber,
|
|
388
439
|
indexWithinCheckpoint,
|
|
389
440
|
txHashesAlreadyIncluded,
|
|
390
|
-
remainingBlobFields,
|
|
391
441
|
});
|
|
392
442
|
|
|
393
443
|
// TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
|
|
@@ -404,56 +454,37 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
404
454
|
} else if ('error' in buildResult) {
|
|
405
455
|
// If there was an error building the block, just exit the loop and give up the rest of the slot
|
|
406
456
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
407
|
-
this.log.warn(`Halting block building for slot ${
|
|
408
|
-
slot: this.slot,
|
|
409
|
-
blocksBuilt,
|
|
410
|
-
error: buildResult.error,
|
|
411
|
-
});
|
|
457
|
+
this.log.warn(`Halting block building for slot ${slot}`, { slot, blocksBuilt, error: buildResult.error });
|
|
412
458
|
}
|
|
413
459
|
break;
|
|
414
460
|
}
|
|
415
461
|
|
|
416
|
-
const { block, usedTxs
|
|
462
|
+
const { block, usedTxs } = buildResult;
|
|
417
463
|
blocksInCheckpoint.push(block);
|
|
418
|
-
|
|
419
|
-
// Update remaining blob fields for the next block
|
|
420
|
-
remainingBlobFields = newRemainingBlobFields;
|
|
421
|
-
|
|
422
|
-
// Sync the proposed block to the archiver to make it available
|
|
423
|
-
// Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
|
|
424
|
-
// Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
|
|
425
|
-
// Fire and forget - don't block the critical path, but log errors
|
|
426
|
-
this.syncProposedBlockToArchiver(block).catch(err => {
|
|
427
|
-
this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
|
|
428
|
-
});
|
|
429
|
-
|
|
430
464
|
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
431
465
|
|
|
432
|
-
// 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.
|
|
433
468
|
if (timingInfo.isLastBlock) {
|
|
434
|
-
this.
|
|
435
|
-
|
|
436
|
-
blockNumber,
|
|
437
|
-
blocksBuilt,
|
|
438
|
-
});
|
|
469
|
+
await this.syncProposedBlockToArchiver(block);
|
|
470
|
+
this.log.verbose(`Completed final block ${blockNumber} for slot ${slot}`, { slot, blockNumber, blocksBuilt });
|
|
439
471
|
blockPendingBroadcast = { block, txs: usedTxs };
|
|
440
472
|
break;
|
|
441
473
|
}
|
|
442
474
|
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
if
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
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));
|
|
457
488
|
|
|
458
489
|
// Wait until the next block's start time
|
|
459
490
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
@@ -467,6 +498,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
467
498
|
return { blocksInCheckpoint, blockPendingBroadcast };
|
|
468
499
|
}
|
|
469
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
|
+
|
|
470
523
|
/** Sleeps until it is time to produce the next block in the slot */
|
|
471
524
|
@trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
|
|
472
525
|
private async waitUntilNextSubslot(nextSubslotStart: number) {
|
|
@@ -486,18 +539,10 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
486
539
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
487
540
|
buildDeadline: Date | undefined;
|
|
488
541
|
txHashesAlreadyIncluded: Set<string>;
|
|
489
|
-
remainingBlobFields: number;
|
|
490
542
|
},
|
|
491
|
-
): Promise<{ block: L2Block; usedTxs: Tx[]
|
|
492
|
-
const {
|
|
493
|
-
|
|
494
|
-
forceCreate,
|
|
495
|
-
blockNumber,
|
|
496
|
-
indexWithinCheckpoint,
|
|
497
|
-
buildDeadline,
|
|
498
|
-
txHashesAlreadyIncluded,
|
|
499
|
-
remainingBlobFields,
|
|
500
|
-
} = opts;
|
|
543
|
+
): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
|
|
544
|
+
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
545
|
+
opts;
|
|
501
546
|
|
|
502
547
|
this.log.verbose(
|
|
503
548
|
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
|
|
@@ -506,8 +551,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
506
551
|
|
|
507
552
|
try {
|
|
508
553
|
// Wait until we have enough txs to build the block
|
|
509
|
-
const minTxs = this.
|
|
510
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
554
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
511
555
|
if (!canStartBuilding) {
|
|
512
556
|
this.log.warn(
|
|
513
557
|
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`,
|
|
@@ -531,19 +575,26 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
531
575
|
);
|
|
532
576
|
this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
|
|
533
577
|
|
|
534
|
-
//
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
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 = {
|
|
539
583
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
584
|
+
maxBlockGas:
|
|
585
|
+
this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
|
|
586
|
+
? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
|
|
587
|
+
: undefined,
|
|
543
588
|
deadline: buildDeadline,
|
|
589
|
+
isBuildingProposal: true,
|
|
590
|
+
minValidTxs,
|
|
591
|
+
maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
|
|
592
|
+
perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
|
|
544
593
|
};
|
|
545
594
|
|
|
546
|
-
// 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.
|
|
547
598
|
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
|
|
548
599
|
checkpointBuilder,
|
|
549
600
|
pendingTxs,
|
|
@@ -555,14 +606,16 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
555
606
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
556
607
|
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
557
608
|
|
|
558
|
-
|
|
559
|
-
// too long, then we may not get to minTxsPerBlock after executing public functions.
|
|
560
|
-
const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
|
|
561
|
-
const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
|
|
562
|
-
if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
|
|
609
|
+
if (buildResult.status === 'insufficient-valid-txs') {
|
|
563
610
|
this.log.warn(
|
|
564
611
|
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
|
|
565
|
-
{
|
|
612
|
+
{
|
|
613
|
+
slot: this.slot,
|
|
614
|
+
blockNumber,
|
|
615
|
+
numTxs: buildResult.processedCount,
|
|
616
|
+
indexWithinCheckpoint,
|
|
617
|
+
minValidTxs,
|
|
618
|
+
},
|
|
566
619
|
);
|
|
567
620
|
this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
|
|
568
621
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
@@ -570,7 +623,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
570
623
|
}
|
|
571
624
|
|
|
572
625
|
// Block creation succeeded, emit stats and metrics
|
|
573
|
-
const {
|
|
626
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
574
627
|
|
|
575
628
|
const blockStats = {
|
|
576
629
|
eventName: 'l2-block-built',
|
|
@@ -581,7 +634,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
581
634
|
|
|
582
635
|
const blockHash = await block.hash();
|
|
583
636
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
584
|
-
const manaPerSec =
|
|
637
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
585
638
|
|
|
586
639
|
this.log.info(
|
|
587
640
|
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
|
|
@@ -589,9 +642,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
589
642
|
);
|
|
590
643
|
|
|
591
644
|
this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
|
|
592
|
-
this.metrics.recordBuiltBlock(blockBuildDuration,
|
|
645
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
593
646
|
|
|
594
|
-
return { block, usedTxs
|
|
647
|
+
return { block, usedTxs };
|
|
595
648
|
} catch (err: any) {
|
|
596
649
|
this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
|
|
597
650
|
this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
|
|
@@ -601,13 +654,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
601
654
|
}
|
|
602
655
|
}
|
|
603
656
|
|
|
604
|
-
/** Uses the checkpoint builder to build a block, catching
|
|
657
|
+
/** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
|
|
605
658
|
private async buildSingleBlockWithCheckpointBuilder(
|
|
606
659
|
checkpointBuilder: CheckpointBuilder,
|
|
607
660
|
pendingTxs: AsyncIterable<Tx>,
|
|
608
661
|
blockNumber: BlockNumber,
|
|
609
662
|
blockTimestamp: bigint,
|
|
610
|
-
blockBuilderOptions:
|
|
663
|
+
blockBuilderOptions: BlockBuilderOptions,
|
|
611
664
|
) {
|
|
612
665
|
try {
|
|
613
666
|
const workTimer = new Timer();
|
|
@@ -615,8 +668,12 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
615
668
|
const blockBuildDuration = workTimer.ms();
|
|
616
669
|
return { ...result, blockBuildDuration, status: 'success' as const };
|
|
617
670
|
} catch (err: unknown) {
|
|
618
|
-
if (isErrorClass(err,
|
|
619
|
-
return {
|
|
671
|
+
if (isErrorClass(err, InsufficientValidTxsError)) {
|
|
672
|
+
return {
|
|
673
|
+
failedTxs: err.failedTxs,
|
|
674
|
+
processedCount: err.processedCount,
|
|
675
|
+
status: 'insufficient-valid-txs' as const,
|
|
676
|
+
};
|
|
620
677
|
}
|
|
621
678
|
throw err;
|
|
622
679
|
}
|
|
@@ -629,7 +686,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
629
686
|
blockNumber: BlockNumber;
|
|
630
687
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
631
688
|
buildDeadline: Date | undefined;
|
|
632
|
-
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
689
|
+
}): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
|
|
633
690
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
634
691
|
|
|
635
692
|
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
@@ -646,7 +703,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
646
703
|
// If we're past deadline, or we have no deadline, give up
|
|
647
704
|
const now = this.dateProvider.nowAsDate();
|
|
648
705
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
649
|
-
return { canStartBuilding: false, availableTxs
|
|
706
|
+
return { canStartBuilding: false, availableTxs, minTxs };
|
|
650
707
|
}
|
|
651
708
|
|
|
652
709
|
// Wait a bit before checking again
|
|
@@ -659,7 +716,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
659
716
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
660
717
|
}
|
|
661
718
|
|
|
662
|
-
return { canStartBuilding: true, availableTxs };
|
|
719
|
+
return { canStartBuilding: true, availableTxs, minTxs };
|
|
663
720
|
}
|
|
664
721
|
|
|
665
722
|
/**
|
|
@@ -711,11 +768,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
711
768
|
|
|
712
769
|
collectedAttestationsCount = attestations.length;
|
|
713
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
|
+
|
|
714
783
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
715
|
-
const sorted = orderAttestations(
|
|
784
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
716
785
|
|
|
717
786
|
// Manipulate the attestations if we've been configured to do so
|
|
718
|
-
if (
|
|
787
|
+
if (
|
|
788
|
+
this.config.injectFakeAttestation ||
|
|
789
|
+
this.config.injectHighSValueAttestation ||
|
|
790
|
+
this.config.injectUnrecoverableSignatureAttestation ||
|
|
791
|
+
this.config.shuffleAttestationOrdering
|
|
792
|
+
) {
|
|
719
793
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
720
794
|
}
|
|
721
795
|
|
|
@@ -744,7 +818,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
744
818
|
this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
|
|
745
819
|
);
|
|
746
820
|
|
|
747
|
-
if (
|
|
821
|
+
if (
|
|
822
|
+
this.config.injectFakeAttestation ||
|
|
823
|
+
this.config.injectHighSValueAttestation ||
|
|
824
|
+
this.config.injectUnrecoverableSignatureAttestation
|
|
825
|
+
) {
|
|
748
826
|
// Find non-empty attestations that are not from the proposer
|
|
749
827
|
const nonProposerIndices: number[] = [];
|
|
750
828
|
for (let i = 0; i < attestations.length; i++) {
|
|
@@ -754,8 +832,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
754
832
|
}
|
|
755
833
|
if (nonProposerIndices.length > 0) {
|
|
756
834
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
757
|
-
this.
|
|
758
|
-
|
|
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
|
+
}
|
|
759
849
|
}
|
|
760
850
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
761
851
|
}
|
|
@@ -791,9 +881,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
791
881
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
792
882
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
793
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.
|
|
794
888
|
*/
|
|
795
889
|
private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
|
|
796
|
-
if (this.config.skipPushProposedBlocksToArchiver !== false) {
|
|
890
|
+
if (this.config.skipPushProposedBlocksToArchiver !== false || this.config.fishermanMode) {
|
|
797
891
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
798
892
|
blockNumber: block.number,
|
|
799
893
|
slot: block.header.globalVariables.slotNumber,
|
|
@@ -826,7 +920,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
826
920
|
slot: this.slot,
|
|
827
921
|
feeAnalysisId: feeAnalysis?.id,
|
|
828
922
|
});
|
|
829
|
-
this.metrics.
|
|
923
|
+
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
830
924
|
}
|
|
831
925
|
|
|
832
926
|
this.publisher.clearPendingRequests();
|
|
@@ -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`, {
|