@aztec/validator-client 0.0.1-commit.c31f2472 → 0.0.1-commit.c52d6e7
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 +63 -18
- package/dest/checkpoint_builder.d.ts +36 -17
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +143 -42
- package/dest/config.d.ts +9 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +45 -7
- 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 +33 -45
- package/dest/factory.d.ts +10 -4
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +19 -6
- package/dest/index.d.ts +2 -3
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -2
- package/dest/key_store/ha_key_store.d.ts +1 -1
- package/dest/key_store/ha_key_store.d.ts.map +1 -1
- package/dest/key_store/ha_key_store.js +3 -3
- 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 +14 -2
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +27 -1
- package/dest/proposal_handler.d.ts +166 -0
- package/dest/proposal_handler.d.ts.map +1 -0
- package/dest/proposal_handler.js +1303 -0
- package/dest/validator.d.ts +65 -24
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +379 -234
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +168 -43
- package/src/config.ts +53 -9
- package/src/duties/validation_service.ts +59 -54
- package/src/factory.ts +29 -4
- package/src/index.ts +1 -2
- package/src/key_store/ha_key_store.ts +3 -3
- package/src/key_store/web3signer_key_store.ts +43 -59
- package/src/metrics.ts +39 -1
- package/src/proposal_handler.ts +1409 -0
- package/src/validator.ts +511 -277
- package/dest/block_proposal_handler.d.ts +0 -63
- package/dest/block_proposal_handler.d.ts.map +0 -1
- package/dest/block_proposal_handler.js +0 -546
- package/dest/tx_validator/index.d.ts +0 -3
- package/dest/tx_validator/index.d.ts.map +0 -1
- package/dest/tx_validator/index.js +0 -2
- package/dest/tx_validator/nullifier_cache.d.ts +0 -14
- package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
- package/dest/tx_validator/nullifier_cache.js +0 -24
- package/dest/tx_validator/tx_validator_factory.d.ts +0 -19
- package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
- package/dest/tx_validator/tx_validator_factory.js +0 -54
- package/src/block_proposal_handler.ts +0 -555
- package/src/tx_validator/index.ts +0 -2
- package/src/tx_validator/nullifier_cache.ts +0 -30
- package/src/tx_validator/tx_validator_factory.ts +0 -154
package/src/validator.ts
CHANGED
|
@@ -1,72 +1,82 @@
|
|
|
1
1
|
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
2
2
|
import { type Blob, getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
3
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
|
-
import {
|
|
5
|
-
BlockNumber,
|
|
6
|
-
CheckpointNumber,
|
|
7
|
-
EpochNumber,
|
|
8
|
-
IndexWithinCheckpoint,
|
|
9
|
-
SlotNumber,
|
|
10
|
-
} from '@aztec/foundation/branded-types';
|
|
4
|
+
import { CheckpointNumber, EpochNumber, IndexWithinCheckpoint, SlotNumber } from '@aztec/foundation/branded-types';
|
|
11
5
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
12
|
-
import { TimeoutError } from '@aztec/foundation/error';
|
|
13
6
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
14
|
-
import
|
|
7
|
+
import { Signature } from '@aztec/foundation/eth-signature';
|
|
8
|
+
import { FifoSet } from '@aztec/foundation/fifo-set';
|
|
15
9
|
import { type LogData, type Logger, createLogger } from '@aztec/foundation/log';
|
|
16
|
-
import { retryUntil } from '@aztec/foundation/retry';
|
|
17
10
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
18
11
|
import { sleep } from '@aztec/foundation/sleep';
|
|
19
12
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
20
13
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
21
|
-
import type { P2P, PeerId } from '@aztec/p2p';
|
|
14
|
+
import type { DuplicateAttestationInfo, DuplicateProposalInfo, OversizedProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
22
15
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
23
|
-
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';
|
|
24
24
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
25
|
-
import type { CommitteeAttestationsAndSigners,
|
|
25
|
+
import type { CommitteeAttestationsAndSigners, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
26
|
+
import type { CheckpointReexecutionTracker } from '@aztec/stdlib/checkpoint';
|
|
26
27
|
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
27
28
|
import type {
|
|
28
|
-
CreateCheckpointProposalLastBlockData,
|
|
29
29
|
ITxProvider,
|
|
30
30
|
Validator,
|
|
31
31
|
ValidatorClientFullConfig,
|
|
32
32
|
WorldStateSynchronizer,
|
|
33
33
|
} from '@aztec/stdlib/interfaces/server';
|
|
34
|
-
import {
|
|
35
|
-
import
|
|
36
|
-
BlockProposal,
|
|
37
|
-
BlockProposalOptions,
|
|
34
|
+
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
35
|
+
import {
|
|
36
|
+
type BlockProposal,
|
|
37
|
+
type BlockProposalOptions,
|
|
38
38
|
CheckpointAttestation,
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
CheckpointProposal,
|
|
40
|
+
type CheckpointProposalCore,
|
|
41
|
+
type CheckpointProposalOptions,
|
|
42
|
+
type CoordinationSignatureContext,
|
|
41
43
|
} from '@aztec/stdlib/p2p';
|
|
42
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
43
44
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
44
|
-
import
|
|
45
|
+
import { ConsensusTimetable } from '@aztec/stdlib/timetable';
|
|
46
|
+
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
|
|
45
47
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
46
48
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
47
|
-
import {
|
|
48
|
-
|
|
49
|
+
import {
|
|
50
|
+
createHASigner,
|
|
51
|
+
createLocalSignerWithProtection,
|
|
52
|
+
createSignerFromSharedDb,
|
|
53
|
+
} from '@aztec/validator-ha-signer/factory';
|
|
54
|
+
import { DutyType, type SigningContext, type SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
|
|
55
|
+
import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
|
|
49
56
|
|
|
50
57
|
import { EventEmitter } from 'events';
|
|
51
58
|
import type { TypedDataDefinition } from 'viem';
|
|
52
59
|
|
|
53
|
-
import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
|
|
54
60
|
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
61
|
+
import { DEFAULT_MAX_GOSSIP_CLOCK_DISPARITY_MS } from './config.js';
|
|
55
62
|
import { ValidationService } from './duties/validation_service.js';
|
|
56
63
|
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
57
64
|
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
58
65
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
59
66
|
import { ValidatorMetrics } from './metrics.js';
|
|
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';
|
|
60
74
|
|
|
61
75
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
62
76
|
// Just cap the set to avoid unbounded growth.
|
|
63
77
|
const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidationFailureReason[] = [
|
|
67
|
-
'state_mismatch',
|
|
68
|
-
'failed_txs',
|
|
69
|
-
];
|
|
78
|
+
const MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS = 1000;
|
|
79
|
+
const MAX_TRACKED_BAD_ATTESTATIONS = 10_000;
|
|
70
80
|
|
|
71
81
|
/**
|
|
72
82
|
* Validator Client
|
|
@@ -76,29 +86,40 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
76
86
|
private validationService: ValidationService;
|
|
77
87
|
private metrics: ValidatorMetrics;
|
|
78
88
|
private log: Logger;
|
|
79
|
-
|
|
80
89
|
// Whether it has already registered handlers on the p2p client
|
|
81
90
|
private hasRegisteredHandlers = false;
|
|
82
91
|
|
|
83
|
-
|
|
84
|
-
private
|
|
92
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
|
|
93
|
+
private lastProposedBlock?: BlockProposal;
|
|
94
|
+
|
|
95
|
+
/** Tracks the last checkpoint proposal we created. */
|
|
96
|
+
private lastProposedCheckpoint?: CheckpointProposal;
|
|
85
97
|
|
|
86
98
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
87
99
|
private epochCacheUpdateLoop: RunningPromise;
|
|
100
|
+
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
|
|
101
|
+
private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
|
|
102
|
+
|
|
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);
|
|
88
107
|
|
|
89
|
-
|
|
108
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
109
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
90
110
|
|
|
91
111
|
protected constructor(
|
|
92
112
|
private keyStore: ExtendedValidatorKeyStore,
|
|
93
113
|
private epochCache: EpochCache,
|
|
94
114
|
private p2pClient: P2P,
|
|
95
|
-
private
|
|
115
|
+
private proposalHandler: ProposalHandler,
|
|
96
116
|
private blockSource: L2BlockSource,
|
|
97
117
|
private checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
98
118
|
private worldState: WorldStateSynchronizer,
|
|
99
119
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
100
120
|
private config: ValidatorClientFullConfig,
|
|
101
121
|
private blobClient: BlobClientInterface,
|
|
122
|
+
private slashingProtectionSigner: ValidatorHASigner,
|
|
102
123
|
private dateProvider: DateProvider = new DateProvider(),
|
|
103
124
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
104
125
|
log = createLogger('validator'),
|
|
@@ -111,11 +132,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
111
132
|
this.tracer = telemetry.getTracer('Validator');
|
|
112
133
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
113
134
|
|
|
114
|
-
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
|
+
);
|
|
115
143
|
|
|
116
144
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
117
145
|
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
|
|
118
|
-
|
|
119
146
|
const myAddresses = this.getValidatorAddresses();
|
|
120
147
|
this.log.verbose(`Initialized validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
121
148
|
}
|
|
@@ -152,6 +179,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
152
179
|
this.log.trace(`No committee found for slot`);
|
|
153
180
|
return;
|
|
154
181
|
}
|
|
182
|
+
this.metrics.setCurrentEpoch(epoch);
|
|
155
183
|
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
156
184
|
const me = this.getValidatorAddresses();
|
|
157
185
|
const committeeSet = new Set(committee.map(v => v.toString()));
|
|
@@ -183,14 +211,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
183
211
|
txProvider: ITxProvider,
|
|
184
212
|
keyStoreManager: KeystoreManager,
|
|
185
213
|
blobClient: BlobClientInterface,
|
|
214
|
+
reexecutionTracker: CheckpointReexecutionTracker,
|
|
186
215
|
dateProvider: DateProvider = new DateProvider(),
|
|
187
216
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
217
|
+
slashingProtectionDb?: SlashingProtectionDatabase,
|
|
188
218
|
) {
|
|
189
219
|
const metrics = new ValidatorMetrics(telemetry);
|
|
190
|
-
const
|
|
220
|
+
const consensusTimetable = new ConsensusTimetable({
|
|
221
|
+
l1Constants: epochCache.getL1Constants(),
|
|
222
|
+
blockDuration: config.blockDurationMs / 1000,
|
|
223
|
+
});
|
|
224
|
+
const blockProposalValidator = new BlockProposalValidator(epochCache, consensusTimetable, {
|
|
191
225
|
txsPermitted: !config.disableTransactions,
|
|
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,
|
|
192
234
|
});
|
|
193
|
-
const
|
|
235
|
+
const proposalHandler = new ProposalHandler(
|
|
194
236
|
checkpointsBuilder,
|
|
195
237
|
worldState,
|
|
196
238
|
blockSource,
|
|
@@ -198,34 +240,57 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
198
240
|
txProvider,
|
|
199
241
|
blockProposalValidator,
|
|
200
242
|
epochCache,
|
|
243
|
+
consensusTimetable,
|
|
201
244
|
config,
|
|
245
|
+
blobClient,
|
|
246
|
+
reexecutionTracker,
|
|
202
247
|
metrics,
|
|
203
248
|
dateProvider,
|
|
204
249
|
telemetry,
|
|
250
|
+
undefined,
|
|
205
251
|
);
|
|
206
252
|
|
|
207
|
-
|
|
208
|
-
|
|
253
|
+
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
254
|
+
let slashingProtectionSigner: ValidatorHASigner;
|
|
255
|
+
if (slashingProtectionDb) {
|
|
256
|
+
// Shared database mode: use a pre-existing database (e.g. for testing HA setups).
|
|
257
|
+
({ signer: slashingProtectionSigner } = createSignerFromSharedDb(slashingProtectionDb, config, {
|
|
258
|
+
telemetryClient: telemetry,
|
|
259
|
+
dateProvider,
|
|
260
|
+
}));
|
|
261
|
+
} else if (config.haSigningEnabled) {
|
|
262
|
+
// Multi-node HA mode: use PostgreSQL-backed distributed locking.
|
|
209
263
|
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
210
264
|
const haConfig = {
|
|
211
265
|
...config,
|
|
212
266
|
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
|
|
213
267
|
};
|
|
214
|
-
|
|
215
|
-
|
|
268
|
+
({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
|
|
269
|
+
telemetryClient: telemetry,
|
|
270
|
+
dateProvider,
|
|
271
|
+
}));
|
|
272
|
+
} else {
|
|
273
|
+
// Single-node mode: use LMDB-backed local signing protection.
|
|
274
|
+
// This prevents double-signing if the node crashes and restarts mid-proposal.
|
|
275
|
+
({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
|
|
276
|
+
telemetryClient: telemetry,
|
|
277
|
+
dateProvider,
|
|
278
|
+
}));
|
|
216
279
|
}
|
|
280
|
+
const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
|
|
217
281
|
|
|
218
282
|
const validator = new ValidatorClient(
|
|
219
283
|
validatorKeyStore,
|
|
220
284
|
epochCache,
|
|
221
285
|
p2pClient,
|
|
222
|
-
|
|
286
|
+
proposalHandler,
|
|
223
287
|
blockSource,
|
|
224
288
|
checkpointsBuilder,
|
|
225
289
|
worldState,
|
|
226
290
|
l1ToL2MessageSource,
|
|
227
291
|
config,
|
|
228
292
|
blobClient,
|
|
293
|
+
slashingProtectionSigner,
|
|
229
294
|
dateProvider,
|
|
230
295
|
telemetry,
|
|
231
296
|
);
|
|
@@ -239,14 +304,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
239
304
|
.filter(addr => !this.config.disabledValidators.some(disabled => disabled.equals(addr)));
|
|
240
305
|
}
|
|
241
306
|
|
|
242
|
-
public
|
|
243
|
-
return this.
|
|
307
|
+
public getProposalHandler() {
|
|
308
|
+
return this.proposalHandler;
|
|
244
309
|
}
|
|
245
310
|
|
|
246
311
|
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition, context: SigningContext) {
|
|
247
312
|
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
248
313
|
}
|
|
249
314
|
|
|
315
|
+
private getSignatureContext(): CoordinationSignatureContext {
|
|
316
|
+
return {
|
|
317
|
+
chainId: this.config.l1ChainId,
|
|
318
|
+
rollupAddress: this.config.rollupAddress,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
250
322
|
public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
|
|
251
323
|
return this.keyStore.getCoinbaseAddress(attestor);
|
|
252
324
|
}
|
|
@@ -259,8 +331,27 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
259
331
|
return this.config;
|
|
260
332
|
}
|
|
261
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
|
+
|
|
262
342
|
public updateConfig(config: Partial<ValidatorClientFullConfig>) {
|
|
263
343
|
this.config = { ...this.config, ...config };
|
|
344
|
+
this.proposalHandler.updateConfig(config);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
public reloadKeystore(newManager: KeystoreManager): void {
|
|
348
|
+
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
349
|
+
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
350
|
+
this.validationService = new ValidationService(
|
|
351
|
+
this.keyStore,
|
|
352
|
+
this.getSignatureContext(),
|
|
353
|
+
this.log.createChild('validation-service'),
|
|
354
|
+
);
|
|
264
355
|
}
|
|
265
356
|
|
|
266
357
|
public async start() {
|
|
@@ -307,7 +398,26 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
307
398
|
checkpoint: CheckpointProposalCore,
|
|
308
399
|
proposalSender: PeerId,
|
|
309
400
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
310
|
-
this.p2pClient.
|
|
401
|
+
this.p2pClient.registerValidatorCheckpointProposalHandler(checkpointHandler);
|
|
402
|
+
|
|
403
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
404
|
+
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
405
|
+
this.handleDuplicateProposal(info);
|
|
406
|
+
});
|
|
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
|
+
|
|
413
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
414
|
+
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
415
|
+
this.handleDuplicateAttestation(info);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
this.p2pClient.registerCheckpointAttestationCallback((attestation: CheckpointAttestation) => {
|
|
419
|
+
this.handleCheckpointAttestation(attestation);
|
|
420
|
+
});
|
|
311
421
|
|
|
312
422
|
const myAddresses = this.getValidatorAddresses();
|
|
313
423
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
@@ -336,6 +446,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
336
446
|
return false;
|
|
337
447
|
}
|
|
338
448
|
|
|
449
|
+
// Log self-proposals from HA peers (same validator key on different nodes)
|
|
450
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
451
|
+
this.log.verbose(`Processing block proposal from HA peer for slot ${slotNumber}`, {
|
|
452
|
+
proposer: proposer.toString(),
|
|
453
|
+
slotNumber,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
339
457
|
// Check if we're in the committee (for metrics purposes)
|
|
340
458
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
341
459
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -347,27 +465,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
347
465
|
fishermanMode: this.config.fishermanMode || false,
|
|
348
466
|
});
|
|
349
467
|
|
|
350
|
-
// Reexecute
|
|
351
|
-
|
|
352
|
-
const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } =
|
|
353
|
-
this.config;
|
|
354
|
-
const shouldReexecute =
|
|
355
|
-
fishermanMode ||
|
|
356
|
-
(slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute) ||
|
|
357
|
-
(partOfCommittee && validatorReexecute) ||
|
|
358
|
-
alwaysReexecuteBlockProposals ||
|
|
359
|
-
this.blobClient.canUpload();
|
|
360
|
-
|
|
361
|
-
const validationResult = await this.blockProposalHandler.handleBlockProposal(
|
|
362
|
-
proposal,
|
|
363
|
-
proposalSender,
|
|
364
|
-
!!shouldReexecute && !escapeHatchOpen,
|
|
365
|
-
);
|
|
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);
|
|
366
470
|
|
|
367
471
|
if (!validationResult.isValid) {
|
|
368
|
-
this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
369
|
-
|
|
370
472
|
const reason = validationResult.reason || 'unknown';
|
|
473
|
+
|
|
474
|
+
this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
|
|
475
|
+
|
|
371
476
|
// Classify failure reason: bad proposal vs node issue
|
|
372
477
|
const badProposalReasons: BlockProposalValidationFailureReason[] = [
|
|
373
478
|
'invalid_proposal',
|
|
@@ -375,6 +480,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
375
480
|
'failed_txs',
|
|
376
481
|
'in_hash_mismatch',
|
|
377
482
|
'parent_block_wrong_slot',
|
|
483
|
+
'duplicate_txs',
|
|
484
|
+
'invalid_embedded_txs',
|
|
378
485
|
];
|
|
379
486
|
|
|
380
487
|
if (badProposalReasons.includes(reason as BlockProposalValidationFailureReason)) {
|
|
@@ -384,15 +491,18 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
384
491
|
this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
|
|
385
492
|
}
|
|
386
493
|
|
|
387
|
-
// Slash invalid block proposals (can happen even when not in committee)
|
|
388
494
|
if (
|
|
389
495
|
!escapeHatchOpen &&
|
|
390
496
|
validationResult.reason &&
|
|
391
|
-
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)
|
|
392
|
-
slashBroadcastedInvalidBlockPenalty > 0n
|
|
497
|
+
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)
|
|
393
498
|
) {
|
|
394
|
-
this.log.
|
|
499
|
+
this.log.info(`Detected invalid block proposal offense`, {
|
|
500
|
+
...proposalInfo,
|
|
501
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
502
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL),
|
|
503
|
+
});
|
|
395
504
|
this.slashInvalidBlock(proposal);
|
|
505
|
+
this.markInvalidProposalSlot(proposal.slotNumber);
|
|
396
506
|
}
|
|
397
507
|
return false;
|
|
398
508
|
}
|
|
@@ -422,51 +532,56 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
422
532
|
proposal: CheckpointProposalCore,
|
|
423
533
|
_proposalSender: PeerId,
|
|
424
534
|
): Promise<CheckpointAttestation[] | undefined> {
|
|
425
|
-
const
|
|
535
|
+
const proposalSlotNumber = proposal.slotNumber;
|
|
426
536
|
const proposer = proposal.getSender();
|
|
427
537
|
|
|
428
538
|
// If escape hatch is open for this slot's epoch, do not attest.
|
|
429
|
-
if (await this.epochCache.isEscapeHatchOpenAtSlot(
|
|
430
|
-
this.log.warn(`Escape hatch open for slot ${
|
|
539
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(proposalSlotNumber)) {
|
|
540
|
+
this.log.warn(`Escape hatch open for slot ${proposalSlotNumber}, skipping checkpoint attestation handling`);
|
|
431
541
|
return undefined;
|
|
432
542
|
}
|
|
433
543
|
|
|
434
|
-
//
|
|
435
|
-
if (!
|
|
436
|
-
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
|
|
544
|
+
// Early-out for equivocation: refuses if we've already attested to a higher slot.
|
|
545
|
+
if (!this.shouldAttestToSlot(proposalSlotNumber)) {
|
|
437
546
|
return undefined;
|
|
438
547
|
}
|
|
439
548
|
|
|
440
|
-
//
|
|
441
|
-
|
|
549
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
550
|
+
if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
551
|
+
this.log.debug(`Not attesting to block proposal from self for slot ${proposalSlotNumber}`, {
|
|
552
|
+
proposer: proposer.toString(),
|
|
553
|
+
proposalSlotNumber,
|
|
554
|
+
});
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Check that I have any address in the committee where this checkpoint will land before attesting
|
|
559
|
+
const inCommittee = await this.epochCache.filterInCommittee(proposalSlotNumber, this.getValidatorAddresses());
|
|
442
560
|
const partOfCommittee = inCommittee.length > 0;
|
|
443
561
|
|
|
444
562
|
const proposalInfo = {
|
|
445
|
-
|
|
563
|
+
proposalSlotNumber,
|
|
446
564
|
archive: proposal.archive.toString(),
|
|
447
|
-
proposer: proposer
|
|
448
|
-
txCount: proposal.txHashes.length,
|
|
565
|
+
proposer: proposer?.toString(),
|
|
449
566
|
};
|
|
450
|
-
this.log.info(`Received checkpoint proposal for slot ${
|
|
567
|
+
this.log.info(`Received checkpoint proposal for slot ${proposalSlotNumber}`, {
|
|
451
568
|
...proposalInfo,
|
|
452
|
-
txHashes: proposal.txHashes.map(t => t.toString()),
|
|
453
569
|
fishermanMode: this.config.fishermanMode || false,
|
|
454
570
|
});
|
|
455
571
|
|
|
456
|
-
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
572
|
+
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
|
|
573
|
+
// Uses the cached result from the all-nodes callback if available (avoids double validation).
|
|
574
|
+
let checkpointNumber: CheckpointNumber;
|
|
457
575
|
if (this.config.skipCheckpointProposalValidation) {
|
|
458
|
-
this.log.warn(`Skipping checkpoint proposal validation for slot ${
|
|
576
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
|
|
577
|
+
checkpointNumber = CheckpointNumber(0);
|
|
459
578
|
} else {
|
|
460
|
-
const validationResult = await this.
|
|
579
|
+
const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
|
|
461
580
|
if (!validationResult.isValid) {
|
|
462
581
|
this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
463
582
|
return undefined;
|
|
464
583
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
// Upload blobs to filestore if we can (fire and forget)
|
|
468
|
-
if (this.blobClient.canUpload()) {
|
|
469
|
-
void this.uploadBlobsForCheckpoint(proposal, proposalInfo);
|
|
584
|
+
checkpointNumber = validationResult.checkpointNumber;
|
|
470
585
|
}
|
|
471
586
|
|
|
472
587
|
// Check that I have any address in current committee before attesting
|
|
@@ -477,14 +592,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
477
592
|
}
|
|
478
593
|
|
|
479
594
|
// Provided all of the above checks pass, we can attest to the proposal
|
|
480
|
-
this.log.info(
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
595
|
+
this.log.info(
|
|
596
|
+
`${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${proposalSlotNumber}`,
|
|
597
|
+
{
|
|
598
|
+
...proposalInfo,
|
|
599
|
+
inCommittee: partOfCommittee,
|
|
600
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
601
|
+
},
|
|
602
|
+
);
|
|
485
603
|
|
|
486
604
|
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
487
605
|
|
|
606
|
+
// Track epoch participation per attester: count each (attester, epoch) pair at most once
|
|
607
|
+
const proposalEpoch = getEpochAtSlot(proposalSlotNumber, this.epochCache.getL1Constants());
|
|
608
|
+
for (const attester of inCommittee) {
|
|
609
|
+
const key = attester.toString();
|
|
610
|
+
const lastEpoch = this.lastAttestedEpochByAttester.get(key);
|
|
611
|
+
if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
|
|
612
|
+
this.lastAttestedEpochByAttester.set(key, proposalEpoch);
|
|
613
|
+
this.metrics.incAttestedEpochCount(attester);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
488
617
|
// Determine which validators should attest
|
|
489
618
|
let attestors: EthAddress[];
|
|
490
619
|
if (partOfCommittee) {
|
|
@@ -503,174 +632,62 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
503
632
|
|
|
504
633
|
if (this.config.fishermanMode) {
|
|
505
634
|
// bail out early and don't save attestations to the pool in fisherman mode
|
|
506
|
-
this.log.info(`Creating checkpoint attestations for slot ${
|
|
635
|
+
this.log.info(`Creating checkpoint attestations for slot ${proposalSlotNumber}`, {
|
|
507
636
|
...proposalInfo,
|
|
508
637
|
attestors: attestors.map(a => a.toString()),
|
|
509
638
|
});
|
|
510
639
|
return undefined;
|
|
511
640
|
}
|
|
512
641
|
|
|
513
|
-
return this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
private async createCheckpointAttestationsFromProposal(
|
|
517
|
-
proposal: CheckpointProposalCore,
|
|
518
|
-
attestors: EthAddress[] = [],
|
|
519
|
-
): Promise<CheckpointAttestation[]> {
|
|
520
|
-
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
521
|
-
await this.p2pClient.addCheckpointAttestations(attestations);
|
|
522
|
-
return attestations;
|
|
642
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
|
|
523
643
|
}
|
|
524
644
|
|
|
525
645
|
/**
|
|
526
|
-
*
|
|
527
|
-
* @returns
|
|
646
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
647
|
+
* @returns true if we should attest, false if we should skip
|
|
528
648
|
*/
|
|
529
|
-
private
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const timeoutSeconds = 10; // TODO(palla/mbps): This should map to the timetable settings
|
|
649
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
650
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
651
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
535
654
|
|
|
536
|
-
//
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
async () => {
|
|
541
|
-
await this.blockSource.syncImmediate();
|
|
542
|
-
return this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
543
|
-
},
|
|
544
|
-
`waiting for block with archive ${proposal.archive.toString()} for slot ${slot}`,
|
|
545
|
-
timeoutSeconds,
|
|
546
|
-
0.5,
|
|
655
|
+
// Check if incoming slot is strictly greater than last attested
|
|
656
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
657
|
+
this.log.warn(
|
|
658
|
+
`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
|
|
547
659
|
);
|
|
548
|
-
|
|
549
|
-
if (err instanceof TimeoutError) {
|
|
550
|
-
this.log.warn(`Timed out waiting for block with archive matching checkpoint proposal`, proposalInfo);
|
|
551
|
-
return { isValid: false, reason: 'last_block_not_found' };
|
|
552
|
-
}
|
|
553
|
-
this.log.error(`Error fetching last block for checkpoint proposal`, err, proposalInfo);
|
|
554
|
-
return { isValid: false, reason: 'block_fetch_error' };
|
|
660
|
+
return false;
|
|
555
661
|
}
|
|
556
662
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
return { isValid: false, reason: 'last_block_not_found' };
|
|
560
|
-
}
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
561
665
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
666
|
+
private async createCheckpointAttestationsFromProposal(
|
|
667
|
+
proposal: CheckpointProposalCore,
|
|
668
|
+
attestors: EthAddress[] = [],
|
|
669
|
+
checkpointNumber: CheckpointNumber,
|
|
670
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
671
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
672
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
673
|
+
return undefined;
|
|
567
674
|
}
|
|
568
675
|
|
|
569
|
-
this.
|
|
570
|
-
...proposalInfo,
|
|
571
|
-
blockNumbers: blocks.map(b => b.number),
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
// Get checkpoint constants from first block
|
|
575
|
-
const firstBlock = blocks[0];
|
|
576
|
-
const constants = this.extractCheckpointConstants(firstBlock);
|
|
577
|
-
const checkpointNumber = firstBlock.checkpointNumber;
|
|
578
|
-
|
|
579
|
-
// Get L1-to-L2 messages for this checkpoint
|
|
580
|
-
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
581
|
-
|
|
582
|
-
// Compute the previous checkpoint out hashes for the epoch.
|
|
583
|
-
// TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
|
|
584
|
-
// actual checkpoints and the blocks/txs in them.
|
|
585
|
-
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
586
|
-
const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch))
|
|
587
|
-
.filter(b => b.number < checkpointNumber)
|
|
588
|
-
.sort((a, b) => a.number - b.number);
|
|
589
|
-
const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
|
|
676
|
+
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
|
|
590
677
|
|
|
591
|
-
//
|
|
592
|
-
|
|
593
|
-
const fork = await this.worldState.fork(parentBlockNumber);
|
|
594
|
-
|
|
595
|
-
try {
|
|
596
|
-
// Create checkpoint builder with all existing blocks
|
|
597
|
-
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
598
|
-
checkpointNumber,
|
|
599
|
-
constants,
|
|
600
|
-
l1ToL2Messages,
|
|
601
|
-
previousCheckpointOutHashes,
|
|
602
|
-
fork,
|
|
603
|
-
blocks,
|
|
604
|
-
this.log.getBindings(),
|
|
605
|
-
);
|
|
678
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
679
|
+
this.lastAttestedProposal = proposal;
|
|
606
680
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
// Compare checkpoint header with proposal
|
|
611
|
-
if (!computedCheckpoint.header.equals(proposal.checkpointHeader)) {
|
|
612
|
-
this.log.warn(`Checkpoint header mismatch`, {
|
|
613
|
-
...proposalInfo,
|
|
614
|
-
computed: computedCheckpoint.header.toInspect(),
|
|
615
|
-
proposal: proposal.checkpointHeader.toInspect(),
|
|
616
|
-
});
|
|
617
|
-
return { isValid: false, reason: 'checkpoint_header_mismatch' };
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Compare archive root with proposal
|
|
621
|
-
if (!computedCheckpoint.archive.root.equals(proposal.archive)) {
|
|
622
|
-
this.log.warn(`Archive root mismatch`, {
|
|
623
|
-
...proposalInfo,
|
|
624
|
-
computed: computedCheckpoint.archive.root.toString(),
|
|
625
|
-
proposal: proposal.archive.toString(),
|
|
626
|
-
});
|
|
627
|
-
return { isValid: false, reason: 'archive_mismatch' };
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
631
|
-
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
632
|
-
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
633
|
-
const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
|
|
634
|
-
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
635
|
-
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
636
|
-
this.log.warn(`Epoch out hash mismatch`, {
|
|
637
|
-
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
638
|
-
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
639
|
-
checkpointOutHash: checkpointOutHash.toString(),
|
|
640
|
-
previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
|
|
641
|
-
...proposalInfo,
|
|
642
|
-
});
|
|
643
|
-
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
647
|
-
return { isValid: true };
|
|
648
|
-
} finally {
|
|
649
|
-
await fork.close();
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
* Extract checkpoint global variables from a block.
|
|
655
|
-
*/
|
|
656
|
-
private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
|
|
657
|
-
const gv = block.header.globalVariables;
|
|
658
|
-
return {
|
|
659
|
-
chainId: gv.chainId,
|
|
660
|
-
version: gv.version,
|
|
661
|
-
slotNumber: gv.slotNumber,
|
|
662
|
-
coinbase: gv.coinbase,
|
|
663
|
-
feeRecipient: gv.feeRecipient,
|
|
664
|
-
gasFees: gv.gasFees,
|
|
665
|
-
};
|
|
681
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
682
|
+
return attestations;
|
|
666
683
|
}
|
|
667
684
|
|
|
668
685
|
/**
|
|
669
686
|
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
670
687
|
*/
|
|
671
|
-
|
|
688
|
+
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
672
689
|
try {
|
|
673
|
-
const lastBlockHeader = await this.blockSource.
|
|
690
|
+
const lastBlockHeader = (await this.blockSource.getBlockData({ archive: proposal.archive }))?.header;
|
|
674
691
|
if (!lastBlockHeader) {
|
|
675
692
|
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
676
693
|
return;
|
|
@@ -683,7 +700,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
683
700
|
}
|
|
684
701
|
|
|
685
702
|
const blobFields = blocks.flatMap(b => b.toBlobFields());
|
|
686
|
-
const blobs: Blob[] = getBlobsPerL1Block(blobFields);
|
|
703
|
+
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
687
704
|
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
688
705
|
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
689
706
|
...proposalInfo,
|
|
@@ -703,12 +720,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
703
720
|
return;
|
|
704
721
|
}
|
|
705
722
|
|
|
706
|
-
// Trim the set if it's too big.
|
|
707
|
-
if (this.proposersOfInvalidBlocks.size > MAX_PROPOSERS_OF_INVALID_BLOCKS) {
|
|
708
|
-
// remove oldest proposer. `values` is guaranteed to be in insertion order.
|
|
709
|
-
this.proposersOfInvalidBlocks.delete(this.proposersOfInvalidBlocks.values().next().value!);
|
|
710
|
-
}
|
|
711
|
-
|
|
712
723
|
this.proposersOfInvalidBlocks.add(proposer.toString());
|
|
713
724
|
|
|
714
725
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
@@ -721,8 +732,192 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
721
732
|
]);
|
|
722
733
|
}
|
|
723
734
|
|
|
735
|
+
private handleInvalidCheckpointProposal(
|
|
736
|
+
proposal: CheckpointProposalCore,
|
|
737
|
+
result: CheckpointProposalValidationFailureResult,
|
|
738
|
+
proposalInfo: LogData,
|
|
739
|
+
): void {
|
|
740
|
+
if (!SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT[result.reason]) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// The slot is already marked invalid by the all-nodes checkpoint handler that invokes this callback,
|
|
745
|
+
// so we only emit the proposer slash event here.
|
|
746
|
+
if (this.slashInvalidCheckpointProposal(proposal)) {
|
|
747
|
+
this.log.info(`Detected invalid checkpoint proposal offense`, {
|
|
748
|
+
...proposalInfo,
|
|
749
|
+
reason: result.reason,
|
|
750
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
751
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL),
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
private slashInvalidCheckpointProposal(proposal: CheckpointProposalCore): boolean {
|
|
757
|
+
const proposer = proposal.getSender();
|
|
758
|
+
if (!proposer) {
|
|
759
|
+
this.log.warn(`Cannot slash checkpoint proposal with invalid signature`, {
|
|
760
|
+
slotNumber: proposal.slotNumber,
|
|
761
|
+
archive: proposal.archive.toString(),
|
|
762
|
+
});
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const offenseType = OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL;
|
|
767
|
+
const offenseKey = `${proposer.toString()}:${offenseType}:${proposal.slotNumber}`;
|
|
768
|
+
if (!this.invalidCheckpointProposalOffenseKeys.addIfAbsent(offenseKey)) {
|
|
769
|
+
return false;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
773
|
+
{
|
|
774
|
+
validator: proposer,
|
|
775
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
776
|
+
offenseType,
|
|
777
|
+
epochOrSlot: BigInt(proposal.slotNumber),
|
|
778
|
+
},
|
|
779
|
+
]);
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
private markInvalidProposalSlot(slotNumber: SlotNumber): void {
|
|
784
|
+
this.proposalHandler.markInvalidProposalSlot(slotNumber);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private handleCheckpointAttestation(attestation: CheckpointAttestation): void {
|
|
788
|
+
const slotNumber = attestation.slotNumber;
|
|
789
|
+
if (
|
|
790
|
+
!this.proposalHandler.hasInvalidProposals(slotNumber) ||
|
|
791
|
+
this.proposalHandler.hasProposalEquivocation(slotNumber)
|
|
792
|
+
) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const attester = attestation.getSender();
|
|
797
|
+
if (!attester) {
|
|
798
|
+
this.log.warn(`Cannot slash checkpoint attestation with invalid signature`, {
|
|
799
|
+
slotNumber,
|
|
800
|
+
archive: attestation.archive.toString(),
|
|
801
|
+
});
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
this.slashAttestedToInvalidCheckpointProposal(slotNumber, attester);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private slashAttestedToInvalidCheckpointProposal(slotNumber: SlotNumber, attester: EthAddress): void {
|
|
809
|
+
const offenseKey = `${attester.toString()}:${OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL}:${slotNumber}`;
|
|
810
|
+
if (!this.badAttestationOffenseKeys.addIfAbsent(offenseKey)) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
this.log.info(`Detected attestation to invalid checkpoint proposal offense`, {
|
|
815
|
+
attester: attester.toString(),
|
|
816
|
+
slotNumber,
|
|
817
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
818
|
+
offenseType: getOffenseTypeName(OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL),
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
822
|
+
{
|
|
823
|
+
validator: attester,
|
|
824
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
825
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
826
|
+
epochOrSlot: BigInt(slotNumber),
|
|
827
|
+
},
|
|
828
|
+
]);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Handle detection of an oversized block proposal: one whose index within its checkpoint lands at or
|
|
833
|
+
* beyond the consensus per-checkpoint block limit. A single signed proposal at an illegal index is
|
|
834
|
+
* self-contained evidence, so emit an invalid-block-proposal slash event for the proposer, deduped per
|
|
835
|
+
* (proposer, slot) since the p2p layer reports every oversized proposal it stores.
|
|
836
|
+
*/
|
|
837
|
+
private handleOversizedProposal(info: OversizedProposalInfo): void {
|
|
838
|
+
const { slot, proposer } = info;
|
|
839
|
+
const offenseType = OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL;
|
|
840
|
+
if (!this.oversizedProposalOffenseKeys.addIfAbsent(`${proposer.toString()}:${offenseType}:${slot}`)) {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
this.log.info(`Detected oversized block proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
845
|
+
proposer: proposer.toString(),
|
|
846
|
+
slot,
|
|
847
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
848
|
+
offenseType: getOffenseTypeName(offenseType),
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
852
|
+
{
|
|
853
|
+
validator: proposer,
|
|
854
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
855
|
+
offenseType,
|
|
856
|
+
epochOrSlot: BigInt(slot),
|
|
857
|
+
},
|
|
858
|
+
]);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
863
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
864
|
+
*/
|
|
865
|
+
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
866
|
+
const { slot, proposer, type } = info;
|
|
867
|
+
this.proposalHandler.markProposalEquivocation(slot);
|
|
868
|
+
|
|
869
|
+
this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
870
|
+
proposer: proposer.toString(),
|
|
871
|
+
slot,
|
|
872
|
+
type,
|
|
873
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
874
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_PROPOSAL),
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
878
|
+
{
|
|
879
|
+
validator: proposer,
|
|
880
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
881
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
882
|
+
epochOrSlot: BigInt(slot),
|
|
883
|
+
},
|
|
884
|
+
]);
|
|
885
|
+
|
|
886
|
+
this.emit(WANT_TO_CLEAR_SLASH_EVENT, [
|
|
887
|
+
{
|
|
888
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
889
|
+
epochOrSlot: BigInt(slot),
|
|
890
|
+
},
|
|
891
|
+
]);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
896
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
897
|
+
*/
|
|
898
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
899
|
+
const { slot, attester } = info;
|
|
900
|
+
|
|
901
|
+
this.log.info(`Detected duplicate attestation offense from ${attester.toString()} at slot ${slot}`, {
|
|
902
|
+
attester: attester.toString(),
|
|
903
|
+
slot,
|
|
904
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
905
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_ATTESTATION),
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
909
|
+
{
|
|
910
|
+
validator: attester,
|
|
911
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
912
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
913
|
+
epochOrSlot: BigInt(slot),
|
|
914
|
+
},
|
|
915
|
+
]);
|
|
916
|
+
}
|
|
917
|
+
|
|
724
918
|
async createBlockProposal(
|
|
725
919
|
blockHeader: BlockHeader,
|
|
920
|
+
checkpointNumber: CheckpointNumber,
|
|
726
921
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
727
922
|
inHash: Fr,
|
|
728
923
|
archive: Fr,
|
|
@@ -730,17 +925,26 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
730
925
|
proposerAddress: EthAddress | undefined,
|
|
731
926
|
options: BlockProposalOptions = {},
|
|
732
927
|
): Promise<BlockProposal> {
|
|
733
|
-
//
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
928
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
929
|
+
if (this.lastProposedBlock) {
|
|
930
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
931
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
932
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
933
|
+
|
|
934
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
935
|
+
throw new Error(
|
|
936
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
937
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
738
941
|
|
|
739
942
|
this.log.info(
|
|
740
943
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
741
944
|
);
|
|
742
945
|
const newProposal = await this.validationService.createBlockProposal(
|
|
743
946
|
blockHeader,
|
|
947
|
+
checkpointNumber,
|
|
744
948
|
indexWithinCheckpoint,
|
|
745
949
|
inHash,
|
|
746
950
|
archive,
|
|
@@ -748,28 +952,55 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
748
952
|
proposerAddress,
|
|
749
953
|
{
|
|
750
954
|
...options,
|
|
751
|
-
broadcastInvalidBlockProposal:
|
|
955
|
+
broadcastInvalidBlockProposal:
|
|
956
|
+
options.broadcastInvalidBlockProposal || this.config.broadcastInvalidBlockProposal,
|
|
752
957
|
},
|
|
753
958
|
);
|
|
754
|
-
this.
|
|
959
|
+
this.lastProposedBlock = newProposal;
|
|
755
960
|
return newProposal;
|
|
756
961
|
}
|
|
757
962
|
|
|
758
963
|
async createCheckpointProposal(
|
|
759
964
|
checkpointHeader: CheckpointHeader,
|
|
760
965
|
archive: Fr,
|
|
761
|
-
|
|
966
|
+
checkpointNumber: CheckpointNumber,
|
|
967
|
+
feeAssetPriceModifier: bigint,
|
|
968
|
+
lastBlockProposal: BlockProposal | undefined,
|
|
762
969
|
proposerAddress: EthAddress | undefined,
|
|
763
970
|
options: CheckpointProposalOptions = {},
|
|
764
971
|
): Promise<CheckpointProposal> {
|
|
972
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
973
|
+
if (this.lastProposedCheckpoint) {
|
|
974
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
975
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
976
|
+
|
|
977
|
+
if (newSlot <= lastSlot) {
|
|
978
|
+
throw new Error(
|
|
979
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
980
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
765
985
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
766
|
-
|
|
986
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
767
987
|
checkpointHeader,
|
|
768
988
|
archive,
|
|
769
|
-
|
|
989
|
+
checkpointNumber,
|
|
990
|
+
feeAssetPriceModifier,
|
|
991
|
+
lastBlockProposal,
|
|
770
992
|
proposerAddress,
|
|
771
993
|
options,
|
|
772
994
|
);
|
|
995
|
+
this.lastProposedCheckpoint = newProposal;
|
|
996
|
+
// Self-record this slot's outcome on the re-execution tracker. Proposers don't run their
|
|
997
|
+
// own proposals through `handleCheckpointProposal`, so without this call the proposer's
|
|
998
|
+
// sentinel would see no outcome for slots it proposed and would mis-attribute itself as
|
|
999
|
+
// inactive. We pass the locally-computed `archive` (not `newProposal.archive`, which may
|
|
1000
|
+
// be intentionally corrupted under test-only flags); from the proposer's local-view
|
|
1001
|
+
// perspective the work it just completed is valid by definition.
|
|
1002
|
+
this.proposalHandler.recordOwnCheckpointProposalAsValid(checkpointHeader.slotNumber, archive, checkpointNumber);
|
|
1003
|
+
return newProposal;
|
|
773
1004
|
}
|
|
774
1005
|
|
|
775
1006
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -780,16 +1011,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
780
1011
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
781
1012
|
proposer: EthAddress,
|
|
782
1013
|
slot: SlotNumber,
|
|
783
|
-
|
|
1014
|
+
checkpointNumber: CheckpointNumber,
|
|
784
1015
|
): Promise<Signature> {
|
|
785
|
-
return await this.validationService.signAttestationsAndSigners(
|
|
1016
|
+
return await this.validationService.signAttestationsAndSigners(
|
|
1017
|
+
attestationsAndSigners,
|
|
1018
|
+
proposer,
|
|
1019
|
+
slot,
|
|
1020
|
+
checkpointNumber,
|
|
1021
|
+
);
|
|
786
1022
|
}
|
|
787
1023
|
|
|
788
|
-
async collectOwnAttestations(
|
|
1024
|
+
async collectOwnAttestations(
|
|
1025
|
+
proposal: CheckpointProposal,
|
|
1026
|
+
checkpointNumber: CheckpointNumber,
|
|
1027
|
+
): Promise<CheckpointAttestation[]> {
|
|
789
1028
|
const slot = proposal.slotNumber;
|
|
790
1029
|
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
791
1030
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
792
|
-
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
1031
|
+
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
|
|
1032
|
+
|
|
1033
|
+
if (!attestations) {
|
|
1034
|
+
return [];
|
|
1035
|
+
}
|
|
793
1036
|
|
|
794
1037
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
795
1038
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
@@ -804,6 +1047,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
804
1047
|
proposal: CheckpointProposal,
|
|
805
1048
|
required: number,
|
|
806
1049
|
deadline: Date,
|
|
1050
|
+
checkpointNumber: CheckpointNumber,
|
|
807
1051
|
): Promise<CheckpointAttestation[]> {
|
|
808
1052
|
// Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
|
|
809
1053
|
const slot = proposal.slotNumber;
|
|
@@ -816,33 +1060,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
816
1060
|
throw new AttestationTimeoutError(0, required, slot);
|
|
817
1061
|
}
|
|
818
1062
|
|
|
819
|
-
await this.collectOwnAttestations(proposal);
|
|
1063
|
+
await this.collectOwnAttestations(proposal, checkpointNumber);
|
|
820
1064
|
|
|
821
|
-
const
|
|
1065
|
+
const proposalPayloadHash = proposal.getPayloadHash();
|
|
822
1066
|
const myAddresses = this.getValidatorAddresses();
|
|
823
1067
|
|
|
824
1068
|
let attestations: CheckpointAttestation[] = [];
|
|
825
1069
|
while (true) {
|
|
826
|
-
//
|
|
827
|
-
//
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
if (!attestation.archive.equals(proposal.archive)) {
|
|
831
|
-
this.log.warn(
|
|
832
|
-
`Received attestation for slot ${slot} with mismatched archive from ${attestation.getSender()?.toString()}`,
|
|
833
|
-
{ attestationArchive: attestation.archive.toString(), proposalArchive: proposal.archive.toString() },
|
|
834
|
-
);
|
|
835
|
-
return false;
|
|
836
|
-
}
|
|
837
|
-
return true;
|
|
838
|
-
},
|
|
839
|
-
);
|
|
1070
|
+
// The pool already filters by proposal payload hash; if any attestation slips through with a
|
|
1071
|
+
// mismatched payload hash, drop it defensively. Equivocations are emitted as separate slash
|
|
1072
|
+
// events from libp2p_service.
|
|
1073
|
+
const collectedAttestations = await this.p2pClient.getCheckpointAttestationsForSlot(slot, proposalPayloadHash);
|
|
840
1074
|
|
|
841
1075
|
// Log new attestations we collected
|
|
842
1076
|
const oldSenders = attestations.map(attestation => attestation.getSender());
|
|
843
1077
|
for (const collected of collectedAttestations) {
|
|
844
1078
|
const collectedSender = collected.getSender();
|
|
845
|
-
// Skip attestations with invalid signatures
|
|
1079
|
+
// Skip attestations with invalid signatures. Should not happen as we don't add invalid attestations to our pool.
|
|
846
1080
|
if (!collectedSender) {
|
|
847
1081
|
this.log.warn(`Skipping attestation with invalid signature for slot ${slot}`);
|
|
848
1082
|
continue;
|