@aztec/sequencer-client 0.0.1-commit.a072138 → 0.0.1-commit.a89ec08
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/client/sequencer-client.d.ts +16 -7
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +56 -23
- package/dest/config.d.ts +25 -6
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +46 -28
- package/dest/global_variable_builder/global_builder.d.ts +14 -10
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -1
- package/dest/global_variable_builder/global_builder.js +22 -21
- package/dest/global_variable_builder/index.d.ts +2 -2
- package/dest/global_variable_builder/index.d.ts.map +1 -1
- package/dest/publisher/config.d.ts +31 -17
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +101 -42
- package/dest/publisher/sequencer-publisher-factory.d.ts +11 -3
- package/dest/publisher/sequencer-publisher-factory.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-factory.js +13 -2
- package/dest/publisher/sequencer-publisher.d.ts +29 -13
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +106 -57
- package/dest/sequencer/checkpoint_proposal_job.d.ts +10 -4
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +162 -107
- package/dest/sequencer/checkpoint_voter.d.ts +1 -2
- package/dest/sequencer/checkpoint_voter.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_voter.js +2 -5
- package/dest/sequencer/metrics.d.ts +17 -5
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +86 -15
- package/dest/sequencer/sequencer.d.ts +29 -13
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +47 -43
- package/dest/sequencer/timetable.d.ts +4 -6
- package/dest/sequencer/timetable.d.ts.map +1 -1
- package/dest/sequencer/timetable.js +7 -11
- package/dest/sequencer/types.d.ts +2 -2
- package/dest/sequencer/types.d.ts.map +1 -1
- package/dest/test/index.d.ts +3 -5
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.d.ts +11 -11
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +47 -34
- package/dest/test/utils.d.ts +3 -3
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +5 -4
- package/package.json +28 -28
- package/src/client/sequencer-client.ts +78 -21
- package/src/config.ts +61 -38
- package/src/global_variable_builder/global_builder.ts +23 -24
- package/src/global_variable_builder/index.ts +1 -1
- package/src/publisher/config.ts +112 -43
- package/src/publisher/sequencer-publisher-factory.ts +23 -6
- package/src/publisher/sequencer-publisher.ts +123 -71
- package/src/sequencer/checkpoint_proposal_job.ts +238 -136
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/metrics.ts +92 -18
- package/src/sequencer/sequencer.ts +60 -49
- package/src/sequencer/timetable.ts +13 -12
- package/src/sequencer/types.ts +1 -1
- package/src/test/index.ts +2 -4
- package/src/test/mock_checkpoint_builder.ts +65 -49
- package/src/test/utils.ts +5 -2
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { NUM_CHECKPOINT_END_MARKER_FIELDS, getNumBlockEndBlobFields } from '@aztec/blob-lib/encoding';
|
|
2
|
-
import { BLOBS_PER_CHECKPOINT, FIELDS_PER_BLOB } from '@aztec/constants';
|
|
3
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
2
|
import {
|
|
5
3
|
BlockNumber,
|
|
@@ -9,6 +7,11 @@ import {
|
|
|
9
7
|
SlotNumber,
|
|
10
8
|
} from '@aztec/foundation/branded-types';
|
|
11
9
|
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
10
|
+
import {
|
|
11
|
+
flipSignature,
|
|
12
|
+
generateRecoverableSignature,
|
|
13
|
+
generateUnrecoverableSignature,
|
|
14
|
+
} from '@aztec/foundation/crypto/secp256k1-signer';
|
|
12
15
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
13
16
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
14
17
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
@@ -27,18 +30,23 @@ import {
|
|
|
27
30
|
type L2BlockSource,
|
|
28
31
|
MaliciousCommitteeAttestationsAndSigners,
|
|
29
32
|
} from '@aztec/stdlib/block';
|
|
30
|
-
import type
|
|
33
|
+
import { type Checkpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
31
34
|
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
32
35
|
import { Gas } from '@aztec/stdlib/gas';
|
|
33
36
|
import {
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
type BlockBuilderOptions,
|
|
38
|
+
InsufficientValidTxsError,
|
|
36
39
|
type ResolvedSequencerConfig,
|
|
37
40
|
type WorldStateSynchronizer,
|
|
38
41
|
} from '@aztec/stdlib/interfaces/server';
|
|
39
42
|
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
40
|
-
import type {
|
|
41
|
-
|
|
43
|
+
import type {
|
|
44
|
+
BlockProposal,
|
|
45
|
+
BlockProposalOptions,
|
|
46
|
+
CheckpointProposal,
|
|
47
|
+
CheckpointProposalOptions,
|
|
48
|
+
} from '@aztec/stdlib/p2p';
|
|
49
|
+
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
42
50
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
43
51
|
import { type FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
44
52
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
@@ -129,7 +137,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
129
137
|
await Promise.all(votesPromises);
|
|
130
138
|
|
|
131
139
|
if (checkpoint) {
|
|
132
|
-
this.metrics.
|
|
140
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
@@ -186,18 +194,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
186
194
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
187
195
|
|
|
188
196
|
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
189
|
-
const
|
|
190
|
-
c => c.
|
|
191
|
-
|
|
192
|
-
|
|
197
|
+
const previousCheckpointOutHashes = (await this.l2BlockSource.getCheckpointsDataForEpoch(this.epoch))
|
|
198
|
+
.filter(c => c.checkpointNumber < this.checkpointNumber)
|
|
199
|
+
.map(c => c.checkpointOutHash);
|
|
200
|
+
|
|
201
|
+
// Get the fee asset price modifier from the oracle
|
|
202
|
+
const feeAssetPriceModifier = await this.publisher.getFeeAssetPriceModifier();
|
|
193
203
|
|
|
194
204
|
// Create a long-lived forked world state for the checkpoint builder
|
|
195
|
-
using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
205
|
+
await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
196
206
|
|
|
197
207
|
// Create checkpoint builder for the entire slot
|
|
198
208
|
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
|
|
199
209
|
this.checkpointNumber,
|
|
200
210
|
checkpointGlobalVariables,
|
|
211
|
+
feeAssetPriceModifier,
|
|
201
212
|
l1ToL2Messages,
|
|
202
213
|
previousCheckpointOutHashes,
|
|
203
214
|
fork,
|
|
@@ -217,6 +228,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
217
228
|
|
|
218
229
|
let blocksInCheckpoint: L2Block[] = [];
|
|
219
230
|
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
231
|
+
const checkpointBuildTimer = new Timer();
|
|
220
232
|
|
|
221
233
|
try {
|
|
222
234
|
// Main loop: build blocks for the checkpoint
|
|
@@ -232,19 +244,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
232
244
|
// These errors are expected in HA mode, so we yield and let another HA node handle the slot
|
|
233
245
|
// The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
|
|
234
246
|
// 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
|
-
});
|
|
247
|
+
if (this.handleHASigningError(err, 'Block proposal')) {
|
|
248
248
|
return undefined;
|
|
249
249
|
}
|
|
250
250
|
throw err;
|
|
@@ -256,11 +256,45 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
256
256
|
return undefined;
|
|
257
257
|
}
|
|
258
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
|
+
|
|
259
268
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
260
269
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
261
270
|
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
|
|
262
271
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
263
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
|
+
|
|
264
298
|
// Do not collect attestations nor publish to L1 in fisherman mode
|
|
265
299
|
if (this.config.fishermanMode) {
|
|
266
300
|
this.log.info(
|
|
@@ -287,6 +321,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
287
321
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
288
322
|
checkpoint.header,
|
|
289
323
|
checkpoint.archive.root,
|
|
324
|
+
feeAssetPriceModifier,
|
|
290
325
|
lastBlock,
|
|
291
326
|
this.proposer,
|
|
292
327
|
checkpointProposalOptions,
|
|
@@ -313,20 +348,8 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
313
348
|
);
|
|
314
349
|
} catch (err) {
|
|
315
350
|
// 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
|
-
});
|
|
351
|
+
// as soon as we see these errors when creating block or checkpoint proposals.
|
|
352
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
330
353
|
return undefined;
|
|
331
354
|
}
|
|
332
355
|
throw err;
|
|
@@ -337,6 +360,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
337
360
|
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
338
361
|
const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
|
|
339
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
|
+
|
|
340
378
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
341
379
|
txTimeoutAt,
|
|
342
380
|
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
@@ -370,9 +408,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
370
408
|
const blocksInCheckpoint: L2Block[] = [];
|
|
371
409
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
372
410
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
373
|
-
|
|
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;
|
|
411
|
+
const slot = this.slot;
|
|
376
412
|
|
|
377
413
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
378
414
|
let blockPendingBroadcast: { block: L2Block; txs: Tx[] } | undefined = undefined;
|
|
@@ -386,11 +422,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
386
422
|
const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
|
|
387
423
|
|
|
388
424
|
if (!timingInfo.canStart) {
|
|
389
|
-
this.log.debug(`Not enough time left in slot to start another block`, {
|
|
390
|
-
slot: this.slot,
|
|
391
|
-
blocksBuilt,
|
|
392
|
-
secondsIntoSlot,
|
|
393
|
-
});
|
|
425
|
+
this.log.debug(`Not enough time left in slot to start another block`, { slot, blocksBuilt, secondsIntoSlot });
|
|
394
426
|
break;
|
|
395
427
|
}
|
|
396
428
|
|
|
@@ -406,7 +438,6 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
406
438
|
blockNumber,
|
|
407
439
|
indexWithinCheckpoint,
|
|
408
440
|
txHashesAlreadyIncluded,
|
|
409
|
-
remainingBlobFields,
|
|
410
441
|
});
|
|
411
442
|
|
|
412
443
|
// TODO(palla/mbps): Review these conditions. We may want to keep trying in some scenarios.
|
|
@@ -423,56 +454,37 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
423
454
|
} else if ('error' in buildResult) {
|
|
424
455
|
// If there was an error building the block, just exit the loop and give up the rest of the slot
|
|
425
456
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
426
|
-
this.log.warn(`Halting block building for slot ${
|
|
427
|
-
slot: this.slot,
|
|
428
|
-
blocksBuilt,
|
|
429
|
-
error: buildResult.error,
|
|
430
|
-
});
|
|
457
|
+
this.log.warn(`Halting block building for slot ${slot}`, { slot, blocksBuilt, error: buildResult.error });
|
|
431
458
|
}
|
|
432
459
|
break;
|
|
433
460
|
}
|
|
434
461
|
|
|
435
|
-
const { block, usedTxs
|
|
462
|
+
const { block, usedTxs } = buildResult;
|
|
436
463
|
blocksInCheckpoint.push(block);
|
|
437
|
-
|
|
438
|
-
// Update remaining blob fields for the next block
|
|
439
|
-
remainingBlobFields = newRemainingBlobFields;
|
|
440
|
-
|
|
441
|
-
// Sync the proposed block to the archiver to make it available
|
|
442
|
-
// Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
|
|
443
|
-
// Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
|
|
444
|
-
// Fire and forget - don't block the critical path, but log errors
|
|
445
|
-
this.syncProposedBlockToArchiver(block).catch(err => {
|
|
446
|
-
this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
|
|
447
|
-
});
|
|
448
|
-
|
|
449
464
|
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
450
465
|
|
|
451
|
-
// 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.
|
|
452
468
|
if (timingInfo.isLastBlock) {
|
|
453
|
-
this.
|
|
454
|
-
|
|
455
|
-
blockNumber,
|
|
456
|
-
blocksBuilt,
|
|
457
|
-
});
|
|
469
|
+
await this.syncProposedBlockToArchiver(block);
|
|
470
|
+
this.log.verbose(`Completed final block ${blockNumber} for slot ${slot}`, { slot, blockNumber, blocksBuilt });
|
|
458
471
|
blockPendingBroadcast = { block, txs: usedTxs };
|
|
459
472
|
break;
|
|
460
473
|
}
|
|
461
474
|
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
if
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
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));
|
|
476
488
|
|
|
477
489
|
// Wait until the next block's start time
|
|
478
490
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
@@ -486,6 +498,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
486
498
|
return { blocksInCheckpoint, blockPendingBroadcast };
|
|
487
499
|
}
|
|
488
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
|
+
|
|
489
523
|
/** Sleeps until it is time to produce the next block in the slot */
|
|
490
524
|
@trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
|
|
491
525
|
private async waitUntilNextSubslot(nextSubslotStart: number) {
|
|
@@ -505,18 +539,10 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
505
539
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
506
540
|
buildDeadline: Date | undefined;
|
|
507
541
|
txHashesAlreadyIncluded: Set<string>;
|
|
508
|
-
remainingBlobFields: number;
|
|
509
542
|
},
|
|
510
|
-
): Promise<{ block: L2Block; usedTxs: Tx[]
|
|
511
|
-
const {
|
|
512
|
-
|
|
513
|
-
forceCreate,
|
|
514
|
-
blockNumber,
|
|
515
|
-
indexWithinCheckpoint,
|
|
516
|
-
buildDeadline,
|
|
517
|
-
txHashesAlreadyIncluded,
|
|
518
|
-
remainingBlobFields,
|
|
519
|
-
} = opts;
|
|
543
|
+
): Promise<{ block: L2Block; usedTxs: Tx[] } | { error: Error } | undefined> {
|
|
544
|
+
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
545
|
+
opts;
|
|
520
546
|
|
|
521
547
|
this.log.verbose(
|
|
522
548
|
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
|
|
@@ -525,8 +551,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
525
551
|
|
|
526
552
|
try {
|
|
527
553
|
// Wait until we have enough txs to build the block
|
|
528
|
-
const minTxs = this.
|
|
529
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
554
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
530
555
|
if (!canStartBuilding) {
|
|
531
556
|
this.log.warn(
|
|
532
557
|
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`,
|
|
@@ -540,7 +565,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
540
565
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
541
566
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
542
567
|
const pendingTxs = filter(
|
|
543
|
-
this.p2pClient.
|
|
568
|
+
this.p2pClient.iterateEligiblePendingTxs(),
|
|
544
569
|
tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
|
|
545
570
|
);
|
|
546
571
|
|
|
@@ -550,19 +575,26 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
550
575
|
);
|
|
551
576
|
this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
|
|
552
577
|
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
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 = {
|
|
558
583
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
584
|
+
maxBlockGas:
|
|
585
|
+
this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
|
|
586
|
+
? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
|
|
587
|
+
: undefined,
|
|
562
588
|
deadline: buildDeadline,
|
|
589
|
+
isBuildingProposal: true,
|
|
590
|
+
minValidTxs,
|
|
591
|
+
maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
|
|
592
|
+
perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
|
|
563
593
|
};
|
|
564
594
|
|
|
565
|
-
// 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.
|
|
566
598
|
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
|
|
567
599
|
checkpointBuilder,
|
|
568
600
|
pendingTxs,
|
|
@@ -574,14 +606,16 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
574
606
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
575
607
|
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
576
608
|
|
|
577
|
-
|
|
578
|
-
// too long, then we may not get to minTxsPerBlock after executing public functions.
|
|
579
|
-
const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
|
|
580
|
-
const numTxs = buildResult.status === 'no-valid-txs' ? 0 : buildResult.numTxs;
|
|
581
|
-
if (buildResult.status === 'no-valid-txs' || (!forceCreate && numTxs < minValidTxs)) {
|
|
609
|
+
if (buildResult.status === 'insufficient-valid-txs') {
|
|
582
610
|
this.log.warn(
|
|
583
611
|
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
|
|
584
|
-
{
|
|
612
|
+
{
|
|
613
|
+
slot: this.slot,
|
|
614
|
+
blockNumber,
|
|
615
|
+
numTxs: buildResult.processedCount,
|
|
616
|
+
indexWithinCheckpoint,
|
|
617
|
+
minValidTxs,
|
|
618
|
+
},
|
|
585
619
|
);
|
|
586
620
|
this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
|
|
587
621
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
@@ -589,7 +623,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
589
623
|
}
|
|
590
624
|
|
|
591
625
|
// Block creation succeeded, emit stats and metrics
|
|
592
|
-
const {
|
|
626
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
593
627
|
|
|
594
628
|
const blockStats = {
|
|
595
629
|
eventName: 'l2-block-built',
|
|
@@ -600,7 +634,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
600
634
|
|
|
601
635
|
const blockHash = await block.hash();
|
|
602
636
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
603
|
-
const manaPerSec =
|
|
637
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
604
638
|
|
|
605
639
|
this.log.info(
|
|
606
640
|
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
|
|
@@ -608,9 +642,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
608
642
|
);
|
|
609
643
|
|
|
610
644
|
this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
|
|
611
|
-
this.metrics.recordBuiltBlock(blockBuildDuration,
|
|
645
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
612
646
|
|
|
613
|
-
return { block, usedTxs
|
|
647
|
+
return { block, usedTxs };
|
|
614
648
|
} catch (err: any) {
|
|
615
649
|
this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
|
|
616
650
|
this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
|
|
@@ -620,13 +654,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
620
654
|
}
|
|
621
655
|
}
|
|
622
656
|
|
|
623
|
-
/** Uses the checkpoint builder to build a block, catching
|
|
657
|
+
/** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
|
|
624
658
|
private async buildSingleBlockWithCheckpointBuilder(
|
|
625
659
|
checkpointBuilder: CheckpointBuilder,
|
|
626
660
|
pendingTxs: AsyncIterable<Tx>,
|
|
627
661
|
blockNumber: BlockNumber,
|
|
628
662
|
blockTimestamp: bigint,
|
|
629
|
-
blockBuilderOptions:
|
|
663
|
+
blockBuilderOptions: BlockBuilderOptions,
|
|
630
664
|
) {
|
|
631
665
|
try {
|
|
632
666
|
const workTimer = new Timer();
|
|
@@ -634,8 +668,12 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
634
668
|
const blockBuildDuration = workTimer.ms();
|
|
635
669
|
return { ...result, blockBuildDuration, status: 'success' as const };
|
|
636
670
|
} catch (err: unknown) {
|
|
637
|
-
if (isErrorClass(err,
|
|
638
|
-
return {
|
|
671
|
+
if (isErrorClass(err, InsufficientValidTxsError)) {
|
|
672
|
+
return {
|
|
673
|
+
failedTxs: err.failedTxs,
|
|
674
|
+
processedCount: err.processedCount,
|
|
675
|
+
status: 'insufficient-valid-txs' as const,
|
|
676
|
+
};
|
|
639
677
|
}
|
|
640
678
|
throw err;
|
|
641
679
|
}
|
|
@@ -648,7 +686,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
648
686
|
blockNumber: BlockNumber;
|
|
649
687
|
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
650
688
|
buildDeadline: Date | undefined;
|
|
651
|
-
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
689
|
+
}): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
|
|
652
690
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
653
691
|
|
|
654
692
|
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
@@ -665,7 +703,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
665
703
|
// If we're past deadline, or we have no deadline, give up
|
|
666
704
|
const now = this.dateProvider.nowAsDate();
|
|
667
705
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
668
|
-
return { canStartBuilding: false, availableTxs
|
|
706
|
+
return { canStartBuilding: false, availableTxs, minTxs };
|
|
669
707
|
}
|
|
670
708
|
|
|
671
709
|
// Wait a bit before checking again
|
|
@@ -674,11 +712,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
674
712
|
`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (have ${availableTxs} but need ${minTxs})`,
|
|
675
713
|
{ blockNumber, slot: this.slot, indexWithinCheckpoint },
|
|
676
714
|
);
|
|
677
|
-
await
|
|
715
|
+
await this.waitForTxsPollingInterval();
|
|
678
716
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
679
717
|
}
|
|
680
718
|
|
|
681
|
-
return { canStartBuilding: true, availableTxs };
|
|
719
|
+
return { canStartBuilding: true, availableTxs, minTxs };
|
|
682
720
|
}
|
|
683
721
|
|
|
684
722
|
/**
|
|
@@ -730,11 +768,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
730
768
|
|
|
731
769
|
collectedAttestationsCount = attestations.length;
|
|
732
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
|
+
|
|
733
783
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
734
|
-
const sorted = orderAttestations(
|
|
784
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
735
785
|
|
|
736
786
|
// Manipulate the attestations if we've been configured to do so
|
|
737
|
-
if (
|
|
787
|
+
if (
|
|
788
|
+
this.config.injectFakeAttestation ||
|
|
789
|
+
this.config.injectHighSValueAttestation ||
|
|
790
|
+
this.config.injectUnrecoverableSignatureAttestation ||
|
|
791
|
+
this.config.shuffleAttestationOrdering
|
|
792
|
+
) {
|
|
738
793
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
739
794
|
}
|
|
740
795
|
|
|
@@ -763,7 +818,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
763
818
|
this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
|
|
764
819
|
);
|
|
765
820
|
|
|
766
|
-
if (
|
|
821
|
+
if (
|
|
822
|
+
this.config.injectFakeAttestation ||
|
|
823
|
+
this.config.injectHighSValueAttestation ||
|
|
824
|
+
this.config.injectUnrecoverableSignatureAttestation
|
|
825
|
+
) {
|
|
767
826
|
// Find non-empty attestations that are not from the proposer
|
|
768
827
|
const nonProposerIndices: number[] = [];
|
|
769
828
|
for (let i = 0; i < attestations.length; i++) {
|
|
@@ -773,8 +832,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
773
832
|
}
|
|
774
833
|
if (nonProposerIndices.length > 0) {
|
|
775
834
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
776
|
-
this.
|
|
777
|
-
|
|
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
|
+
}
|
|
778
849
|
}
|
|
779
850
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
780
851
|
}
|
|
@@ -803,16 +874,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
803
874
|
const failedTxData = failedTxs.map(fail => fail.tx);
|
|
804
875
|
const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
|
|
805
876
|
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
806
|
-
await this.p2pClient.
|
|
877
|
+
await this.p2pClient.handleFailedExecution(failedTxHashes);
|
|
807
878
|
}
|
|
808
879
|
|
|
809
880
|
/**
|
|
810
881
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
811
882
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
812
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.
|
|
813
888
|
*/
|
|
814
889
|
private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
|
|
815
|
-
if (this.config.skipPushProposedBlocksToArchiver !== false) {
|
|
890
|
+
if (this.config.skipPushProposedBlocksToArchiver !== false || this.config.fishermanMode) {
|
|
816
891
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
817
892
|
blockNumber: block.number,
|
|
818
893
|
slot: block.header.globalVariables.slotNumber,
|
|
@@ -845,12 +920,34 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
845
920
|
slot: this.slot,
|
|
846
921
|
feeAnalysisId: feeAnalysis?.id,
|
|
847
922
|
});
|
|
848
|
-
this.metrics.
|
|
923
|
+
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
849
924
|
}
|
|
850
925
|
|
|
851
926
|
this.publisher.clearPendingRequests();
|
|
852
927
|
}
|
|
853
928
|
|
|
929
|
+
/**
|
|
930
|
+
* Helper to handle HA double-signing errors. Returns true if the error was handled (caller should yield).
|
|
931
|
+
*/
|
|
932
|
+
private handleHASigningError(err: any, errorContext: string): boolean {
|
|
933
|
+
if (err instanceof DutyAlreadySignedError) {
|
|
934
|
+
this.log.info(`${errorContext} for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
935
|
+
slot: this.slot,
|
|
936
|
+
signedByNode: err.signedByNode,
|
|
937
|
+
});
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
940
|
+
if (err instanceof SlashingProtectionError) {
|
|
941
|
+
this.log.info(`${errorContext} for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
942
|
+
slot: this.slot,
|
|
943
|
+
existingMessageHash: err.existingMessageHash,
|
|
944
|
+
attemptedMessageHash: err.attemptedMessageHash,
|
|
945
|
+
});
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
return false;
|
|
949
|
+
}
|
|
950
|
+
|
|
854
951
|
/** Waits until a specific time within the current slot */
|
|
855
952
|
@trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
|
|
856
953
|
protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
|
|
@@ -859,6 +956,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
859
956
|
await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
|
|
860
957
|
}
|
|
861
958
|
|
|
959
|
+
/** Waits the polling interval for transactions. Extracted for test overriding. */
|
|
960
|
+
protected async waitForTxsPollingInterval(): Promise<void> {
|
|
961
|
+
await sleep(TXS_POLLING_MS);
|
|
962
|
+
}
|
|
963
|
+
|
|
862
964
|
private getSlotStartBuildTimestamp(): number {
|
|
863
965
|
return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
|
|
864
966
|
}
|