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