@aztec/sequencer-client 0.0.1-commit.87a0206 → 0.0.1-commit.88c5703d4
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 +24 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +101 -16
- package/dest/config.d.ts +25 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +49 -28
- package/dest/global_variable_builder/global_builder.d.ts +2 -4
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +5 -4
- package/dest/publisher/config.d.ts +35 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +106 -42
- package/dest/publisher/index.d.ts +2 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/l1_tx_failed_store/factory.d.ts +11 -0
- package/dest/publisher/l1_tx_failed_store/factory.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/factory.js +22 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts +59 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/failed_tx_store.js +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts +15 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/file_store_failed_tx_store.js +34 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts +4 -0
- package/dest/publisher/l1_tx_failed_store/index.d.ts.map +1 -0
- package/dest/publisher/l1_tx_failed_store/index.js +2 -0
- 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 +27 -2
- package/dest/publisher/sequencer-publisher.d.ts +26 -7
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +310 -31
- package/dest/sequencer/checkpoint_proposal_job.d.ts +2 -4
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +122 -73
- 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 +26 -13
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +36 -39
- 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 +14 -10
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +45 -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 +27 -28
- package/src/client/sequencer-client.ts +135 -18
- package/src/config.ts +64 -38
- package/src/global_variable_builder/global_builder.ts +4 -3
- package/src/publisher/config.ts +121 -43
- package/src/publisher/index.ts +3 -0
- package/src/publisher/l1_tx_failed_store/factory.ts +32 -0
- package/src/publisher/l1_tx_failed_store/failed_tx_store.ts +55 -0
- package/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts +46 -0
- package/src/publisher/l1_tx_failed_store/index.ts +3 -0
- package/src/publisher/sequencer-publisher-factory.ts +38 -6
- package/src/publisher/sequencer-publisher.ts +311 -44
- package/src/sequencer/checkpoint_proposal_job.ts +167 -77
- package/src/sequencer/metrics.ts +92 -18
- package/src/sequencer/sequencer.ts +45 -45
- 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 +62 -48
- 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,18 @@ 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
|
-
|
|
37
|
+
InsufficientValidTxsError,
|
|
35
38
|
type PublicProcessorLimits,
|
|
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
43
|
import type { BlockProposalOptions, CheckpointProposal, CheckpointProposalOptions } from '@aztec/stdlib/p2p';
|
|
41
|
-
import { orderAttestations } from '@aztec/stdlib/p2p';
|
|
44
|
+
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
42
45
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
43
46
|
import { type FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
44
47
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
@@ -129,7 +132,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
129
132
|
await Promise.all(votesPromises);
|
|
130
133
|
|
|
131
134
|
if (checkpoint) {
|
|
132
|
-
this.metrics.
|
|
135
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
@@ -186,18 +189,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
186
189
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
187
190
|
|
|
188
191
|
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
189
|
-
const
|
|
190
|
-
c => c.
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.epoch))
|
|
193
|
+
.filter(c => c.checkpointNumber < this.checkpointNumber)
|
|
194
|
+
.map(c => c.checkpointOutHash);
|
|
195
|
+
|
|
196
|
+
// Get the fee asset price modifier from the oracle
|
|
197
|
+
const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
|
|
193
198
|
|
|
194
199
|
// Create a long-lived forked world state for the checkpoint builder
|
|
195
|
-
using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
200
|
+
await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
196
201
|
|
|
197
202
|
// Create checkpoint builder for the entire slot
|
|
198
203
|
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
|
|
199
204
|
this.checkpointNumber,
|
|
200
205
|
checkpointGlobalVariables,
|
|
206
|
+
feeAssetPriceModifier,
|
|
201
207
|
l1ToL2Messages,
|
|
202
208
|
previousCheckpointOutHashes,
|
|
203
209
|
fork,
|
|
@@ -217,6 +223,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
217
223
|
|
|
218
224
|
let blocksInCheckpoint: L2Block[] = [];
|
|
219
225
|
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
226
|
+
const checkpointBuildTimer = new Timer();
|
|
220
227
|
|
|
221
228
|
try {
|
|
222
229
|
// Main loop: build blocks for the checkpoint
|
|
@@ -244,11 +251,44 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
244
251
|
return undefined;
|
|
245
252
|
}
|
|
246
253
|
|
|
254
|
+
const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
|
|
255
|
+
if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
|
|
256
|
+
this.log.warn(
|
|
257
|
+
`Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
|
|
258
|
+
{ slot: this.slot, blocksBuilt: blocksInCheckpoint.length, minBlocksForCheckpoint },
|
|
259
|
+
);
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
247
263
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
248
264
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
249
265
|
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
|
|
250
266
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
251
267
|
|
|
268
|
+
// Final validation round for the checkpoint before we propose it, just for safety
|
|
269
|
+
try {
|
|
270
|
+
validateCheckpoint(checkpoint, {
|
|
271
|
+
rollupManaLimit: this.l1Constants.rollupManaLimit,
|
|
272
|
+
maxL2BlockGas: this.config.maxL2BlockGas,
|
|
273
|
+
maxDABlockGas: this.config.maxDABlockGas,
|
|
274
|
+
maxTxsPerBlock: this.config.maxTxsPerBlock,
|
|
275
|
+
maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
|
|
276
|
+
});
|
|
277
|
+
} catch (err) {
|
|
278
|
+
this.log.error(`Built an invalid checkpoint at slot ${this.slot} (skipping proposal)`, err, {
|
|
279
|
+
checkpoint: checkpoint.header.toInspect(),
|
|
280
|
+
});
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Record checkpoint-level build metrics
|
|
285
|
+
this.metrics.recordCheckpointBuild(
|
|
286
|
+
checkpointBuildTimer.ms(),
|
|
287
|
+
blocksInCheckpoint.length,
|
|
288
|
+
checkpoint.getStats().txCount,
|
|
289
|
+
Number(checkpoint.header.totalManaUsed.toBigInt()),
|
|
290
|
+
);
|
|
291
|
+
|
|
252
292
|
// Do not collect attestations nor publish to L1 in fisherman mode
|
|
253
293
|
if (this.config.fishermanMode) {
|
|
254
294
|
this.log.info(
|
|
@@ -275,6 +315,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
275
315
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
276
316
|
checkpoint.header,
|
|
277
317
|
checkpoint.archive.root,
|
|
318
|
+
feeAssetPriceModifier,
|
|
278
319
|
lastBlock,
|
|
279
320
|
this.proposer,
|
|
280
321
|
checkpointProposalOptions,
|
|
@@ -313,6 +354,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
313
354
|
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
314
355
|
const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
|
|
315
356
|
const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
|
|
357
|
+
|
|
358
|
+
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
359
|
+
if (
|
|
360
|
+
this.config.skipPublishingCheckpointsPercent !== undefined &&
|
|
361
|
+
this.config.skipPublishingCheckpointsPercent > 0
|
|
362
|
+
) {
|
|
363
|
+
const result = Math.max(0, randomInt(100));
|
|
364
|
+
if (result < this.config.skipPublishingCheckpointsPercent) {
|
|
365
|
+
this.log.warn(
|
|
366
|
+
`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`,
|
|
367
|
+
);
|
|
368
|
+
return checkpoint;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
316
372
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
317
373
|
txTimeoutAt,
|
|
318
374
|
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
@@ -347,9 +403,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
347
403
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
348
404
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
349
405
|
|
|
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;
|
|
352
|
-
|
|
353
406
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
354
407
|
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
355
408
|
|
|
@@ -382,7 +435,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
382
435
|
blockNumber,
|
|
383
436
|
indexWithinCheckpoint,
|
|
384
437
|
txHashesAlreadyIncluded,
|
|
385
|
-
remainingBlobFields,
|
|
386
438
|
});
|
|
387
439
|
|
|
388
440
|
// TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
|
|
@@ -408,19 +460,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
408
460
|
break;
|
|
409
461
|
}
|
|
410
462
|
|
|
411
|
-
const { block, usedTxs
|
|
463
|
+
const { block, usedTxs } = buildResult;
|
|
412
464
|
blocksInCheckpoint.push(block);
|
|
413
465
|
|
|
414
|
-
// Update remaining blob fields for the next block
|
|
415
|
-
remainingBlobFields = newRemainingBlobFields;
|
|
416
|
-
|
|
417
466
|
// Sync the proposed block to the archiver to make it available
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
this.syncProposedBlockToArchiver(block).catch(err => {
|
|
422
|
-
this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
|
|
423
|
-
});
|
|
467
|
+
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building
|
|
468
|
+
// If this throws, we abort the entire checkpoint
|
|
469
|
+
await this.syncProposedBlockToArchiver(block);
|
|
424
470
|
|
|
425
471
|
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
426
472
|
|
|
@@ -481,18 +527,10 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
481
527
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
482
528
|
buildDeadline: Date | undefined;
|
|
483
529
|
txHashesAlreadyIncluded: Set<string>;
|
|
484
|
-
remainingBlobFields: number;
|
|
485
530
|
},
|
|
486
|
-
): Promise<{ block: L2Block; usedTxs: Tx[]
|
|
487
|
-
const {
|
|
488
|
-
|
|
489
|
-
forceCreate,
|
|
490
|
-
blockNumber,
|
|
491
|
-
indexWithinCheckpoint,
|
|
492
|
-
buildDeadline,
|
|
493
|
-
txHashesAlreadyIncluded,
|
|
494
|
-
remainingBlobFields,
|
|
495
|
-
} = opts;
|
|
531
|
+
): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
|
|
532
|
+
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
533
|
+
opts;
|
|
496
534
|
|
|
497
535
|
this.log.verbose(
|
|
498
536
|
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
|
|
@@ -501,8 +539,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
501
539
|
|
|
502
540
|
try {
|
|
503
541
|
// Wait until we have enough txs to build the block
|
|
504
|
-
const minTxs = this.
|
|
505
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
542
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
506
543
|
if (!canStartBuilding) {
|
|
507
544
|
this.log.warn(
|
|
508
545
|
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`,
|
|
@@ -516,7 +553,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
516
553
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
517
554
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
518
555
|
const pendingTxs = filter(
|
|
519
|
-
this.p2pClient.
|
|
556
|
+
this.p2pClient.iterateEligiblePendingTxs(),
|
|
520
557
|
tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
|
|
521
558
|
);
|
|
522
559
|
|
|
@@ -526,19 +563,24 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
526
563
|
);
|
|
527
564
|
this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
|
|
528
565
|
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const blockBuilderOptions: PublicProcessorLimits = {
|
|
566
|
+
// Per-block limits derived at startup by computeBlockLimits(), further capped
|
|
567
|
+
// by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
|
|
568
|
+
// minValidTxs is passed into the builder so it can reject the block *before* updating state.
|
|
569
|
+
const minValidTxs = forceCreate ? 0 : (this.config.minValidTxsPerBlock ?? minTxs);
|
|
570
|
+
const blockBuilderOptions: PublicProcessorLimits & { minValidTxs?: number } = {
|
|
534
571
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
572
|
+
maxBlockGas:
|
|
573
|
+
this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
|
|
574
|
+
? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
|
|
575
|
+
: undefined,
|
|
538
576
|
deadline: buildDeadline,
|
|
577
|
+
isBuildingProposal: true,
|
|
578
|
+
minValidTxs,
|
|
539
579
|
};
|
|
540
580
|
|
|
541
|
-
// Actually build the block by executing txs
|
|
581
|
+
// Actually build the block by executing txs. The builder throws InsufficientValidTxsError
|
|
582
|
+
// if the number of successfully processed txs is below minValidTxs, ensuring state is not
|
|
583
|
+
// updated for blocks that will be discarded.
|
|
542
584
|
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
|
|
543
585
|
checkpointBuilder,
|
|
544
586
|
pendingTxs,
|
|
@@ -550,14 +592,16 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
550
592
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
551
593
|
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
552
594
|
|
|
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)) {
|
|
595
|
+
if (buildResult.status === 'insufficient-valid-txs') {
|
|
558
596
|
this.log.warn(
|
|
559
597
|
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
|
|
560
|
-
{
|
|
598
|
+
{
|
|
599
|
+
slot: this.slot,
|
|
600
|
+
blockNumber,
|
|
601
|
+
numTxs: buildResult.processedCount,
|
|
602
|
+
indexWithinCheckpoint,
|
|
603
|
+
minValidTxs,
|
|
604
|
+
},
|
|
561
605
|
);
|
|
562
606
|
this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
|
|
563
607
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
@@ -565,7 +609,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
565
609
|
}
|
|
566
610
|
|
|
567
611
|
// Block creation succeeded, emit stats and metrics
|
|
568
|
-
const {
|
|
612
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
569
613
|
|
|
570
614
|
const blockStats = {
|
|
571
615
|
eventName: 'l2-block-built',
|
|
@@ -576,7 +620,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
576
620
|
|
|
577
621
|
const blockHash = await block.hash();
|
|
578
622
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
579
|
-
const manaPerSec =
|
|
623
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
580
624
|
|
|
581
625
|
this.log.info(
|
|
582
626
|
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
|
|
@@ -584,9 +628,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
584
628
|
);
|
|
585
629
|
|
|
586
630
|
this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
|
|
587
|
-
this.metrics.recordBuiltBlock(blockBuildDuration,
|
|
631
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
588
632
|
|
|
589
|
-
return { block, usedTxs
|
|
633
|
+
return { block, usedTxs };
|
|
590
634
|
} catch (err: any) {
|
|
591
635
|
this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
|
|
592
636
|
this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
|
|
@@ -596,13 +640,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
596
640
|
}
|
|
597
641
|
}
|
|
598
642
|
|
|
599
|
-
/** Uses the checkpoint builder to build a block, catching
|
|
643
|
+
/** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
|
|
600
644
|
private async buildSingleBlockWithCheckpointBuilder(
|
|
601
645
|
checkpointBuilder: CheckpointBuilder,
|
|
602
646
|
pendingTxs: AsyncIterable<Tx>,
|
|
603
647
|
blockNumber: BlockNumber,
|
|
604
648
|
blockTimestamp: bigint,
|
|
605
|
-
blockBuilderOptions: PublicProcessorLimits,
|
|
649
|
+
blockBuilderOptions: PublicProcessorLimits & { minValidTxs?: number },
|
|
606
650
|
) {
|
|
607
651
|
try {
|
|
608
652
|
const workTimer = new Timer();
|
|
@@ -610,8 +654,12 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
610
654
|
const blockBuildDuration = workTimer.ms();
|
|
611
655
|
return { ...result, blockBuildDuration, status: 'success' as const };
|
|
612
656
|
} catch (err: unknown) {
|
|
613
|
-
if (isErrorClass(err,
|
|
614
|
-
return {
|
|
657
|
+
if (isErrorClass(err, InsufficientValidTxsError)) {
|
|
658
|
+
return {
|
|
659
|
+
failedTxs: err.failedTxs,
|
|
660
|
+
processedCount: err.processedCount,
|
|
661
|
+
status: 'insufficient-valid-txs' as const,
|
|
662
|
+
};
|
|
615
663
|
}
|
|
616
664
|
throw err;
|
|
617
665
|
}
|
|
@@ -624,7 +672,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
624
672
|
blockNumber: BlockNumber;
|
|
625
673
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
626
674
|
buildDeadline: Date | undefined;
|
|
627
|
-
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
675
|
+
}): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
|
|
628
676
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
629
677
|
|
|
630
678
|
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
@@ -641,7 +689,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
641
689
|
// If we're past deadline, or we have no deadline, give up
|
|
642
690
|
const now = this.dateProvider.nowAsDate();
|
|
643
691
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
644
|
-
return { canStartBuilding: false, availableTxs
|
|
692
|
+
return { canStartBuilding: false, availableTxs, minTxs };
|
|
645
693
|
}
|
|
646
694
|
|
|
647
695
|
// Wait a bit before checking again
|
|
@@ -654,7 +702,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
654
702
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
655
703
|
}
|
|
656
704
|
|
|
657
|
-
return { canStartBuilding: true, availableTxs };
|
|
705
|
+
return { canStartBuilding: true, availableTxs, minTxs };
|
|
658
706
|
}
|
|
659
707
|
|
|
660
708
|
/**
|
|
@@ -706,11 +754,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
706
754
|
|
|
707
755
|
collectedAttestationsCount = attestations.length;
|
|
708
756
|
|
|
757
|
+
// Trim attestations to minimum required to save L1 calldata gas
|
|
758
|
+
const localAddresses = this.validatorClient.getValidatorAddresses();
|
|
759
|
+
const trimmed = trimAttestations(
|
|
760
|
+
attestations,
|
|
761
|
+
numberOfRequiredAttestations,
|
|
762
|
+
this.attestorAddress,
|
|
763
|
+
localAddresses,
|
|
764
|
+
);
|
|
765
|
+
if (trimmed.length < attestations.length) {
|
|
766
|
+
this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
|
|
767
|
+
}
|
|
768
|
+
|
|
709
769
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
710
|
-
const sorted = orderAttestations(
|
|
770
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
711
771
|
|
|
712
772
|
// Manipulate the attestations if we've been configured to do so
|
|
713
|
-
if (
|
|
773
|
+
if (
|
|
774
|
+
this.config.injectFakeAttestation ||
|
|
775
|
+
this.config.injectHighSValueAttestation ||
|
|
776
|
+
this.config.injectUnrecoverableSignatureAttestation ||
|
|
777
|
+
this.config.shuffleAttestationOrdering
|
|
778
|
+
) {
|
|
714
779
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
715
780
|
}
|
|
716
781
|
|
|
@@ -739,7 +804,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
739
804
|
this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
|
|
740
805
|
);
|
|
741
806
|
|
|
742
|
-
if (
|
|
807
|
+
if (
|
|
808
|
+
this.config.injectFakeAttestation ||
|
|
809
|
+
this.config.injectHighSValueAttestation ||
|
|
810
|
+
this.config.injectUnrecoverableSignatureAttestation
|
|
811
|
+
) {
|
|
743
812
|
// Find non-empty attestations that are not from the proposer
|
|
744
813
|
const nonProposerIndices: number[] = [];
|
|
745
814
|
for (let i = 0; i < attestations.length; i++) {
|
|
@@ -749,8 +818,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
749
818
|
}
|
|
750
819
|
if (nonProposerIndices.length > 0) {
|
|
751
820
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
752
|
-
this.
|
|
753
|
-
|
|
821
|
+
if (this.config.injectHighSValueAttestation) {
|
|
822
|
+
this.log.warn(
|
|
823
|
+
`Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
824
|
+
);
|
|
825
|
+
unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
|
|
826
|
+
} else if (this.config.injectUnrecoverableSignatureAttestation) {
|
|
827
|
+
this.log.warn(
|
|
828
|
+
`Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
829
|
+
);
|
|
830
|
+
unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
|
|
831
|
+
} else {
|
|
832
|
+
this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
833
|
+
unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
|
|
834
|
+
}
|
|
754
835
|
}
|
|
755
836
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
756
837
|
}
|
|
@@ -759,11 +840,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
759
840
|
this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
|
|
760
841
|
|
|
761
842
|
const shuffled = [...attestations];
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
843
|
+
|
|
844
|
+
// Find two non-proposer positions that both have non-empty signatures to swap.
|
|
845
|
+
// This ensures the bitmap doesn't change, so the MaliciousCommitteeAttestationsAndSigners
|
|
846
|
+
// signers array stays correctly aligned with L1's committee reconstruction.
|
|
847
|
+
const swappable: number[] = [];
|
|
848
|
+
for (let k = 0; k < shuffled.length; k++) {
|
|
849
|
+
if (!shuffled[k].signature.isEmpty() && k !== proposerIndex) {
|
|
850
|
+
swappable.push(k);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (swappable.length >= 2) {
|
|
854
|
+
const [i, j] = [swappable[0], swappable[1]];
|
|
855
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
856
|
+
}
|
|
767
857
|
|
|
768
858
|
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
769
859
|
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
@@ -779,7 +869,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
779
869
|
const failedTxData = failedTxs.map(fail => fail.tx);
|
|
780
870
|
const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
|
|
781
871
|
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
782
|
-
await this.p2pClient.
|
|
872
|
+
await this.p2pClient.handleFailedExecution(failedTxHashes);
|
|
783
873
|
}
|
|
784
874
|
|
|
785
875
|
/**
|
|
@@ -821,7 +911,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
821
911
|
slot: this.slot,
|
|
822
912
|
feeAnalysisId: feeAnalysis?.id,
|
|
823
913
|
});
|
|
824
|
-
this.metrics.
|
|
914
|
+
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
825
915
|
}
|
|
826
916
|
|
|
827
917
|
this.publisher.clearPendingRequests();
|