@aztec/sequencer-client 0.0.1-commit.f146247c → 0.0.1-commit.f224bb98b
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 +23 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +99 -16
- package/dest/config.d.ts +24 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +40 -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/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 +299 -30
- package/dest/sequencer/checkpoint_proposal_job.d.ts +8 -4
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +132 -79
- 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 +41 -40
- 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 +8 -8
- 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 +28 -28
- package/src/client/sequencer-client.ts +135 -18
- package/src/config.ts +54 -38
- package/src/global_variable_builder/global_builder.ts +1 -1
- 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 +300 -43
- package/src/sequencer/checkpoint_proposal_job.ts +171 -86
- package/src/sequencer/metrics.ts +92 -18
- package/src/sequencer/sequencer.ts +52 -46
- 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 +60 -46
- 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,7 +30,7 @@ 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 {
|
|
@@ -38,7 +41,7 @@ import {
|
|
|
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
|
|
@@ -232,19 +239,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
232
239
|
// These errors are expected in HA mode, so we yield and let another HA node handle the slot
|
|
233
240
|
// The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
|
|
234
241
|
// which is normal for block building (may have picked different txs)
|
|
235
|
-
if (err
|
|
236
|
-
this.log.info(`Checkpoint proposal for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
237
|
-
slot: this.slot,
|
|
238
|
-
signedByNode: err.signedByNode,
|
|
239
|
-
});
|
|
240
|
-
return undefined;
|
|
241
|
-
}
|
|
242
|
-
if (err instanceof SlashingProtectionError) {
|
|
243
|
-
this.log.info(`Checkpoint proposal for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
244
|
-
slot: this.slot,
|
|
245
|
-
existingMessageHash: err.existingMessageHash,
|
|
246
|
-
attemptedMessageHash: err.attemptedMessageHash,
|
|
247
|
-
});
|
|
242
|
+
if (this.handleHASigningError(err, 'Block proposal')) {
|
|
248
243
|
return undefined;
|
|
249
244
|
}
|
|
250
245
|
throw err;
|
|
@@ -256,11 +251,44 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
256
251
|
return undefined;
|
|
257
252
|
}
|
|
258
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
|
+
|
|
259
263
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
260
264
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
261
265
|
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
|
|
262
266
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
263
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
|
+
|
|
264
292
|
// Do not collect attestations nor publish to L1 in fisherman mode
|
|
265
293
|
if (this.config.fishermanMode) {
|
|
266
294
|
this.log.info(
|
|
@@ -287,6 +315,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
287
315
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
288
316
|
checkpoint.header,
|
|
289
317
|
checkpoint.archive.root,
|
|
318
|
+
feeAssetPriceModifier,
|
|
290
319
|
lastBlock,
|
|
291
320
|
this.proposer,
|
|
292
321
|
checkpointProposalOptions,
|
|
@@ -313,20 +342,8 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
313
342
|
);
|
|
314
343
|
} catch (err) {
|
|
315
344
|
// We shouldn't really get here since we yield to another HA node
|
|
316
|
-
// as soon as we see these errors when creating block proposals.
|
|
317
|
-
if (err
|
|
318
|
-
this.log.info(`Attestations signature for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
319
|
-
slot: this.slot,
|
|
320
|
-
signedByNode: err.signedByNode,
|
|
321
|
-
});
|
|
322
|
-
return undefined;
|
|
323
|
-
}
|
|
324
|
-
if (err instanceof SlashingProtectionError) {
|
|
325
|
-
this.log.info(`Attestations signature for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
326
|
-
slot: this.slot,
|
|
327
|
-
existingMessageHash: err.existingMessageHash,
|
|
328
|
-
attemptedMessageHash: err.attemptedMessageHash,
|
|
329
|
-
});
|
|
345
|
+
// as soon as we see these errors when creating block or checkpoint proposals.
|
|
346
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
330
347
|
return undefined;
|
|
331
348
|
}
|
|
332
349
|
throw err;
|
|
@@ -337,6 +354,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
337
354
|
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
338
355
|
const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
|
|
339
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
|
+
|
|
340
372
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
341
373
|
txTimeoutAt,
|
|
342
374
|
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
@@ -371,9 +403,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
371
403
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
372
404
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
373
405
|
|
|
374
|
-
// Remaining blob fields available for blocks (checkpoint end marker already subtracted)
|
|
375
|
-
let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
|
|
376
|
-
|
|
377
406
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
378
407
|
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
379
408
|
|
|
@@ -406,7 +435,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
406
435
|
blockNumber,
|
|
407
436
|
indexWithinCheckpoint,
|
|
408
437
|
txHashesAlreadyIncluded,
|
|
409
|
-
remainingBlobFields,
|
|
410
438
|
});
|
|
411
439
|
|
|
412
440
|
// TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
|
|
@@ -432,12 +460,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
432
460
|
break;
|
|
433
461
|
}
|
|
434
462
|
|
|
435
|
-
const { block, usedTxs
|
|
463
|
+
const { block, usedTxs } = buildResult;
|
|
436
464
|
blocksInCheckpoint.push(block);
|
|
437
465
|
|
|
438
|
-
// Update remaining blob fields for the next block
|
|
439
|
-
remainingBlobFields = newRemainingBlobFields;
|
|
440
|
-
|
|
441
466
|
// Sync the proposed block to the archiver to make it available
|
|
442
467
|
// Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
|
|
443
468
|
// Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
|
|
@@ -505,18 +530,10 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
505
530
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
506
531
|
buildDeadline: Date | undefined;
|
|
507
532
|
txHashesAlreadyIncluded: Set<string>;
|
|
508
|
-
remainingBlobFields: number;
|
|
509
533
|
},
|
|
510
|
-
): Promise<{ block: L2Block; usedTxs: Tx[]
|
|
511
|
-
const {
|
|
512
|
-
|
|
513
|
-
forceCreate,
|
|
514
|
-
blockNumber,
|
|
515
|
-
indexWithinCheckpoint,
|
|
516
|
-
buildDeadline,
|
|
517
|
-
txHashesAlreadyIncluded,
|
|
518
|
-
remainingBlobFields,
|
|
519
|
-
} = opts;
|
|
534
|
+
): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
|
|
535
|
+
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
536
|
+
opts;
|
|
520
537
|
|
|
521
538
|
this.log.verbose(
|
|
522
539
|
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
|
|
@@ -525,8 +542,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
525
542
|
|
|
526
543
|
try {
|
|
527
544
|
// Wait until we have enough txs to build the block
|
|
528
|
-
const minTxs = this.
|
|
529
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
545
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
530
546
|
if (!canStartBuilding) {
|
|
531
547
|
this.log.warn(
|
|
532
548
|
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`,
|
|
@@ -540,7 +556,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
540
556
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
541
557
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
542
558
|
const pendingTxs = filter(
|
|
543
|
-
this.p2pClient.
|
|
559
|
+
this.p2pClient.iterateEligiblePendingTxs(),
|
|
544
560
|
tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
|
|
545
561
|
);
|
|
546
562
|
|
|
@@ -550,16 +566,16 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
550
566
|
);
|
|
551
567
|
this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
|
|
552
568
|
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
const maxBlobFieldsForTxs = remainingBlobFields - blockEndOverhead;
|
|
556
|
-
|
|
569
|
+
// Per-block limits derived at startup by computeBlockLimits(), further capped
|
|
570
|
+
// by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
|
|
557
571
|
const blockBuilderOptions: PublicProcessorLimits = {
|
|
558
572
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
573
|
+
maxBlockGas:
|
|
574
|
+
this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
|
|
575
|
+
? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
|
|
576
|
+
: undefined,
|
|
562
577
|
deadline: buildDeadline,
|
|
578
|
+
isBuildingProposal: true,
|
|
563
579
|
};
|
|
564
580
|
|
|
565
581
|
// Actually build the block by executing txs
|
|
@@ -589,7 +605,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
589
605
|
}
|
|
590
606
|
|
|
591
607
|
// Block creation succeeded, emit stats and metrics
|
|
592
|
-
const {
|
|
608
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration } = buildResult;
|
|
593
609
|
|
|
594
610
|
const blockStats = {
|
|
595
611
|
eventName: 'l2-block-built',
|
|
@@ -600,7 +616,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
600
616
|
|
|
601
617
|
const blockHash = await block.hash();
|
|
602
618
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
603
|
-
const manaPerSec =
|
|
619
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
604
620
|
|
|
605
621
|
this.log.info(
|
|
606
622
|
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
|
|
@@ -608,9 +624,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
608
624
|
);
|
|
609
625
|
|
|
610
626
|
this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
|
|
611
|
-
this.metrics.recordBuiltBlock(blockBuildDuration,
|
|
627
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
612
628
|
|
|
613
|
-
return { block, usedTxs
|
|
629
|
+
return { block, usedTxs };
|
|
614
630
|
} catch (err: any) {
|
|
615
631
|
this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
|
|
616
632
|
this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
|
|
@@ -648,7 +664,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
648
664
|
blockNumber: BlockNumber;
|
|
649
665
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
650
666
|
buildDeadline: Date | undefined;
|
|
651
|
-
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
667
|
+
}): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
|
|
652
668
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
653
669
|
|
|
654
670
|
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
@@ -665,7 +681,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
665
681
|
// If we're past deadline, or we have no deadline, give up
|
|
666
682
|
const now = this.dateProvider.nowAsDate();
|
|
667
683
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
668
|
-
return { canStartBuilding: false, availableTxs
|
|
684
|
+
return { canStartBuilding: false, availableTxs, minTxs };
|
|
669
685
|
}
|
|
670
686
|
|
|
671
687
|
// Wait a bit before checking again
|
|
@@ -674,11 +690,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
674
690
|
`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (have ${availableTxs} but need ${minTxs})`,
|
|
675
691
|
{ blockNumber, slot: this.slot, indexWithinCheckpoint },
|
|
676
692
|
);
|
|
677
|
-
await
|
|
693
|
+
await this.waitForTxsPollingInterval();
|
|
678
694
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
679
695
|
}
|
|
680
696
|
|
|
681
|
-
return { canStartBuilding: true, availableTxs };
|
|
697
|
+
return { canStartBuilding: true, availableTxs, minTxs };
|
|
682
698
|
}
|
|
683
699
|
|
|
684
700
|
/**
|
|
@@ -730,11 +746,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
730
746
|
|
|
731
747
|
collectedAttestationsCount = attestations.length;
|
|
732
748
|
|
|
749
|
+
// Trim attestations to minimum required to save L1 calldata gas
|
|
750
|
+
const localAddresses = this.validatorClient.getValidatorAddresses();
|
|
751
|
+
const trimmed = trimAttestations(
|
|
752
|
+
attestations,
|
|
753
|
+
numberOfRequiredAttestations,
|
|
754
|
+
this.attestorAddress,
|
|
755
|
+
localAddresses,
|
|
756
|
+
);
|
|
757
|
+
if (trimmed.length < attestations.length) {
|
|
758
|
+
this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
|
|
759
|
+
}
|
|
760
|
+
|
|
733
761
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
734
|
-
const sorted = orderAttestations(
|
|
762
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
735
763
|
|
|
736
764
|
// Manipulate the attestations if we've been configured to do so
|
|
737
|
-
if (
|
|
765
|
+
if (
|
|
766
|
+
this.config.injectFakeAttestation ||
|
|
767
|
+
this.config.injectHighSValueAttestation ||
|
|
768
|
+
this.config.injectUnrecoverableSignatureAttestation ||
|
|
769
|
+
this.config.shuffleAttestationOrdering
|
|
770
|
+
) {
|
|
738
771
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
739
772
|
}
|
|
740
773
|
|
|
@@ -763,7 +796,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
763
796
|
this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
|
|
764
797
|
);
|
|
765
798
|
|
|
766
|
-
if (
|
|
799
|
+
if (
|
|
800
|
+
this.config.injectFakeAttestation ||
|
|
801
|
+
this.config.injectHighSValueAttestation ||
|
|
802
|
+
this.config.injectUnrecoverableSignatureAttestation
|
|
803
|
+
) {
|
|
767
804
|
// Find non-empty attestations that are not from the proposer
|
|
768
805
|
const nonProposerIndices: number[] = [];
|
|
769
806
|
for (let i = 0; i < attestations.length; i++) {
|
|
@@ -773,8 +810,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
773
810
|
}
|
|
774
811
|
if (nonProposerIndices.length > 0) {
|
|
775
812
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
776
|
-
this.
|
|
777
|
-
|
|
813
|
+
if (this.config.injectHighSValueAttestation) {
|
|
814
|
+
this.log.warn(
|
|
815
|
+
`Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
816
|
+
);
|
|
817
|
+
unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
|
|
818
|
+
} else if (this.config.injectUnrecoverableSignatureAttestation) {
|
|
819
|
+
this.log.warn(
|
|
820
|
+
`Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
821
|
+
);
|
|
822
|
+
unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
|
|
823
|
+
} else {
|
|
824
|
+
this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
825
|
+
unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
|
|
826
|
+
}
|
|
778
827
|
}
|
|
779
828
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
780
829
|
}
|
|
@@ -783,11 +832,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
783
832
|
this.log.warn(`Shuffling attestation ordering in checkpoint for slot ${slotNumber} (proposer #${proposerIndex})`);
|
|
784
833
|
|
|
785
834
|
const shuffled = [...attestations];
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
835
|
+
|
|
836
|
+
// Find two non-proposer positions that both have non-empty signatures to swap.
|
|
837
|
+
// This ensures the bitmap doesn't change, so the MaliciousCommitteeAttestationsAndSigners
|
|
838
|
+
// signers array stays correctly aligned with L1's committee reconstruction.
|
|
839
|
+
const swappable: number[] = [];
|
|
840
|
+
for (let k = 0; k < shuffled.length; k++) {
|
|
841
|
+
if (!shuffled[k].signature.isEmpty() && k !== proposerIndex) {
|
|
842
|
+
swappable.push(k);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (swappable.length >= 2) {
|
|
846
|
+
const [i, j] = [swappable[0], swappable[1]];
|
|
847
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
848
|
+
}
|
|
791
849
|
|
|
792
850
|
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
793
851
|
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
@@ -803,7 +861,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
803
861
|
const failedTxData = failedTxs.map(fail => fail.tx);
|
|
804
862
|
const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
|
|
805
863
|
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
806
|
-
await this.p2pClient.
|
|
864
|
+
await this.p2pClient.handleFailedExecution(failedTxHashes);
|
|
807
865
|
}
|
|
808
866
|
|
|
809
867
|
/**
|
|
@@ -845,12 +903,34 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
845
903
|
slot: this.slot,
|
|
846
904
|
feeAnalysisId: feeAnalysis?.id,
|
|
847
905
|
});
|
|
848
|
-
this.metrics.
|
|
906
|
+
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
849
907
|
}
|
|
850
908
|
|
|
851
909
|
this.publisher.clearPendingRequests();
|
|
852
910
|
}
|
|
853
911
|
|
|
912
|
+
/**
|
|
913
|
+
* Helper to handle HA double-signing errors. Returns true if the error was handled (caller should yield).
|
|
914
|
+
*/
|
|
915
|
+
private handleHASigningError(err: any, errorContext: string): boolean {
|
|
916
|
+
if (err instanceof DutyAlreadySignedError) {
|
|
917
|
+
this.log.info(`${errorContext} for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
918
|
+
slot: this.slot,
|
|
919
|
+
signedByNode: err.signedByNode,
|
|
920
|
+
});
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
if (err instanceof SlashingProtectionError) {
|
|
924
|
+
this.log.info(`${errorContext} for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
925
|
+
slot: this.slot,
|
|
926
|
+
existingMessageHash: err.existingMessageHash,
|
|
927
|
+
attemptedMessageHash: err.attemptedMessageHash,
|
|
928
|
+
});
|
|
929
|
+
return true;
|
|
930
|
+
}
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
|
|
854
934
|
/** Waits until a specific time within the current slot */
|
|
855
935
|
@trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
|
|
856
936
|
protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
|
|
@@ -859,6 +939,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
859
939
|
await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
|
|
860
940
|
}
|
|
861
941
|
|
|
942
|
+
/** Waits the polling interval for transactions. Extracted for test overriding. */
|
|
943
|
+
protected async waitForTxsPollingInterval(): Promise<void> {
|
|
944
|
+
await sleep(TXS_POLLING_MS);
|
|
945
|
+
}
|
|
946
|
+
|
|
862
947
|
private getSlotStartBuildTimestamp(): number {
|
|
863
948
|
return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
|
|
864
949
|
}
|