@aztec/sequencer-client 0.0.1-commit.2b2662070 → 0.0.1-commit.2c0ee1788
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/config.d.ts +2 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +12 -7
- package/dest/publisher/sequencer-publisher.d.ts +1 -1
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -1
- package/dest/publisher/sequencer-publisher.js +2 -2
- package/dest/sequencer/checkpoint_proposal_job.d.ts +22 -3
- package/dest/sequencer/checkpoint_proposal_job.d.ts.map +1 -1
- package/dest/sequencer/checkpoint_proposal_job.js +361 -43
- package/dest/sequencer/events.d.ts +6 -1
- package/dest/sequencer/events.d.ts.map +1 -1
- package/dest/sequencer/metrics.d.ts +5 -1
- package/dest/sequencer/metrics.d.ts.map +1 -1
- package/dest/sequencer/metrics.js +22 -0
- package/dest/sequencer/sequencer.d.ts +12 -4
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +33 -8
- package/dest/test/utils.d.ts +1 -1
- package/dest/test/utils.d.ts.map +1 -1
- package/dest/test/utils.js +7 -6
- package/package.json +27 -27
- package/src/config.ts +11 -4
- package/src/publisher/sequencer-publisher.ts +2 -4
- package/src/sequencer/README.md +20 -21
- package/src/sequencer/checkpoint_proposal_job.ts +414 -44
- package/src/sequencer/events.ts +5 -0
- package/src/sequencer/metrics.ts +29 -0
- package/src/sequencer/sequencer.ts +37 -9
- package/src/test/utils.ts +28 -10
|
@@ -18,6 +18,7 @@ import { EthAddress } from '@aztec/foundation/eth-address';
|
|
|
18
18
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
19
19
|
import { filter } from '@aztec/foundation/iterator';
|
|
20
20
|
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
21
|
+
import { retryUntil } from '@aztec/foundation/retry';
|
|
21
22
|
import { sleep, sleepUntil } from '@aztec/foundation/sleep';
|
|
22
23
|
import { type DateProvider, Timer } from '@aztec/foundation/timer';
|
|
23
24
|
import { type TypedEventEmitter, isErrorClass, unfreeze } from '@aztec/foundation/types';
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
type L2BlockSink,
|
|
31
32
|
type L2BlockSource,
|
|
32
33
|
MaliciousCommitteeAttestationsAndSigners,
|
|
34
|
+
type ValidateCheckpointResult,
|
|
33
35
|
} from '@aztec/stdlib/block';
|
|
34
36
|
import { type Checkpoint, type ProposedCheckpointData, validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
35
37
|
import { computeQuorum, getSlotStartBuildTimestamp, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
@@ -44,8 +46,10 @@ import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@azte
|
|
|
44
46
|
import type {
|
|
45
47
|
BlockProposal,
|
|
46
48
|
BlockProposalOptions,
|
|
49
|
+
CheckpointAttestation,
|
|
47
50
|
CheckpointProposal,
|
|
48
51
|
CheckpointProposalOptions,
|
|
52
|
+
CoordinationSignatureContext,
|
|
49
53
|
} from '@aztec/stdlib/p2p';
|
|
50
54
|
import { orderAttestations, trimAttestations } from '@aztec/stdlib/p2p';
|
|
51
55
|
import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
|
|
@@ -95,6 +99,7 @@ type CheckpointProposalResult = {
|
|
|
95
99
|
*/
|
|
96
100
|
export class CheckpointProposalJob implements Traceable {
|
|
97
101
|
protected readonly log: Logger;
|
|
102
|
+
private readonly checkpointEventLog: Logger;
|
|
98
103
|
|
|
99
104
|
/** Tracks the fire-and-forget L1 submission promise so it can be awaited during shutdown. */
|
|
100
105
|
private pendingL1Submission: Promise<void> | undefined;
|
|
@@ -102,6 +107,10 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
102
107
|
/** Pipelined parent chain state used while building and later submitting this checkpoint. */
|
|
103
108
|
private pipelinedParentSimulationOverridesPlan?: SimulationOverridesPlan;
|
|
104
109
|
|
|
110
|
+
private getSignatureContext(): CoordinationSignatureContext {
|
|
111
|
+
return this.signatureContext;
|
|
112
|
+
}
|
|
113
|
+
|
|
105
114
|
constructor(
|
|
106
115
|
private readonly slotNow: SlotNumber,
|
|
107
116
|
private readonly targetSlot: SlotNumber,
|
|
@@ -122,6 +131,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
122
131
|
private readonly checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
123
132
|
private readonly blockSink: L2BlockSink,
|
|
124
133
|
private readonly l1Constants: SequencerRollupConstants,
|
|
134
|
+
private readonly signatureContext: CoordinationSignatureContext,
|
|
125
135
|
protected config: ResolvedSequencerConfig,
|
|
126
136
|
protected timetable: SequencerTimetable,
|
|
127
137
|
private readonly slasherClient: SlasherClientInterface | undefined,
|
|
@@ -129,7 +139,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
129
139
|
private readonly dateProvider: DateProvider,
|
|
130
140
|
private readonly metrics: SequencerMetrics,
|
|
131
141
|
private readonly checkpointMetrics: CheckpointProposalJobMetricsRecorder,
|
|
132
|
-
|
|
142
|
+
protected readonly eventEmitter: TypedEventEmitter<SequencerEvents>,
|
|
133
143
|
private readonly setStateFn: (state: SequencerState, slot?: SlotNumber) => void,
|
|
134
144
|
public readonly tracer: Tracer,
|
|
135
145
|
bindings?: LoggerBindings,
|
|
@@ -139,6 +149,10 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
139
149
|
...bindings,
|
|
140
150
|
instanceId: `slot-${this.slotNow}`,
|
|
141
151
|
});
|
|
152
|
+
this.checkpointEventLog = createLogger('sequencer:checkpoint-events', {
|
|
153
|
+
...bindings,
|
|
154
|
+
instanceId: `slot-${this.slotNow}`,
|
|
155
|
+
});
|
|
142
156
|
}
|
|
143
157
|
|
|
144
158
|
/** Awaits the pending L1 submission if one is in progress. Call during shutdown. */
|
|
@@ -147,6 +161,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
147
161
|
await this.pendingL1Submission;
|
|
148
162
|
}
|
|
149
163
|
|
|
164
|
+
private logCheckpointEvent(eventName: string, message: string, fields: Record<string, unknown>): void {
|
|
165
|
+
this.checkpointEventLog.debug(message, {
|
|
166
|
+
eventName: `sequencer-checkpoint-${eventName}`,
|
|
167
|
+
...fields,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
150
171
|
/**
|
|
151
172
|
* Executes the checkpoint proposal job.
|
|
152
173
|
* Builds blocks, assembles checkpoint, and broadcasts the proposal (blocking).
|
|
@@ -208,49 +229,74 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
208
229
|
broadcast: CheckpointProposalBroadcast,
|
|
209
230
|
votesPromises: Promise<unknown>[],
|
|
210
231
|
): Promise<void> {
|
|
211
|
-
const { checkpoint
|
|
232
|
+
const { checkpoint } = broadcast;
|
|
233
|
+
const isPipelining = this.epochCache.isProposerPipeliningEnabled();
|
|
234
|
+
|
|
212
235
|
try {
|
|
236
|
+
// Wait for all votes actions, enqueued at the beginning, to resolve
|
|
213
237
|
await Promise.all(votesPromises);
|
|
214
238
|
|
|
215
|
-
|
|
216
|
-
const
|
|
239
|
+
// Try to collect attestations from the committee
|
|
240
|
+
const signedAttestations = await this.getSignedCommitteeAttestations(broadcast);
|
|
217
241
|
|
|
218
|
-
|
|
242
|
+
// If pipelining, wait for the previous checkpoint to land on L1 before submitting,
|
|
243
|
+
// so we can check it matches the proposed checkpoint we used as parent, and has valid attestations.
|
|
244
|
+
if (signedAttestations && (!isPipelining || (await this.waitForValidParentCheckpointOnL1()))) {
|
|
245
|
+
await this.enqueueCheckpointForSubmission({ checkpoint, ...signedAttestations });
|
|
246
|
+
}
|
|
219
247
|
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
} catch (err) {
|
|
231
|
-
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
232
|
-
return;
|
|
248
|
+
// If we failed to collect attestations, at least check if we need to issue an invalidation
|
|
249
|
+
// Note that if we are not pipelining, we enqueued the invalidation at the beginning
|
|
250
|
+
if (!signedAttestations && isPipelining && (await this.waitForSyncedL2SlotNumber(this.slotNow))) {
|
|
251
|
+
const validationStatus = await this.l2BlockSource.getPendingChainValidationStatus();
|
|
252
|
+
if (!validationStatus.valid) {
|
|
253
|
+
this.log.warn(
|
|
254
|
+
`Checkpoint ${validationStatus.checkpoint.checkpointNumber} has invalid attestations, enqueuing invalidation in spite of attestation collection failure`,
|
|
255
|
+
{ checkpoint: validationStatus.checkpoint, reason: validationStatus.reason },
|
|
256
|
+
);
|
|
257
|
+
await this.enqueueInvalidation(validationStatus);
|
|
233
258
|
}
|
|
234
|
-
throw err;
|
|
235
259
|
}
|
|
236
260
|
|
|
237
|
-
//
|
|
238
|
-
await this.enqueueCheckpointForSubmission({ checkpoint, attestations, attestationsSignature });
|
|
239
|
-
|
|
261
|
+
// Send whatever was enqueued: votes + (propose | invalidation | nothing).
|
|
240
262
|
// Compute the earliest time to submit: pipeline slot start when pipelining, now otherwise.
|
|
241
|
-
const submitAfter =
|
|
263
|
+
const submitAfter = isPipelining
|
|
242
264
|
? new Date(Number(getTimestampForSlot(this.targetSlot, this.l1Constants)) * 1000)
|
|
243
265
|
: new Date(this.dateProvider.now());
|
|
244
266
|
|
|
245
267
|
const l1Response = await this.publisher.sendRequestsAt(submitAfter);
|
|
246
268
|
const proposedAction = l1Response?.successfulActions.find(a => a === 'propose');
|
|
247
269
|
if (proposedAction) {
|
|
270
|
+
this.logCheckpointEvent('published', `Checkpoint published for slot ${this.targetSlot}`, {
|
|
271
|
+
slot: this.targetSlot,
|
|
272
|
+
checkpointNumber: this.checkpointNumber,
|
|
273
|
+
successfulActions: l1Response?.successfulActions,
|
|
274
|
+
sentActions: l1Response?.sentActions,
|
|
275
|
+
});
|
|
248
276
|
this.eventEmitter.emit('checkpoint-published', { checkpoint: this.checkpointNumber, slot: this.targetSlot });
|
|
249
277
|
const coinbase = checkpoint.header.coinbase;
|
|
250
278
|
await this.metrics.incFilledSlot(this.publisher.getSenderAddress().toString(), coinbase);
|
|
251
279
|
} else {
|
|
280
|
+
this.logCheckpointEvent('publish-failed', `Checkpoint publish failed for slot ${this.targetSlot}`, {
|
|
281
|
+
slot: this.targetSlot,
|
|
282
|
+
checkpointNumber: this.checkpointNumber,
|
|
283
|
+
successfulActions: l1Response?.successfulActions,
|
|
284
|
+
failedActions: l1Response?.failedActions,
|
|
285
|
+
sentActions: l1Response?.sentActions,
|
|
286
|
+
expiredActions: l1Response?.expiredActions,
|
|
287
|
+
reason: 'propose_action_not_successful',
|
|
288
|
+
});
|
|
289
|
+
this.log.warn(`Checkpoint publish failed for slot ${this.targetSlot}`, {
|
|
290
|
+
slot: this.targetSlot,
|
|
291
|
+
checkpointNumber: this.checkpointNumber,
|
|
292
|
+
successfulActions: l1Response?.successfulActions,
|
|
293
|
+
failedActions: l1Response?.failedActions,
|
|
294
|
+
sentActions: l1Response?.sentActions,
|
|
295
|
+
expiredActions: l1Response?.expiredActions,
|
|
296
|
+
reason: 'propose_action_not_successful',
|
|
297
|
+
});
|
|
252
298
|
this.eventEmitter.emit('checkpoint-publish-failed', { ...l1Response, slot: this.targetSlot });
|
|
253
|
-
if (
|
|
299
|
+
if (isPipelining) {
|
|
254
300
|
this.metrics.recordPipelineDiscard();
|
|
255
301
|
}
|
|
256
302
|
}
|
|
@@ -258,9 +304,18 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
258
304
|
if (err instanceof SequencerInterruptedError) {
|
|
259
305
|
return;
|
|
260
306
|
}
|
|
261
|
-
this.
|
|
307
|
+
this.logCheckpointEvent('publish-failed', `Checkpoint publish failed for slot ${this.targetSlot}`, {
|
|
308
|
+
slot: this.targetSlot,
|
|
309
|
+
checkpointNumber: this.checkpointNumber,
|
|
310
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
311
|
+
});
|
|
312
|
+
this.log.error(`Background attestation/L1 pipeline failed for slot ${this.targetSlot}`, err, {
|
|
313
|
+
slot: this.targetSlot,
|
|
314
|
+
checkpointNumber: this.checkpointNumber,
|
|
315
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
316
|
+
});
|
|
262
317
|
this.eventEmitter.emit('checkpoint-publish-failed', { slot: this.targetSlot });
|
|
263
|
-
if (
|
|
318
|
+
if (isPipelining) {
|
|
264
319
|
this.metrics.recordPipelineDiscard();
|
|
265
320
|
}
|
|
266
321
|
}
|
|
@@ -303,6 +358,141 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
303
358
|
});
|
|
304
359
|
}
|
|
305
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Wait until the archiver syncs past the given L2 slot number.
|
|
363
|
+
* The deadline is the end of `this.targetSlot`, beyond which any pipelined work would miss its
|
|
364
|
+
* L1 submission window and is no longer useful.
|
|
365
|
+
*/
|
|
366
|
+
private async waitForSyncedL2SlotNumber(waitForSlot: SlotNumber): Promise<boolean> {
|
|
367
|
+
const targetSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
|
|
368
|
+
const targetSlotEndMs = (targetSlotStart + this.l1Constants.slotDuration) * 1000;
|
|
369
|
+
const syncDelayTolerance = this.l1Constants.ethereumSlotDuration * 2 * 1000;
|
|
370
|
+
const timeoutSeconds = Math.max(0.1, (targetSlotEndMs + syncDelayTolerance - this.dateProvider.now()) / 1000);
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
return await retryUntil(
|
|
374
|
+
async () => {
|
|
375
|
+
const syncedSlot = await this.l2BlockSource.getSyncedL2SlotNumber();
|
|
376
|
+
return syncedSlot !== undefined && syncedSlot >= waitForSlot;
|
|
377
|
+
},
|
|
378
|
+
`archiver sync past slot ${waitForSlot}`,
|
|
379
|
+
timeoutSeconds,
|
|
380
|
+
0.2,
|
|
381
|
+
);
|
|
382
|
+
} catch {
|
|
383
|
+
this.log.warn(
|
|
384
|
+
`Archiver did not sync L1 past slot ${waitForSlot} before slot ${this.targetSlot} expired, discarding pipelined work`,
|
|
385
|
+
{ checkpointNumber: this.checkpointNumber },
|
|
386
|
+
);
|
|
387
|
+
this.emitPipelinedCheckpointDiscarded('archiver-sync-timeout');
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Waits for the parent checkpoint to land on L1 before submitting a pipelined checkpoint.
|
|
394
|
+
* Polls until the archiver has synced L1 past the parent's slot, then verifies:
|
|
395
|
+
* - If we built on a proposed parent: it must have landed on L1 with matching hash and valid attestations.
|
|
396
|
+
* - If we built without a proposed parent: no new checkpoint must have appeared for that slot.
|
|
397
|
+
* If the parent has invalid attestations, enqueues an invalidation. Returns whether to proceed with the proposal.
|
|
398
|
+
*/
|
|
399
|
+
protected async waitForValidParentCheckpointOnL1(): Promise<boolean> {
|
|
400
|
+
const parentCheckpointNumber = CheckpointNumber(this.checkpointNumber - 1);
|
|
401
|
+
|
|
402
|
+
// Wait until archiver has synced L1 past the parent's slot (slotNow)
|
|
403
|
+
if (!(await this.waitForSyncedL2SlotNumber(this.slotNow))) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const tips = await this.l2BlockSource.getL2Tips();
|
|
408
|
+
const checkpointedNumber = tips.checkpointed.checkpoint.number;
|
|
409
|
+
|
|
410
|
+
// We built on top of a proposed checkpoint. Verify it landed on L1 as expected.
|
|
411
|
+
if (this.proposedCheckpointData) {
|
|
412
|
+
// After syncing from L1 we see the chain tip has invalid attestations. This means the parent checkpoint was posted
|
|
413
|
+
// with invalid attestations, or it built on top of something with invalid attestations and didnt invalidate them.
|
|
414
|
+
// Either way, we thought our parent would be valid, so we have to throw away our work. But at least we'll try and
|
|
415
|
+
// invalidate on L1 so we clean up the chain for the next proposer. And we'll slash them, but that's handled elsewhere.
|
|
416
|
+
const validationStatus = await this.l2BlockSource.getPendingChainValidationStatus();
|
|
417
|
+
if (!validationStatus.valid) {
|
|
418
|
+
this.log.warn(
|
|
419
|
+
`Parent checkpoint ${parentCheckpointNumber} has invalid attestations, discarding pipelined work`,
|
|
420
|
+
{ checkpointNumber: this.checkpointNumber, reason: validationStatus.reason },
|
|
421
|
+
);
|
|
422
|
+
this.emitPipelinedCheckpointDiscarded('parent-invalid-attestations');
|
|
423
|
+
await this.enqueueInvalidation(validationStatus);
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// The pending chain is valid. But did the parent checkpoint land on L1 at all?
|
|
428
|
+
if (checkpointedNumber < parentCheckpointNumber) {
|
|
429
|
+
this.log.warn(`Parent checkpoint ${parentCheckpointNumber} did not land on L1, discarding pipelined work`, {
|
|
430
|
+
checkpointNumber: this.checkpointNumber,
|
|
431
|
+
checkpointedNumber,
|
|
432
|
+
});
|
|
433
|
+
this.emitPipelinedCheckpointDiscarded('parent-not-on-l1');
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// It landed. But is it the one we were expecting?
|
|
438
|
+
const expectedHash = this.proposedCheckpointData.header.hash().toString();
|
|
439
|
+
if (tips.checkpointed.checkpoint.hash !== expectedHash) {
|
|
440
|
+
this.log.warn(`Parent checkpoint ${parentCheckpointNumber} hash mismatch on L1, discarding pipelined work`, {
|
|
441
|
+
checkpointNumber: this.checkpointNumber,
|
|
442
|
+
expectedHash,
|
|
443
|
+
actualHash: tips.checkpointed.checkpoint.hash,
|
|
444
|
+
});
|
|
445
|
+
this.emitPipelinedCheckpointDiscarded('parent-hash-mismatch');
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return true;
|
|
450
|
+
} else {
|
|
451
|
+
// We didn't see a proposed checkpoint at build time, so we built on checkpointed parent from two slots ago.
|
|
452
|
+
// But if a new checkpoint for the previous slot appeared on L1 in the meantime, our checkpoint assumed the wrong parent,
|
|
453
|
+
// so we have to discard our work. This can happen if we're somehow cut off from p2p and fail to see the checkpoint
|
|
454
|
+
// proposal for the previous slot.
|
|
455
|
+
if (checkpointedNumber > parentCheckpointNumber) {
|
|
456
|
+
this.log.warn(
|
|
457
|
+
`Unexpected checkpoint ${checkpointedNumber} landed on L1 after we built on top of parent ${parentCheckpointNumber}, discarding pipelined work`,
|
|
458
|
+
{ checkpointNumber: this.checkpointNumber, checkpointedNumber },
|
|
459
|
+
);
|
|
460
|
+
this.emitPipelinedCheckpointDiscarded('unexpected-parent-appeared');
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Emits the pipelined-checkpoint-discarded event and records the metric. */
|
|
469
|
+
private emitPipelinedCheckpointDiscarded(reason: string): void {
|
|
470
|
+
this.metrics.recordPipelineParentCheckpointMismatch(reason);
|
|
471
|
+
this.eventEmitter.emit('pipelined-checkpoint-discarded', {
|
|
472
|
+
slot: this.targetSlot,
|
|
473
|
+
checkpointNumber: this.checkpointNumber,
|
|
474
|
+
reason,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Simulates and enqueues an invalidation request for the invalid parent checkpoint. */
|
|
479
|
+
private async enqueueInvalidation(validationStatus: ValidateCheckpointResult): Promise<void> {
|
|
480
|
+
if (this.config.skipInvalidateBlockAsProposer) {
|
|
481
|
+
this.log.warn(`Skipping checkpoint invalidation as proposer due to test configuration`);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const invalidateRequest = await this.publisher.simulateInvalidateCheckpoint(validationStatus);
|
|
485
|
+
if (invalidateRequest) {
|
|
486
|
+
const submissionSlotStart = Number(getTimestampForSlot(this.targetSlot, this.l1Constants));
|
|
487
|
+
const txTimeoutAt = new Date((submissionSlotStart + this.l1Constants.slotDuration) * 1000);
|
|
488
|
+
this.publisher.enqueueInvalidateCheckpoint(invalidateRequest, { txTimeoutAt });
|
|
489
|
+
} else {
|
|
490
|
+
this.log.info(`Invalidation simulation returned undefined, checkpoint may have been removed already`, {
|
|
491
|
+
checkpointNumber: this.checkpointNumber,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
306
496
|
@trackSpan('CheckpointProposalJob.proposeCheckpoint', function () {
|
|
307
497
|
return {
|
|
308
498
|
// nullish operator needed for tests
|
|
@@ -328,11 +518,15 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
328
518
|
|
|
329
519
|
// Start the checkpoint
|
|
330
520
|
this.setStateFn(SequencerState.INITIALIZING_CHECKPOINT, this.targetSlot);
|
|
331
|
-
this.
|
|
521
|
+
this.logCheckpointEvent('slot-started', `Starting checkpoint proposal for slot ${this.targetSlot}`, {
|
|
332
522
|
buildSlot: this.slotNow,
|
|
333
523
|
submissionSlot: this.targetSlot,
|
|
524
|
+
slot: this.targetSlot,
|
|
525
|
+
checkpointNumber: this.checkpointNumber,
|
|
334
526
|
pipelining: this.epochCache.isProposerPipeliningEnabled(),
|
|
335
527
|
proposer: this.proposer?.toString(),
|
|
528
|
+
attestorAddress: this.attestorAddress.toString(),
|
|
529
|
+
publisherAddress: this.publisher.getSenderAddress().toString(),
|
|
336
530
|
coinbase: coinbase.toString(),
|
|
337
531
|
});
|
|
338
532
|
this.metrics.incOpenSlot(this.targetSlot, this.proposer?.toString() ?? 'unknown');
|
|
@@ -424,16 +618,38 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
424
618
|
}
|
|
425
619
|
|
|
426
620
|
if (blocksInCheckpoint.length === 0) {
|
|
427
|
-
this.
|
|
621
|
+
this.logCheckpointEvent('build-failed', `Checkpoint build failed for slot ${this.targetSlot}`, {
|
|
622
|
+
slot: this.targetSlot,
|
|
623
|
+
checkpointNumber: this.checkpointNumber,
|
|
624
|
+
reason: 'no_blocks_built',
|
|
625
|
+
});
|
|
626
|
+
this.log.warn(`No blocks were built for slot ${this.targetSlot}`, {
|
|
627
|
+
slot: this.targetSlot,
|
|
628
|
+
checkpointNumber: this.checkpointNumber,
|
|
629
|
+
reason: 'no_blocks_built',
|
|
630
|
+
});
|
|
428
631
|
this.eventEmitter.emit('checkpoint-empty', { slot: this.targetSlot });
|
|
429
632
|
return undefined;
|
|
430
633
|
}
|
|
431
634
|
|
|
432
635
|
const minBlocksForCheckpoint = this.config.minBlocksForCheckpoint;
|
|
433
636
|
if (minBlocksForCheckpoint !== undefined && blocksInCheckpoint.length < minBlocksForCheckpoint) {
|
|
637
|
+
this.logCheckpointEvent('build-failed', `Checkpoint build failed for slot ${this.targetSlot}`, {
|
|
638
|
+
slot: this.targetSlot,
|
|
639
|
+
checkpointNumber: this.checkpointNumber,
|
|
640
|
+
blocksBuilt: blocksInCheckpoint.length,
|
|
641
|
+
minBlocksForCheckpoint,
|
|
642
|
+
reason: 'min_blocks_not_met',
|
|
643
|
+
});
|
|
434
644
|
this.log.warn(
|
|
435
645
|
`Checkpoint has fewer blocks than minimum (${blocksInCheckpoint.length} < ${minBlocksForCheckpoint}), skipping proposal`,
|
|
436
|
-
{
|
|
646
|
+
{
|
|
647
|
+
slot: this.targetSlot,
|
|
648
|
+
checkpointNumber: this.checkpointNumber,
|
|
649
|
+
blocksBuilt: blocksInCheckpoint.length,
|
|
650
|
+
minBlocksForCheckpoint,
|
|
651
|
+
reason: 'min_blocks_not_met',
|
|
652
|
+
},
|
|
437
653
|
);
|
|
438
654
|
return undefined;
|
|
439
655
|
}
|
|
@@ -454,7 +670,18 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
454
670
|
maxTxsPerCheckpoint: this.config.maxTxsPerCheckpoint,
|
|
455
671
|
});
|
|
456
672
|
} catch (err) {
|
|
673
|
+
this.logCheckpointEvent('build-failed', `Checkpoint build failed for slot ${this.targetSlot}`, {
|
|
674
|
+
slot: this.targetSlot,
|
|
675
|
+
checkpointNumber: this.checkpointNumber,
|
|
676
|
+
blocksBuilt: blocksInCheckpoint.length,
|
|
677
|
+
reason: 'invalid_checkpoint',
|
|
678
|
+
checkpoint: checkpoint.header.toInspect(),
|
|
679
|
+
});
|
|
457
680
|
this.log.error(`Built an invalid checkpoint at slot ${this.slotNow} (skipping proposal)`, err, {
|
|
681
|
+
slot: this.targetSlot,
|
|
682
|
+
checkpointNumber: this.checkpointNumber,
|
|
683
|
+
blocksBuilt: blocksInCheckpoint.length,
|
|
684
|
+
reason: 'invalid_checkpoint',
|
|
458
685
|
checkpoint: checkpoint.header.toInspect(),
|
|
459
686
|
});
|
|
460
687
|
return undefined;
|
|
@@ -467,6 +694,17 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
467
694
|
checkpoint.getStats().txCount,
|
|
468
695
|
Number(checkpoint.header.totalManaUsed.toBigInt()),
|
|
469
696
|
);
|
|
697
|
+
this.logCheckpointEvent('built', `Checkpoint built for slot ${this.targetSlot}`, {
|
|
698
|
+
slot: this.targetSlot,
|
|
699
|
+
buildSlot: this.slotNow,
|
|
700
|
+
checkpointNumber: this.checkpointNumber,
|
|
701
|
+
proposer: this.proposer?.toString(),
|
|
702
|
+
attestorAddress: this.attestorAddress.toString(),
|
|
703
|
+
publisherAddress: this.publisher.getSenderAddress().toString(),
|
|
704
|
+
blocksBuilt: blocksInCheckpoint.length,
|
|
705
|
+
txCount: checkpoint.getStats().txCount,
|
|
706
|
+
totalMana: Number(checkpoint.header.totalManaUsed.toBigInt()),
|
|
707
|
+
});
|
|
470
708
|
|
|
471
709
|
// In fisherman mode, return the checkpoint without broadcasting or collecting attestations
|
|
472
710
|
if (this.config.fishermanMode) {
|
|
@@ -496,8 +734,10 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
496
734
|
);
|
|
497
735
|
|
|
498
736
|
const blockProposedAt = this.dateProvider.now();
|
|
499
|
-
|
|
500
|
-
|
|
737
|
+
if (!this.config.skipBroadcastProposals) {
|
|
738
|
+
await this.p2pClient.broadcastCheckpointProposal(proposal);
|
|
739
|
+
this.checkpointMetrics.noteCheckpointBroadcast(this.dateProvider.now());
|
|
740
|
+
}
|
|
501
741
|
|
|
502
742
|
// Return immediately after broadcast — attestation collection happens in the background
|
|
503
743
|
return { checkpoint, proposal, blockProposedAt };
|
|
@@ -537,6 +777,15 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
537
777
|
const indexWithinCheckpoint = IndexWithinCheckpoint(blocksBuilt);
|
|
538
778
|
const blockNumber = BlockNumber(initialBlockNumber + blocksBuilt);
|
|
539
779
|
|
|
780
|
+
if (blocksBuilt >= this.config.maxBlocksPerCheckpoint) {
|
|
781
|
+
this.log.debug(`Reached max blocks per checkpoint`, {
|
|
782
|
+
slot: this.targetSlot,
|
|
783
|
+
blocksBuilt,
|
|
784
|
+
maxBlocksPerCheckpoint: this.config.maxBlocksPerCheckpoint,
|
|
785
|
+
});
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
|
|
540
789
|
const secondsIntoSlot = this.getSecondsIntoSlot();
|
|
541
790
|
const timingInfo = this.timetable.canStartNextBlock(secondsIntoSlot);
|
|
542
791
|
|
|
@@ -618,7 +867,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
618
867
|
}
|
|
619
868
|
|
|
620
869
|
// Once we have a signed proposal and the archiver agreed with our proposed block, then we broadcast it.
|
|
621
|
-
proposal &&
|
|
870
|
+
if (proposal && !this.config.skipBroadcastProposals) {
|
|
871
|
+
await this.p2pClient.broadcastProposal(proposal);
|
|
872
|
+
}
|
|
622
873
|
|
|
623
874
|
// Wait until the next block's start time
|
|
624
875
|
await this.waitUntilNextSubslot(timingInfo.deadline);
|
|
@@ -692,9 +943,26 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
692
943
|
// Wait until we have enough txs to build the block
|
|
693
944
|
const { availableTxs, canStartBuilding, minTxs } = await this.waitForMinTxs(opts);
|
|
694
945
|
if (!canStartBuilding) {
|
|
946
|
+
this.logCheckpointEvent('block-build-failed', `Block build failed for slot ${this.targetSlot}`, {
|
|
947
|
+
reason: 'insufficient_txs',
|
|
948
|
+
blockNumber,
|
|
949
|
+
slot: this.targetSlot,
|
|
950
|
+
checkpointNumber: this.checkpointNumber,
|
|
951
|
+
indexWithinCheckpoint,
|
|
952
|
+
availableTxs,
|
|
953
|
+
minTxs,
|
|
954
|
+
});
|
|
695
955
|
this.log.warn(
|
|
696
956
|
`Not enough txs to build block ${blockNumber} at index ${indexWithinCheckpoint} in slot ${this.targetSlot} (got ${availableTxs} txs but needs ${minTxs})`,
|
|
697
|
-
{
|
|
957
|
+
{
|
|
958
|
+
reason: 'insufficient_txs',
|
|
959
|
+
blockNumber,
|
|
960
|
+
slot: this.targetSlot,
|
|
961
|
+
checkpointNumber: this.checkpointNumber,
|
|
962
|
+
indexWithinCheckpoint,
|
|
963
|
+
availableTxs,
|
|
964
|
+
minTxs,
|
|
965
|
+
},
|
|
698
966
|
);
|
|
699
967
|
this.eventEmitter.emit('block-tx-count-check-failed', { minTxs, availableTxs, slot: this.targetSlot });
|
|
700
968
|
this.metrics.recordBlockProposalFailed('insufficient_txs');
|
|
@@ -746,10 +1014,21 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
746
1014
|
await this.dropFailedTxsFromP2P(buildResult.failedTxs);
|
|
747
1015
|
|
|
748
1016
|
if (buildResult.status === 'insufficient-valid-txs') {
|
|
1017
|
+
this.logCheckpointEvent('block-build-failed', `Block build failed for slot ${this.targetSlot}`, {
|
|
1018
|
+
reason: 'insufficient_valid_txs',
|
|
1019
|
+
slot: this.targetSlot,
|
|
1020
|
+
checkpointNumber: this.checkpointNumber,
|
|
1021
|
+
blockNumber,
|
|
1022
|
+
numTxs: buildResult.processedCount,
|
|
1023
|
+
indexWithinCheckpoint,
|
|
1024
|
+
minValidTxs,
|
|
1025
|
+
});
|
|
749
1026
|
this.log.warn(
|
|
750
1027
|
`Block ${blockNumber} at index ${indexWithinCheckpoint} on slot ${this.targetSlot} has too few valid txs to be proposed`,
|
|
751
1028
|
{
|
|
1029
|
+
reason: 'insufficient_valid_txs',
|
|
752
1030
|
slot: this.targetSlot,
|
|
1031
|
+
checkpointNumber: this.checkpointNumber,
|
|
753
1032
|
blockNumber,
|
|
754
1033
|
numTxs: buildResult.processedCount,
|
|
755
1034
|
indexWithinCheckpoint,
|
|
@@ -798,7 +1077,18 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
798
1077
|
reason: err.message,
|
|
799
1078
|
slot: this.targetSlot,
|
|
800
1079
|
});
|
|
801
|
-
this.
|
|
1080
|
+
this.logCheckpointEvent('block-build-failed', `Block build failed for slot ${this.targetSlot}`, {
|
|
1081
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1082
|
+
slot: this.targetSlot,
|
|
1083
|
+
checkpointNumber: this.checkpointNumber,
|
|
1084
|
+
blockNumber,
|
|
1085
|
+
});
|
|
1086
|
+
this.log.error(`Error building block`, err, {
|
|
1087
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1088
|
+
slot: this.targetSlot,
|
|
1089
|
+
checkpointNumber: this.checkpointNumber,
|
|
1090
|
+
blockNumber,
|
|
1091
|
+
});
|
|
802
1092
|
this.metrics.recordBlockProposalFailed(err.name || 'unknown_error');
|
|
803
1093
|
this.metrics.recordFailedBlock();
|
|
804
1094
|
return { error: err };
|
|
@@ -870,15 +1160,47 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
870
1160
|
return { canStartBuilding: true, availableTxs, minTxs };
|
|
871
1161
|
}
|
|
872
1162
|
|
|
1163
|
+
private async getSignedCommitteeAttestations(
|
|
1164
|
+
broadcast: CheckpointProposalBroadcast,
|
|
1165
|
+
): Promise<{ attestations: CommitteeAttestationsAndSigners; attestationsSignature: Signature } | undefined> {
|
|
1166
|
+
const { proposal, blockProposedAt } = broadcast;
|
|
1167
|
+
this.setStateFn(SequencerState.COLLECTING_ATTESTATIONS, this.targetSlot);
|
|
1168
|
+
const attestations = await this.waitForAttestations(proposal);
|
|
1169
|
+
if (!attestations) {
|
|
1170
|
+
return undefined;
|
|
1171
|
+
}
|
|
1172
|
+
this.checkpointMetrics.recordCheckpointAttestationDelay(this.dateProvider.now() - blockProposedAt);
|
|
1173
|
+
|
|
1174
|
+
// Proposer must sign over the attestations before pushing them to L1
|
|
1175
|
+
const signer = this.proposer ?? this.publisher.getSenderAddress();
|
|
1176
|
+
try {
|
|
1177
|
+
const attestationsSignature = await this.validatorClient.signAttestationsAndSigners(
|
|
1178
|
+
attestations,
|
|
1179
|
+
signer,
|
|
1180
|
+
this.targetSlot,
|
|
1181
|
+
this.checkpointNumber,
|
|
1182
|
+
);
|
|
1183
|
+
return { attestations, attestationsSignature };
|
|
1184
|
+
} catch (err) {
|
|
1185
|
+
if (this.handleHASigningError(err, 'Attestations signature')) {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
this.log.error(`Error signing attestations for checkpoint proposal at slot ${proposal.slotNumber}`, err);
|
|
1189
|
+
return undefined;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
873
1193
|
/**
|
|
874
1194
|
* Waits for enough attestations to be collected via p2p.
|
|
875
1195
|
* This is run after all blocks for the checkpoint have been built.
|
|
876
1196
|
*/
|
|
877
1197
|
@trackSpan('CheckpointProposalJob.waitForAttestations')
|
|
878
|
-
private async waitForAttestations(
|
|
1198
|
+
private async waitForAttestations(
|
|
1199
|
+
proposal: CheckpointProposal,
|
|
1200
|
+
): Promise<CommitteeAttestationsAndSigners | undefined> {
|
|
879
1201
|
if (this.config.fishermanMode) {
|
|
880
1202
|
this.log.debug('Skipping attestation collection in fisherman mode');
|
|
881
|
-
return CommitteeAttestationsAndSigners.empty();
|
|
1203
|
+
return CommitteeAttestationsAndSigners.empty(this.getSignatureContext());
|
|
882
1204
|
}
|
|
883
1205
|
|
|
884
1206
|
const slotNumber = proposal.slotNumber;
|
|
@@ -888,7 +1210,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
888
1210
|
throw new Error('No committee when collecting attestations');
|
|
889
1211
|
} else if (committee.length === 0) {
|
|
890
1212
|
this.log.verbose(`Attesting committee is empty`);
|
|
891
|
-
return CommitteeAttestationsAndSigners.empty();
|
|
1213
|
+
return CommitteeAttestationsAndSigners.empty(this.getSignatureContext());
|
|
892
1214
|
} else {
|
|
893
1215
|
this.log.debug(`Attesting committee length is ${committee.length}`, { committee });
|
|
894
1216
|
}
|
|
@@ -898,7 +1220,13 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
898
1220
|
if (this.config.skipCollectingAttestations) {
|
|
899
1221
|
this.log.warn('Skipping attestation collection as per config (attesting with own keys only)');
|
|
900
1222
|
const attestations = await this.validatorClient?.collectOwnAttestations(proposal, this.checkpointNumber);
|
|
901
|
-
|
|
1223
|
+
this.logCheckpointAttestations('collected', committee, attestations ?? [], numberOfRequiredAttestations, {
|
|
1224
|
+
reason: 'collect_own_only',
|
|
1225
|
+
});
|
|
1226
|
+
return new CommitteeAttestationsAndSigners(
|
|
1227
|
+
orderAttestations(attestations ?? [], committee),
|
|
1228
|
+
this.getSignatureContext(),
|
|
1229
|
+
);
|
|
902
1230
|
}
|
|
903
1231
|
|
|
904
1232
|
const attestationTimeAllowed = this.config.enforceTimeTable
|
|
@@ -934,6 +1262,9 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
934
1262
|
|
|
935
1263
|
// Rollup contract requires that the signatures are provided in the order of the committee
|
|
936
1264
|
const sorted = orderAttestations(trimmed, committee);
|
|
1265
|
+
this.logCheckpointAttestations('collected', committee, attestations, numberOfRequiredAttestations, {
|
|
1266
|
+
submittedCount: trimmed.length,
|
|
1267
|
+
});
|
|
937
1268
|
|
|
938
1269
|
// Manipulate the attestations if we've been configured to do so
|
|
939
1270
|
if (
|
|
@@ -945,17 +1276,56 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
945
1276
|
return this.manipulateAttestations(proposal.slotNumber, epoch, seed, committee, sorted);
|
|
946
1277
|
}
|
|
947
1278
|
|
|
948
|
-
return new CommitteeAttestationsAndSigners(sorted);
|
|
1279
|
+
return new CommitteeAttestationsAndSigners(sorted, this.getSignatureContext());
|
|
949
1280
|
} catch (err) {
|
|
950
1281
|
if (err && err instanceof AttestationTimeoutError) {
|
|
951
1282
|
collectedAttestationsCount = err.collectedCount;
|
|
1283
|
+
this.logCheckpointAttestations('failed', committee, undefined, numberOfRequiredAttestations, {
|
|
1284
|
+
collectedCount: collectedAttestationsCount,
|
|
1285
|
+
reason: 'timeout',
|
|
1286
|
+
});
|
|
1287
|
+
this.log.error(
|
|
1288
|
+
`Timeout while waiting for attestations for checkpoint proposal at slot ${proposal.slotNumber} (collected ${collectedAttestationsCount}/${numberOfRequiredAttestations})`,
|
|
1289
|
+
err,
|
|
1290
|
+
);
|
|
1291
|
+
} else {
|
|
1292
|
+
this.logCheckpointAttestations('failed', committee, undefined, numberOfRequiredAttestations, {
|
|
1293
|
+
collectedCount: collectedAttestationsCount,
|
|
1294
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1295
|
+
});
|
|
1296
|
+
this.log.error(`Error collecting attestations for checkpoint proposal at slot ${proposal.slotNumber}`, err);
|
|
952
1297
|
}
|
|
953
|
-
|
|
1298
|
+
return undefined;
|
|
954
1299
|
} finally {
|
|
955
1300
|
this.metrics.recordCollectedAttestations(collectedAttestationsCount, collectAttestationsTimer.ms());
|
|
956
1301
|
}
|
|
957
1302
|
}
|
|
958
1303
|
|
|
1304
|
+
private logCheckpointAttestations(
|
|
1305
|
+
status: 'collected' | 'failed',
|
|
1306
|
+
committee: EthAddress[],
|
|
1307
|
+
attestations: CheckpointAttestation[] | undefined,
|
|
1308
|
+
requiredAttestations: number,
|
|
1309
|
+
opts: { collectedCount?: number; submittedCount?: number; reason?: string } = {},
|
|
1310
|
+
) {
|
|
1311
|
+
const signedValidators =
|
|
1312
|
+
attestations
|
|
1313
|
+
?.map(attestation => attestation.getSender()?.toString())
|
|
1314
|
+
.filter((address): address is `0x${string}` => address !== undefined) ?? [];
|
|
1315
|
+
const collectedCount = opts.collectedCount ?? new Set(signedValidators).size;
|
|
1316
|
+
const missingValidatorCount = status === 'failed' ? Math.max(0, requiredAttestations - collectedCount) : undefined;
|
|
1317
|
+
this.logCheckpointEvent(`attestations-${status}`, `Checkpoint attestations ${status} for slot ${this.targetSlot}`, {
|
|
1318
|
+
slot: this.targetSlot,
|
|
1319
|
+
checkpointNumber: this.checkpointNumber,
|
|
1320
|
+
committeeSize: committee.length,
|
|
1321
|
+
requiredAttestations,
|
|
1322
|
+
collectedAttestations: collectedCount,
|
|
1323
|
+
...(opts.submittedCount !== undefined && { submittedAttestations: opts.submittedCount }),
|
|
1324
|
+
...(missingValidatorCount !== undefined && { missingValidatorCount }),
|
|
1325
|
+
...(opts.reason !== undefined && { reason: opts.reason }),
|
|
1326
|
+
});
|
|
1327
|
+
}
|
|
1328
|
+
|
|
959
1329
|
/** Breaks the attestations before publishing based on attack configs */
|
|
960
1330
|
private manipulateAttestations(
|
|
961
1331
|
slotNumber: SlotNumber,
|
|
@@ -999,7 +1369,7 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
999
1369
|
unfreeze(attestations[targetIndex]).signature = generateRecoverableSignature();
|
|
1000
1370
|
}
|
|
1001
1371
|
}
|
|
1002
|
-
return new CommitteeAttestationsAndSigners(attestations);
|
|
1372
|
+
return new CommitteeAttestationsAndSigners(attestations, this.getSignatureContext());
|
|
1003
1373
|
}
|
|
1004
1374
|
|
|
1005
1375
|
if (this.config.shuffleAttestationOrdering) {
|
|
@@ -1021,11 +1391,11 @@ export class CheckpointProposalJob implements Traceable {
|
|
|
1021
1391
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
1022
1392
|
}
|
|
1023
1393
|
|
|
1024
|
-
const signers = new CommitteeAttestationsAndSigners(attestations).getSigners();
|
|
1025
|
-
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers);
|
|
1394
|
+
const signers = new CommitteeAttestationsAndSigners(attestations, this.getSignatureContext()).getSigners();
|
|
1395
|
+
return new MaliciousCommitteeAttestationsAndSigners(shuffled, signers, this.getSignatureContext());
|
|
1026
1396
|
}
|
|
1027
1397
|
|
|
1028
|
-
return new CommitteeAttestationsAndSigners(attestations);
|
|
1398
|
+
return new CommitteeAttestationsAndSigners(attestations, this.getSignatureContext());
|
|
1029
1399
|
}
|
|
1030
1400
|
|
|
1031
1401
|
private async dropFailedTxsFromP2P(failedTxs: FailedTx[]) {
|