@aztec/validator-client 0.0.1-commit.2448fdb → 0.0.1-commit.2606882
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 +10 -11
- package/dest/checkpoint_builder.js +1 -1
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +17 -11
- 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 +8 -3
- 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 +54 -14
- package/dest/proposal_handler.d.ts.map +1 -1
- package/dest/proposal_handler.js +369 -149
- package/dest/validator.d.ts +33 -12
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +267 -91
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +1 -1
- package/src/config.ts +17 -10
- package/src/duties/validation_service.ts +51 -47
- package/src/factory.ts +11 -0
- package/src/metrics.ts +18 -0
- package/src/proposal_handler.ts +440 -182
- package/src/validator.ts +341 -116
package/dest/validator.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
2
|
+
import { CheckpointNumber } from '@aztec/foundation/branded-types';
|
|
3
|
+
import { FifoSet } from '@aztec/foundation/fifo-set';
|
|
1
4
|
import { createLogger } from '@aztec/foundation/log';
|
|
2
5
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
3
6
|
import { sleep } from '@aztec/foundation/sleep';
|
|
4
7
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
5
8
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
6
|
-
import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
|
|
9
|
+
import { OffenseType, WANT_TO_CLEAR_SLASH_EVENT, WANT_TO_SLASH_EVENT, getOffenseTypeName } from '@aztec/slasher';
|
|
7
10
|
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
8
11
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
9
12
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
10
|
-
import { createHASigner, createSignerFromSharedDb } from '@aztec/validator-ha-signer/factory';
|
|
13
|
+
import { createHASigner, createLocalSignerWithProtection, createSignerFromSharedDb } from '@aztec/validator-ha-signer/factory';
|
|
11
14
|
import { DutyType } from '@aztec/validator-ha-signer/types';
|
|
12
15
|
import { EventEmitter } from 'events';
|
|
13
16
|
import { ValidationService } from './duties/validation_service.js';
|
|
@@ -18,11 +21,36 @@ import { ProposalHandler } from './proposal_handler.js';
|
|
|
18
21
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
19
22
|
// Just cap the set to avoid unbounded growth.
|
|
20
23
|
const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
24
|
+
const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
|
|
25
|
+
const MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS = 1000;
|
|
26
|
+
const MAX_TRACKED_BAD_ATTESTATIONS = 10_000;
|
|
21
27
|
// What errors from the block proposal handler result in slashing
|
|
22
28
|
const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
23
29
|
'state_mismatch',
|
|
24
|
-
'failed_txs'
|
|
30
|
+
'failed_txs',
|
|
31
|
+
'global_variables_mismatch',
|
|
32
|
+
'invalid_proposal',
|
|
33
|
+
'parent_block_wrong_slot',
|
|
34
|
+
'in_hash_mismatch'
|
|
25
35
|
];
|
|
36
|
+
const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT = {
|
|
37
|
+
// enabled
|
|
38
|
+
['invalid_fee_asset_price_modifier']: true,
|
|
39
|
+
['checkpoint_header_mismatch']: true,
|
|
40
|
+
// These late mismatches should normally be caught by earlier checks, but if reached after validating the local
|
|
41
|
+
// checkpoint inputs, the proposer-signed payload disagrees with deterministic recomputation.
|
|
42
|
+
['archive_mismatch']: true,
|
|
43
|
+
['out_hash_mismatch']: true,
|
|
44
|
+
['no_blocks_for_slot']: true,
|
|
45
|
+
['too_many_blocks_in_checkpoint']: true,
|
|
46
|
+
['checkpoint_validation_failed']: true,
|
|
47
|
+
['last_block_archive_mismatch']: true,
|
|
48
|
+
// disabled
|
|
49
|
+
['invalid_signature']: false,
|
|
50
|
+
['last_block_not_found']: false,
|
|
51
|
+
['block_fetch_error']: false,
|
|
52
|
+
['checkpoint_already_published']: false
|
|
53
|
+
};
|
|
26
54
|
/**
|
|
27
55
|
* Validator Client
|
|
28
56
|
*/ export class ValidatorClient extends EventEmitter {
|
|
@@ -30,9 +58,13 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
30
58
|
epochCache;
|
|
31
59
|
p2pClient;
|
|
32
60
|
proposalHandler;
|
|
61
|
+
blockSource;
|
|
62
|
+
checkpointsBuilder;
|
|
63
|
+
worldState;
|
|
64
|
+
l1ToL2MessageSource;
|
|
33
65
|
config;
|
|
34
66
|
blobClient;
|
|
35
|
-
|
|
67
|
+
slashingProtectionSigner;
|
|
36
68
|
dateProvider;
|
|
37
69
|
tracer;
|
|
38
70
|
validationService;
|
|
@@ -46,14 +78,19 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
46
78
|
epochCacheUpdateLoop;
|
|
47
79
|
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */ lastAttestedEpochByAttester;
|
|
48
80
|
proposersOfInvalidBlocks;
|
|
81
|
+
slotsWithInvalidProposals;
|
|
82
|
+
invalidCheckpointProposalOffenseKeys;
|
|
83
|
+
badAttestationOffenseKeys;
|
|
84
|
+
slotsWithProposalEquivocation;
|
|
49
85
|
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */ lastAttestedProposal;
|
|
50
|
-
constructor(keyStore, epochCache, p2pClient, proposalHandler, config, blobClient,
|
|
51
|
-
super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.proposalHandler = proposalHandler, this.config = config, this.blobClient = blobClient, this.
|
|
86
|
+
constructor(keyStore, epochCache, p2pClient, proposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, slashingProtectionSigner, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
87
|
+
super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.proposalHandler = proposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.slashingProtectionSigner = slashingProtectionSigner, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.lastAttestedEpochByAttester = new Map(), this.proposersOfInvalidBlocks = FifoSet.withLimit(MAX_PROPOSERS_OF_INVALID_BLOCKS), this.slotsWithInvalidProposals = FifoSet.withLimit(MAX_TRACKED_INVALID_PROPOSAL_SLOTS), this.invalidCheckpointProposalOffenseKeys = FifoSet.withLimit(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS), this.badAttestationOffenseKeys = FifoSet.withLimit(MAX_TRACKED_BAD_ATTESTATIONS), this.slotsWithProposalEquivocation = FifoSet.withLimit(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
|
|
52
88
|
// Create child logger with fisherman prefix if in fisherman mode
|
|
53
89
|
this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
|
|
54
90
|
this.tracer = telemetry.getTracer('Validator');
|
|
55
91
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
56
|
-
this.validationService = new ValidationService(keyStore, this.log.createChild('validation-service'));
|
|
92
|
+
this.validationService = new ValidationService(keyStore, this.getSignatureContext(), this.log.createChild('validation-service'));
|
|
93
|
+
this.proposalHandler.setCheckpointProposalValidationFailureCallback((proposal, result, proposalInfo)=>this.handleInvalidCheckpointProposal(proposal, result, proposalInfo));
|
|
57
94
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
58
95
|
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
|
|
59
96
|
const myAddresses = this.getValidatorAddresses();
|
|
@@ -103,32 +140,48 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
103
140
|
this.log.error(`Error updating epoch committee`, err);
|
|
104
141
|
}
|
|
105
142
|
}
|
|
106
|
-
static async new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), slashingProtectionDb) {
|
|
143
|
+
static async new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, reexecutionTracker, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), slashingProtectionDb) {
|
|
107
144
|
const metrics = new ValidatorMetrics(telemetry);
|
|
108
145
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
109
146
|
txsPermitted: !config.disableTransactions,
|
|
110
|
-
maxTxsPerBlock: config.validateMaxTxsPerBlock
|
|
147
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
148
|
+
maxBlocksPerCheckpoint: config.maxBlocksPerCheckpoint,
|
|
149
|
+
skipSlotValidation: config.skipProposalSlotValidation,
|
|
150
|
+
signatureContext: {
|
|
151
|
+
chainId: config.l1ChainId,
|
|
152
|
+
rollupAddress: config.rollupAddress
|
|
153
|
+
}
|
|
111
154
|
});
|
|
112
|
-
const proposalHandler = new ProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, blobClient, metrics, dateProvider, telemetry);
|
|
155
|
+
const proposalHandler = new ProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, blobClient, reexecutionTracker, metrics, dateProvider, telemetry, undefined);
|
|
113
156
|
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
114
|
-
let
|
|
115
|
-
let haSigner;
|
|
157
|
+
let slashingProtectionSigner;
|
|
116
158
|
if (slashingProtectionDb) {
|
|
117
159
|
// Shared database mode: use a pre-existing database (e.g. for testing HA setups).
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
160
|
+
({ signer: slashingProtectionSigner } = createSignerFromSharedDb(slashingProtectionDb, config, {
|
|
161
|
+
telemetryClient: telemetry,
|
|
162
|
+
dateProvider
|
|
163
|
+
}));
|
|
121
164
|
} else if (config.haSigningEnabled) {
|
|
165
|
+
// Multi-node HA mode: use PostgreSQL-backed distributed locking.
|
|
122
166
|
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
123
167
|
const haConfig = {
|
|
124
168
|
...config,
|
|
125
169
|
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000
|
|
126
170
|
};
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
171
|
+
({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
|
|
172
|
+
telemetryClient: telemetry,
|
|
173
|
+
dateProvider
|
|
174
|
+
}));
|
|
175
|
+
} else {
|
|
176
|
+
// Single-node mode: use LMDB-backed local signing protection.
|
|
177
|
+
// This prevents double-signing if the node crashes and restarts mid-proposal.
|
|
178
|
+
({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
|
|
179
|
+
telemetryClient: telemetry,
|
|
180
|
+
dateProvider
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
const validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
|
|
184
|
+
const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, proposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, slashingProtectionSigner, dateProvider, telemetry);
|
|
132
185
|
return validator;
|
|
133
186
|
}
|
|
134
187
|
getValidatorAddresses() {
|
|
@@ -140,6 +193,12 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
140
193
|
signWithAddress(addr, msg, context) {
|
|
141
194
|
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
142
195
|
}
|
|
196
|
+
getSignatureContext() {
|
|
197
|
+
return {
|
|
198
|
+
chainId: this.config.l1ChainId,
|
|
199
|
+
rollupAddress: this.config.rollupAddress
|
|
200
|
+
};
|
|
201
|
+
}
|
|
143
202
|
getCoinbaseForAttestor(attestor) {
|
|
144
203
|
return this.keyStore.getCoinbaseAddress(attestor);
|
|
145
204
|
}
|
|
@@ -149,25 +208,23 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
149
208
|
getConfig() {
|
|
150
209
|
return this.config;
|
|
151
210
|
}
|
|
211
|
+
hasProposalEquivocation(slotNumber) {
|
|
212
|
+
return this.slotsWithProposalEquivocation.has(slotNumber);
|
|
213
|
+
}
|
|
214
|
+
hasInvalidProposals(slotNumber) {
|
|
215
|
+
return this.slotsWithInvalidProposals.has(slotNumber);
|
|
216
|
+
}
|
|
152
217
|
updateConfig(config) {
|
|
153
218
|
this.config = {
|
|
154
219
|
...this.config,
|
|
155
220
|
...config
|
|
156
221
|
};
|
|
222
|
+
this.proposalHandler.updateConfig(config);
|
|
157
223
|
}
|
|
158
224
|
reloadKeystore(newManager) {
|
|
159
|
-
if (this.config.haSigningEnabled && !this.haSigner) {
|
|
160
|
-
this.log.warn('HA signing is enabled in config but was not initialized at startup. ' + 'Restart the node to enable HA signing.');
|
|
161
|
-
} else if (!this.config.haSigningEnabled && this.haSigner) {
|
|
162
|
-
this.log.warn('HA signing was disabled via config update but the HA signer is still active. ' + 'Restart the node to fully disable HA signing.');
|
|
163
|
-
}
|
|
164
225
|
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
} else {
|
|
168
|
-
this.keyStore = newAdapter;
|
|
169
|
-
}
|
|
170
|
-
this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
|
|
226
|
+
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
227
|
+
this.validationService = new ValidationService(this.keyStore, this.getSignatureContext(), this.log.createChild('validation-service'));
|
|
171
228
|
}
|
|
172
229
|
async start() {
|
|
173
230
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
@@ -200,7 +257,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
200
257
|
// The checkpoint is received as CheckpointProposalCore since the lastBlock is extracted
|
|
201
258
|
// and processed separately via the block handler above.
|
|
202
259
|
const checkpointHandler = (checkpoint, proposalSender)=>this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
203
|
-
this.p2pClient.
|
|
260
|
+
this.p2pClient.registerValidatorCheckpointProposalHandler(checkpointHandler);
|
|
204
261
|
// Duplicate proposal handler - triggers slashing for equivocation
|
|
205
262
|
this.p2pClient.registerDuplicateProposalCallback((info)=>{
|
|
206
263
|
this.handleDuplicateProposal(info);
|
|
@@ -209,6 +266,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
209
266
|
this.p2pClient.registerDuplicateAttestationCallback((info)=>{
|
|
210
267
|
this.handleDuplicateAttestation(info);
|
|
211
268
|
});
|
|
269
|
+
this.p2pClient.registerCheckpointAttestationCallback((attestation)=>{
|
|
270
|
+
this.handleCheckpointAttestation(attestation);
|
|
271
|
+
});
|
|
212
272
|
const myAddresses = this.getValidatorAddresses();
|
|
213
273
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
214
274
|
await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
|
|
@@ -248,11 +308,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
248
308
|
txHashes: proposal.txHashes.map((t)=>t.toString()),
|
|
249
309
|
fishermanMode: this.config.fishermanMode || false
|
|
250
310
|
});
|
|
251
|
-
// Reexecute
|
|
252
|
-
|
|
253
|
-
const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
|
|
254
|
-
const shouldReexecute = fishermanMode || slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute || partOfCommittee && validatorReexecute || alwaysReexecuteBlockProposals || this.blobClient.canUpload();
|
|
255
|
-
const validationResult = await this.proposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute && !escapeHatchOpen);
|
|
311
|
+
// Reexecute outside the escape hatch so slashing observers can detect invalid proposals even when penalties are 0.
|
|
312
|
+
const validationResult = await this.proposalHandler.handleBlockProposal(proposal, proposalSender, !escapeHatchOpen);
|
|
256
313
|
if (!validationResult.isValid) {
|
|
257
314
|
const reason = validationResult.reason || 'unknown';
|
|
258
315
|
this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
|
|
@@ -270,10 +327,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
270
327
|
// Node issues so we can't validate
|
|
271
328
|
this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
|
|
272
329
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
330
|
+
if (!escapeHatchOpen && validationResult.reason && SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)) {
|
|
331
|
+
this.log.info(`Detected invalid block proposal offense`, {
|
|
332
|
+
...proposalInfo,
|
|
333
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
334
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL)
|
|
335
|
+
});
|
|
276
336
|
this.slashInvalidBlock(proposal);
|
|
337
|
+
this.markInvalidProposalSlot(proposal.slotNumber);
|
|
277
338
|
}
|
|
278
339
|
return false;
|
|
279
340
|
}
|
|
@@ -295,42 +356,50 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
295
356
|
* the lastBlock is extracted and processed separately via the block handler.
|
|
296
357
|
* @returns Checkpoint attestations if valid, undefined otherwise
|
|
297
358
|
*/ async attestToCheckpointProposal(proposal, _proposalSender) {
|
|
298
|
-
const
|
|
359
|
+
const proposalSlotNumber = proposal.slotNumber;
|
|
299
360
|
const proposer = proposal.getSender();
|
|
300
361
|
// If escape hatch is open for this slot's epoch, do not attest.
|
|
301
|
-
if (await this.epochCache.isEscapeHatchOpenAtSlot(
|
|
302
|
-
this.log.warn(`Escape hatch open for slot ${
|
|
362
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(proposalSlotNumber)) {
|
|
363
|
+
this.log.warn(`Escape hatch open for slot ${proposalSlotNumber}, skipping checkpoint attestation handling`);
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
// Early-out for equivocation: refuses if we've already attested to a higher slot.
|
|
367
|
+
if (!this.shouldAttestToSlot(proposalSlotNumber)) {
|
|
303
368
|
return undefined;
|
|
304
369
|
}
|
|
305
370
|
// Ignore proposals from ourselves (may happen in HA setups)
|
|
306
371
|
if (proposer && this.getValidatorAddresses().some((addr)=>addr.equals(proposer))) {
|
|
307
|
-
this.log.debug(`Ignoring block proposal from self for slot ${
|
|
372
|
+
this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
|
|
308
373
|
proposer: proposer.toString(),
|
|
309
|
-
|
|
374
|
+
proposalSlotNumber
|
|
310
375
|
});
|
|
311
376
|
return undefined;
|
|
312
377
|
}
|
|
313
378
|
// Check that I have any address in the committee where this checkpoint will land before attesting
|
|
314
|
-
const inCommittee = await this.epochCache.filterInCommittee(
|
|
379
|
+
const inCommittee = await this.epochCache.filterInCommittee(proposalSlotNumber, this.getValidatorAddresses());
|
|
315
380
|
const partOfCommittee = inCommittee.length > 0;
|
|
316
381
|
const proposalInfo = {
|
|
317
|
-
|
|
382
|
+
proposalSlotNumber,
|
|
318
383
|
archive: proposal.archive.toString(),
|
|
319
384
|
proposer: proposer?.toString()
|
|
320
385
|
};
|
|
321
|
-
this.log.info(`Received checkpoint proposal for slot ${
|
|
386
|
+
this.log.info(`Received checkpoint proposal for slot ${proposalSlotNumber}`, {
|
|
322
387
|
...proposalInfo,
|
|
323
388
|
fishermanMode: this.config.fishermanMode || false
|
|
324
389
|
});
|
|
325
|
-
// Validate the checkpoint proposal
|
|
390
|
+
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
|
|
391
|
+
// Uses the cached result from the all-nodes callback if available (avoids double validation).
|
|
392
|
+
let checkpointNumber;
|
|
326
393
|
if (this.config.skipCheckpointProposalValidation) {
|
|
327
|
-
this.log.warn(`Skipping checkpoint proposal validation for slot ${
|
|
394
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
|
|
395
|
+
checkpointNumber = CheckpointNumber(0);
|
|
328
396
|
} else {
|
|
329
397
|
const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
|
|
330
398
|
if (!validationResult.isValid) {
|
|
331
399
|
this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
332
400
|
return undefined;
|
|
333
401
|
}
|
|
402
|
+
checkpointNumber = validationResult.checkpointNumber;
|
|
334
403
|
}
|
|
335
404
|
// Check that I have any address in current committee before attesting
|
|
336
405
|
// In fisherman mode, we still create attestations for validation even if not in committee
|
|
@@ -339,14 +408,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
339
408
|
return undefined;
|
|
340
409
|
}
|
|
341
410
|
// Provided all of the above checks pass, we can attest to the proposal
|
|
342
|
-
this.log.info(`${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${
|
|
411
|
+
this.log.info(`${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${proposalSlotNumber}`, {
|
|
343
412
|
...proposalInfo,
|
|
344
413
|
inCommittee: partOfCommittee,
|
|
345
414
|
fishermanMode: this.config.fishermanMode || false
|
|
346
415
|
});
|
|
347
416
|
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
348
417
|
// Track epoch participation per attester: count each (attester, epoch) pair at most once
|
|
349
|
-
const proposalEpoch = getEpochAtSlot(
|
|
418
|
+
const proposalEpoch = getEpochAtSlot(proposalSlotNumber, this.epochCache.getL1Constants());
|
|
350
419
|
for (const attester of inCommittee){
|
|
351
420
|
const key = attester.toString();
|
|
352
421
|
const lastEpoch = this.lastAttestedEpochByAttester.get(key);
|
|
@@ -371,13 +440,13 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
371
440
|
}
|
|
372
441
|
if (this.config.fishermanMode) {
|
|
373
442
|
// bail out early and don't save attestations to the pool in fisherman mode
|
|
374
|
-
this.log.info(`Creating checkpoint attestations for slot ${
|
|
443
|
+
this.log.info(`Creating checkpoint attestations for slot ${proposalSlotNumber}`, {
|
|
375
444
|
...proposalInfo,
|
|
376
445
|
attestors: attestors.map((a)=>a.toString())
|
|
377
446
|
});
|
|
378
447
|
return undefined;
|
|
379
448
|
}
|
|
380
|
-
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
449
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
|
|
381
450
|
}
|
|
382
451
|
/**
|
|
383
452
|
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
@@ -394,17 +463,44 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
394
463
|
}
|
|
395
464
|
return true;
|
|
396
465
|
}
|
|
397
|
-
async createCheckpointAttestationsFromProposal(proposal, attestors = []) {
|
|
466
|
+
async createCheckpointAttestationsFromProposal(proposal, attestors = [], checkpointNumber) {
|
|
398
467
|
// Equivocation check: must happen right before signing to minimize the race window
|
|
399
468
|
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
400
469
|
return undefined;
|
|
401
470
|
}
|
|
402
|
-
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
471
|
+
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
|
|
403
472
|
// Track the proposal we attested to (to prevent equivocation)
|
|
404
473
|
this.lastAttestedProposal = proposal;
|
|
405
474
|
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
406
475
|
return attestations;
|
|
407
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
479
|
+
*/ async uploadBlobsForCheckpoint(proposal, proposalInfo) {
|
|
480
|
+
try {
|
|
481
|
+
const lastBlockHeader = (await this.blockSource.getBlockData({
|
|
482
|
+
archive: proposal.archive
|
|
483
|
+
}))?.header;
|
|
484
|
+
if (!lastBlockHeader) {
|
|
485
|
+
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
|
|
489
|
+
if (blocks.length === 0) {
|
|
490
|
+
this.log.warn(`No blocks found for blob upload`, proposalInfo);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const blobFields = blocks.flatMap((b)=>b.toBlobFields());
|
|
494
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
495
|
+
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
496
|
+
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
497
|
+
...proposalInfo,
|
|
498
|
+
numBlobs: blobs.length
|
|
499
|
+
});
|
|
500
|
+
} catch (err) {
|
|
501
|
+
this.log.warn(`Failed to upload blobs for checkpoint: ${err}`, proposalInfo);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
408
504
|
slashInvalidBlock(proposal) {
|
|
409
505
|
const proposer = proposal.getSender();
|
|
410
506
|
// Skip if signature is invalid (shouldn't happen since we validate earlier)
|
|
@@ -412,11 +508,6 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
412
508
|
this.log.warn(`Cannot slash proposal with invalid signature`);
|
|
413
509
|
return;
|
|
414
510
|
}
|
|
415
|
-
// Trim the set if it's too big.
|
|
416
|
-
if (this.proposersOfInvalidBlocks.size > MAX_PROPOSERS_OF_INVALID_BLOCKS) {
|
|
417
|
-
// remove oldest proposer. `values` is guaranteed to be in insertion order.
|
|
418
|
-
this.proposersOfInvalidBlocks.delete(this.proposersOfInvalidBlocks.values().next().value);
|
|
419
|
-
}
|
|
420
511
|
this.proposersOfInvalidBlocks.add(proposer.toString());
|
|
421
512
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
422
513
|
{
|
|
@@ -427,17 +518,95 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
427
518
|
}
|
|
428
519
|
]);
|
|
429
520
|
}
|
|
521
|
+
handleInvalidCheckpointProposal(proposal, result, proposalInfo) {
|
|
522
|
+
if (!SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT[result.reason]) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
this.markInvalidProposalSlot(proposal.slotNumber);
|
|
526
|
+
if (this.slashInvalidCheckpointProposal(proposal)) {
|
|
527
|
+
this.log.info(`Detected invalid checkpoint proposal offense`, {
|
|
528
|
+
...proposalInfo,
|
|
529
|
+
reason: result.reason,
|
|
530
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
531
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL)
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
slashInvalidCheckpointProposal(proposal) {
|
|
536
|
+
const proposer = proposal.getSender();
|
|
537
|
+
if (!proposer) {
|
|
538
|
+
this.log.warn(`Cannot slash checkpoint proposal with invalid signature`, {
|
|
539
|
+
slotNumber: proposal.slotNumber,
|
|
540
|
+
archive: proposal.archive.toString()
|
|
541
|
+
});
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
const offenseType = OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL;
|
|
545
|
+
const offenseKey = `${proposer.toString()}:${offenseType}:${proposal.slotNumber}`;
|
|
546
|
+
if (!this.invalidCheckpointProposalOffenseKeys.addIfAbsent(offenseKey)) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
550
|
+
{
|
|
551
|
+
validator: proposer,
|
|
552
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
553
|
+
offenseType,
|
|
554
|
+
epochOrSlot: BigInt(proposal.slotNumber)
|
|
555
|
+
}
|
|
556
|
+
]);
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
markInvalidProposalSlot(slotNumber) {
|
|
560
|
+
this.slotsWithInvalidProposals.add(slotNumber);
|
|
561
|
+
}
|
|
562
|
+
handleCheckpointAttestation(attestation) {
|
|
563
|
+
const slotNumber = attestation.slotNumber;
|
|
564
|
+
if (!this.slotsWithInvalidProposals.has(slotNumber) || this.slotsWithProposalEquivocation.has(slotNumber)) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const attester = attestation.getSender();
|
|
568
|
+
if (!attester) {
|
|
569
|
+
this.log.warn(`Cannot slash checkpoint attestation with invalid signature`, {
|
|
570
|
+
slotNumber,
|
|
571
|
+
archive: attestation.archive.toString()
|
|
572
|
+
});
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
this.slashAttestedToInvalidCheckpointProposal(slotNumber, attester);
|
|
576
|
+
}
|
|
577
|
+
slashAttestedToInvalidCheckpointProposal(slotNumber, attester) {
|
|
578
|
+
const offenseKey = `${attester.toString()}:${OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL}:${slotNumber}`;
|
|
579
|
+
if (!this.badAttestationOffenseKeys.addIfAbsent(offenseKey)) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
this.log.info(`Detected attestation to invalid checkpoint proposal offense`, {
|
|
583
|
+
attester: attester.toString(),
|
|
584
|
+
slotNumber,
|
|
585
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
586
|
+
offenseType: getOffenseTypeName(OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL)
|
|
587
|
+
});
|
|
588
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
589
|
+
{
|
|
590
|
+
validator: attester,
|
|
591
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
592
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
593
|
+
epochOrSlot: BigInt(slotNumber)
|
|
594
|
+
}
|
|
595
|
+
]);
|
|
596
|
+
}
|
|
430
597
|
/**
|
|
431
598
|
* Handle detection of a duplicate proposal (equivocation).
|
|
432
599
|
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
433
600
|
*/ handleDuplicateProposal(info) {
|
|
434
601
|
const { slot, proposer, type } = info;
|
|
435
|
-
this.
|
|
602
|
+
this.slotsWithProposalEquivocation.add(slot);
|
|
603
|
+
this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
436
604
|
proposer: proposer.toString(),
|
|
437
605
|
slot,
|
|
438
|
-
type
|
|
606
|
+
type,
|
|
607
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
608
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_PROPOSAL)
|
|
439
609
|
});
|
|
440
|
-
// Emit slash event
|
|
441
610
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
442
611
|
{
|
|
443
612
|
validator: proposer,
|
|
@@ -446,15 +615,23 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
446
615
|
epochOrSlot: BigInt(slot)
|
|
447
616
|
}
|
|
448
617
|
]);
|
|
618
|
+
this.emit(WANT_TO_CLEAR_SLASH_EVENT, [
|
|
619
|
+
{
|
|
620
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
621
|
+
epochOrSlot: BigInt(slot)
|
|
622
|
+
}
|
|
623
|
+
]);
|
|
449
624
|
}
|
|
450
625
|
/**
|
|
451
626
|
* Handle detection of a duplicate attestation (equivocation).
|
|
452
627
|
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
453
628
|
*/ handleDuplicateAttestation(info) {
|
|
454
629
|
const { slot, attester } = info;
|
|
455
|
-
this.log.
|
|
630
|
+
this.log.info(`Detected duplicate attestation offense from ${attester.toString()} at slot ${slot}`, {
|
|
456
631
|
attester: attester.toString(),
|
|
457
|
-
slot
|
|
632
|
+
slot,
|
|
633
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
634
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_ATTESTATION)
|
|
458
635
|
});
|
|
459
636
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
460
637
|
{
|
|
@@ -465,7 +642,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
465
642
|
}
|
|
466
643
|
]);
|
|
467
644
|
}
|
|
468
|
-
async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
|
|
645
|
+
async createBlockProposal(blockHeader, checkpointNumber, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
|
|
469
646
|
// Validate that we're not creating a proposal for an older or equal position
|
|
470
647
|
if (this.lastProposedBlock) {
|
|
471
648
|
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
@@ -476,14 +653,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
476
653
|
}
|
|
477
654
|
}
|
|
478
655
|
this.log.info(`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`);
|
|
479
|
-
const newProposal = await this.validationService.createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, {
|
|
656
|
+
const newProposal = await this.validationService.createBlockProposal(blockHeader, checkpointNumber, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, {
|
|
480
657
|
...options,
|
|
481
|
-
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
|
|
658
|
+
broadcastInvalidBlockProposal: options.broadcastInvalidBlockProposal || this.config.broadcastInvalidBlockProposal
|
|
482
659
|
});
|
|
483
660
|
this.lastProposedBlock = newProposal;
|
|
484
661
|
return newProposal;
|
|
485
662
|
}
|
|
486
|
-
async createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier,
|
|
663
|
+
async createCheckpointProposal(checkpointHeader, archive, checkpointNumber, feeAssetPriceModifier, lastBlockProposal, proposerAddress, options = {}) {
|
|
487
664
|
// Validate that we're not creating a proposal for an older or equal slot
|
|
488
665
|
if (this.lastProposedCheckpoint) {
|
|
489
666
|
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
@@ -493,23 +670,30 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
493
670
|
}
|
|
494
671
|
}
|
|
495
672
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
496
|
-
const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier,
|
|
673
|
+
const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, checkpointNumber, feeAssetPriceModifier, lastBlockProposal, proposerAddress, options);
|
|
497
674
|
this.lastProposedCheckpoint = newProposal;
|
|
675
|
+
// Self-record this slot's outcome on the re-execution tracker. Proposers don't run their
|
|
676
|
+
// own proposals through `handleCheckpointProposal`, so without this call the proposer's
|
|
677
|
+
// sentinel would see no outcome for slots it proposed and would mis-attribute itself as
|
|
678
|
+
// inactive. We pass the locally-computed `archive` (not `newProposal.archive`, which may
|
|
679
|
+
// be intentionally corrupted under test-only flags); from the proposer's local-view
|
|
680
|
+
// perspective the work it just completed is valid by definition.
|
|
681
|
+
this.proposalHandler.recordOwnCheckpointProposalAsValid(checkpointHeader.slotNumber, archive, checkpointNumber);
|
|
498
682
|
return newProposal;
|
|
499
683
|
}
|
|
500
684
|
async broadcastBlockProposal(proposal) {
|
|
501
685
|
await this.p2pClient.broadcastProposal(proposal);
|
|
502
686
|
}
|
|
503
|
-
async signAttestationsAndSigners(attestationsAndSigners, proposer, slot,
|
|
504
|
-
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot,
|
|
687
|
+
async signAttestationsAndSigners(attestationsAndSigners, proposer, slot, checkpointNumber) {
|
|
688
|
+
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, checkpointNumber);
|
|
505
689
|
}
|
|
506
|
-
async collectOwnAttestations(proposal) {
|
|
690
|
+
async collectOwnAttestations(proposal, checkpointNumber) {
|
|
507
691
|
const slot = proposal.slotNumber;
|
|
508
692
|
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
509
693
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, {
|
|
510
694
|
inCommittee
|
|
511
695
|
});
|
|
512
|
-
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
696
|
+
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
|
|
513
697
|
if (!attestations) {
|
|
514
698
|
return [];
|
|
515
699
|
}
|
|
@@ -521,7 +705,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
521
705
|
});
|
|
522
706
|
return attestations;
|
|
523
707
|
}
|
|
524
|
-
async collectAttestations(proposal, required, deadline) {
|
|
708
|
+
async collectAttestations(proposal, required, deadline, checkpointNumber) {
|
|
525
709
|
// Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
|
|
526
710
|
const slot = proposal.slotNumber;
|
|
527
711
|
this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
|
|
@@ -529,28 +713,20 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
529
713
|
this.log.error(`Deadline ${deadline.toISOString()} for collecting ${required} attestations for slot ${slot} is in the past`);
|
|
530
714
|
throw new AttestationTimeoutError(0, required, slot);
|
|
531
715
|
}
|
|
532
|
-
await this.collectOwnAttestations(proposal);
|
|
533
|
-
const
|
|
716
|
+
await this.collectOwnAttestations(proposal, checkpointNumber);
|
|
717
|
+
const proposalPayloadHash = proposal.getPayloadHash();
|
|
534
718
|
const myAddresses = this.getValidatorAddresses();
|
|
535
719
|
let attestations = [];
|
|
536
720
|
while(true){
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
this.log.warn(`Received attestation for slot ${slot} with mismatched archive from ${attestation.getSender()?.toString()}`, {
|
|
542
|
-
attestationArchive: attestation.archive.toString(),
|
|
543
|
-
proposalArchive: proposal.archive.toString()
|
|
544
|
-
});
|
|
545
|
-
return false;
|
|
546
|
-
}
|
|
547
|
-
return true;
|
|
548
|
-
});
|
|
721
|
+
// The pool already filters by proposal payload hash; if any attestation slips through with a
|
|
722
|
+
// mismatched payload hash, drop it defensively. Equivocations are emitted as separate slash
|
|
723
|
+
// events from libp2p_service.
|
|
724
|
+
const collectedAttestations = await this.p2pClient.getCheckpointAttestationsForSlot(slot, proposalPayloadHash);
|
|
549
725
|
// Log new attestations we collected
|
|
550
726
|
const oldSenders = attestations.map((attestation)=>attestation.getSender());
|
|
551
727
|
for (const collected of collectedAttestations){
|
|
552
728
|
const collectedSender = collected.getSender();
|
|
553
|
-
// Skip attestations with invalid signatures
|
|
729
|
+
// Skip attestations with invalid signatures. Should not happen as we don't add invalid attestations to our pool.
|
|
554
730
|
if (!collectedSender) {
|
|
555
731
|
this.log.warn(`Skipping attestation with invalid signature for slot ${slot}`);
|
|
556
732
|
continue;
|