@aztec/validator-client 0.0.1-commit.9ef841308 → 0.0.1-commit.a4600f49
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/README.md +12 -9
- package/dest/checkpoint_builder.d.ts +6 -4
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +16 -8
- package/dest/config.d.ts +9 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +23 -5
- package/dest/duties/validation_service.d.ts +12 -13
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +32 -38
- package/dest/factory.d.ts +4 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +16 -4
- package/dest/key_store/web3signer_key_store.d.ts +10 -2
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
- package/dest/key_store/web3signer_key_store.js +32 -41
- package/dest/metrics.d.ts +5 -1
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +12 -0
- package/dest/proposal_handler.d.ts +85 -14
- package/dest/proposal_handler.d.ts.map +1 -1
- package/dest/proposal_handler.js +518 -167
- package/dest/validator.d.ts +40 -13
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +249 -65
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +16 -7
- package/src/config.ts +31 -7
- package/src/duties/validation_service.ts +51 -47
- package/src/factory.ts +20 -1
- package/src/key_store/web3signer_key_store.ts +43 -59
- package/src/metrics.ts +18 -0
- package/src/proposal_handler.ts +605 -198
- package/src/validator.ts +322 -82
package/src/validator.ts
CHANGED
|
@@ -1,28 +1,31 @@
|
|
|
1
1
|
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
2
|
+
import { type Blob, getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
2
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
3
|
-
import {
|
|
4
|
-
BlockNumber,
|
|
5
|
-
CheckpointNumber,
|
|
6
|
-
EpochNumber,
|
|
7
|
-
IndexWithinCheckpoint,
|
|
8
|
-
SlotNumber,
|
|
9
|
-
} from '@aztec/foundation/branded-types';
|
|
4
|
+
import { CheckpointNumber, EpochNumber, IndexWithinCheckpoint, SlotNumber } from '@aztec/foundation/branded-types';
|
|
10
5
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
11
6
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
7
|
+
import { Signature } from '@aztec/foundation/eth-signature';
|
|
8
|
+
import { FifoSet } from '@aztec/foundation/fifo-set';
|
|
9
|
+
import { type LogData, type Logger, createLogger } from '@aztec/foundation/log';
|
|
14
10
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
15
11
|
import { sleep } from '@aztec/foundation/sleep';
|
|
16
12
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
17
13
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
18
|
-
import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
14
|
+
import type { DuplicateAttestationInfo, DuplicateProposalInfo, OversizedProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
19
15
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
20
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
OffenseType,
|
|
18
|
+
WANT_TO_CLEAR_SLASH_EVENT,
|
|
19
|
+
WANT_TO_SLASH_EVENT,
|
|
20
|
+
type Watcher,
|
|
21
|
+
type WatcherEmitter,
|
|
22
|
+
getOffenseTypeName,
|
|
23
|
+
} from '@aztec/slasher';
|
|
21
24
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
22
25
|
import type { CommitteeAttestationsAndSigners, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
26
|
+
import type { CheckpointReexecutionTracker } from '@aztec/stdlib/checkpoint';
|
|
23
27
|
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
24
28
|
import type {
|
|
25
|
-
CreateCheckpointProposalLastBlockData,
|
|
26
29
|
ITxProvider,
|
|
27
30
|
Validator,
|
|
28
31
|
ValidatorClientFullConfig,
|
|
@@ -32,12 +35,14 @@ import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
|
32
35
|
import {
|
|
33
36
|
type BlockProposal,
|
|
34
37
|
type BlockProposalOptions,
|
|
35
|
-
|
|
38
|
+
CheckpointAttestation,
|
|
36
39
|
CheckpointProposal,
|
|
37
40
|
type CheckpointProposalCore,
|
|
38
41
|
type CheckpointProposalOptions,
|
|
42
|
+
type CoordinationSignatureContext,
|
|
39
43
|
} from '@aztec/stdlib/p2p';
|
|
40
44
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
45
|
+
import { ConsensusTimetable } from '@aztec/stdlib/timetable';
|
|
41
46
|
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
|
|
42
47
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
43
48
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
@@ -53,22 +58,25 @@ import { EventEmitter } from 'events';
|
|
|
53
58
|
import type { TypedDataDefinition } from 'viem';
|
|
54
59
|
|
|
55
60
|
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
61
|
+
import { DEFAULT_MAX_GOSSIP_CLOCK_DISPARITY_MS } from './config.js';
|
|
56
62
|
import { ValidationService } from './duties/validation_service.js';
|
|
57
63
|
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
58
64
|
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
59
65
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
60
66
|
import { ValidatorMetrics } from './metrics.js';
|
|
61
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
type BlockProposalValidationFailureReason,
|
|
69
|
+
type CheckpointProposalValidationFailureResult,
|
|
70
|
+
ProposalHandler,
|
|
71
|
+
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT,
|
|
72
|
+
SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT,
|
|
73
|
+
} from './proposal_handler.js';
|
|
62
74
|
|
|
63
75
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
64
76
|
// Just cap the set to avoid unbounded growth.
|
|
65
77
|
const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidationFailureReason[] = [
|
|
69
|
-
'state_mismatch',
|
|
70
|
-
'failed_txs',
|
|
71
|
-
];
|
|
78
|
+
const MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS = 1000;
|
|
79
|
+
const MAX_TRACKED_BAD_ATTESTATIONS = 10_000;
|
|
72
80
|
|
|
73
81
|
/**
|
|
74
82
|
* Validator Client
|
|
@@ -92,7 +100,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
92
100
|
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
|
|
93
101
|
private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
|
|
94
102
|
|
|
95
|
-
private proposersOfInvalidBlocks
|
|
103
|
+
private proposersOfInvalidBlocks = FifoSet.withLimit<string>(MAX_PROPOSERS_OF_INVALID_BLOCKS);
|
|
104
|
+
private invalidCheckpointProposalOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS);
|
|
105
|
+
private oversizedProposalOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS);
|
|
106
|
+
private badAttestationOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_BAD_ATTESTATIONS);
|
|
96
107
|
|
|
97
108
|
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
98
109
|
private lastAttestedProposal?: CheckpointProposalCore;
|
|
@@ -102,6 +113,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
102
113
|
private epochCache: EpochCache,
|
|
103
114
|
private p2pClient: P2P,
|
|
104
115
|
private proposalHandler: ProposalHandler,
|
|
116
|
+
private blockSource: L2BlockSource,
|
|
117
|
+
private checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
118
|
+
private worldState: WorldStateSynchronizer,
|
|
119
|
+
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
105
120
|
private config: ValidatorClientFullConfig,
|
|
106
121
|
private blobClient: BlobClientInterface,
|
|
107
122
|
private slashingProtectionSigner: ValidatorHASigner,
|
|
@@ -117,11 +132,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
117
132
|
this.tracer = telemetry.getTracer('Validator');
|
|
118
133
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
119
134
|
|
|
120
|
-
this.validationService = new ValidationService(
|
|
135
|
+
this.validationService = new ValidationService(
|
|
136
|
+
keyStore,
|
|
137
|
+
this.getSignatureContext(),
|
|
138
|
+
this.log.createChild('validation-service'),
|
|
139
|
+
);
|
|
140
|
+
this.proposalHandler.setCheckpointProposalValidationFailureCallback((proposal, result, proposalInfo) =>
|
|
141
|
+
this.handleInvalidCheckpointProposal(proposal, result, proposalInfo),
|
|
142
|
+
);
|
|
121
143
|
|
|
122
144
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
123
145
|
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
|
|
124
|
-
|
|
125
146
|
const myAddresses = this.getValidatorAddresses();
|
|
126
147
|
this.log.verbose(`Initialized validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
127
148
|
}
|
|
@@ -190,14 +211,26 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
190
211
|
txProvider: ITxProvider,
|
|
191
212
|
keyStoreManager: KeystoreManager,
|
|
192
213
|
blobClient: BlobClientInterface,
|
|
214
|
+
reexecutionTracker: CheckpointReexecutionTracker,
|
|
193
215
|
dateProvider: DateProvider = new DateProvider(),
|
|
194
216
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
195
217
|
slashingProtectionDb?: SlashingProtectionDatabase,
|
|
196
218
|
) {
|
|
197
219
|
const metrics = new ValidatorMetrics(telemetry);
|
|
198
|
-
const
|
|
220
|
+
const consensusTimetable = new ConsensusTimetable({
|
|
221
|
+
l1Constants: epochCache.getL1Constants(),
|
|
222
|
+
blockDuration: config.blockDurationMs / 1000,
|
|
223
|
+
});
|
|
224
|
+
const blockProposalValidator = new BlockProposalValidator(epochCache, consensusTimetable, {
|
|
199
225
|
txsPermitted: !config.disableTransactions,
|
|
200
226
|
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
227
|
+
maxBlocksPerCheckpoint: config.maxBlocksPerCheckpoint,
|
|
228
|
+
skipSlotValidation: config.skipProposalSlotValidation,
|
|
229
|
+
signatureContext: {
|
|
230
|
+
chainId: config.l1ChainId,
|
|
231
|
+
rollupAddress: config.rollupAddress,
|
|
232
|
+
},
|
|
233
|
+
clockDisparityMs: config.maxGossipClockDisparityMs ?? DEFAULT_MAX_GOSSIP_CLOCK_DISPARITY_MS,
|
|
201
234
|
});
|
|
202
235
|
const proposalHandler = new ProposalHandler(
|
|
203
236
|
checkpointsBuilder,
|
|
@@ -207,11 +240,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
207
240
|
txProvider,
|
|
208
241
|
blockProposalValidator,
|
|
209
242
|
epochCache,
|
|
243
|
+
consensusTimetable,
|
|
210
244
|
config,
|
|
211
245
|
blobClient,
|
|
246
|
+
reexecutionTracker,
|
|
212
247
|
metrics,
|
|
213
248
|
dateProvider,
|
|
214
249
|
telemetry,
|
|
250
|
+
undefined,
|
|
215
251
|
);
|
|
216
252
|
|
|
217
253
|
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
@@ -248,6 +284,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
248
284
|
epochCache,
|
|
249
285
|
p2pClient,
|
|
250
286
|
proposalHandler,
|
|
287
|
+
blockSource,
|
|
288
|
+
checkpointsBuilder,
|
|
289
|
+
worldState,
|
|
290
|
+
l1ToL2MessageSource,
|
|
251
291
|
config,
|
|
252
292
|
blobClient,
|
|
253
293
|
slashingProtectionSigner,
|
|
@@ -272,6 +312,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
272
312
|
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
273
313
|
}
|
|
274
314
|
|
|
315
|
+
private getSignatureContext(): CoordinationSignatureContext {
|
|
316
|
+
return {
|
|
317
|
+
chainId: this.config.l1ChainId,
|
|
318
|
+
rollupAddress: this.config.rollupAddress,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
275
322
|
public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
|
|
276
323
|
return this.keyStore.getCoinbaseAddress(attestor);
|
|
277
324
|
}
|
|
@@ -284,14 +331,27 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
284
331
|
return this.config;
|
|
285
332
|
}
|
|
286
333
|
|
|
334
|
+
public hasProposalEquivocation(slotNumber: SlotNumber): boolean {
|
|
335
|
+
return this.proposalHandler.hasProposalEquivocation(slotNumber);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
public hasInvalidProposals(slotNumber: SlotNumber): boolean {
|
|
339
|
+
return this.proposalHandler.hasInvalidProposals(slotNumber);
|
|
340
|
+
}
|
|
341
|
+
|
|
287
342
|
public updateConfig(config: Partial<ValidatorClientFullConfig>) {
|
|
288
343
|
this.config = { ...this.config, ...config };
|
|
344
|
+
this.proposalHandler.updateConfig(config);
|
|
289
345
|
}
|
|
290
346
|
|
|
291
347
|
public reloadKeystore(newManager: KeystoreManager): void {
|
|
292
348
|
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
293
349
|
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
294
|
-
this.validationService = new ValidationService(
|
|
350
|
+
this.validationService = new ValidationService(
|
|
351
|
+
this.keyStore,
|
|
352
|
+
this.getSignatureContext(),
|
|
353
|
+
this.log.createChild('validation-service'),
|
|
354
|
+
);
|
|
295
355
|
}
|
|
296
356
|
|
|
297
357
|
public async start() {
|
|
@@ -338,18 +398,27 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
338
398
|
checkpoint: CheckpointProposalCore,
|
|
339
399
|
proposalSender: PeerId,
|
|
340
400
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
341
|
-
this.p2pClient.
|
|
401
|
+
this.p2pClient.registerValidatorCheckpointProposalHandler(checkpointHandler);
|
|
342
402
|
|
|
343
403
|
// Duplicate proposal handler - triggers slashing for equivocation
|
|
344
404
|
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
345
405
|
this.handleDuplicateProposal(info);
|
|
346
406
|
});
|
|
347
407
|
|
|
408
|
+
// Oversized proposal handler - triggers slashing for proposals beyond the per-checkpoint block limit
|
|
409
|
+
this.p2pClient.registerOversizedProposalCallback((info: OversizedProposalInfo) => {
|
|
410
|
+
this.handleOversizedProposal(info);
|
|
411
|
+
});
|
|
412
|
+
|
|
348
413
|
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
349
414
|
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
350
415
|
this.handleDuplicateAttestation(info);
|
|
351
416
|
});
|
|
352
417
|
|
|
418
|
+
this.p2pClient.registerCheckpointAttestationCallback((attestation: CheckpointAttestation) => {
|
|
419
|
+
this.handleCheckpointAttestation(attestation);
|
|
420
|
+
});
|
|
421
|
+
|
|
353
422
|
const myAddresses = this.getValidatorAddresses();
|
|
354
423
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
355
424
|
|
|
@@ -396,21 +465,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
396
465
|
fishermanMode: this.config.fishermanMode || false,
|
|
397
466
|
});
|
|
398
467
|
|
|
399
|
-
// Reexecute
|
|
400
|
-
|
|
401
|
-
const { slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
|
|
402
|
-
const shouldReexecute =
|
|
403
|
-
fishermanMode ||
|
|
404
|
-
slashBroadcastedInvalidBlockPenalty > 0n ||
|
|
405
|
-
partOfCommittee ||
|
|
406
|
-
alwaysReexecuteBlockProposals ||
|
|
407
|
-
this.blobClient.canUpload();
|
|
408
|
-
|
|
409
|
-
const validationResult = await this.proposalHandler.handleBlockProposal(
|
|
410
|
-
proposal,
|
|
411
|
-
proposalSender,
|
|
412
|
-
!!shouldReexecute && !escapeHatchOpen,
|
|
413
|
-
);
|
|
468
|
+
// Reexecute outside the escape hatch so slashing observers can detect invalid proposals even when penalties are 0.
|
|
469
|
+
const validationResult = await this.proposalHandler.handleBlockProposal(proposal, proposalSender, !escapeHatchOpen);
|
|
414
470
|
|
|
415
471
|
if (!validationResult.isValid) {
|
|
416
472
|
const reason = validationResult.reason || 'unknown';
|
|
@@ -433,15 +489,18 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
433
489
|
this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
|
|
434
490
|
}
|
|
435
491
|
|
|
436
|
-
// Slash invalid block proposals (can happen even when not in committee)
|
|
437
492
|
if (
|
|
438
493
|
!escapeHatchOpen &&
|
|
439
494
|
validationResult.reason &&
|
|
440
|
-
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)
|
|
441
|
-
slashBroadcastedInvalidBlockPenalty > 0n
|
|
495
|
+
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)
|
|
442
496
|
) {
|
|
443
|
-
this.log.
|
|
497
|
+
this.log.info(`Detected invalid block proposal offense`, {
|
|
498
|
+
...proposalInfo,
|
|
499
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
500
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL),
|
|
501
|
+
});
|
|
444
502
|
this.slashInvalidBlock(proposal);
|
|
503
|
+
this.markInvalidProposalSlot(proposal.slotNumber);
|
|
445
504
|
}
|
|
446
505
|
return false;
|
|
447
506
|
}
|
|
@@ -480,9 +539,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
480
539
|
return undefined;
|
|
481
540
|
}
|
|
482
541
|
|
|
542
|
+
// Early-out for equivocation: refuses if we've already attested to a higher slot.
|
|
543
|
+
if (!this.shouldAttestToSlot(proposalSlotNumber)) {
|
|
544
|
+
return undefined;
|
|
545
|
+
}
|
|
546
|
+
|
|
483
547
|
// Ignore proposals from ourselves (may happen in HA setups)
|
|
484
548
|
if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
485
|
-
this.log.debug(`
|
|
549
|
+
this.log.debug(`Not attesting to block proposal from self for slot ${proposalSlotNumber}`, {
|
|
486
550
|
proposer: proposer.toString(),
|
|
487
551
|
proposalSlotNumber,
|
|
488
552
|
});
|
|
@@ -503,15 +567,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
503
567
|
fishermanMode: this.config.fishermanMode || false,
|
|
504
568
|
});
|
|
505
569
|
|
|
506
|
-
// Validate the checkpoint proposal
|
|
570
|
+
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
|
|
571
|
+
// Uses the cached result from the all-nodes callback if available (avoids double validation).
|
|
572
|
+
let checkpointNumber: CheckpointNumber;
|
|
507
573
|
if (this.config.skipCheckpointProposalValidation) {
|
|
508
574
|
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
|
|
575
|
+
checkpointNumber = CheckpointNumber(0);
|
|
509
576
|
} else {
|
|
510
577
|
const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
|
|
511
578
|
if (!validationResult.isValid) {
|
|
512
579
|
this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
513
580
|
return undefined;
|
|
514
581
|
}
|
|
582
|
+
checkpointNumber = validationResult.checkpointNumber;
|
|
515
583
|
}
|
|
516
584
|
|
|
517
585
|
// Check that I have any address in current committee before attesting
|
|
@@ -569,7 +637,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
569
637
|
return undefined;
|
|
570
638
|
}
|
|
571
639
|
|
|
572
|
-
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
640
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
|
|
573
641
|
}
|
|
574
642
|
|
|
575
643
|
/**
|
|
@@ -596,13 +664,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
596
664
|
private async createCheckpointAttestationsFromProposal(
|
|
597
665
|
proposal: CheckpointProposalCore,
|
|
598
666
|
attestors: EthAddress[] = [],
|
|
667
|
+
checkpointNumber: CheckpointNumber,
|
|
599
668
|
): Promise<CheckpointAttestation[] | undefined> {
|
|
600
669
|
// Equivocation check: must happen right before signing to minimize the race window
|
|
601
670
|
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
602
671
|
return undefined;
|
|
603
672
|
}
|
|
604
673
|
|
|
605
|
-
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
674
|
+
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
|
|
606
675
|
|
|
607
676
|
// Track the proposal we attested to (to prevent equivocation)
|
|
608
677
|
this.lastAttestedProposal = proposal;
|
|
@@ -611,6 +680,35 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
611
680
|
return attestations;
|
|
612
681
|
}
|
|
613
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
685
|
+
*/
|
|
686
|
+
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
687
|
+
try {
|
|
688
|
+
const lastBlockHeader = (await this.blockSource.getBlockData({ archive: proposal.archive }))?.header;
|
|
689
|
+
if (!lastBlockHeader) {
|
|
690
|
+
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
|
|
695
|
+
if (blocks.length === 0) {
|
|
696
|
+
this.log.warn(`No blocks found for blob upload`, proposalInfo);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const blobFields = blocks.flatMap(b => b.toBlobFields());
|
|
701
|
+
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
702
|
+
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
703
|
+
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
704
|
+
...proposalInfo,
|
|
705
|
+
numBlobs: blobs.length,
|
|
706
|
+
});
|
|
707
|
+
} catch (err) {
|
|
708
|
+
this.log.warn(`Failed to upload blobs for checkpoint: ${err}`, proposalInfo);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
614
712
|
private slashInvalidBlock(proposal: BlockProposal) {
|
|
615
713
|
const proposer = proposal.getSender();
|
|
616
714
|
|
|
@@ -620,12 +718,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
620
718
|
return;
|
|
621
719
|
}
|
|
622
720
|
|
|
623
|
-
// Trim the set if it's too big.
|
|
624
|
-
if (this.proposersOfInvalidBlocks.size > MAX_PROPOSERS_OF_INVALID_BLOCKS) {
|
|
625
|
-
// remove oldest proposer. `values` is guaranteed to be in insertion order.
|
|
626
|
-
this.proposersOfInvalidBlocks.delete(this.proposersOfInvalidBlocks.values().next().value!);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
721
|
this.proposersOfInvalidBlocks.add(proposer.toString());
|
|
630
722
|
|
|
631
723
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
@@ -638,20 +730,148 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
638
730
|
]);
|
|
639
731
|
}
|
|
640
732
|
|
|
733
|
+
private handleInvalidCheckpointProposal(
|
|
734
|
+
proposal: CheckpointProposalCore,
|
|
735
|
+
result: CheckpointProposalValidationFailureResult,
|
|
736
|
+
proposalInfo: LogData,
|
|
737
|
+
): void {
|
|
738
|
+
if (!SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT[result.reason]) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// The slot is already marked invalid by the all-nodes checkpoint handler that invokes this callback,
|
|
743
|
+
// so we only emit the proposer slash event here.
|
|
744
|
+
if (this.slashInvalidCheckpointProposal(proposal)) {
|
|
745
|
+
this.log.info(`Detected invalid checkpoint proposal offense`, {
|
|
746
|
+
...proposalInfo,
|
|
747
|
+
reason: result.reason,
|
|
748
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
749
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL),
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
private slashInvalidCheckpointProposal(proposal: CheckpointProposalCore): boolean {
|
|
755
|
+
const proposer = proposal.getSender();
|
|
756
|
+
if (!proposer) {
|
|
757
|
+
this.log.warn(`Cannot slash checkpoint proposal with invalid signature`, {
|
|
758
|
+
slotNumber: proposal.slotNumber,
|
|
759
|
+
archive: proposal.archive.toString(),
|
|
760
|
+
});
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const offenseType = OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL;
|
|
765
|
+
const offenseKey = `${proposer.toString()}:${offenseType}:${proposal.slotNumber}`;
|
|
766
|
+
if (!this.invalidCheckpointProposalOffenseKeys.addIfAbsent(offenseKey)) {
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
771
|
+
{
|
|
772
|
+
validator: proposer,
|
|
773
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
774
|
+
offenseType,
|
|
775
|
+
epochOrSlot: BigInt(proposal.slotNumber),
|
|
776
|
+
},
|
|
777
|
+
]);
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private markInvalidProposalSlot(slotNumber: SlotNumber): void {
|
|
782
|
+
this.proposalHandler.markInvalidProposalSlot(slotNumber);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
private handleCheckpointAttestation(attestation: CheckpointAttestation): void {
|
|
786
|
+
const slotNumber = attestation.slotNumber;
|
|
787
|
+
if (
|
|
788
|
+
!this.proposalHandler.hasInvalidProposals(slotNumber) ||
|
|
789
|
+
this.proposalHandler.hasProposalEquivocation(slotNumber)
|
|
790
|
+
) {
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const attester = attestation.getSender();
|
|
795
|
+
if (!attester) {
|
|
796
|
+
this.log.warn(`Cannot slash checkpoint attestation with invalid signature`, {
|
|
797
|
+
slotNumber,
|
|
798
|
+
archive: attestation.archive.toString(),
|
|
799
|
+
});
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
this.slashAttestedToInvalidCheckpointProposal(slotNumber, attester);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
private slashAttestedToInvalidCheckpointProposal(slotNumber: SlotNumber, attester: EthAddress): void {
|
|
807
|
+
const offenseKey = `${attester.toString()}:${OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL}:${slotNumber}`;
|
|
808
|
+
if (!this.badAttestationOffenseKeys.addIfAbsent(offenseKey)) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
this.log.info(`Detected attestation to invalid checkpoint proposal offense`, {
|
|
813
|
+
attester: attester.toString(),
|
|
814
|
+
slotNumber,
|
|
815
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
816
|
+
offenseType: getOffenseTypeName(OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL),
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
820
|
+
{
|
|
821
|
+
validator: attester,
|
|
822
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
823
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
824
|
+
epochOrSlot: BigInt(slotNumber),
|
|
825
|
+
},
|
|
826
|
+
]);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Handle detection of an oversized block proposal: one whose index within its checkpoint lands at or
|
|
831
|
+
* beyond the consensus per-checkpoint block limit. A single signed proposal at an illegal index is
|
|
832
|
+
* self-contained evidence, so emit an invalid-block-proposal slash event for the proposer, deduped per
|
|
833
|
+
* (proposer, slot) since the p2p layer reports every oversized proposal it stores.
|
|
834
|
+
*/
|
|
835
|
+
private handleOversizedProposal(info: OversizedProposalInfo): void {
|
|
836
|
+
const { slot, proposer } = info;
|
|
837
|
+
const offenseType = OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL;
|
|
838
|
+
if (!this.oversizedProposalOffenseKeys.addIfAbsent(`${proposer.toString()}:${offenseType}:${slot}`)) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
this.log.info(`Detected oversized block proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
843
|
+
proposer: proposer.toString(),
|
|
844
|
+
slot,
|
|
845
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
846
|
+
offenseType: getOffenseTypeName(offenseType),
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
850
|
+
{
|
|
851
|
+
validator: proposer,
|
|
852
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
853
|
+
offenseType,
|
|
854
|
+
epochOrSlot: BigInt(slot),
|
|
855
|
+
},
|
|
856
|
+
]);
|
|
857
|
+
}
|
|
858
|
+
|
|
641
859
|
/**
|
|
642
860
|
* Handle detection of a duplicate proposal (equivocation).
|
|
643
861
|
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
644
862
|
*/
|
|
645
863
|
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
646
864
|
const { slot, proposer, type } = info;
|
|
865
|
+
this.proposalHandler.markProposalEquivocation(slot);
|
|
647
866
|
|
|
648
|
-
this.log.
|
|
867
|
+
this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
649
868
|
proposer: proposer.toString(),
|
|
650
869
|
slot,
|
|
651
870
|
type,
|
|
871
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
872
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_PROPOSAL),
|
|
652
873
|
});
|
|
653
874
|
|
|
654
|
-
// Emit slash event
|
|
655
875
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
656
876
|
{
|
|
657
877
|
validator: proposer,
|
|
@@ -660,6 +880,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
660
880
|
epochOrSlot: BigInt(slot),
|
|
661
881
|
},
|
|
662
882
|
]);
|
|
883
|
+
|
|
884
|
+
this.emit(WANT_TO_CLEAR_SLASH_EVENT, [
|
|
885
|
+
{
|
|
886
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
887
|
+
epochOrSlot: BigInt(slot),
|
|
888
|
+
},
|
|
889
|
+
]);
|
|
663
890
|
}
|
|
664
891
|
|
|
665
892
|
/**
|
|
@@ -669,9 +896,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
669
896
|
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
670
897
|
const { slot, attester } = info;
|
|
671
898
|
|
|
672
|
-
this.log.
|
|
899
|
+
this.log.info(`Detected duplicate attestation offense from ${attester.toString()} at slot ${slot}`, {
|
|
673
900
|
attester: attester.toString(),
|
|
674
901
|
slot,
|
|
902
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
903
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_ATTESTATION),
|
|
675
904
|
});
|
|
676
905
|
|
|
677
906
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
@@ -686,6 +915,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
686
915
|
|
|
687
916
|
async createBlockProposal(
|
|
688
917
|
blockHeader: BlockHeader,
|
|
918
|
+
checkpointNumber: CheckpointNumber,
|
|
689
919
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
690
920
|
inHash: Fr,
|
|
691
921
|
archive: Fr,
|
|
@@ -712,6 +942,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
712
942
|
);
|
|
713
943
|
const newProposal = await this.validationService.createBlockProposal(
|
|
714
944
|
blockHeader,
|
|
945
|
+
checkpointNumber,
|
|
715
946
|
indexWithinCheckpoint,
|
|
716
947
|
inHash,
|
|
717
948
|
archive,
|
|
@@ -719,7 +950,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
719
950
|
proposerAddress,
|
|
720
951
|
{
|
|
721
952
|
...options,
|
|
722
|
-
broadcastInvalidBlockProposal:
|
|
953
|
+
broadcastInvalidBlockProposal:
|
|
954
|
+
options.broadcastInvalidBlockProposal || this.config.broadcastInvalidBlockProposal,
|
|
723
955
|
},
|
|
724
956
|
);
|
|
725
957
|
this.lastProposedBlock = newProposal;
|
|
@@ -729,8 +961,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
729
961
|
async createCheckpointProposal(
|
|
730
962
|
checkpointHeader: CheckpointHeader,
|
|
731
963
|
archive: Fr,
|
|
964
|
+
checkpointNumber: CheckpointNumber,
|
|
732
965
|
feeAssetPriceModifier: bigint,
|
|
733
|
-
|
|
966
|
+
lastBlockProposal: BlockProposal | undefined,
|
|
734
967
|
proposerAddress: EthAddress | undefined,
|
|
735
968
|
options: CheckpointProposalOptions = {},
|
|
736
969
|
): Promise<CheckpointProposal> {
|
|
@@ -751,12 +984,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
751
984
|
const newProposal = await this.validationService.createCheckpointProposal(
|
|
752
985
|
checkpointHeader,
|
|
753
986
|
archive,
|
|
987
|
+
checkpointNumber,
|
|
754
988
|
feeAssetPriceModifier,
|
|
755
|
-
|
|
989
|
+
lastBlockProposal,
|
|
756
990
|
proposerAddress,
|
|
757
991
|
options,
|
|
758
992
|
);
|
|
759
993
|
this.lastProposedCheckpoint = newProposal;
|
|
994
|
+
// Self-record this slot's outcome on the re-execution tracker. Proposers don't run their
|
|
995
|
+
// own proposals through `handleCheckpointProposal`, so without this call the proposer's
|
|
996
|
+
// sentinel would see no outcome for slots it proposed and would mis-attribute itself as
|
|
997
|
+
// inactive. We pass the locally-computed `archive` (not `newProposal.archive`, which may
|
|
998
|
+
// be intentionally corrupted under test-only flags); from the proposer's local-view
|
|
999
|
+
// perspective the work it just completed is valid by definition.
|
|
1000
|
+
this.proposalHandler.recordOwnCheckpointProposalAsValid(checkpointHeader.slotNumber, archive, checkpointNumber);
|
|
760
1001
|
return newProposal;
|
|
761
1002
|
}
|
|
762
1003
|
|
|
@@ -768,16 +1009,24 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
768
1009
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
769
1010
|
proposer: EthAddress,
|
|
770
1011
|
slot: SlotNumber,
|
|
771
|
-
|
|
1012
|
+
checkpointNumber: CheckpointNumber,
|
|
772
1013
|
): Promise<Signature> {
|
|
773
|
-
return await this.validationService.signAttestationsAndSigners(
|
|
1014
|
+
return await this.validationService.signAttestationsAndSigners(
|
|
1015
|
+
attestationsAndSigners,
|
|
1016
|
+
proposer,
|
|
1017
|
+
slot,
|
|
1018
|
+
checkpointNumber,
|
|
1019
|
+
);
|
|
774
1020
|
}
|
|
775
1021
|
|
|
776
|
-
async collectOwnAttestations(
|
|
1022
|
+
async collectOwnAttestations(
|
|
1023
|
+
proposal: CheckpointProposal,
|
|
1024
|
+
checkpointNumber: CheckpointNumber,
|
|
1025
|
+
): Promise<CheckpointAttestation[]> {
|
|
777
1026
|
const slot = proposal.slotNumber;
|
|
778
1027
|
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
779
1028
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
780
|
-
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
1029
|
+
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
|
|
781
1030
|
|
|
782
1031
|
if (!attestations) {
|
|
783
1032
|
return [];
|
|
@@ -796,6 +1045,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
796
1045
|
proposal: CheckpointProposal,
|
|
797
1046
|
required: number,
|
|
798
1047
|
deadline: Date,
|
|
1048
|
+
checkpointNumber: CheckpointNumber,
|
|
799
1049
|
): Promise<CheckpointAttestation[]> {
|
|
800
1050
|
// Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
|
|
801
1051
|
const slot = proposal.slotNumber;
|
|
@@ -808,33 +1058,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
808
1058
|
throw new AttestationTimeoutError(0, required, slot);
|
|
809
1059
|
}
|
|
810
1060
|
|
|
811
|
-
await this.collectOwnAttestations(proposal);
|
|
1061
|
+
await this.collectOwnAttestations(proposal, checkpointNumber);
|
|
812
1062
|
|
|
813
|
-
const
|
|
1063
|
+
const proposalPayloadHash = proposal.getPayloadHash();
|
|
814
1064
|
const myAddresses = this.getValidatorAddresses();
|
|
815
1065
|
|
|
816
1066
|
let attestations: CheckpointAttestation[] = [];
|
|
817
1067
|
while (true) {
|
|
818
|
-
//
|
|
819
|
-
//
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
if (!attestation.archive.equals(proposal.archive)) {
|
|
823
|
-
this.log.warn(
|
|
824
|
-
`Received attestation for slot ${slot} with mismatched archive from ${attestation.getSender()?.toString()}`,
|
|
825
|
-
{ attestationArchive: attestation.archive.toString(), proposalArchive: proposal.archive.toString() },
|
|
826
|
-
);
|
|
827
|
-
return false;
|
|
828
|
-
}
|
|
829
|
-
return true;
|
|
830
|
-
},
|
|
831
|
-
);
|
|
1068
|
+
// The pool already filters by proposal payload hash; if any attestation slips through with a
|
|
1069
|
+
// mismatched payload hash, drop it defensively. Equivocations are emitted as separate slash
|
|
1070
|
+
// events from libp2p_service.
|
|
1071
|
+
const collectedAttestations = await this.p2pClient.getCheckpointAttestationsForSlot(slot, proposalPayloadHash);
|
|
832
1072
|
|
|
833
1073
|
// Log new attestations we collected
|
|
834
1074
|
const oldSenders = attestations.map(attestation => attestation.getSender());
|
|
835
1075
|
for (const collected of collectedAttestations) {
|
|
836
1076
|
const collectedSender = collected.getSender();
|
|
837
|
-
// Skip attestations with invalid signatures
|
|
1077
|
+
// Skip attestations with invalid signatures. Should not happen as we don't add invalid attestations to our pool.
|
|
838
1078
|
if (!collectedSender) {
|
|
839
1079
|
this.log.warn(`Skipping attestation with invalid signature for slot ${slot}`);
|
|
840
1080
|
continue;
|