@aztec/validator-client 0.0.1-commit.9d2bcf6d → 0.0.1-commit.9d619b6c6
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 +60 -18
- package/dest/checkpoint_builder.d.ts +21 -8
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +124 -46
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +26 -6
- package/dest/duties/validation_service.d.ts +7 -9
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +15 -33
- package/dest/factory.d.ts +7 -4
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +6 -5
- 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.js +1 -1
- package/dest/metrics.d.ts +14 -2
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +24 -0
- package/dest/proposal_handler.d.ts +108 -0
- package/dest/proposal_handler.d.ts.map +1 -0
- package/dest/proposal_handler.js +974 -0
- package/dest/validator.d.ts +45 -23
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +193 -199
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +142 -39
- package/src/config.ts +26 -6
- package/src/duties/validation_service.ts +25 -37
- package/src/factory.ts +10 -3
- package/src/index.ts +1 -2
- package/src/key_store/ha_key_store.ts +1 -1
- package/src/metrics.ts +37 -1
- package/src/proposal_handler.ts +1042 -0
- package/src/validator.ts +269 -227
- 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,62 +1,58 @@
|
|
|
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
7
|
import type { Signature } from '@aztec/foundation/eth-signature';
|
|
15
8
|
import { type LogData, type Logger, createLogger } from '@aztec/foundation/log';
|
|
16
|
-
import { retryUntil } from '@aztec/foundation/retry';
|
|
17
9
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
18
10
|
import { sleep } from '@aztec/foundation/sleep';
|
|
19
11
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
20
12
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
21
|
-
import type { P2P, PeerId } from '@aztec/p2p';
|
|
13
|
+
import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
22
14
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
23
15
|
import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
|
|
24
16
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
25
|
-
import type { CommitteeAttestationsAndSigners,
|
|
17
|
+
import type { CommitteeAttestationsAndSigners, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
26
18
|
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
27
19
|
import type {
|
|
28
|
-
CreateCheckpointProposalLastBlockData,
|
|
29
20
|
ITxProvider,
|
|
30
21
|
Validator,
|
|
31
22
|
ValidatorClientFullConfig,
|
|
32
23
|
WorldStateSynchronizer,
|
|
33
24
|
} from '@aztec/stdlib/interfaces/server';
|
|
34
|
-
import {
|
|
35
|
-
import
|
|
36
|
-
BlockProposal,
|
|
37
|
-
BlockProposalOptions,
|
|
38
|
-
CheckpointAttestation,
|
|
39
|
-
|
|
40
|
-
|
|
25
|
+
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
26
|
+
import {
|
|
27
|
+
type BlockProposal,
|
|
28
|
+
type BlockProposalOptions,
|
|
29
|
+
type CheckpointAttestation,
|
|
30
|
+
CheckpointProposal,
|
|
31
|
+
type CheckpointProposalCore,
|
|
32
|
+
type CheckpointProposalOptions,
|
|
41
33
|
} from '@aztec/stdlib/p2p';
|
|
42
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
43
34
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
44
|
-
import type { BlockHeader,
|
|
35
|
+
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
|
|
45
36
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
46
37
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
47
|
-
import {
|
|
48
|
-
|
|
38
|
+
import {
|
|
39
|
+
createHASigner,
|
|
40
|
+
createLocalSignerWithProtection,
|
|
41
|
+
createSignerFromSharedDb,
|
|
42
|
+
} from '@aztec/validator-ha-signer/factory';
|
|
43
|
+
import { DutyType, type SigningContext, type SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
|
|
44
|
+
import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
|
|
49
45
|
|
|
50
46
|
import { EventEmitter } from 'events';
|
|
51
47
|
import type { TypedDataDefinition } from 'viem';
|
|
52
48
|
|
|
53
|
-
import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
|
|
54
49
|
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
55
50
|
import { ValidationService } from './duties/validation_service.js';
|
|
56
51
|
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
57
52
|
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
58
53
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
59
54
|
import { ValidatorMetrics } from './metrics.js';
|
|
55
|
+
import { type BlockProposalValidationFailureReason, ProposalHandler } from './proposal_handler.js';
|
|
60
56
|
|
|
61
57
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
62
58
|
// Just cap the set to avoid unbounded growth.
|
|
@@ -76,29 +72,37 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
76
72
|
private validationService: ValidationService;
|
|
77
73
|
private metrics: ValidatorMetrics;
|
|
78
74
|
private log: Logger;
|
|
79
|
-
|
|
80
75
|
// Whether it has already registered handlers on the p2p client
|
|
81
76
|
private hasRegisteredHandlers = false;
|
|
82
77
|
|
|
83
|
-
|
|
84
|
-
private
|
|
78
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
|
|
79
|
+
private lastProposedBlock?: BlockProposal;
|
|
80
|
+
|
|
81
|
+
/** Tracks the last checkpoint proposal we created. */
|
|
82
|
+
private lastProposedCheckpoint?: CheckpointProposal;
|
|
85
83
|
|
|
86
84
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
87
85
|
private epochCacheUpdateLoop: RunningPromise;
|
|
86
|
+
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
|
|
87
|
+
private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
|
|
88
88
|
|
|
89
89
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
90
90
|
|
|
91
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
92
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
93
|
+
|
|
91
94
|
protected constructor(
|
|
92
95
|
private keyStore: ExtendedValidatorKeyStore,
|
|
93
96
|
private epochCache: EpochCache,
|
|
94
97
|
private p2pClient: P2P,
|
|
95
|
-
private
|
|
98
|
+
private proposalHandler: ProposalHandler,
|
|
96
99
|
private blockSource: L2BlockSource,
|
|
97
100
|
private checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
98
101
|
private worldState: WorldStateSynchronizer,
|
|
99
102
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
100
103
|
private config: ValidatorClientFullConfig,
|
|
101
104
|
private blobClient: BlobClientInterface,
|
|
105
|
+
private slashingProtectionSigner: ValidatorHASigner,
|
|
102
106
|
private dateProvider: DateProvider = new DateProvider(),
|
|
103
107
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
104
108
|
log = createLogger('validator'),
|
|
@@ -152,6 +156,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
152
156
|
this.log.trace(`No committee found for slot`);
|
|
153
157
|
return;
|
|
154
158
|
}
|
|
159
|
+
this.metrics.setCurrentEpoch(epoch);
|
|
155
160
|
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
156
161
|
const me = this.getValidatorAddresses();
|
|
157
162
|
const committeeSet = new Set(committee.map(v => v.toString()));
|
|
@@ -185,12 +190,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
185
190
|
blobClient: BlobClientInterface,
|
|
186
191
|
dateProvider: DateProvider = new DateProvider(),
|
|
187
192
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
193
|
+
slashingProtectionDb?: SlashingProtectionDatabase,
|
|
188
194
|
) {
|
|
189
195
|
const metrics = new ValidatorMetrics(telemetry);
|
|
190
196
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
191
197
|
txsPermitted: !config.disableTransactions,
|
|
198
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
192
199
|
});
|
|
193
|
-
const
|
|
200
|
+
const proposalHandler = new ProposalHandler(
|
|
194
201
|
checkpointsBuilder,
|
|
195
202
|
worldState,
|
|
196
203
|
blockSource,
|
|
@@ -199,33 +206,54 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
199
206
|
blockProposalValidator,
|
|
200
207
|
epochCache,
|
|
201
208
|
config,
|
|
209
|
+
blobClient,
|
|
202
210
|
metrics,
|
|
203
211
|
dateProvider,
|
|
204
212
|
telemetry,
|
|
213
|
+
undefined,
|
|
205
214
|
);
|
|
206
215
|
|
|
207
|
-
|
|
208
|
-
|
|
216
|
+
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
217
|
+
let slashingProtectionSigner: ValidatorHASigner;
|
|
218
|
+
if (slashingProtectionDb) {
|
|
219
|
+
// Shared database mode: use a pre-existing database (e.g. for testing HA setups).
|
|
220
|
+
({ signer: slashingProtectionSigner } = createSignerFromSharedDb(slashingProtectionDb, config, {
|
|
221
|
+
telemetryClient: telemetry,
|
|
222
|
+
dateProvider,
|
|
223
|
+
}));
|
|
224
|
+
} else if (config.haSigningEnabled) {
|
|
225
|
+
// Multi-node HA mode: use PostgreSQL-backed distributed locking.
|
|
209
226
|
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
210
227
|
const haConfig = {
|
|
211
228
|
...config,
|
|
212
229
|
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
|
|
213
230
|
};
|
|
214
|
-
|
|
215
|
-
|
|
231
|
+
({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
|
|
232
|
+
telemetryClient: telemetry,
|
|
233
|
+
dateProvider,
|
|
234
|
+
}));
|
|
235
|
+
} else {
|
|
236
|
+
// Single-node mode: use LMDB-backed local signing protection.
|
|
237
|
+
// This prevents double-signing if the node crashes and restarts mid-proposal.
|
|
238
|
+
({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
|
|
239
|
+
telemetryClient: telemetry,
|
|
240
|
+
dateProvider,
|
|
241
|
+
}));
|
|
216
242
|
}
|
|
243
|
+
const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
|
|
217
244
|
|
|
218
245
|
const validator = new ValidatorClient(
|
|
219
246
|
validatorKeyStore,
|
|
220
247
|
epochCache,
|
|
221
248
|
p2pClient,
|
|
222
|
-
|
|
249
|
+
proposalHandler,
|
|
223
250
|
blockSource,
|
|
224
251
|
checkpointsBuilder,
|
|
225
252
|
worldState,
|
|
226
253
|
l1ToL2MessageSource,
|
|
227
254
|
config,
|
|
228
255
|
blobClient,
|
|
256
|
+
slashingProtectionSigner,
|
|
229
257
|
dateProvider,
|
|
230
258
|
telemetry,
|
|
231
259
|
);
|
|
@@ -239,8 +267,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
239
267
|
.filter(addr => !this.config.disabledValidators.some(disabled => disabled.equals(addr)));
|
|
240
268
|
}
|
|
241
269
|
|
|
242
|
-
public
|
|
243
|
-
return this.
|
|
270
|
+
public getProposalHandler() {
|
|
271
|
+
return this.proposalHandler;
|
|
244
272
|
}
|
|
245
273
|
|
|
246
274
|
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition, context: SigningContext) {
|
|
@@ -263,6 +291,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
263
291
|
this.config = { ...this.config, ...config };
|
|
264
292
|
}
|
|
265
293
|
|
|
294
|
+
public reloadKeystore(newManager: KeystoreManager): void {
|
|
295
|
+
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
296
|
+
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
297
|
+
this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
|
|
298
|
+
}
|
|
299
|
+
|
|
266
300
|
public async start() {
|
|
267
301
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
268
302
|
this.log.warn(`Validator client already started`);
|
|
@@ -307,7 +341,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
307
341
|
checkpoint: CheckpointProposalCore,
|
|
308
342
|
proposalSender: PeerId,
|
|
309
343
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
310
|
-
this.p2pClient.
|
|
344
|
+
this.p2pClient.registerValidatorCheckpointProposalHandler(checkpointHandler);
|
|
345
|
+
|
|
346
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
347
|
+
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
348
|
+
this.handleDuplicateProposal(info);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
352
|
+
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
353
|
+
this.handleDuplicateAttestation(info);
|
|
354
|
+
});
|
|
311
355
|
|
|
312
356
|
const myAddresses = this.getValidatorAddresses();
|
|
313
357
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
@@ -336,6 +380,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
336
380
|
return false;
|
|
337
381
|
}
|
|
338
382
|
|
|
383
|
+
// Log self-proposals from HA peers (same validator key on different nodes)
|
|
384
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
385
|
+
this.log.verbose(`Processing block proposal from HA peer for slot ${slotNumber}`, {
|
|
386
|
+
proposer: proposer.toString(),
|
|
387
|
+
slotNumber,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
339
391
|
// Check if we're in the committee (for metrics purposes)
|
|
340
392
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
341
393
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -349,25 +401,25 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
349
401
|
|
|
350
402
|
// Reexecute txs if we are part of the committee, or if slashing is enabled, or if we are configured to always reexecute.
|
|
351
403
|
// In fisherman mode, we always reexecute to validate proposals.
|
|
352
|
-
const {
|
|
353
|
-
this.config;
|
|
404
|
+
const { slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
|
|
354
405
|
const shouldReexecute =
|
|
355
406
|
fishermanMode ||
|
|
356
|
-
|
|
357
|
-
|
|
407
|
+
slashBroadcastedInvalidBlockPenalty > 0n ||
|
|
408
|
+
partOfCommittee ||
|
|
358
409
|
alwaysReexecuteBlockProposals ||
|
|
359
410
|
this.blobClient.canUpload();
|
|
360
411
|
|
|
361
|
-
const validationResult = await this.
|
|
412
|
+
const validationResult = await this.proposalHandler.handleBlockProposal(
|
|
362
413
|
proposal,
|
|
363
414
|
proposalSender,
|
|
364
415
|
!!shouldReexecute && !escapeHatchOpen,
|
|
365
416
|
);
|
|
366
417
|
|
|
367
418
|
if (!validationResult.isValid) {
|
|
368
|
-
this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
369
|
-
|
|
370
419
|
const reason = validationResult.reason || 'unknown';
|
|
420
|
+
|
|
421
|
+
this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
|
|
422
|
+
|
|
371
423
|
// Classify failure reason: bad proposal vs node issue
|
|
372
424
|
const badProposalReasons: BlockProposalValidationFailureReason[] = [
|
|
373
425
|
'invalid_proposal',
|
|
@@ -422,51 +474,51 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
422
474
|
proposal: CheckpointProposalCore,
|
|
423
475
|
_proposalSender: PeerId,
|
|
424
476
|
): Promise<CheckpointAttestation[] | undefined> {
|
|
425
|
-
const
|
|
477
|
+
const proposalSlotNumber = proposal.slotNumber;
|
|
426
478
|
const proposer = proposal.getSender();
|
|
427
479
|
|
|
428
480
|
// 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 ${
|
|
481
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(proposalSlotNumber)) {
|
|
482
|
+
this.log.warn(`Escape hatch open for slot ${proposalSlotNumber}, skipping checkpoint attestation handling`);
|
|
431
483
|
return undefined;
|
|
432
484
|
}
|
|
433
485
|
|
|
434
|
-
//
|
|
435
|
-
if (
|
|
436
|
-
this.log.
|
|
486
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
487
|
+
if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
488
|
+
this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
|
|
489
|
+
proposer: proposer.toString(),
|
|
490
|
+
proposalSlotNumber,
|
|
491
|
+
});
|
|
437
492
|
return undefined;
|
|
438
493
|
}
|
|
439
494
|
|
|
440
|
-
// Check that I have any address in
|
|
441
|
-
const inCommittee = await this.epochCache.filterInCommittee(
|
|
495
|
+
// Check that I have any address in the committee where this checkpoint will land before attesting
|
|
496
|
+
const inCommittee = await this.epochCache.filterInCommittee(proposalSlotNumber, this.getValidatorAddresses());
|
|
442
497
|
const partOfCommittee = inCommittee.length > 0;
|
|
443
498
|
|
|
444
499
|
const proposalInfo = {
|
|
445
|
-
|
|
500
|
+
proposalSlotNumber,
|
|
446
501
|
archive: proposal.archive.toString(),
|
|
447
|
-
proposer: proposer
|
|
448
|
-
txCount: proposal.txHashes.length,
|
|
502
|
+
proposer: proposer?.toString(),
|
|
449
503
|
};
|
|
450
|
-
this.log.info(`Received checkpoint proposal for slot ${
|
|
504
|
+
this.log.info(`Received checkpoint proposal for slot ${proposalSlotNumber}`, {
|
|
451
505
|
...proposalInfo,
|
|
452
|
-
txHashes: proposal.txHashes.map(t => t.toString()),
|
|
453
506
|
fishermanMode: this.config.fishermanMode || false,
|
|
454
507
|
});
|
|
455
508
|
|
|
456
|
-
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
509
|
+
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
|
|
510
|
+
// Uses the cached result from the all-nodes callback if available (avoids double validation).
|
|
511
|
+
let checkpointNumber: CheckpointNumber;
|
|
457
512
|
if (this.config.skipCheckpointProposalValidation) {
|
|
458
|
-
this.log.warn(`Skipping checkpoint proposal validation for slot ${
|
|
513
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
|
|
514
|
+
checkpointNumber = CheckpointNumber(0);
|
|
459
515
|
} else {
|
|
460
|
-
const validationResult = await this.
|
|
516
|
+
const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
|
|
461
517
|
if (!validationResult.isValid) {
|
|
462
518
|
this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
463
519
|
return undefined;
|
|
464
520
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
// Upload blobs to filestore if we can (fire and forget)
|
|
468
|
-
if (this.blobClient.canUpload()) {
|
|
469
|
-
void this.uploadBlobsForCheckpoint(proposal, proposalInfo);
|
|
521
|
+
checkpointNumber = validationResult.checkpointNumber;
|
|
470
522
|
}
|
|
471
523
|
|
|
472
524
|
// Check that I have any address in current committee before attesting
|
|
@@ -477,14 +529,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
477
529
|
}
|
|
478
530
|
|
|
479
531
|
// Provided all of the above checks pass, we can attest to the proposal
|
|
480
|
-
this.log.info(
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
532
|
+
this.log.info(
|
|
533
|
+
`${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${proposalSlotNumber}`,
|
|
534
|
+
{
|
|
535
|
+
...proposalInfo,
|
|
536
|
+
inCommittee: partOfCommittee,
|
|
537
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
538
|
+
},
|
|
539
|
+
);
|
|
485
540
|
|
|
486
541
|
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
487
542
|
|
|
543
|
+
// Track epoch participation per attester: count each (attester, epoch) pair at most once
|
|
544
|
+
const proposalEpoch = getEpochAtSlot(proposalSlotNumber, this.epochCache.getL1Constants());
|
|
545
|
+
for (const attester of inCommittee) {
|
|
546
|
+
const key = attester.toString();
|
|
547
|
+
const lastEpoch = this.lastAttestedEpochByAttester.get(key);
|
|
548
|
+
if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
|
|
549
|
+
this.lastAttestedEpochByAttester.set(key, proposalEpoch);
|
|
550
|
+
this.metrics.incAttestedEpochCount(attester);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
488
554
|
// Determine which validators should attest
|
|
489
555
|
let attestors: EthAddress[];
|
|
490
556
|
if (partOfCommittee) {
|
|
@@ -503,172 +569,60 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
503
569
|
|
|
504
570
|
if (this.config.fishermanMode) {
|
|
505
571
|
// bail out early and don't save attestations to the pool in fisherman mode
|
|
506
|
-
this.log.info(`Creating checkpoint attestations for slot ${
|
|
572
|
+
this.log.info(`Creating checkpoint attestations for slot ${proposalSlotNumber}`, {
|
|
507
573
|
...proposalInfo,
|
|
508
574
|
attestors: attestors.map(a => a.toString()),
|
|
509
575
|
});
|
|
510
576
|
return undefined;
|
|
511
577
|
}
|
|
512
578
|
|
|
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;
|
|
579
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
|
|
523
580
|
}
|
|
524
581
|
|
|
525
582
|
/**
|
|
526
|
-
*
|
|
527
|
-
* @returns
|
|
583
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
584
|
+
* @returns true if we should attest, false if we should skip
|
|
528
585
|
*/
|
|
529
|
-
private
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const timeoutSeconds = 10; // TODO(palla/mbps): This should map to the timetable settings
|
|
586
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
587
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
588
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
535
591
|
|
|
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,
|
|
592
|
+
// Check if incoming slot is strictly greater than last attested
|
|
593
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
594
|
+
this.log.warn(
|
|
595
|
+
`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
|
|
547
596
|
);
|
|
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' };
|
|
597
|
+
return false;
|
|
555
598
|
}
|
|
556
599
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
return { isValid: false, reason: 'last_block_not_found' };
|
|
560
|
-
}
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
561
602
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
603
|
+
private async createCheckpointAttestationsFromProposal(
|
|
604
|
+
proposal: CheckpointProposalCore,
|
|
605
|
+
attestors: EthAddress[] = [],
|
|
606
|
+
checkpointNumber: CheckpointNumber,
|
|
607
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
608
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
609
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
610
|
+
return undefined;
|
|
567
611
|
}
|
|
568
612
|
|
|
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;
|
|
613
|
+
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
|
|
578
614
|
|
|
579
|
-
//
|
|
580
|
-
|
|
615
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
616
|
+
this.lastAttestedProposal = proposal;
|
|
581
617
|
|
|
582
|
-
|
|
583
|
-
|
|
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());
|
|
590
|
-
|
|
591
|
-
// Fork world state at the block before the first block
|
|
592
|
-
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
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
|
-
);
|
|
606
|
-
|
|
607
|
-
// Complete the checkpoint to get computed values
|
|
608
|
-
const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
|
|
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
|
-
};
|
|
618
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
619
|
+
return attestations;
|
|
666
620
|
}
|
|
667
621
|
|
|
668
622
|
/**
|
|
669
623
|
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
670
624
|
*/
|
|
671
|
-
|
|
625
|
+
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
672
626
|
try {
|
|
673
627
|
const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
674
628
|
if (!lastBlockHeader) {
|
|
@@ -683,7 +637,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
683
637
|
}
|
|
684
638
|
|
|
685
639
|
const blobFields = blocks.flatMap(b => b.toBlobFields());
|
|
686
|
-
const blobs: Blob[] = getBlobsPerL1Block(blobFields);
|
|
640
|
+
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
687
641
|
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
688
642
|
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
689
643
|
...proposalInfo,
|
|
@@ -721,8 +675,55 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
721
675
|
]);
|
|
722
676
|
}
|
|
723
677
|
|
|
678
|
+
/**
|
|
679
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
680
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
681
|
+
*/
|
|
682
|
+
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
683
|
+
const { slot, proposer, type } = info;
|
|
684
|
+
|
|
685
|
+
this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
|
|
686
|
+
proposer: proposer.toString(),
|
|
687
|
+
slot,
|
|
688
|
+
type,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Emit slash event
|
|
692
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
693
|
+
{
|
|
694
|
+
validator: proposer,
|
|
695
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
696
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
697
|
+
epochOrSlot: BigInt(slot),
|
|
698
|
+
},
|
|
699
|
+
]);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
704
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
705
|
+
*/
|
|
706
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
707
|
+
const { slot, attester } = info;
|
|
708
|
+
|
|
709
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
710
|
+
attester: attester.toString(),
|
|
711
|
+
slot,
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
715
|
+
{
|
|
716
|
+
validator: attester,
|
|
717
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
718
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
719
|
+
epochOrSlot: BigInt(slot),
|
|
720
|
+
},
|
|
721
|
+
]);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
724
|
async createBlockProposal(
|
|
725
725
|
blockHeader: BlockHeader,
|
|
726
|
+
checkpointNumber: CheckpointNumber,
|
|
726
727
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
727
728
|
inHash: Fr,
|
|
728
729
|
archive: Fr,
|
|
@@ -730,17 +731,26 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
730
731
|
proposerAddress: EthAddress | undefined,
|
|
731
732
|
options: BlockProposalOptions = {},
|
|
732
733
|
): Promise<BlockProposal> {
|
|
733
|
-
//
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
734
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
735
|
+
if (this.lastProposedBlock) {
|
|
736
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
737
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
738
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
739
|
+
|
|
740
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
741
|
+
throw new Error(
|
|
742
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
743
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
738
747
|
|
|
739
748
|
this.log.info(
|
|
740
749
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
741
750
|
);
|
|
742
751
|
const newProposal = await this.validationService.createBlockProposal(
|
|
743
752
|
blockHeader,
|
|
753
|
+
checkpointNumber,
|
|
744
754
|
indexWithinCheckpoint,
|
|
745
755
|
inHash,
|
|
746
756
|
archive,
|
|
@@ -751,25 +761,44 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
751
761
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
752
762
|
},
|
|
753
763
|
);
|
|
754
|
-
this.
|
|
764
|
+
this.lastProposedBlock = newProposal;
|
|
755
765
|
return newProposal;
|
|
756
766
|
}
|
|
757
767
|
|
|
758
768
|
async createCheckpointProposal(
|
|
759
769
|
checkpointHeader: CheckpointHeader,
|
|
760
770
|
archive: Fr,
|
|
761
|
-
|
|
771
|
+
checkpointNumber: CheckpointNumber,
|
|
772
|
+
feeAssetPriceModifier: bigint,
|
|
773
|
+
lastBlockProposal: BlockProposal | undefined,
|
|
762
774
|
proposerAddress: EthAddress | undefined,
|
|
763
775
|
options: CheckpointProposalOptions = {},
|
|
764
776
|
): Promise<CheckpointProposal> {
|
|
777
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
778
|
+
if (this.lastProposedCheckpoint) {
|
|
779
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
780
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
781
|
+
|
|
782
|
+
if (newSlot <= lastSlot) {
|
|
783
|
+
throw new Error(
|
|
784
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
785
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
765
790
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
766
|
-
|
|
791
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
767
792
|
checkpointHeader,
|
|
768
793
|
archive,
|
|
769
|
-
|
|
794
|
+
checkpointNumber,
|
|
795
|
+
feeAssetPriceModifier,
|
|
796
|
+
lastBlockProposal,
|
|
770
797
|
proposerAddress,
|
|
771
798
|
options,
|
|
772
799
|
);
|
|
800
|
+
this.lastProposedCheckpoint = newProposal;
|
|
801
|
+
return newProposal;
|
|
773
802
|
}
|
|
774
803
|
|
|
775
804
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -780,16 +809,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
780
809
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
781
810
|
proposer: EthAddress,
|
|
782
811
|
slot: SlotNumber,
|
|
783
|
-
|
|
812
|
+
checkpointNumber: CheckpointNumber,
|
|
784
813
|
): Promise<Signature> {
|
|
785
|
-
return await this.validationService.signAttestationsAndSigners(
|
|
814
|
+
return await this.validationService.signAttestationsAndSigners(
|
|
815
|
+
attestationsAndSigners,
|
|
816
|
+
proposer,
|
|
817
|
+
slot,
|
|
818
|
+
checkpointNumber,
|
|
819
|
+
);
|
|
786
820
|
}
|
|
787
821
|
|
|
788
|
-
async collectOwnAttestations(
|
|
822
|
+
async collectOwnAttestations(
|
|
823
|
+
proposal: CheckpointProposal,
|
|
824
|
+
checkpointNumber: CheckpointNumber,
|
|
825
|
+
): Promise<CheckpointAttestation[]> {
|
|
789
826
|
const slot = proposal.slotNumber;
|
|
790
827
|
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
791
828
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
792
|
-
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
829
|
+
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
|
|
830
|
+
|
|
831
|
+
if (!attestations) {
|
|
832
|
+
return [];
|
|
833
|
+
}
|
|
793
834
|
|
|
794
835
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
795
836
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
@@ -804,6 +845,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
804
845
|
proposal: CheckpointProposal,
|
|
805
846
|
required: number,
|
|
806
847
|
deadline: Date,
|
|
848
|
+
checkpointNumber: CheckpointNumber,
|
|
807
849
|
): Promise<CheckpointAttestation[]> {
|
|
808
850
|
// Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
|
|
809
851
|
const slot = proposal.slotNumber;
|
|
@@ -816,7 +858,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
816
858
|
throw new AttestationTimeoutError(0, required, slot);
|
|
817
859
|
}
|
|
818
860
|
|
|
819
|
-
await this.collectOwnAttestations(proposal);
|
|
861
|
+
await this.collectOwnAttestations(proposal, checkpointNumber);
|
|
820
862
|
|
|
821
863
|
const proposalId = proposal.archive.toString();
|
|
822
864
|
const myAddresses = this.getValidatorAddresses();
|