@aztec/sequencer-client 0.0.1-commit.fffb133c → 0.0.1-dev
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-metrics.d.ts +1 -1
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher-metrics.js +12 -4
- package/dest/publisher/sequencer-publisher.d.ts +28 -14
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +137 -74
- package/dest/sequencer/checkpoint_proposal_job.d.ts +35 -10
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +212 -137
- 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 +111 -30
- package/dest/sequencer/sequencer.d.ts +29 -13
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +48 -44
- 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 +12 -12
- package/dest/test/mock_checkpoint_builder.d.ts.map +1 -1
- package/dest/test/mock_checkpoint_builder.js +47 -36
- 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-metrics.ts +7 -3
- package/src/publisher/sequencer-publisher.ts +149 -90
- package/src/sequencer/checkpoint_proposal_job.ts +309 -186
- package/src/sequencer/checkpoint_voter.ts +1 -12
- package/src/sequencer/metrics.ts +124 -32
- package/src/sequencer/sequencer.ts +61 -50
- 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 +67 -53
- package/src/test/utils.ts +5 -2
|
@@ -1,16 +1,25 @@
|
|
|
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
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BlockNumber,
|
|
4
|
+
CheckpointNumber,
|
|
5
|
+
EpochNumber,
|
|
6
|
+
IndexWithinCheckpoint,
|
|
7
|
+
SlotNumber,
|
|
8
|
+
} from '@aztec/foundation/branded-types';
|
|
5
9
|
import { randomInt } from '@aztec/foundation/crypto/random';
|
|
10
|
+
import {
|
|
11
|
+
flipSignature,
|
|
12
|
+
generateRecoverableSignature,
|
|
13
|
+
generateUnrecoverableSignature,
|
|
14
|
+
} from '@aztec/foundation/crypto/secp256k1-signer';
|
|
6
15
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
7
16
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
8
17
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
9
18
|
import { filter } from '@aztec/foundation/iterator';
|
|
10
|
-
import type
|
|
19
|
+
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
11
20
|
import { sleep, sleepUntil } from '@aztec/foundation/sleep';
|
|
12
21
|
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
13
|
-
import { type TypedEventEmitter, unfreeze } from '@aztec/foundation/types';
|
|
22
|
+
import { type TypedEventEmitter, isErrorClass, unfreeze } from '@aztec/foundation/types';
|
|
14
23
|
import type { P2P } from '@aztec/p2p';
|
|
15
24
|
import type { SlasherClientInterface } from '@aztec/slasher';
|
|
16
25
|
import {
|
|
@@ -21,17 +30,23 @@ import {
|
|
|
21
30
|
type L2BlockSource,
|
|
22
31
|
MaliciousCommitteeAttestationsAndSigners,
|
|
23
32
|
} from '@aztec/stdlib/block';
|
|
24
|
-
import type
|
|
33
|
+
import { type Checkpoint, validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
25
34
|
import { getSlotStartBuildTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
26
35
|
import { Gas } from '@aztec/stdlib/gas';
|
|
27
|
-
import
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
import {
|
|
37
|
+
type BlockBuilderOptions,
|
|
38
|
+
InsufficientValidTxsError,
|
|
39
|
+
type ResolvedSequencerConfig,
|
|
40
|
+
type WorldStateSynchronizer,
|
|
31
41
|
} from '@aztec/stdlib/interfaces/server';
|
|
32
42
|
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
33
|
-
import type {
|
|
34
|
-
|
|
43
|
+
import type {
|
|
44
|
+
BlockProposal,
|
|
45
|
+
BlockProposalOptions,
|
|
46
|
+
CheckpointProposal,
|
|
47
|
+
CheckpointProposalOptions,
|
|
48
|
+
} from '@aztec/stdlib/p2p';
|
|
49
|
+
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
35
50
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
36
51
|
import { type FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
37
52
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
@@ -59,6 +74,8 @@ const TXS_POLLING_MS = 500;
|
|
|
59
74
|
* the Sequencer once the check for being the proposer for the slot has succeeded.
|
|
60
75
|
*/
|
|
61
76
|
export class CheckpointProposalJob implements Traceable {
|
|
77
|
+
protected readonly log: Logger;
|
|
78
|
+
|
|
62
79
|
constructor(
|
|
63
80
|
private readonly epoch: EpochNumber,
|
|
64
81
|
private readonly slot: SlotNumber,
|
|
@@ -86,9 +103,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
86
103
|
private readonly metrics: SequencerMetrics,
|
|
87
104
|
private readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
|
|
88
105
|
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
|
|
89
|
-
protected readonly log: Logger,
|
|
90
106
|
public readonly tracer: Tracer,
|
|
91
|
-
|
|
107
|
+
bindings?: LoggerBindings,
|
|
108
|
+
) {
|
|
109
|
+
this.log = createLogger('sequencer:checkpoint-proposal', { ...bindings, instanceId: `slot-${slot}` });
|
|
110
|
+
}
|
|
92
111
|
|
|
93
112
|
/**
|
|
94
113
|
* Executes the checkpoint proposal job.
|
|
@@ -118,7 +137,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
118
137
|
await Promise.all(votesPromises);
|
|
119
138
|
|
|
120
139
|
if (checkpoint) {
|
|
121
|
-
this.metrics.
|
|
140
|
+
this.metrics.recordCheckpointProposalSuccess();
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
// Do not post anything to L1 if we are fishermen, but do perform L1 fee analysis
|
|
@@ -175,21 +194,25 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
175
194
|
const inHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
176
195
|
|
|
177
196
|
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
178
|
-
const
|
|
179
|
-
c => c.
|
|
180
|
-
|
|
181
|
-
|
|
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();
|
|
182
203
|
|
|
183
204
|
// Create a long-lived forked world state for the checkpoint builder
|
|
184
|
-
using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
205
|
+
await using fork = await this.worldState.fork(this.syncedToBlockNumber, { closeDelayMs: 12_000 });
|
|
185
206
|
|
|
186
207
|
// Create checkpoint builder for the entire slot
|
|
187
208
|
const checkpointBuilder = await this.checkpointsBuilder.startCheckpoint(
|
|
188
209
|
this.checkpointNumber,
|
|
189
210
|
checkpointGlobalVariables,
|
|
211
|
+
feeAssetPriceModifier,
|
|
190
212
|
l1ToL2Messages,
|
|
191
213
|
previousCheckpointOutHashes,
|
|
192
214
|
fork,
|
|
215
|
+
this.log.getBindings(),
|
|
193
216
|
);
|
|
194
217
|
|
|
195
218
|
// Options for the validator client when creating block and checkpoint proposals
|
|
@@ -204,7 +227,8 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
204
227
|
};
|
|
205
228
|
|
|
206
229
|
let blocksInCheckpoint: L2Block[] = [];
|
|
207
|
-
let blockPendingBroadcast:
|
|
230
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
231
|
+
const checkpointBuildTimer = new Timer();
|
|
208
232
|
|
|
209
233
|
try {
|
|
210
234
|
// Main loop: build blocks for the checkpoint
|
|
@@ -220,19 +244,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
220
244
|
// These errors are expected in HA mode, so we yield and let another HA node handle the slot
|
|
221
245
|
// The only distinction between the 2 errors is SlashingProtectionError throws when the payload is different,
|
|
222
246
|
// which is normal for block building (may have picked different txs)
|
|
223
|
-
if (err
|
|
224
|
-
this.log.info(`Checkpoint proposal for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
225
|
-
slot: this.slot,
|
|
226
|
-
signedByNode: err.signedByNode,
|
|
227
|
-
});
|
|
228
|
-
return undefined;
|
|
229
|
-
}
|
|
230
|
-
if (err instanceof SlashingProtectionError) {
|
|
231
|
-
this.log.info(`Checkpoint proposal for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
232
|
-
slot: this.slot,
|
|
233
|
-
existingMessageHash: err.existingMessageHash,
|
|
234
|
-
attemptedMessageHash: err.attemptedMessageHash,
|
|
235
|
-
});
|
|
247
|
+
if (this.handleHASigningError(err, 'Block proposal')) {
|
|
236
248
|
return undefined;
|
|
237
249
|
}
|
|
238
250
|
throw err;
|
|
@@ -244,11 +256,45 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
244
256
|
return undefined;
|
|
245
257
|
}
|
|
246
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
|
+
|
|
247
268
|
// Assemble and broadcast the checkpoint proposal, including the last block that was not
|
|
248
269
|
// broadcasted yet, and wait to collect the committee attestations.
|
|
249
270
|
this.setStateFn(SequencerState.ASSEMBLING_CHECKPOINT, this.slot);
|
|
250
271
|
const checkpoint = await checkpointBuilder.completeCheckpoint();
|
|
251
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
|
+
|
|
252
298
|
// Do not collect attestations nor publish to L1 in fisherman mode
|
|
253
299
|
if (this.config.fishermanMode) {
|
|
254
300
|
this.log.info(
|
|
@@ -264,18 +310,12 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
264
310
|
return checkpoint;
|
|
265
311
|
}
|
|
266
312
|
|
|
267
|
-
// Include the block pending broadcast in the checkpoint proposal if any
|
|
268
|
-
const lastBlock = blockPendingBroadcast && {
|
|
269
|
-
blockHeader: blockPendingBroadcast.block.header,
|
|
270
|
-
indexWithinCheckpoint: blockPendingBroadcast.block.indexWithinCheckpoint,
|
|
271
|
-
txs: blockPendingBroadcast.txs,
|
|
272
|
-
};
|
|
273
|
-
|
|
274
313
|
// Create the checkpoint proposal and broadcast it
|
|
275
314
|
const proposal = await this.validatorClient.createCheckpointProposal(
|
|
276
315
|
checkpoint.header,
|
|
277
316
|
checkpoint.archive.root,
|
|
278
|
-
|
|
317
|
+
feeAssetPriceModifier,
|
|
318
|
+
blockPendingBroadcast,
|
|
279
319
|
this.proposer,
|
|
280
320
|
checkpointProposalOptions,
|
|
281
321
|
);
|
|
@@ -301,20 +341,8 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
301
341
|
);
|
|
302
342
|
} catch (err) {
|
|
303
343
|
// We shouldn't really get here since we yield to another HA node
|
|
304
|
-
// as soon as we see these errors when creating block proposals.
|
|
305
|
-
if (err
|
|
306
|
-
this.log.info(`Attestations signature for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
307
|
-
slot: this.slot,
|
|
308
|
-
signedByNode: err.signedByNode,
|
|
309
|
-
});
|
|
310
|
-
return undefined;
|
|
311
|
-
}
|
|
312
|
-
if (err instanceof SlashingProtectionError) {
|
|
313
|
-
this.log.info(`Attestations signature for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
314
|
-
slot: this.slot,
|
|
315
|
-
existingMessageHash: err.existingMessageHash,
|
|
316
|
-
attemptedMessageHash: err.attemptedMessageHash,
|
|
317
|
-
});
|
|
344
|
+
// as soon as we see these errors when creating block or checkpoint proposals.
|
|
345
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
318
346
|
return undefined;
|
|
319
347
|
}
|
|
320
348
|
throw err;
|
|
@@ -325,6 +353,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
325
353
|
const aztecSlotDuration = this.l1Constants.slotDuration;
|
|
326
354
|
const slotStartBuildTimestamp = this.getSlotStartBuildTimestamp();
|
|
327
355
|
const txTimeoutAt = new Date((slotStartBuildTimestamp + aztecSlotDuration) * 1000);
|
|
356
|
+
|
|
357
|
+
// If we have been configured to potentially skip publishing checkpoint then roll the dice here
|
|
358
|
+
if (
|
|
359
|
+
this.config.skipPublishingCheckpointsPercent !== undefined &&
|
|
360
|
+
this.config.skipPublishingCheckpointsPercent > 0
|
|
361
|
+
) {
|
|
362
|
+
const result = Math.max(0, randomInt(100));
|
|
363
|
+
if (result < this.config.skipPublishingCheckpointsPercent) {
|
|
364
|
+
this.log.warn(
|
|
365
|
+
`Skipping publishing proposal for checkpoint ${checkpoint.number}. Configured percentage: ${this.config.skipPublishingCheckpointsPercent}, generated value: ${result}`,
|
|
366
|
+
);
|
|
367
|
+
return checkpoint;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
328
371
|
await this.publisher.enqueueProposeCheckpoint(checkpoint, attestations, attestationsSignature, {
|
|
329
372
|
txTimeoutAt,
|
|
330
373
|
forcePendingCheckpointNumber: this.invalidateCheckpoint?.forcePendingCheckpointNumber,
|
|
@@ -353,32 +396,26 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
353
396
|
blockProposalOptions: BlockProposalOptions,
|
|
354
397
|
): Promise<{
|
|
355
398
|
blocksInCheckpoint: L2Block[];
|
|
356
|
-
blockPendingBroadcast:
|
|
399
|
+
blockPendingBroadcast: BlockProposal | undefined;
|
|
357
400
|
}> {
|
|
358
401
|
const blocksInCheckpoint: L2Block[] = [];
|
|
359
402
|
const txHashesAlreadyIncluded = new Set<string>();
|
|
360
403
|
const initialBlockNumber = BlockNumber(this.syncedToBlockNumber + 1);
|
|
361
|
-
|
|
362
|
-
// Remaining blob fields available for blocks (checkpoint end marker already subtracted)
|
|
363
|
-
let remainingBlobFields = BLOBS_PER_CHECKPOINT * FIELDS_PER_BLOB - NUM_CHECKPOINT_END_MARKER_FIELDS;
|
|
404
|
+
const slot = this.slot;
|
|
364
405
|
|
|
365
406
|
// Last block in the checkpoint will usually be flagged as pending broadcast, so we send it along with the checkpoint proposal
|
|
366
|
-
let blockPendingBroadcast:
|
|
407
|
+
let blockPendingBroadcast: BlockProposal | undefined = undefined;
|
|
367
408
|
|
|
368
409
|
while (true) {
|
|
369
410
|
const blocksBuilt = blocksInCheckpoint.length;
|
|
370
|
-
const indexWithinCheckpoint = blocksBuilt;
|
|
411
|
+
const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
|
|
371
412
|
const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
|
|
372
413
|
|
|
373
414
|
const secondsIntoSlot = this.getSecondsIntoSlot();
|
|
374
415
|
const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
|
|
375
416
|
|
|
376
417
|
if (!timingInfo.canStart) {
|
|
377
|
-
this.log.debug(`Not enough time left in slot to start another block`, {
|
|
378
|
-
slot: this.slot,
|
|
379
|
-
blocksBuilt,
|
|
380
|
-
secondsIntoSlot,
|
|
381
|
-
});
|
|
418
|
+
this.log.debug(`Not enough time left in slot to start another block`, { slot, blocksBuilt, secondsIntoSlot });
|
|
382
419
|
break;
|
|
383
420
|
}
|
|
384
421
|
|
|
@@ -394,72 +431,50 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
394
431
|
blockNumber,
|
|
395
432
|
indexWithinCheckpoint,
|
|
396
433
|
txHashesAlreadyIncluded,
|
|
397
|
-
remainingBlobFields,
|
|
398
434
|
});
|
|
399
435
|
|
|
400
|
-
if
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
436
|
+
// If we failed to build the block due to insufficient txs, we try again if there is still time left in the slot
|
|
437
|
+
if ('failure' in buildResult) {
|
|
438
|
+
// If this was the last subslot, or we're running with a single block per slot, we're done
|
|
439
|
+
if (timingInfo.isLastBlock || timingInfo.deadline === undefined) {
|
|
440
|
+
break;
|
|
441
|
+
}
|
|
442
|
+
// Otherwise, if there is still time for more blocks, we wait until the next subslot and try again
|
|
405
443
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
406
444
|
continue;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// If there was an error building the block, we just exit the loop and give up the rest of the slot.
|
|
448
|
+
// We don't want to risk building more blocks if something went wrong.
|
|
449
|
+
if ('error' in buildResult) {
|
|
412
450
|
if (!(buildResult.error instanceof SequencerInterruptedError)) {
|
|
413
|
-
this.log.warn(`Halting block building for slot ${
|
|
414
|
-
slot: this.slot,
|
|
415
|
-
blocksBuilt,
|
|
416
|
-
error: buildResult.error,
|
|
417
|
-
});
|
|
451
|
+
this.log.warn(`Halting block building for slot ${slot}`, { slot, blocksBuilt, error: buildResult.error });
|
|
418
452
|
}
|
|
419
453
|
break;
|
|
420
454
|
}
|
|
421
455
|
|
|
422
|
-
const { block, usedTxs
|
|
456
|
+
const { block, usedTxs } = buildResult;
|
|
423
457
|
blocksInCheckpoint.push(block);
|
|
458
|
+
usedTxs.forEach(tx => txHashesAlreadyIncluded.add(tx.txHash.toString()));
|
|
424
459
|
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
// Sync the proposed block to the archiver to make it available
|
|
429
|
-
// Note that the checkpoint builder uses its own fork so it should not need to wait for this syncing
|
|
430
|
-
// Eventually we should refactor the checkpoint builder to not need a separate long-lived fork
|
|
431
|
-
// Fire and forget - don't block the critical path, but log errors
|
|
432
|
-
this.syncProposedBlockToArchiver(block).catch(err => {
|
|
433
|
-
this.log.error(`Failed to sync proposed block ${block.number} to archiver`, { blockNumber: block.number, err });
|
|
434
|
-
});
|
|
460
|
+
// Sign the block proposal. This will throw if HA signing fails.
|
|
461
|
+
const proposal = await this.createBlockProposal(block, inHash, usedTxs, blockProposalOptions);
|
|
435
462
|
|
|
436
|
-
|
|
463
|
+
// Sync the proposed block to the archiver to make it available, only after we've managed to sign the proposal,
|
|
464
|
+
// so we avoid polluting our archive with a block that would fail.
|
|
465
|
+
// We wait for the sync to succeed, as this helps catch consistency errors, even if it means we lose some time for block-building.
|
|
466
|
+
// If this throws, we abort the entire checkpoint.
|
|
467
|
+
await this.syncProposedBlockToArchiver(block);
|
|
437
468
|
|
|
438
|
-
// If this is the last block,
|
|
469
|
+
// If this is the last block, do not broadcast it, since it will be included in the checkpoint proposal.
|
|
439
470
|
if (timingInfo.isLastBlock) {
|
|
440
|
-
this.log.verbose(`Completed final block ${blockNumber} for slot ${
|
|
441
|
-
|
|
442
|
-
blockNumber,
|
|
443
|
-
blocksBuilt,
|
|
444
|
-
});
|
|
445
|
-
blockPendingBroadcast = { block, txs: usedTxs };
|
|
471
|
+
this.log.verbose(`Completed final block ${blockNumber} for slot ${slot}`, { slot, blockNumber, blocksBuilt });
|
|
472
|
+
blockPendingBroadcast = proposal;
|
|
446
473
|
break;
|
|
447
474
|
}
|
|
448
475
|
|
|
449
|
-
//
|
|
450
|
-
|
|
451
|
-
if (!this.config.fishermanMode) {
|
|
452
|
-
const proposal = await this.validatorClient.createBlockProposal(
|
|
453
|
-
block.header,
|
|
454
|
-
block.indexWithinCheckpoint,
|
|
455
|
-
inHash,
|
|
456
|
-
block.archive.root,
|
|
457
|
-
usedTxs,
|
|
458
|
-
this.proposer,
|
|
459
|
-
blockProposalOptions,
|
|
460
|
-
);
|
|
461
|
-
await this.p2pClient.broadcastProposal(proposal);
|
|
462
|
-
}
|
|
476
|
+
// Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
|
|
477
|
+
proposal && (await this.p2pClient.broadcastProposal(proposal));
|
|
463
478
|
|
|
464
479
|
// Wait until the next block's start time
|
|
465
480
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
@@ -473,6 +488,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
473
488
|
return { blocksInCheckpoint, blockPendingBroadcast };
|
|
474
489
|
}
|
|
475
490
|
|
|
491
|
+
/** Creates a block proposal for a given block via the validator client (unless in fisherman mode) */
|
|
492
|
+
private createBlockProposal(
|
|
493
|
+
block: L2Block,
|
|
494
|
+
inHash: Fr,
|
|
495
|
+
usedTxs: Tx[],
|
|
496
|
+
blockProposalOptions: BlockProposalOptions,
|
|
497
|
+
): Promise<BlockProposal | undefined> {
|
|
498
|
+
if (this.config.fishermanMode) {
|
|
499
|
+
this.log.info(`Skipping block proposal for block ${block.number} in fisherman mode`);
|
|
500
|
+
return Promise.resolve(undefined);
|
|
501
|
+
}
|
|
502
|
+
return this.validatorClient.createBlockProposal(
|
|
503
|
+
block.header,
|
|
504
|
+
block.indexWithinCheckpoint,
|
|
505
|
+
inHash,
|
|
506
|
+
block.archive.root,
|
|
507
|
+
usedTxs,
|
|
508
|
+
this.proposer,
|
|
509
|
+
blockProposalOptions,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
476
513
|
/** Sleeps until it is time to produce the next block in the slot */
|
|
477
514
|
@trackSpan('CheckpointProposalJob.waitUntilNextSubslot')
|
|
478
515
|
private async waitUntilNextSubslot(nextSubslotStart: number) {
|
|
@@ -483,27 +520,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
483
520
|
|
|
484
521
|
/** Builds a single block. Called from the main block building loop. */
|
|
485
522
|
@trackSpan('CheckpointProposalJob.buildSingleBlock')
|
|
486
|
-
|
|
523
|
+
protected async buildSingleBlock(
|
|
487
524
|
checkpointBuilder: CheckpointBuilder,
|
|
488
525
|
opts: {
|
|
489
526
|
forceCreate?: boolean;
|
|
490
527
|
blockTimestamp: bigint;
|
|
491
528
|
blockNumber: BlockNumber;
|
|
492
|
-
indexWithinCheckpoint:
|
|
529
|
+
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
493
530
|
buildDeadline: Date | undefined;
|
|
494
531
|
txHashesAlreadyIncluded: Set<string>;
|
|
495
|
-
remainingBlobFields: number;
|
|
496
532
|
},
|
|
497
|
-
): Promise<
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
indexWithinCheckpoint,
|
|
503
|
-
buildDeadline,
|
|
504
|
-
txHashesAlreadyIncluded,
|
|
505
|
-
remainingBlobFields,
|
|
506
|
-
} = opts;
|
|
533
|
+
): Promise<
|
|
534
|
+
{ block: L2Block; usedTxs: Tx[] } | { failure: 'insufficient-txs' | 'insufficient-valid-txs' } | { error: Error }
|
|
535
|
+
> {
|
|
536
|
+
const { blockTimestamp, forceCreate, blockNumber, indexWithinCheckpoint, buildDeadline, txHashesAlreadyIncluded } =
|
|
537
|
+
opts;
|
|
507
538
|
|
|
508
539
|
this.log.verbose(
|
|
509
540
|
`Preparing block ${blockNumber} index ${indexWithinCheckpoint} at checkpoint ${this.checkpointNumber} for slot ${this.slot}`,
|
|
@@ -512,8 +543,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
512
543
|
|
|
513
544
|
try {
|
|
514
545
|
// Wait until we have enough txs to build the block
|
|
515
|
-
const minTxs = this.
|
|
516
|
-
const { availableTxs, canStartBuilding } = await this.waitForMinTxs(opts);
|
|
546
|
+
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
517
547
|
if (!canStartBuilding) {
|
|
518
548
|
this.log.warn(
|
|
519
549
|
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (got ${availableTxs} txs but needs ${minTxs})`,
|
|
@@ -521,13 +551,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
521
551
|
);
|
|
522
552
|
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.slot });
|
|
523
553
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
524
|
-
return
|
|
554
|
+
return { failure: 'insufficient-txs' };
|
|
525
555
|
}
|
|
526
556
|
|
|
527
557
|
// Create iterator to pending txs. We filter out txs already included in previous blocks in the checkpoint
|
|
528
558
|
// just in case p2p failed to sync the provisional block and didn't get to remove those txs from the mempool yet.
|
|
529
559
|
const pendingTxs = filter(
|
|
530
|
-
this.p2pClient.
|
|
560
|
+
this.p2pClient.iterateEligiblePendingTxs(),
|
|
531
561
|
tx => !txHashesAlreadyIncluded.has(tx.txHash.toString()),
|
|
532
562
|
);
|
|
533
563
|
|
|
@@ -537,64 +567,66 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
537
567
|
);
|
|
538
568
|
this.setStateFn(SequencerState.CREATING_BLOCK, this.slot);
|
|
539
569
|
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const blockBuilderOptions:
|
|
570
|
+
// Per-block limits are operator overrides (from SEQ_MAX_L2_BLOCK_GAS etc.) further capped
|
|
571
|
+
// by remaining checkpoint-level budgets inside CheckpointBuilder before each block is built.
|
|
572
|
+
// minValidTxs is passed into the builder so it can reject the block *before* updating state.
|
|
573
|
+
const minValidTxs = forceCreate ? 0 : (this.config.minValidTxsPerBlock ?? minTxs);
|
|
574
|
+
const blockBuilderOptions: BlockBuilderOptions = {
|
|
545
575
|
maxTransactions: this.config.maxTxsPerBlock,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
576
|
+
maxBlockGas:
|
|
577
|
+
this.config.maxL2BlockGas !== undefined || this.config.maxDABlockGas !== undefined
|
|
578
|
+
? new Gas(this.config.maxDABlockGas ?? Infinity, this.config.maxL2BlockGas ?? Infinity)
|
|
579
|
+
: undefined,
|
|
549
580
|
deadline: buildDeadline,
|
|
581
|
+
isBuildingProposal: true,
|
|
582
|
+
minValidTxs,
|
|
583
|
+
maxBlocksPerCheckpoint: this.timetable.maxNumberOfBlocks,
|
|
584
|
+
perBlockAllocationMultiplier: this.config.perBlockAllocationMultiplier,
|
|
550
585
|
};
|
|
551
586
|
|
|
552
|
-
// Actually build the block by executing txs
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
usedTxBlobFields,
|
|
563
|
-
} = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
|
|
564
|
-
const blockBuildDuration = workTimer.ms();
|
|
587
|
+
// Actually build the block by executing txs. The builder throws InsufficientValidTxsError
|
|
588
|
+
// if the number of successfully processed txs is below minValidTxs, ensuring state is not
|
|
589
|
+
// updated for blocks that will be discarded.
|
|
590
|
+
const buildResult = await this.buildSingleBlockWithCheckpointBuilder(
|
|
591
|
+
checkpointBuilder,
|
|
592
|
+
pendingTxs,
|
|
593
|
+
blockNumber,
|
|
594
|
+
blockTimestamp,
|
|
595
|
+
blockBuilderOptions,
|
|
596
|
+
);
|
|
565
597
|
|
|
566
598
|
// If any txs failed during execution, drop them from the mempool so we don't pick them up again
|
|
567
|
-
await this.dropFailedTxsFromP2P(failedTxs);
|
|
599
|
+
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
568
600
|
|
|
569
|
-
|
|
570
|
-
// too long, then we may not get to minTxsPerBlock after executing public functions.
|
|
571
|
-
const minValidTxs = this.config.minValidTxsPerBlock ?? minTxs;
|
|
572
|
-
if (!forceCreate && numTxs < minValidTxs) {
|
|
601
|
+
if (buildResult.status === 'insufficient-valid-txs') {
|
|
573
602
|
this.log.warn(
|
|
574
|
-
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed
|
|
575
|
-
{
|
|
603
|
+
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.slot} has too few valid txs to be proposed`,
|
|
604
|
+
{
|
|
605
|
+
slot: this.slot,
|
|
606
|
+
blockNumber,
|
|
607
|
+
numTxs: buildResult.processedCount,
|
|
608
|
+
indexWithinCheckpoint,
|
|
609
|
+
minValidTxs,
|
|
610
|
+
},
|
|
576
611
|
);
|
|
577
|
-
this.eventEmitter.emit('block-
|
|
578
|
-
minTxs: minValidTxs,
|
|
579
|
-
availableTxs: numTxs,
|
|
580
|
-
slot: this.slot,
|
|
581
|
-
});
|
|
612
|
+
this.eventEmitter.emit('block-build-failed', { reason: `Insufficient valid txs`, slot: this.slot });
|
|
582
613
|
this.metrics.recordBlockProposalFailed('insufficient_valid_txs');
|
|
583
|
-
return
|
|
614
|
+
return { failure: 'insufficient-valid-txs' };
|
|
584
615
|
}
|
|
585
616
|
|
|
586
617
|
// Block creation succeeded, emit stats and metrics
|
|
618
|
+
const { block, publicProcessorDuration, usedTxs, blockBuildDuration, numTxs } = buildResult;
|
|
619
|
+
|
|
587
620
|
const blockStats = {
|
|
588
621
|
eventName: 'l2-block-built',
|
|
589
622
|
duration: blockBuildDuration,
|
|
590
623
|
publicProcessDuration: publicProcessorDuration,
|
|
591
|
-
rollupCircuitsDuration: blockBuildingTimer.ms(),
|
|
592
624
|
...block.getStats(),
|
|
593
625
|
} satisfies L2BlockBuiltStats;
|
|
594
626
|
|
|
595
627
|
const blockHash = await block.hash();
|
|
596
628
|
const txHashes = block.body.txEffects.map(tx => tx.txHash);
|
|
597
|
-
const manaPerSec =
|
|
629
|
+
const manaPerSec = block.header.totalManaUsed.toNumberUnsafe() / (blockBuildDuration / 1000);
|
|
598
630
|
|
|
599
631
|
this.log.info(
|
|
600
632
|
`Built block ${block.number} at checkpoint ${this.checkpointNumber} for slot ${this.slot} with ${numTxs} txs`,
|
|
@@ -602,9 +634,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
602
634
|
);
|
|
603
635
|
|
|
604
636
|
this.eventEmitter.emit('block-proposed', { blockNumber: block.number, slot: this.slot });
|
|
605
|
-
this.metrics.recordBuiltBlock(blockBuildDuration,
|
|
637
|
+
this.metrics.recordBuiltBlock(blockBuildDuration, block.header.totalManaUsed.toNumberUnsafe());
|
|
606
638
|
|
|
607
|
-
return { block, usedTxs
|
|
639
|
+
return { block, usedTxs };
|
|
608
640
|
} catch (err: any) {
|
|
609
641
|
this.eventEmitter.emit('block-build-failed', { reason: err.message, slot: this.slot });
|
|
610
642
|
this.log.error(`Error building block`, err, { blockNumber, slot: this.slot });
|
|
@@ -614,17 +646,44 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
614
646
|
}
|
|
615
647
|
}
|
|
616
648
|
|
|
649
|
+
/** Uses the checkpoint builder to build a block, catching InsufficientValidTxsError. */
|
|
650
|
+
private async buildSingleBlockWithCheckpointBuilder(
|
|
651
|
+
checkpointBuilder: CheckpointBuilder,
|
|
652
|
+
pendingTxs: AsyncIterable<Tx>,
|
|
653
|
+
blockNumber: BlockNumber,
|
|
654
|
+
blockTimestamp: bigint,
|
|
655
|
+
blockBuilderOptions: BlockBuilderOptions,
|
|
656
|
+
) {
|
|
657
|
+
try {
|
|
658
|
+
const workTimer = new Timer();
|
|
659
|
+
const result = await checkpointBuilder.buildBlock(pendingTxs, blockNumber, blockTimestamp, blockBuilderOptions);
|
|
660
|
+
const blockBuildDuration = workTimer.ms();
|
|
661
|
+
return { ...result, blockBuildDuration, status: 'success' as const };
|
|
662
|
+
} catch (err: unknown) {
|
|
663
|
+
if (isErrorClass(err, InsufficientValidTxsError)) {
|
|
664
|
+
return {
|
|
665
|
+
failedTxs: err.failedTxs,
|
|
666
|
+
processedCount: err.processedCount,
|
|
667
|
+
status: 'insufficient-valid-txs' as const,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
throw err;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
617
674
|
/** Waits until minTxs are available on the pool for building a block. */
|
|
618
675
|
@trackSpan('CheckpointProposalJob.waitForMinTxs')
|
|
619
676
|
private async waitForMinTxs(opts: {
|
|
620
677
|
forceCreate?: boolean;
|
|
621
678
|
blockNumber: BlockNumber;
|
|
622
|
-
indexWithinCheckpoint:
|
|
679
|
+
indexWithinCheckpoint: IndexWithinCheckpoint;
|
|
623
680
|
buildDeadline: Date | undefined;
|
|
624
|
-
}): Promise<{ canStartBuilding: boolean; availableTxs: number }> {
|
|
625
|
-
const minTxs = this.config.minTxsPerBlock;
|
|
681
|
+
}): Promise<{ canStartBuilding: boolean; availableTxs: number; minTxs: number }> {
|
|
626
682
|
const { indexWithinCheckpoint, blockNumber, buildDeadline, forceCreate } = opts;
|
|
627
683
|
|
|
684
|
+
// We only allow a block with 0 txs in the first block of the checkpoint
|
|
685
|
+
const minTxs = indexWithinCheckpoint > 0 && this.config.minTxsPerBlock === 0 ? 1 : this.config.minTxsPerBlock;
|
|
686
|
+
|
|
628
687
|
// Deadline is undefined if we are not enforcing the timetable, meaning we'll exit immediately when out of time
|
|
629
688
|
const startBuildingDeadline = buildDeadline
|
|
630
689
|
? new Date(buildDeadline.getTime() - this.timetable.minExecutionTime * 1000)
|
|
@@ -636,7 +695,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
636
695
|
// If we're past deadline, or we have no deadline, give up
|
|
637
696
|
const now = this.dateProvider.nowAsDate();
|
|
638
697
|
if (startBuildingDeadline === undefined || now >= startBuildingDeadline) {
|
|
639
|
-
return { canStartBuilding: false, availableTxs
|
|
698
|
+
return { canStartBuilding: false, availableTxs, minTxs };
|
|
640
699
|
}
|
|
641
700
|
|
|
642
701
|
// Wait a bit before checking again
|
|
@@ -645,11 +704,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
645
704
|
`Waiting for enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.slot} (have ${availableTxs} but need ${minTxs})`,
|
|
646
705
|
{ blockNumber, slot: this.slot, indexWithinCheckpoint },
|
|
647
706
|
);
|
|
648
|
-
await
|
|
707
|
+
await this.waitForTxsPollingInterval();
|
|
649
708
|
availableTxs = await this.p2pClient.getPendingTxCount();
|
|
650
709
|
}
|
|
651
710
|
|
|
652
|
-
return { canStartBuilding: true, availableTxs };
|
|
711
|
+
return { canStartBuilding: true, availableTxs, minTxs };
|
|
653
712
|
}
|
|
654
713
|
|
|
655
714
|
/**
|
|
@@ -686,7 +745,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
686
745
|
const attestationTimeAllowed = this.config.enforceTimeTable
|
|
687
746
|
? this.timetable.getMaxAllowedTime(SequencerState.PUBLISHING_CHECKPOINT)!
|
|
688
747
|
: this.l1Constants.slotDuration;
|
|
689
|
-
const attestationDeadline = new Date(this.
|
|
748
|
+
const attestationDeadline = new Date((this.getSlotStartBuildTimestamp() + attestationTimeAllowed) * 1000);
|
|
690
749
|
|
|
691
750
|
this.metrics.recordRequiredAttestations(numberOfRequiredAttestations, attestationTimeAllowed);
|
|
692
751
|
|
|
@@ -701,11 +760,28 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
701
760
|
|
|
702
761
|
collectedAttestationsCount = attestations.length;
|
|
703
762
|
|
|
763
|
+
// Trim attestations to minimum required to save L1 calldata gas
|
|
764
|
+
const localAddresses = this.validatorClient.getValidatorAddresses();
|
|
765
|
+
const trimmed = trimAttestations(
|
|
766
|
+
attestations,
|
|
767
|
+
numberOfRequiredAttestations,
|
|
768
|
+
this.attestorAddress,
|
|
769
|
+
localAddresses,
|
|
770
|
+
);
|
|
771
|
+
if (trimmed.length < attestations.length) {
|
|
772
|
+
this.log.debug(`Trimmed attestations from ${attestations.length} to ${trimmed.length} for L1 submission`);
|
|
773
|
+
}
|
|
774
|
+
|
|
704
775
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
705
|
-
const sorted = orderAttestations(
|
|
776
|
+
const sorted = orderAttestations(trimmed, committee);
|
|
706
777
|
|
|
707
778
|
// Manipulate the attestations if we've been configured to do so
|
|
708
|
-
if (
|
|
779
|
+
if (
|
|
780
|
+
this.config.injectFakeAttestation ||
|
|
781
|
+
this.config.injectHighSValueAttestation ||
|
|
782
|
+
this.config.injectUnrecoverableSignatureAttestation ||
|
|
783
|
+
this.config.shuffleAttestationOrdering
|
|
784
|
+
) {
|
|
709
785
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
710
786
|
}
|
|
711
787
|
|
|
@@ -734,7 +810,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
734
810
|
this.epochCache.computeProposerIndex(slotNumber, epoch, seed, BigInt(committee.length)),
|
|
735
811
|
);
|
|
736
812
|
|
|
737
|
-
if (
|
|
813
|
+
if (
|
|
814
|
+
this.config.injectFakeAttestation ||
|
|
815
|
+
this.config.injectHighSValueAttestation ||
|
|
816
|
+
this.config.injectUnrecoverableSignatureAttestation
|
|
817
|
+
) {
|
|
738
818
|
// Find non-empty attestations that are not from the proposer
|
|
739
819
|
const nonProposerIndices: number[] = [];
|
|
740
820
|
for (let i = 0; i < attestations.length; i++) {
|
|
@@ -744,8 +824,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
744
824
|
}
|
|
745
825
|
if (nonProposerIndices.length > 0) {
|
|
746
826
|
const targetIndex = nonProposerIndices[randomInt(nonProposerIndices.length)];
|
|
747
|
-
this.
|
|
748
|
-
|
|
827
|
+
if (this.config.injectHighSValueAttestation) {
|
|
828
|
+
this.log.warn(
|
|
829
|
+
`Injecting high-s value attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
830
|
+
);
|
|
831
|
+
unfreeze(attestations[targetIndex]).signature = flipSignature(attestations[targetIndex].signature);
|
|
832
|
+
} else if (this.config.injectUnrecoverableSignatureAttestation) {
|
|
833
|
+
this.log.warn(
|
|
834
|
+
`Injecting unrecoverable signature attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`,
|
|
835
|
+
);
|
|
836
|
+
unfreeze(attestations[targetIndex]).signature = generateUnrecoverableSignature();
|
|
837
|
+
} else {
|
|
838
|
+
this.log.warn(`Injecting fake attestation in checkpoint for slot ${slotNumber} at index ${targetIndex}`);
|
|
839
|
+
unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
|
|
840
|
+
}
|
|
749
841
|
}
|
|
750
842
|
return new CommitteeAttestationsAndSigners(attestations);
|
|
751
843
|
}
|
|
@@ -774,16 +866,20 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
774
866
|
const failedTxData = failedTxs.map(fail => fail.tx);
|
|
775
867
|
const failedTxHashes = failedTxData.map(tx => tx.getTxHash());
|
|
776
868
|
this.log.verbose(`Dropping failed txs ${failedTxHashes.join(', ')}`);
|
|
777
|
-
await this.p2pClient.
|
|
869
|
+
await this.p2pClient.handleFailedExecution(failedTxHashes);
|
|
778
870
|
}
|
|
779
871
|
|
|
780
872
|
/**
|
|
781
873
|
* Adds the proposed block to the archiver so it's available via P2P.
|
|
782
874
|
* Gossip doesn't echo messages back to the sender, so the proposer's archiver/world-state
|
|
783
875
|
* would never receive its own block without this explicit sync.
|
|
876
|
+
*
|
|
877
|
+
* In fisherman mode we skip this push: the fisherman builds blocks locally for validation
|
|
878
|
+
* and fee analysis only, and pushing them to the archiver causes spurious reorg cascades
|
|
879
|
+
* whenever the real proposer's block arrives from L1.
|
|
784
880
|
*/
|
|
785
881
|
private async syncProposedBlockToArchiver(block: L2Block): Promise<void> {
|
|
786
|
-
if (this.config.skipPushProposedBlocksToArchiver !== false) {
|
|
882
|
+
if (this.config.skipPushProposedBlocksToArchiver !== false || this.config.fishermanMode) {
|
|
787
883
|
this.log.warn(`Skipping push of proposed block ${block.number} to archiver`, {
|
|
788
884
|
blockNumber: block.number,
|
|
789
885
|
slot: block.header.globalVariables.slotNumber,
|
|
@@ -816,12 +912,34 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
816
912
|
slot: this.slot,
|
|
817
913
|
feeAnalysisId: feeAnalysis?.id,
|
|
818
914
|
});
|
|
819
|
-
this.metrics.
|
|
915
|
+
this.metrics.recordCheckpointProposalFailed('block_build_failed');
|
|
820
916
|
}
|
|
821
917
|
|
|
822
918
|
this.publisher.clearPendingRequests();
|
|
823
919
|
}
|
|
824
920
|
|
|
921
|
+
/**
|
|
922
|
+
* Helper to handle HA double-signing errors. Returns true if the error was handled (caller should yield).
|
|
923
|
+
*/
|
|
924
|
+
private handleHASigningError(err: any, errorContext: string): boolean {
|
|
925
|
+
if (err instanceof DutyAlreadySignedError) {
|
|
926
|
+
this.log.info(`${errorContext} for slot ${this.slot} already signed by another HA node, yielding`, {
|
|
927
|
+
slot: this.slot,
|
|
928
|
+
signedByNode: err.signedByNode,
|
|
929
|
+
});
|
|
930
|
+
return true;
|
|
931
|
+
}
|
|
932
|
+
if (err instanceof SlashingProtectionError) {
|
|
933
|
+
this.log.info(`${errorContext} for slot ${this.slot} blocked by slashing protection, yielding`, {
|
|
934
|
+
slot: this.slot,
|
|
935
|
+
existingMessageHash: err.existingMessageHash,
|
|
936
|
+
attemptedMessageHash: err.attemptedMessageHash,
|
|
937
|
+
});
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
|
|
825
943
|
/** Waits until a specific time within the current slot */
|
|
826
944
|
@trackSpan('CheckpointProposalJob.waitUntilTimeInSlot')
|
|
827
945
|
protected async waitUntilTimeInSlot(targetSecondsIntoSlot: number): Promise<void> {
|
|
@@ -830,6 +948,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
830
948
|
await sleepUntil(new Date(targetTimestamp * 1000), this.dateProvider.nowAsDate());
|
|
831
949
|
}
|
|
832
950
|
|
|
951
|
+
/** Waits the polling interval for transactions. Extracted for test overriding. */
|
|
952
|
+
protected async waitForTxsPollingInterval(): Promise<void> {
|
|
953
|
+
await sleep(TXS_POLLING_MS);
|
|
954
|
+
}
|
|
955
|
+
|
|
833
956
|
private getSlotStartBuildTimestamp(): number {
|
|
834
957
|
return getSlotStartBuildTimestamp(this.slot, this.l1Constants);
|
|
835
958
|
}
|