@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/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
7
|
import type { Signature } from '@aztec/foundation/eth-signature';
|
|
13
|
-
import {
|
|
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
14
|
import type { DuplicateAttestationInfo, DuplicateProposalInfo, 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,
|
|
@@ -36,12 +39,17 @@ import {
|
|
|
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';
|
|
41
45
|
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
|
|
42
46
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
43
47
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
44
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
createHASigner,
|
|
50
|
+
createLocalSignerWithProtection,
|
|
51
|
+
createSignerFromSharedDb,
|
|
52
|
+
} from '@aztec/validator-ha-signer/factory';
|
|
45
53
|
import { DutyType, type SigningContext, type SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
|
|
46
54
|
import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
|
|
47
55
|
|
|
@@ -54,18 +62,50 @@ import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
|
54
62
|
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
55
63
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
56
64
|
import { ValidatorMetrics } from './metrics.js';
|
|
57
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
type BlockProposalValidationFailureReason,
|
|
67
|
+
type CheckpointProposalValidationFailureReason,
|
|
68
|
+
type CheckpointProposalValidationFailureResult,
|
|
69
|
+
ProposalHandler,
|
|
70
|
+
} from './proposal_handler.js';
|
|
58
71
|
|
|
59
72
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
60
73
|
// Just cap the set to avoid unbounded growth.
|
|
61
74
|
const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
75
|
+
const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
|
|
76
|
+
const MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS = 1000;
|
|
77
|
+
const MAX_TRACKED_BAD_ATTESTATIONS = 10_000;
|
|
62
78
|
|
|
63
79
|
// What errors from the block proposal handler result in slashing
|
|
64
80
|
const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidationFailureReason[] = [
|
|
65
81
|
'state_mismatch',
|
|
66
82
|
'failed_txs',
|
|
83
|
+
'global_variables_mismatch',
|
|
84
|
+
'invalid_proposal',
|
|
85
|
+
'parent_block_wrong_slot',
|
|
86
|
+
'in_hash_mismatch',
|
|
67
87
|
];
|
|
68
88
|
|
|
89
|
+
const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT: Record<CheckpointProposalValidationFailureReason, boolean> = {
|
|
90
|
+
// enabled
|
|
91
|
+
['invalid_fee_asset_price_modifier']: true,
|
|
92
|
+
['checkpoint_header_mismatch']: true,
|
|
93
|
+
// These late mismatches should normally be caught by earlier checks, but if reached after validating the local
|
|
94
|
+
// checkpoint inputs, the proposer-signed payload disagrees with deterministic recomputation.
|
|
95
|
+
['archive_mismatch']: true,
|
|
96
|
+
['out_hash_mismatch']: true,
|
|
97
|
+
['no_blocks_for_slot']: true,
|
|
98
|
+
['too_many_blocks_in_checkpoint']: true,
|
|
99
|
+
['checkpoint_validation_failed']: true,
|
|
100
|
+
['last_block_archive_mismatch']: true,
|
|
101
|
+
|
|
102
|
+
// disabled
|
|
103
|
+
['invalid_signature']: false,
|
|
104
|
+
['last_block_not_found']: false,
|
|
105
|
+
['block_fetch_error']: false,
|
|
106
|
+
['checkpoint_already_published']: false,
|
|
107
|
+
};
|
|
108
|
+
|
|
69
109
|
/**
|
|
70
110
|
* Validator Client
|
|
71
111
|
*/
|
|
@@ -88,7 +128,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
88
128
|
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
|
|
89
129
|
private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
|
|
90
130
|
|
|
91
|
-
private proposersOfInvalidBlocks
|
|
131
|
+
private proposersOfInvalidBlocks = FifoSet.withLimit<string>(MAX_PROPOSERS_OF_INVALID_BLOCKS);
|
|
132
|
+
private slotsWithInvalidProposals = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
|
|
133
|
+
private invalidCheckpointProposalOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS);
|
|
134
|
+
private badAttestationOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_BAD_ATTESTATIONS);
|
|
135
|
+
private slotsWithProposalEquivocation = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
|
|
92
136
|
|
|
93
137
|
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
94
138
|
private lastAttestedProposal?: CheckpointProposalCore;
|
|
@@ -98,9 +142,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
98
142
|
private epochCache: EpochCache,
|
|
99
143
|
private p2pClient: P2P,
|
|
100
144
|
private proposalHandler: ProposalHandler,
|
|
145
|
+
private blockSource: L2BlockSource,
|
|
146
|
+
private checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
147
|
+
private worldState: WorldStateSynchronizer,
|
|
148
|
+
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
101
149
|
private config: ValidatorClientFullConfig,
|
|
102
150
|
private blobClient: BlobClientInterface,
|
|
103
|
-
private
|
|
151
|
+
private slashingProtectionSigner: ValidatorHASigner,
|
|
104
152
|
private dateProvider: DateProvider = new DateProvider(),
|
|
105
153
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
106
154
|
log = createLogger('validator'),
|
|
@@ -113,11 +161,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
113
161
|
this.tracer = telemetry.getTracer('Validator');
|
|
114
162
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
115
163
|
|
|
116
|
-
this.validationService = new ValidationService(
|
|
164
|
+
this.validationService = new ValidationService(
|
|
165
|
+
keyStore,
|
|
166
|
+
this.getSignatureContext(),
|
|
167
|
+
this.log.createChild('validation-service'),
|
|
168
|
+
);
|
|
169
|
+
this.proposalHandler.setCheckpointProposalValidationFailureCallback((proposal, result, proposalInfo) =>
|
|
170
|
+
this.handleInvalidCheckpointProposal(proposal, result, proposalInfo),
|
|
171
|
+
);
|
|
117
172
|
|
|
118
173
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
119
174
|
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
|
|
120
|
-
|
|
121
175
|
const myAddresses = this.getValidatorAddresses();
|
|
122
176
|
this.log.verbose(`Initialized validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
123
177
|
}
|
|
@@ -186,6 +240,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
186
240
|
txProvider: ITxProvider,
|
|
187
241
|
keyStoreManager: KeystoreManager,
|
|
188
242
|
blobClient: BlobClientInterface,
|
|
243
|
+
reexecutionTracker: CheckpointReexecutionTracker,
|
|
189
244
|
dateProvider: DateProvider = new DateProvider(),
|
|
190
245
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
191
246
|
slashingProtectionDb?: SlashingProtectionDatabase,
|
|
@@ -194,6 +249,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
194
249
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
195
250
|
txsPermitted: !config.disableTransactions,
|
|
196
251
|
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
252
|
+
maxBlocksPerCheckpoint: config.maxBlocksPerCheckpoint,
|
|
253
|
+
skipSlotValidation: config.skipProposalSlotValidation,
|
|
254
|
+
signatureContext: {
|
|
255
|
+
chainId: config.l1ChainId,
|
|
256
|
+
rollupAddress: config.rollupAddress,
|
|
257
|
+
},
|
|
197
258
|
});
|
|
198
259
|
const proposalHandler = new ProposalHandler(
|
|
199
260
|
checkpointsBuilder,
|
|
@@ -205,38 +266,54 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
205
266
|
epochCache,
|
|
206
267
|
config,
|
|
207
268
|
blobClient,
|
|
269
|
+
reexecutionTracker,
|
|
208
270
|
metrics,
|
|
209
271
|
dateProvider,
|
|
210
272
|
telemetry,
|
|
273
|
+
undefined,
|
|
211
274
|
);
|
|
212
275
|
|
|
213
276
|
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
214
|
-
let
|
|
215
|
-
let haSigner: ValidatorHASigner | undefined;
|
|
277
|
+
let slashingProtectionSigner: ValidatorHASigner;
|
|
216
278
|
if (slashingProtectionDb) {
|
|
217
279
|
// Shared database mode: use a pre-existing database (e.g. for testing HA setups).
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
280
|
+
({ signer: slashingProtectionSigner } = createSignerFromSharedDb(slashingProtectionDb, config, {
|
|
281
|
+
telemetryClient: telemetry,
|
|
282
|
+
dateProvider,
|
|
283
|
+
}));
|
|
221
284
|
} else if (config.haSigningEnabled) {
|
|
285
|
+
// Multi-node HA mode: use PostgreSQL-backed distributed locking.
|
|
222
286
|
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
223
287
|
const haConfig = {
|
|
224
288
|
...config,
|
|
225
289
|
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
|
|
226
290
|
};
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
291
|
+
({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
|
|
292
|
+
telemetryClient: telemetry,
|
|
293
|
+
dateProvider,
|
|
294
|
+
}));
|
|
295
|
+
} else {
|
|
296
|
+
// Single-node mode: use LMDB-backed local signing protection.
|
|
297
|
+
// This prevents double-signing if the node crashes and restarts mid-proposal.
|
|
298
|
+
({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
|
|
299
|
+
telemetryClient: telemetry,
|
|
300
|
+
dateProvider,
|
|
301
|
+
}));
|
|
230
302
|
}
|
|
303
|
+
const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
|
|
231
304
|
|
|
232
305
|
const validator = new ValidatorClient(
|
|
233
306
|
validatorKeyStore,
|
|
234
307
|
epochCache,
|
|
235
308
|
p2pClient,
|
|
236
309
|
proposalHandler,
|
|
310
|
+
blockSource,
|
|
311
|
+
checkpointsBuilder,
|
|
312
|
+
worldState,
|
|
313
|
+
l1ToL2MessageSource,
|
|
237
314
|
config,
|
|
238
315
|
blobClient,
|
|
239
|
-
|
|
316
|
+
slashingProtectionSigner,
|
|
240
317
|
dateProvider,
|
|
241
318
|
telemetry,
|
|
242
319
|
);
|
|
@@ -258,6 +335,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
258
335
|
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
259
336
|
}
|
|
260
337
|
|
|
338
|
+
private getSignatureContext(): CoordinationSignatureContext {
|
|
339
|
+
return {
|
|
340
|
+
chainId: this.config.l1ChainId,
|
|
341
|
+
rollupAddress: this.config.rollupAddress,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
261
345
|
public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
|
|
262
346
|
return this.keyStore.getCoinbaseAddress(attestor);
|
|
263
347
|
}
|
|
@@ -270,30 +354,27 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
270
354
|
return this.config;
|
|
271
355
|
}
|
|
272
356
|
|
|
357
|
+
public hasProposalEquivocation(slotNumber: SlotNumber): boolean {
|
|
358
|
+
return this.slotsWithProposalEquivocation.has(slotNumber);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
public hasInvalidProposals(slotNumber: SlotNumber): boolean {
|
|
362
|
+
return this.slotsWithInvalidProposals.has(slotNumber);
|
|
363
|
+
}
|
|
364
|
+
|
|
273
365
|
public updateConfig(config: Partial<ValidatorClientFullConfig>) {
|
|
274
366
|
this.config = { ...this.config, ...config };
|
|
367
|
+
this.proposalHandler.updateConfig(config);
|
|
275
368
|
}
|
|
276
369
|
|
|
277
370
|
public reloadKeystore(newManager: KeystoreManager): void {
|
|
278
|
-
if (this.config.haSigningEnabled && !this.haSigner) {
|
|
279
|
-
this.log.warn(
|
|
280
|
-
'HA signing is enabled in config but was not initialized at startup. ' +
|
|
281
|
-
'Restart the node to enable HA signing.',
|
|
282
|
-
);
|
|
283
|
-
} else if (!this.config.haSigningEnabled && this.haSigner) {
|
|
284
|
-
this.log.warn(
|
|
285
|
-
'HA signing was disabled via config update but the HA signer is still active. ' +
|
|
286
|
-
'Restart the node to fully disable HA signing.',
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
371
|
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
this.
|
|
295
|
-
|
|
296
|
-
|
|
372
|
+
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
373
|
+
this.validationService = new ValidationService(
|
|
374
|
+
this.keyStore,
|
|
375
|
+
this.getSignatureContext(),
|
|
376
|
+
this.log.createChild('validation-service'),
|
|
377
|
+
);
|
|
297
378
|
}
|
|
298
379
|
|
|
299
380
|
public async start() {
|
|
@@ -340,7 +421,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
340
421
|
checkpoint: CheckpointProposalCore,
|
|
341
422
|
proposalSender: PeerId,
|
|
342
423
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
343
|
-
this.p2pClient.
|
|
424
|
+
this.p2pClient.registerValidatorCheckpointProposalHandler(checkpointHandler);
|
|
344
425
|
|
|
345
426
|
// Duplicate proposal handler - triggers slashing for equivocation
|
|
346
427
|
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
@@ -352,6 +433,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
352
433
|
this.handleDuplicateAttestation(info);
|
|
353
434
|
});
|
|
354
435
|
|
|
436
|
+
this.p2pClient.registerCheckpointAttestationCallback((attestation: CheckpointAttestation) => {
|
|
437
|
+
this.handleCheckpointAttestation(attestation);
|
|
438
|
+
});
|
|
439
|
+
|
|
355
440
|
const myAddresses = this.getValidatorAddresses();
|
|
356
441
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
357
442
|
|
|
@@ -398,22 +483,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
398
483
|
fishermanMode: this.config.fishermanMode || false,
|
|
399
484
|
});
|
|
400
485
|
|
|
401
|
-
// Reexecute
|
|
402
|
-
|
|
403
|
-
const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } =
|
|
404
|
-
this.config;
|
|
405
|
-
const shouldReexecute =
|
|
406
|
-
fishermanMode ||
|
|
407
|
-
(slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute) ||
|
|
408
|
-
(partOfCommittee && validatorReexecute) ||
|
|
409
|
-
alwaysReexecuteBlockProposals ||
|
|
410
|
-
this.blobClient.canUpload();
|
|
411
|
-
|
|
412
|
-
const validationResult = await this.proposalHandler.handleBlockProposal(
|
|
413
|
-
proposal,
|
|
414
|
-
proposalSender,
|
|
415
|
-
!!shouldReexecute && !escapeHatchOpen,
|
|
416
|
-
);
|
|
486
|
+
// Reexecute outside the escape hatch so slashing observers can detect invalid proposals even when penalties are 0.
|
|
487
|
+
const validationResult = await this.proposalHandler.handleBlockProposal(proposal, proposalSender, !escapeHatchOpen);
|
|
417
488
|
|
|
418
489
|
if (!validationResult.isValid) {
|
|
419
490
|
const reason = validationResult.reason || 'unknown';
|
|
@@ -436,15 +507,18 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
436
507
|
this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
|
|
437
508
|
}
|
|
438
509
|
|
|
439
|
-
// Slash invalid block proposals (can happen even when not in committee)
|
|
440
510
|
if (
|
|
441
511
|
!escapeHatchOpen &&
|
|
442
512
|
validationResult.reason &&
|
|
443
|
-
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)
|
|
444
|
-
slashBroadcastedInvalidBlockPenalty > 0n
|
|
513
|
+
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)
|
|
445
514
|
) {
|
|
446
|
-
this.log.
|
|
515
|
+
this.log.info(`Detected invalid block proposal offense`, {
|
|
516
|
+
...proposalInfo,
|
|
517
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
518
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL),
|
|
519
|
+
});
|
|
447
520
|
this.slashInvalidBlock(proposal);
|
|
521
|
+
this.markInvalidProposalSlot(proposal.slotNumber);
|
|
448
522
|
}
|
|
449
523
|
return false;
|
|
450
524
|
}
|
|
@@ -474,47 +548,56 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
474
548
|
proposal: CheckpointProposalCore,
|
|
475
549
|
_proposalSender: PeerId,
|
|
476
550
|
): Promise<CheckpointAttestation[] | undefined> {
|
|
477
|
-
const
|
|
551
|
+
const proposalSlotNumber = proposal.slotNumber;
|
|
478
552
|
const proposer = proposal.getSender();
|
|
479
553
|
|
|
480
554
|
// If escape hatch is open for this slot's epoch, do not attest.
|
|
481
|
-
if (await this.epochCache.isEscapeHatchOpenAtSlot(
|
|
482
|
-
this.log.warn(`Escape hatch open for slot ${
|
|
555
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(proposalSlotNumber)) {
|
|
556
|
+
this.log.warn(`Escape hatch open for slot ${proposalSlotNumber}, skipping checkpoint attestation handling`);
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Early-out for equivocation: refuses if we've already attested to a higher slot.
|
|
561
|
+
if (!this.shouldAttestToSlot(proposalSlotNumber)) {
|
|
483
562
|
return undefined;
|
|
484
563
|
}
|
|
485
564
|
|
|
486
565
|
// Ignore proposals from ourselves (may happen in HA setups)
|
|
487
566
|
if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
488
|
-
this.log.debug(`Ignoring block proposal from self for slot ${
|
|
567
|
+
this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
|
|
489
568
|
proposer: proposer.toString(),
|
|
490
|
-
|
|
569
|
+
proposalSlotNumber,
|
|
491
570
|
});
|
|
492
571
|
return undefined;
|
|
493
572
|
}
|
|
494
573
|
|
|
495
574
|
// Check that I have any address in the committee where this checkpoint will land before attesting
|
|
496
|
-
const inCommittee = await this.epochCache.filterInCommittee(
|
|
575
|
+
const inCommittee = await this.epochCache.filterInCommittee(proposalSlotNumber, this.getValidatorAddresses());
|
|
497
576
|
const partOfCommittee = inCommittee.length > 0;
|
|
498
577
|
|
|
499
578
|
const proposalInfo = {
|
|
500
|
-
|
|
579
|
+
proposalSlotNumber,
|
|
501
580
|
archive: proposal.archive.toString(),
|
|
502
581
|
proposer: proposer?.toString(),
|
|
503
582
|
};
|
|
504
|
-
this.log.info(`Received checkpoint proposal for slot ${
|
|
583
|
+
this.log.info(`Received checkpoint proposal for slot ${proposalSlotNumber}`, {
|
|
505
584
|
...proposalInfo,
|
|
506
585
|
fishermanMode: this.config.fishermanMode || false,
|
|
507
586
|
});
|
|
508
587
|
|
|
509
|
-
// Validate the checkpoint proposal
|
|
588
|
+
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
|
|
589
|
+
// Uses the cached result from the all-nodes callback if available (avoids double validation).
|
|
590
|
+
let checkpointNumber: CheckpointNumber;
|
|
510
591
|
if (this.config.skipCheckpointProposalValidation) {
|
|
511
|
-
this.log.warn(`Skipping checkpoint proposal validation for slot ${
|
|
592
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
|
|
593
|
+
checkpointNumber = CheckpointNumber(0);
|
|
512
594
|
} else {
|
|
513
595
|
const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
|
|
514
596
|
if (!validationResult.isValid) {
|
|
515
597
|
this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
516
598
|
return undefined;
|
|
517
599
|
}
|
|
600
|
+
checkpointNumber = validationResult.checkpointNumber;
|
|
518
601
|
}
|
|
519
602
|
|
|
520
603
|
// Check that I have any address in current committee before attesting
|
|
@@ -525,16 +608,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
525
608
|
}
|
|
526
609
|
|
|
527
610
|
// Provided all of the above checks pass, we can attest to the proposal
|
|
528
|
-
this.log.info(
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
611
|
+
this.log.info(
|
|
612
|
+
`${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${proposalSlotNumber}`,
|
|
613
|
+
{
|
|
614
|
+
...proposalInfo,
|
|
615
|
+
inCommittee: partOfCommittee,
|
|
616
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
617
|
+
},
|
|
618
|
+
);
|
|
533
619
|
|
|
534
620
|
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
535
621
|
|
|
536
622
|
// Track epoch participation per attester: count each (attester, epoch) pair at most once
|
|
537
|
-
const proposalEpoch = getEpochAtSlot(
|
|
623
|
+
const proposalEpoch = getEpochAtSlot(proposalSlotNumber, this.epochCache.getL1Constants());
|
|
538
624
|
for (const attester of inCommittee) {
|
|
539
625
|
const key = attester.toString();
|
|
540
626
|
const lastEpoch = this.lastAttestedEpochByAttester.get(key);
|
|
@@ -562,14 +648,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
562
648
|
|
|
563
649
|
if (this.config.fishermanMode) {
|
|
564
650
|
// bail out early and don't save attestations to the pool in fisherman mode
|
|
565
|
-
this.log.info(`Creating checkpoint attestations for slot ${
|
|
651
|
+
this.log.info(`Creating checkpoint attestations for slot ${proposalSlotNumber}`, {
|
|
566
652
|
...proposalInfo,
|
|
567
653
|
attestors: attestors.map(a => a.toString()),
|
|
568
654
|
});
|
|
569
655
|
return undefined;
|
|
570
656
|
}
|
|
571
657
|
|
|
572
|
-
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
658
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
|
|
573
659
|
}
|
|
574
660
|
|
|
575
661
|
/**
|
|
@@ -596,13 +682,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
596
682
|
private async createCheckpointAttestationsFromProposal(
|
|
597
683
|
proposal: CheckpointProposalCore,
|
|
598
684
|
attestors: EthAddress[] = [],
|
|
685
|
+
checkpointNumber: CheckpointNumber,
|
|
599
686
|
): Promise<CheckpointAttestation[] | undefined> {
|
|
600
687
|
// Equivocation check: must happen right before signing to minimize the race window
|
|
601
688
|
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
602
689
|
return undefined;
|
|
603
690
|
}
|
|
604
691
|
|
|
605
|
-
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
692
|
+
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
|
|
606
693
|
|
|
607
694
|
// Track the proposal we attested to (to prevent equivocation)
|
|
608
695
|
this.lastAttestedProposal = proposal;
|
|
@@ -611,6 +698,35 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
611
698
|
return attestations;
|
|
612
699
|
}
|
|
613
700
|
|
|
701
|
+
/**
|
|
702
|
+
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
703
|
+
*/
|
|
704
|
+
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
705
|
+
try {
|
|
706
|
+
const lastBlockHeader = (await this.blockSource.getBlockData({ archive: proposal.archive }))?.header;
|
|
707
|
+
if (!lastBlockHeader) {
|
|
708
|
+
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
|
|
713
|
+
if (blocks.length === 0) {
|
|
714
|
+
this.log.warn(`No blocks found for blob upload`, proposalInfo);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const blobFields = blocks.flatMap(b => b.toBlobFields());
|
|
719
|
+
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
720
|
+
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
721
|
+
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
722
|
+
...proposalInfo,
|
|
723
|
+
numBlobs: blobs.length,
|
|
724
|
+
});
|
|
725
|
+
} catch (err) {
|
|
726
|
+
this.log.warn(`Failed to upload blobs for checkpoint: ${err}`, proposalInfo);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
614
730
|
private slashInvalidBlock(proposal: BlockProposal) {
|
|
615
731
|
const proposer = proposal.getSender();
|
|
616
732
|
|
|
@@ -620,12 +736,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
620
736
|
return;
|
|
621
737
|
}
|
|
622
738
|
|
|
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
739
|
this.proposersOfInvalidBlocks.add(proposer.toString());
|
|
630
740
|
|
|
631
741
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
@@ -638,20 +748,115 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
638
748
|
]);
|
|
639
749
|
}
|
|
640
750
|
|
|
751
|
+
private handleInvalidCheckpointProposal(
|
|
752
|
+
proposal: CheckpointProposalCore,
|
|
753
|
+
result: CheckpointProposalValidationFailureResult,
|
|
754
|
+
proposalInfo: LogData,
|
|
755
|
+
): void {
|
|
756
|
+
if (!SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT[result.reason]) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
this.markInvalidProposalSlot(proposal.slotNumber);
|
|
761
|
+
|
|
762
|
+
if (this.slashInvalidCheckpointProposal(proposal)) {
|
|
763
|
+
this.log.info(`Detected invalid checkpoint proposal offense`, {
|
|
764
|
+
...proposalInfo,
|
|
765
|
+
reason: result.reason,
|
|
766
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
767
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL),
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
private slashInvalidCheckpointProposal(proposal: CheckpointProposalCore): boolean {
|
|
773
|
+
const proposer = proposal.getSender();
|
|
774
|
+
if (!proposer) {
|
|
775
|
+
this.log.warn(`Cannot slash checkpoint proposal with invalid signature`, {
|
|
776
|
+
slotNumber: proposal.slotNumber,
|
|
777
|
+
archive: proposal.archive.toString(),
|
|
778
|
+
});
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const offenseType = OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL;
|
|
783
|
+
const offenseKey = `${proposer.toString()}:${offenseType}:${proposal.slotNumber}`;
|
|
784
|
+
if (!this.invalidCheckpointProposalOffenseKeys.addIfAbsent(offenseKey)) {
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
789
|
+
{
|
|
790
|
+
validator: proposer,
|
|
791
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
792
|
+
offenseType,
|
|
793
|
+
epochOrSlot: BigInt(proposal.slotNumber),
|
|
794
|
+
},
|
|
795
|
+
]);
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private markInvalidProposalSlot(slotNumber: SlotNumber): void {
|
|
800
|
+
this.slotsWithInvalidProposals.add(slotNumber);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private handleCheckpointAttestation(attestation: CheckpointAttestation): void {
|
|
804
|
+
const slotNumber = attestation.slotNumber;
|
|
805
|
+
if (!this.slotsWithInvalidProposals.has(slotNumber) || this.slotsWithProposalEquivocation.has(slotNumber)) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const attester = attestation.getSender();
|
|
810
|
+
if (!attester) {
|
|
811
|
+
this.log.warn(`Cannot slash checkpoint attestation with invalid signature`, {
|
|
812
|
+
slotNumber,
|
|
813
|
+
archive: attestation.archive.toString(),
|
|
814
|
+
});
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
this.slashAttestedToInvalidCheckpointProposal(slotNumber, attester);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
private slashAttestedToInvalidCheckpointProposal(slotNumber: SlotNumber, attester: EthAddress): void {
|
|
822
|
+
const offenseKey = `${attester.toString()}:${OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL}:${slotNumber}`;
|
|
823
|
+
if (!this.badAttestationOffenseKeys.addIfAbsent(offenseKey)) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
this.log.info(`Detected attestation to invalid checkpoint proposal offense`, {
|
|
828
|
+
attester: attester.toString(),
|
|
829
|
+
slotNumber,
|
|
830
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
831
|
+
offenseType: getOffenseTypeName(OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL),
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
835
|
+
{
|
|
836
|
+
validator: attester,
|
|
837
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
838
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
839
|
+
epochOrSlot: BigInt(slotNumber),
|
|
840
|
+
},
|
|
841
|
+
]);
|
|
842
|
+
}
|
|
843
|
+
|
|
641
844
|
/**
|
|
642
845
|
* Handle detection of a duplicate proposal (equivocation).
|
|
643
846
|
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
644
847
|
*/
|
|
645
848
|
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
646
849
|
const { slot, proposer, type } = info;
|
|
850
|
+
this.slotsWithProposalEquivocation.add(slot);
|
|
647
851
|
|
|
648
|
-
this.log.
|
|
852
|
+
this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
649
853
|
proposer: proposer.toString(),
|
|
650
854
|
slot,
|
|
651
855
|
type,
|
|
856
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
857
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_PROPOSAL),
|
|
652
858
|
});
|
|
653
859
|
|
|
654
|
-
// Emit slash event
|
|
655
860
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
656
861
|
{
|
|
657
862
|
validator: proposer,
|
|
@@ -660,6 +865,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
660
865
|
epochOrSlot: BigInt(slot),
|
|
661
866
|
},
|
|
662
867
|
]);
|
|
868
|
+
|
|
869
|
+
this.emit(WANT_TO_CLEAR_SLASH_EVENT, [
|
|
870
|
+
{
|
|
871
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
872
|
+
epochOrSlot: BigInt(slot),
|
|
873
|
+
},
|
|
874
|
+
]);
|
|
663
875
|
}
|
|
664
876
|
|
|
665
877
|
/**
|
|
@@ -669,9 +881,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
669
881
|
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
670
882
|
const { slot, attester } = info;
|
|
671
883
|
|
|
672
|
-
this.log.
|
|
884
|
+
this.log.info(`Detected duplicate attestation offense from ${attester.toString()} at slot ${slot}`, {
|
|
673
885
|
attester: attester.toString(),
|
|
674
886
|
slot,
|
|
887
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
888
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_ATTESTATION),
|
|
675
889
|
});
|
|
676
890
|
|
|
677
891
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
@@ -686,6 +900,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
686
900
|
|
|
687
901
|
async createBlockProposal(
|
|
688
902
|
blockHeader: BlockHeader,
|
|
903
|
+
checkpointNumber: CheckpointNumber,
|
|
689
904
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
690
905
|
inHash: Fr,
|
|
691
906
|
archive: Fr,
|
|
@@ -712,6 +927,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
712
927
|
);
|
|
713
928
|
const newProposal = await this.validationService.createBlockProposal(
|
|
714
929
|
blockHeader,
|
|
930
|
+
checkpointNumber,
|
|
715
931
|
indexWithinCheckpoint,
|
|
716
932
|
inHash,
|
|
717
933
|
archive,
|
|
@@ -719,7 +935,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
719
935
|
proposerAddress,
|
|
720
936
|
{
|
|
721
937
|
...options,
|
|
722
|
-
broadcastInvalidBlockProposal:
|
|
938
|
+
broadcastInvalidBlockProposal:
|
|
939
|
+
options.broadcastInvalidBlockProposal || this.config.broadcastInvalidBlockProposal,
|
|
723
940
|
},
|
|
724
941
|
);
|
|
725
942
|
this.lastProposedBlock = newProposal;
|
|
@@ -729,8 +946,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
729
946
|
async createCheckpointProposal(
|
|
730
947
|
checkpointHeader: CheckpointHeader,
|
|
731
948
|
archive: Fr,
|
|
949
|
+
checkpointNumber: CheckpointNumber,
|
|
732
950
|
feeAssetPriceModifier: bigint,
|
|
733
|
-
|
|
951
|
+
lastBlockProposal: BlockProposal | undefined,
|
|
734
952
|
proposerAddress: EthAddress | undefined,
|
|
735
953
|
options: CheckpointProposalOptions = {},
|
|
736
954
|
): Promise<CheckpointProposal> {
|
|
@@ -751,12 +969,20 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
751
969
|
const newProposal = await this.validationService.createCheckpointProposal(
|
|
752
970
|
checkpointHeader,
|
|
753
971
|
archive,
|
|
972
|
+
checkpointNumber,
|
|
754
973
|
feeAssetPriceModifier,
|
|
755
|
-
|
|
974
|
+
lastBlockProposal,
|
|
756
975
|
proposerAddress,
|
|
757
976
|
options,
|
|
758
977
|
);
|
|
759
978
|
this.lastProposedCheckpoint = newProposal;
|
|
979
|
+
// Self-record this slot's outcome on the re-execution tracker. Proposers don't run their
|
|
980
|
+
// own proposals through `handleCheckpointProposal`, so without this call the proposer's
|
|
981
|
+
// sentinel would see no outcome for slots it proposed and would mis-attribute itself as
|
|
982
|
+
// inactive. We pass the locally-computed `archive` (not `newProposal.archive`, which may
|
|
983
|
+
// be intentionally corrupted under test-only flags); from the proposer's local-view
|
|
984
|
+
// perspective the work it just completed is valid by definition.
|
|
985
|
+
this.proposalHandler.recordOwnCheckpointProposalAsValid(checkpointHeader.slotNumber, archive, checkpointNumber);
|
|
760
986
|
return newProposal;
|
|
761
987
|
}
|
|
762
988
|
|
|
@@ -768,16 +994,24 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
768
994
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
769
995
|
proposer: EthAddress,
|
|
770
996
|
slot: SlotNumber,
|
|
771
|
-
|
|
997
|
+
checkpointNumber: CheckpointNumber,
|
|
772
998
|
): Promise<Signature> {
|
|
773
|
-
return await this.validationService.signAttestationsAndSigners(
|
|
999
|
+
return await this.validationService.signAttestationsAndSigners(
|
|
1000
|
+
attestationsAndSigners,
|
|
1001
|
+
proposer,
|
|
1002
|
+
slot,
|
|
1003
|
+
checkpointNumber,
|
|
1004
|
+
);
|
|
774
1005
|
}
|
|
775
1006
|
|
|
776
|
-
async collectOwnAttestations(
|
|
1007
|
+
async collectOwnAttestations(
|
|
1008
|
+
proposal: CheckpointProposal,
|
|
1009
|
+
checkpointNumber: CheckpointNumber,
|
|
1010
|
+
): Promise<CheckpointAttestation[]> {
|
|
777
1011
|
const slot = proposal.slotNumber;
|
|
778
1012
|
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
779
1013
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
780
|
-
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
1014
|
+
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
|
|
781
1015
|
|
|
782
1016
|
if (!attestations) {
|
|
783
1017
|
return [];
|
|
@@ -796,6 +1030,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
796
1030
|
proposal: CheckpointProposal,
|
|
797
1031
|
required: number,
|
|
798
1032
|
deadline: Date,
|
|
1033
|
+
checkpointNumber: CheckpointNumber,
|
|
799
1034
|
): Promise<CheckpointAttestation[]> {
|
|
800
1035
|
// Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
|
|
801
1036
|
const slot = proposal.slotNumber;
|
|
@@ -808,33 +1043,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
808
1043
|
throw new AttestationTimeoutError(0, required, slot);
|
|
809
1044
|
}
|
|
810
1045
|
|
|
811
|
-
await this.collectOwnAttestations(proposal);
|
|
1046
|
+
await this.collectOwnAttestations(proposal, checkpointNumber);
|
|
812
1047
|
|
|
813
|
-
const
|
|
1048
|
+
const proposalPayloadHash = proposal.getPayloadHash();
|
|
814
1049
|
const myAddresses = this.getValidatorAddresses();
|
|
815
1050
|
|
|
816
1051
|
let attestations: CheckpointAttestation[] = [];
|
|
817
1052
|
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
|
-
);
|
|
1053
|
+
// The pool already filters by proposal payload hash; if any attestation slips through with a
|
|
1054
|
+
// mismatched payload hash, drop it defensively. Equivocations are emitted as separate slash
|
|
1055
|
+
// events from libp2p_service.
|
|
1056
|
+
const collectedAttestations = await this.p2pClient.getCheckpointAttestationsForSlot(slot, proposalPayloadHash);
|
|
832
1057
|
|
|
833
1058
|
// Log new attestations we collected
|
|
834
1059
|
const oldSenders = attestations.map(attestation => attestation.getSender());
|
|
835
1060
|
for (const collected of collectedAttestations) {
|
|
836
1061
|
const collectedSender = collected.getSender();
|
|
837
|
-
// Skip attestations with invalid signatures
|
|
1062
|
+
// Skip attestations with invalid signatures. Should not happen as we don't add invalid attestations to our pool.
|
|
838
1063
|
if (!collectedSender) {
|
|
839
1064
|
this.log.warn(`Skipping attestation with invalid signature for slot ${slot}`);
|
|
840
1065
|
continue;
|