@aztec/validator-client 0.0.1-commit.7d4e6cd → 0.0.1-commit.808bf7f90
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 +53 -24
- package/dest/block_proposal_handler.d.ts +9 -9
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +35 -54
- package/dest/checkpoint_builder.d.ts +24 -25
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +60 -34
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +12 -14
- package/dest/duties/validation_service.d.ts +20 -7
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +75 -22
- package/dest/factory.d.ts +2 -2
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +1 -1
- package/dest/key_store/ha_key_store.d.ts +99 -0
- package/dest/key_store/ha_key_store.d.ts.map +1 -0
- package/dest/key_store/ha_key_store.js +208 -0
- package/dest/key_store/index.d.ts +2 -1
- package/dest/key_store/index.d.ts.map +1 -1
- package/dest/key_store/index.js +1 -0
- package/dest/key_store/interface.d.ts +36 -6
- package/dest/key_store/interface.d.ts.map +1 -1
- package/dest/key_store/local_key_store.d.ts +10 -5
- package/dest/key_store/local_key_store.d.ts.map +1 -1
- package/dest/key_store/local_key_store.js +8 -4
- package/dest/key_store/node_keystore_adapter.d.ts +18 -5
- package/dest/key_store/node_keystore_adapter.d.ts.map +1 -1
- package/dest/key_store/node_keystore_adapter.js +18 -4
- package/dest/key_store/web3signer_key_store.d.ts +10 -5
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
- package/dest/key_store/web3signer_key_store.js +8 -4
- package/dest/metrics.d.ts +4 -3
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +34 -5
- package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.js +17 -16
- package/dest/validator.d.ts +43 -18
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +233 -94
- package/package.json +21 -17
- package/src/block_proposal_handler.ts +48 -69
- package/src/checkpoint_builder.ts +101 -38
- package/src/config.ts +11 -13
- package/src/duties/validation_service.ts +100 -25
- package/src/factory.ts +1 -0
- package/src/key_store/ha_key_store.ts +269 -0
- package/src/key_store/index.ts +1 -0
- package/src/key_store/interface.ts +44 -5
- package/src/key_store/local_key_store.ts +13 -4
- package/src/key_store/node_keystore_adapter.ts +27 -4
- package/src/key_store/web3signer_key_store.ts +17 -4
- package/src/metrics.ts +45 -6
- package/src/tx_validator/tx_validator_factory.ts +52 -31
- package/src/validator.ts +303 -114
package/src/validator.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
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 {
|
|
4
|
+
import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
|
|
5
|
+
import {
|
|
6
|
+
BlockNumber,
|
|
7
|
+
CheckpointNumber,
|
|
8
|
+
EpochNumber,
|
|
9
|
+
IndexWithinCheckpoint,
|
|
10
|
+
SlotNumber,
|
|
11
|
+
} from '@aztec/foundation/branded-types';
|
|
5
12
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
6
13
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
7
14
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
@@ -12,30 +19,35 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
|
12
19
|
import { sleep } from '@aztec/foundation/sleep';
|
|
13
20
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
14
21
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
15
|
-
import type { P2P, PeerId
|
|
22
|
+
import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
16
23
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
17
24
|
import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
|
|
18
25
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
19
|
-
import type { CommitteeAttestationsAndSigners,
|
|
26
|
+
import type { CommitteeAttestationsAndSigners, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
27
|
+
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
20
28
|
import type {
|
|
21
29
|
CreateCheckpointProposalLastBlockData,
|
|
30
|
+
ITxProvider,
|
|
22
31
|
Validator,
|
|
23
32
|
ValidatorClientFullConfig,
|
|
24
33
|
WorldStateSynchronizer,
|
|
25
34
|
} from '@aztec/stdlib/interfaces/server';
|
|
26
|
-
import type
|
|
27
|
-
import
|
|
28
|
-
BlockProposal,
|
|
29
|
-
BlockProposalOptions,
|
|
30
|
-
CheckpointAttestation,
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
import { type L1ToL2MessageSource, accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
36
|
+
import {
|
|
37
|
+
type BlockProposal,
|
|
38
|
+
type BlockProposalOptions,
|
|
39
|
+
type CheckpointAttestation,
|
|
40
|
+
CheckpointProposal,
|
|
41
|
+
type CheckpointProposalCore,
|
|
42
|
+
type CheckpointProposalOptions,
|
|
33
43
|
} from '@aztec/stdlib/p2p';
|
|
34
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
35
44
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
36
45
|
import type { BlockHeader, CheckpointGlobalVariables, Tx } from '@aztec/stdlib/tx';
|
|
37
46
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
38
47
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
48
|
+
import { createHASigner } from '@aztec/validator-ha-signer/factory';
|
|
49
|
+
import { DutyType, type SigningContext } from '@aztec/validator-ha-signer/types';
|
|
50
|
+
import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
|
|
39
51
|
|
|
40
52
|
import { EventEmitter } from 'events';
|
|
41
53
|
import type { TypedDataDefinition } from 'viem';
|
|
@@ -43,6 +55,8 @@ import type { TypedDataDefinition } from 'viem';
|
|
|
43
55
|
import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
|
|
44
56
|
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
45
57
|
import { ValidationService } from './duties/validation_service.js';
|
|
58
|
+
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
59
|
+
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
46
60
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
47
61
|
import { ValidatorMetrics } from './metrics.js';
|
|
48
62
|
|
|
@@ -64,25 +78,25 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
64
78
|
private validationService: ValidationService;
|
|
65
79
|
private metrics: ValidatorMetrics;
|
|
66
80
|
private log: Logger;
|
|
67
|
-
|
|
68
81
|
// Whether it has already registered handlers on the p2p client
|
|
69
82
|
private hasRegisteredHandlers = false;
|
|
70
83
|
|
|
71
|
-
|
|
72
|
-
private
|
|
84
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
|
|
85
|
+
private lastProposedBlock?: BlockProposal;
|
|
86
|
+
|
|
87
|
+
/** Tracks the last checkpoint proposal we created. */
|
|
88
|
+
private lastProposedCheckpoint?: CheckpointProposal;
|
|
73
89
|
|
|
74
90
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
75
91
|
private epochCacheUpdateLoop: RunningPromise;
|
|
76
92
|
|
|
77
93
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
78
94
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
82
|
-
private validatedBlockSlots: Set<SlotNumber> = new Set();
|
|
95
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
96
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
83
97
|
|
|
84
98
|
protected constructor(
|
|
85
|
-
private keyStore:
|
|
99
|
+
private keyStore: ExtendedValidatorKeyStore,
|
|
86
100
|
private epochCache: EpochCache,
|
|
87
101
|
private p2pClient: P2P,
|
|
88
102
|
private blockProposalHandler: BlockProposalHandler,
|
|
@@ -92,6 +106,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
92
106
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
93
107
|
private config: ValidatorClientFullConfig,
|
|
94
108
|
private blobClient: BlobClientInterface,
|
|
109
|
+
private haSigner: ValidatorHASigner | undefined,
|
|
95
110
|
private dateProvider: DateProvider = new DateProvider(),
|
|
96
111
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
97
112
|
log = createLogger('validator'),
|
|
@@ -165,7 +180,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
165
180
|
}
|
|
166
181
|
}
|
|
167
182
|
|
|
168
|
-
static new(
|
|
183
|
+
static async new(
|
|
169
184
|
config: ValidatorClientFullConfig,
|
|
170
185
|
checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
171
186
|
worldState: WorldStateSynchronizer,
|
|
@@ -173,7 +188,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
173
188
|
p2pClient: P2P,
|
|
174
189
|
blockSource: L2BlockSource & L2BlockSink,
|
|
175
190
|
l1ToL2MessageSource: L1ToL2MessageSource,
|
|
176
|
-
txProvider:
|
|
191
|
+
txProvider: ITxProvider,
|
|
177
192
|
keyStoreManager: KeystoreManager,
|
|
178
193
|
blobClient: BlobClientInterface,
|
|
179
194
|
dateProvider: DateProvider = new DateProvider(),
|
|
@@ -190,14 +205,29 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
190
205
|
l1ToL2MessageSource,
|
|
191
206
|
txProvider,
|
|
192
207
|
blockProposalValidator,
|
|
208
|
+
epochCache,
|
|
193
209
|
config,
|
|
194
210
|
metrics,
|
|
195
211
|
dateProvider,
|
|
196
212
|
telemetry,
|
|
197
213
|
);
|
|
198
214
|
|
|
215
|
+
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
216
|
+
let validatorKeyStore: ExtendedValidatorKeyStore = nodeKeystoreAdapter;
|
|
217
|
+
let haSigner: ValidatorHASigner | undefined;
|
|
218
|
+
if (config.haSigningEnabled) {
|
|
219
|
+
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
220
|
+
const haConfig = {
|
|
221
|
+
...config,
|
|
222
|
+
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
|
|
223
|
+
};
|
|
224
|
+
const { signer } = await createHASigner(haConfig, { telemetryClient: telemetry, dateProvider });
|
|
225
|
+
haSigner = signer;
|
|
226
|
+
validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, signer);
|
|
227
|
+
}
|
|
228
|
+
|
|
199
229
|
const validator = new ValidatorClient(
|
|
200
|
-
|
|
230
|
+
validatorKeyStore,
|
|
201
231
|
epochCache,
|
|
202
232
|
p2pClient,
|
|
203
233
|
blockProposalHandler,
|
|
@@ -207,6 +237,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
207
237
|
l1ToL2MessageSource,
|
|
208
238
|
config,
|
|
209
239
|
blobClient,
|
|
240
|
+
haSigner,
|
|
210
241
|
dateProvider,
|
|
211
242
|
telemetry,
|
|
212
243
|
);
|
|
@@ -224,8 +255,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
224
255
|
return this.blockProposalHandler;
|
|
225
256
|
}
|
|
226
257
|
|
|
227
|
-
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition) {
|
|
228
|
-
return this.keyStore.signTypedDataWithAddress(addr, msg);
|
|
258
|
+
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition, context: SigningContext) {
|
|
259
|
+
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
229
260
|
}
|
|
230
261
|
|
|
231
262
|
public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
|
|
@@ -244,12 +275,36 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
244
275
|
this.config = { ...this.config, ...config };
|
|
245
276
|
}
|
|
246
277
|
|
|
278
|
+
public reloadKeystore(newManager: KeystoreManager): void {
|
|
279
|
+
if (this.config.haSigningEnabled && !this.haSigner) {
|
|
280
|
+
this.log.warn(
|
|
281
|
+
'HA signing is enabled in config but was not initialized at startup. ' +
|
|
282
|
+
'Restart the node to enable HA signing.',
|
|
283
|
+
);
|
|
284
|
+
} else if (!this.config.haSigningEnabled && this.haSigner) {
|
|
285
|
+
this.log.warn(
|
|
286
|
+
'HA signing was disabled via config update but the HA signer is still active. ' +
|
|
287
|
+
'Restart the node to fully disable HA signing.',
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
292
|
+
if (this.haSigner) {
|
|
293
|
+
this.keyStore = new HAKeyStore(newAdapter, this.haSigner);
|
|
294
|
+
} else {
|
|
295
|
+
this.keyStore = newAdapter;
|
|
296
|
+
}
|
|
297
|
+
this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
|
|
298
|
+
}
|
|
299
|
+
|
|
247
300
|
public async start() {
|
|
248
301
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
249
302
|
this.log.warn(`Validator client already started`);
|
|
250
303
|
return;
|
|
251
304
|
}
|
|
252
305
|
|
|
306
|
+
await this.keyStore.start();
|
|
307
|
+
|
|
253
308
|
await this.registerHandlers();
|
|
254
309
|
|
|
255
310
|
const myAddresses = this.getValidatorAddresses();
|
|
@@ -265,6 +320,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
265
320
|
|
|
266
321
|
public async stop() {
|
|
267
322
|
await this.epochCacheUpdateLoop.stop();
|
|
323
|
+
await this.keyStore.stop();
|
|
268
324
|
}
|
|
269
325
|
|
|
270
326
|
/** Register handlers on the p2p client */
|
|
@@ -287,6 +343,16 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
287
343
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
288
344
|
this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
|
|
289
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
|
+
});
|
|
355
|
+
|
|
290
356
|
const myAddresses = this.getValidatorAddresses();
|
|
291
357
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
292
358
|
|
|
@@ -301,6 +367,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
301
367
|
*/
|
|
302
368
|
async validateBlockProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> {
|
|
303
369
|
const slotNumber = proposal.slotNumber;
|
|
370
|
+
|
|
371
|
+
// Note: During escape hatch, we still want to "validate" proposals for observability,
|
|
372
|
+
// but we intentionally reject them and disable slashing invalid block and attestation flow.
|
|
373
|
+
const escapeHatchOpen = await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber);
|
|
374
|
+
|
|
304
375
|
const proposer = proposal.getSender();
|
|
305
376
|
|
|
306
377
|
// Reject proposals with invalid signatures
|
|
@@ -309,6 +380,15 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
309
380
|
return false;
|
|
310
381
|
}
|
|
311
382
|
|
|
383
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
384
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
385
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
386
|
+
proposer: proposer.toString(),
|
|
387
|
+
slotNumber,
|
|
388
|
+
});
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
|
|
312
392
|
// Check if we're in the committee (for metrics purposes)
|
|
313
393
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
314
394
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -334,7 +414,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
334
414
|
const validationResult = await this.blockProposalHandler.handleBlockProposal(
|
|
335
415
|
proposal,
|
|
336
416
|
proposalSender,
|
|
337
|
-
!!shouldReexecute,
|
|
417
|
+
!!shouldReexecute && !escapeHatchOpen,
|
|
338
418
|
);
|
|
339
419
|
|
|
340
420
|
if (!validationResult.isValid) {
|
|
@@ -359,6 +439,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
359
439
|
|
|
360
440
|
// Slash invalid block proposals (can happen even when not in committee)
|
|
361
441
|
if (
|
|
442
|
+
!escapeHatchOpen &&
|
|
362
443
|
validationResult.reason &&
|
|
363
444
|
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) &&
|
|
364
445
|
slashBroadcastedInvalidBlockPenalty > 0n
|
|
@@ -373,11 +454,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
373
454
|
...proposalInfo,
|
|
374
455
|
inCommittee: partOfCommittee,
|
|
375
456
|
fishermanMode: this.config.fishermanMode || false,
|
|
457
|
+
escapeHatchOpen,
|
|
376
458
|
});
|
|
377
459
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
460
|
+
if (escapeHatchOpen) {
|
|
461
|
+
this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
381
464
|
|
|
382
465
|
return true;
|
|
383
466
|
}
|
|
@@ -395,12 +478,35 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
395
478
|
const slotNumber = proposal.slotNumber;
|
|
396
479
|
const proposer = proposal.getSender();
|
|
397
480
|
|
|
481
|
+
// If escape hatch is open for this slot's epoch, do not attest.
|
|
482
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber)) {
|
|
483
|
+
this.log.warn(`Escape hatch open for slot ${slotNumber}, skipping checkpoint attestation handling`);
|
|
484
|
+
return undefined;
|
|
485
|
+
}
|
|
486
|
+
|
|
398
487
|
// Reject proposals with invalid signatures
|
|
399
488
|
if (!proposer) {
|
|
400
489
|
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
|
|
401
490
|
return undefined;
|
|
402
491
|
}
|
|
403
492
|
|
|
493
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
494
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
495
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
496
|
+
proposer: proposer.toString(),
|
|
497
|
+
slotNumber,
|
|
498
|
+
});
|
|
499
|
+
return undefined;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Validate fee asset price modifier is within allowed range
|
|
503
|
+
if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
504
|
+
this.log.warn(
|
|
505
|
+
`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${slotNumber}`,
|
|
506
|
+
);
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
|
|
404
510
|
// Check that I have any address in current committee before attesting
|
|
405
511
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
406
512
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -417,17 +523,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
417
523
|
fishermanMode: this.config.fishermanMode || false,
|
|
418
524
|
});
|
|
419
525
|
|
|
420
|
-
// TODO(palla/mbps): Remove this once checkpoint validation is stable.
|
|
421
|
-
// Check that we have successfully validated a block for this slot before attesting to the checkpoint.
|
|
422
|
-
if (!this.validatedBlockSlots.has(slotNumber)) {
|
|
423
|
-
this.log.warn(`No validated block found for slot ${slotNumber}, refusing to attest to checkpoint`, proposalInfo);
|
|
424
|
-
return undefined;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
526
|
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
527
|
+
if (this.config.skipCheckpointProposalValidation) {
|
|
528
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
431
529
|
} else {
|
|
432
530
|
const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
|
|
433
531
|
if (!validationResult.isValid) {
|
|
@@ -482,15 +580,45 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
482
580
|
return undefined;
|
|
483
581
|
}
|
|
484
582
|
|
|
485
|
-
return this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
583
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
588
|
+
* @returns true if we should attest, false if we should skip
|
|
589
|
+
*/
|
|
590
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
591
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
592
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Check if incoming slot is strictly greater than last attested
|
|
597
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
598
|
+
this.log.warn(
|
|
599
|
+
`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
|
|
600
|
+
);
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return true;
|
|
486
605
|
}
|
|
487
606
|
|
|
488
607
|
private async createCheckpointAttestationsFromProposal(
|
|
489
608
|
proposal: CheckpointProposalCore,
|
|
490
609
|
attestors: EthAddress[] = [],
|
|
491
|
-
): Promise<CheckpointAttestation[]> {
|
|
610
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
611
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
612
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
613
|
+
return undefined;
|
|
614
|
+
}
|
|
615
|
+
|
|
492
616
|
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
493
|
-
|
|
617
|
+
|
|
618
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
619
|
+
this.lastAttestedProposal = proposal;
|
|
620
|
+
|
|
621
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
494
622
|
return attestations;
|
|
495
623
|
}
|
|
496
624
|
|
|
@@ -503,7 +631,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
503
631
|
proposalInfo: LogData,
|
|
504
632
|
): Promise<{ isValid: true } | { isValid: false; reason: string }> {
|
|
505
633
|
const slot = proposal.slotNumber;
|
|
506
|
-
|
|
634
|
+
|
|
635
|
+
// Timeout block syncing at the start of the next slot
|
|
636
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
637
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
638
|
+
const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
|
|
507
639
|
|
|
508
640
|
// Wait for last block to sync by archive
|
|
509
641
|
let lastBlockHeader: BlockHeader | undefined;
|
|
@@ -531,21 +663,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
531
663
|
return { isValid: false, reason: 'last_block_not_found' };
|
|
532
664
|
}
|
|
533
665
|
|
|
534
|
-
// Get the last full block to determine checkpoint number
|
|
535
|
-
const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
|
|
536
|
-
if (!lastBlock) {
|
|
537
|
-
this.log.warn(`Last block ${lastBlockHeader.getBlockNumber()} not found`, proposalInfo);
|
|
538
|
-
return { isValid: false, reason: 'last_block_not_found' };
|
|
539
|
-
}
|
|
540
|
-
const checkpointNumber = lastBlock.checkpointNumber;
|
|
541
|
-
|
|
542
666
|
// Get all full blocks for the slot and checkpoint
|
|
543
|
-
const blocks = await this.getBlocksForSlot(slot
|
|
667
|
+
const blocks = await this.blockSource.getBlocksForSlot(slot);
|
|
544
668
|
if (blocks.length === 0) {
|
|
545
669
|
this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
|
|
546
670
|
return { isValid: false, reason: 'no_blocks_for_slot' };
|
|
547
671
|
}
|
|
548
672
|
|
|
673
|
+
// Ensure the last block for this slot matches the archive in the checkpoint proposal
|
|
674
|
+
if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
|
|
675
|
+
this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
|
|
676
|
+
return { isValid: false, reason: 'last_block_archive_mismatch' };
|
|
677
|
+
}
|
|
678
|
+
|
|
549
679
|
this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
|
|
550
680
|
...proposalInfo,
|
|
551
681
|
blockNumbers: blocks.map(b => b.number),
|
|
@@ -554,10 +684,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
554
684
|
// Get checkpoint constants from first block
|
|
555
685
|
const firstBlock = blocks[0];
|
|
556
686
|
const constants = this.extractCheckpointConstants(firstBlock);
|
|
687
|
+
const checkpointNumber = firstBlock.checkpointNumber;
|
|
557
688
|
|
|
558
689
|
// Get L1-to-L2 messages for this checkpoint
|
|
559
690
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
560
691
|
|
|
692
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
693
|
+
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
694
|
+
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch))
|
|
695
|
+
.filter(c => c.checkpointNumber < checkpointNumber)
|
|
696
|
+
.map(c => c.checkpointOutHash);
|
|
697
|
+
|
|
561
698
|
// Fork world state at the block before the first block
|
|
562
699
|
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
563
700
|
const fork = await this.worldState.fork(parentBlockNumber);
|
|
@@ -567,9 +704,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
567
704
|
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
568
705
|
checkpointNumber,
|
|
569
706
|
constants,
|
|
707
|
+
proposal.feeAssetPriceModifier,
|
|
570
708
|
l1ToL2Messages,
|
|
709
|
+
previousCheckpointOutHashes,
|
|
571
710
|
fork,
|
|
572
711
|
blocks,
|
|
712
|
+
this.log.getBindings(),
|
|
573
713
|
);
|
|
574
714
|
|
|
575
715
|
// Complete the checkpoint to get computed values
|
|
@@ -595,6 +735,22 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
595
735
|
return { isValid: false, reason: 'archive_mismatch' };
|
|
596
736
|
}
|
|
597
737
|
|
|
738
|
+
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
739
|
+
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
740
|
+
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
741
|
+
const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
|
|
742
|
+
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
743
|
+
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
744
|
+
this.log.warn(`Epoch out hash mismatch`, {
|
|
745
|
+
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
746
|
+
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
747
|
+
checkpointOutHash: checkpointOutHash.toString(),
|
|
748
|
+
previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
|
|
749
|
+
...proposalInfo,
|
|
750
|
+
});
|
|
751
|
+
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
752
|
+
}
|
|
753
|
+
|
|
598
754
|
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
599
755
|
return { isValid: true };
|
|
600
756
|
} finally {
|
|
@@ -602,55 +758,16 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
602
758
|
}
|
|
603
759
|
}
|
|
604
760
|
|
|
605
|
-
/**
|
|
606
|
-
* Get all full blocks for a given slot and checkpoint by walking backwards from the last block.
|
|
607
|
-
* Returns blocks in ascending order (earliest to latest).
|
|
608
|
-
* TODO(palla/mbps): Add getL2BlocksForSlot() to L2BlockSource interface for efficiency.
|
|
609
|
-
*/
|
|
610
|
-
private async getBlocksForSlot(
|
|
611
|
-
slot: SlotNumber,
|
|
612
|
-
lastBlockHeader: BlockHeader,
|
|
613
|
-
checkpointNumber: CheckpointNumber,
|
|
614
|
-
): Promise<L2BlockNew[]> {
|
|
615
|
-
const blocks: L2BlockNew[] = [];
|
|
616
|
-
let currentHeader = lastBlockHeader;
|
|
617
|
-
const { genesisArchiveRoot } = await this.blockSource.getGenesisValues();
|
|
618
|
-
|
|
619
|
-
while (currentHeader.getSlot() === slot) {
|
|
620
|
-
const block = await this.blockSource.getL2BlockNew(currentHeader.getBlockNumber());
|
|
621
|
-
if (!block) {
|
|
622
|
-
this.log.warn(`Block ${currentHeader.getBlockNumber()} not found while getting blocks for slot ${slot}`);
|
|
623
|
-
break;
|
|
624
|
-
}
|
|
625
|
-
if (block.checkpointNumber !== checkpointNumber) {
|
|
626
|
-
break;
|
|
627
|
-
}
|
|
628
|
-
blocks.unshift(block);
|
|
629
|
-
|
|
630
|
-
const prevArchive = currentHeader.lastArchive.root;
|
|
631
|
-
if (prevArchive.equals(genesisArchiveRoot)) {
|
|
632
|
-
break;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
const prevHeader = await this.blockSource.getBlockHeaderByArchive(prevArchive);
|
|
636
|
-
if (!prevHeader || prevHeader.getSlot() !== slot) {
|
|
637
|
-
break;
|
|
638
|
-
}
|
|
639
|
-
currentHeader = prevHeader;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
return blocks;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
761
|
/**
|
|
646
762
|
* Extract checkpoint global variables from a block.
|
|
647
763
|
*/
|
|
648
|
-
private extractCheckpointConstants(block:
|
|
764
|
+
private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
|
|
649
765
|
const gv = block.header.globalVariables;
|
|
650
766
|
return {
|
|
651
767
|
chainId: gv.chainId,
|
|
652
768
|
version: gv.version,
|
|
653
769
|
slotNumber: gv.slotNumber,
|
|
770
|
+
timestamp: gv.timestamp,
|
|
654
771
|
coinbase: gv.coinbase,
|
|
655
772
|
feeRecipient: gv.feeRecipient,
|
|
656
773
|
gasFees: gv.gasFees,
|
|
@@ -660,7 +777,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
660
777
|
/**
|
|
661
778
|
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
662
779
|
*/
|
|
663
|
-
|
|
780
|
+
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
664
781
|
try {
|
|
665
782
|
const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
666
783
|
if (!lastBlockHeader) {
|
|
@@ -668,21 +785,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
668
785
|
return;
|
|
669
786
|
}
|
|
670
787
|
|
|
671
|
-
|
|
672
|
-
const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
|
|
673
|
-
if (!lastBlock) {
|
|
674
|
-
this.log.warn(`Failed to get last block for blob upload`, proposalInfo);
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
const blocks = await this.getBlocksForSlot(proposal.slotNumber, lastBlockHeader, lastBlock.checkpointNumber);
|
|
788
|
+
const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
|
|
679
789
|
if (blocks.length === 0) {
|
|
680
790
|
this.log.warn(`No blocks found for blob upload`, proposalInfo);
|
|
681
791
|
return;
|
|
682
792
|
}
|
|
683
793
|
|
|
684
794
|
const blobFields = blocks.flatMap(b => b.toBlobFields());
|
|
685
|
-
const blobs: Blob[] = getBlobsPerL1Block(blobFields);
|
|
795
|
+
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
686
796
|
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
687
797
|
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
688
798
|
...proposalInfo,
|
|
@@ -720,20 +830,74 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
720
830
|
]);
|
|
721
831
|
}
|
|
722
832
|
|
|
833
|
+
/**
|
|
834
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
835
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
836
|
+
*/
|
|
837
|
+
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
838
|
+
const { slot, proposer, type } = info;
|
|
839
|
+
|
|
840
|
+
this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
|
|
841
|
+
proposer: proposer.toString(),
|
|
842
|
+
slot,
|
|
843
|
+
type,
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
// Emit slash event
|
|
847
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
848
|
+
{
|
|
849
|
+
validator: proposer,
|
|
850
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
851
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
852
|
+
epochOrSlot: BigInt(slot),
|
|
853
|
+
},
|
|
854
|
+
]);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
859
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
860
|
+
*/
|
|
861
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
862
|
+
const { slot, attester } = info;
|
|
863
|
+
|
|
864
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
865
|
+
attester: attester.toString(),
|
|
866
|
+
slot,
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
870
|
+
{
|
|
871
|
+
validator: attester,
|
|
872
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
873
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
874
|
+
epochOrSlot: BigInt(slot),
|
|
875
|
+
},
|
|
876
|
+
]);
|
|
877
|
+
}
|
|
878
|
+
|
|
723
879
|
async createBlockProposal(
|
|
724
880
|
blockHeader: BlockHeader,
|
|
725
|
-
indexWithinCheckpoint:
|
|
881
|
+
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
726
882
|
inHash: Fr,
|
|
727
883
|
archive: Fr,
|
|
728
884
|
txs: Tx[],
|
|
729
885
|
proposerAddress: EthAddress | undefined,
|
|
730
|
-
options: BlockProposalOptions,
|
|
886
|
+
options: BlockProposalOptions = {},
|
|
731
887
|
): Promise<BlockProposal> {
|
|
732
|
-
//
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
888
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
889
|
+
if (this.lastProposedBlock) {
|
|
890
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
891
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
892
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
893
|
+
|
|
894
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
895
|
+
throw new Error(
|
|
896
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
897
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
737
901
|
|
|
738
902
|
this.log.info(
|
|
739
903
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
@@ -750,25 +914,42 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
750
914
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
751
915
|
},
|
|
752
916
|
);
|
|
753
|
-
this.
|
|
917
|
+
this.lastProposedBlock = newProposal;
|
|
754
918
|
return newProposal;
|
|
755
919
|
}
|
|
756
920
|
|
|
757
921
|
async createCheckpointProposal(
|
|
758
922
|
checkpointHeader: CheckpointHeader,
|
|
759
923
|
archive: Fr,
|
|
924
|
+
feeAssetPriceModifier: bigint,
|
|
760
925
|
lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
|
|
761
926
|
proposerAddress: EthAddress | undefined,
|
|
762
|
-
options: CheckpointProposalOptions,
|
|
927
|
+
options: CheckpointProposalOptions = {},
|
|
763
928
|
): Promise<CheckpointProposal> {
|
|
929
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
930
|
+
if (this.lastProposedCheckpoint) {
|
|
931
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
932
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
933
|
+
|
|
934
|
+
if (newSlot <= lastSlot) {
|
|
935
|
+
throw new Error(
|
|
936
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
937
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
764
942
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
765
|
-
|
|
943
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
766
944
|
checkpointHeader,
|
|
767
945
|
archive,
|
|
946
|
+
feeAssetPriceModifier,
|
|
768
947
|
lastBlockInfo,
|
|
769
948
|
proposerAddress,
|
|
770
949
|
options,
|
|
771
950
|
);
|
|
951
|
+
this.lastProposedCheckpoint = newProposal;
|
|
952
|
+
return newProposal;
|
|
772
953
|
}
|
|
773
954
|
|
|
774
955
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -778,8 +959,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
778
959
|
async signAttestationsAndSigners(
|
|
779
960
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
780
961
|
proposer: EthAddress,
|
|
962
|
+
slot: SlotNumber,
|
|
963
|
+
blockNumber: BlockNumber | CheckpointNumber,
|
|
781
964
|
): Promise<Signature> {
|
|
782
|
-
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
|
|
965
|
+
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
|
|
783
966
|
}
|
|
784
967
|
|
|
785
968
|
async collectOwnAttestations(proposal: CheckpointProposal): Promise<CheckpointAttestation[]> {
|
|
@@ -788,6 +971,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
788
971
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
789
972
|
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
790
973
|
|
|
974
|
+
if (!attestations) {
|
|
975
|
+
return [];
|
|
976
|
+
}
|
|
977
|
+
|
|
791
978
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
792
979
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
793
980
|
// due to inactivity for missed attestations.
|
|
@@ -886,7 +1073,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
886
1073
|
}
|
|
887
1074
|
|
|
888
1075
|
const payloadToSign = authRequest.getPayloadToSign();
|
|
889
|
-
|
|
1076
|
+
// AUTH_REQUEST doesn't require HA protection - multiple signatures are safe
|
|
1077
|
+
const context: SigningContext = { dutyType: DutyType.AUTH_REQUEST };
|
|
1078
|
+
const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign, context);
|
|
890
1079
|
const authResponse = new AuthResponse(statusMessage, signature);
|
|
891
1080
|
return authResponse.toBuffer();
|
|
892
1081
|
}
|