@aztec/validator-client 0.0.1-commit.d1f2d6c → 0.0.1-commit.d20b825a7
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 +26 -14
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +136 -45
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +28 -9
- 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.d.ts +1 -1
- package/dest/key_store/ha_key_store.d.ts.map +1 -1
- package/dest/key_store/ha_key_store.js +3 -3
- package/dest/metrics.d.ts +16 -3
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +58 -5
- 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 -24
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +194 -206
- package/package.json +19 -17
- package/src/checkpoint_builder.ts +183 -50
- package/src/config.ts +28 -9
- 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 +3 -3
- package/src/metrics.ts +81 -6
- package/src/proposal_handler.ts +1042 -0
- package/src/validator.ts +272 -241
- package/dest/block_proposal_handler.d.ts +0 -64
- package/dest/block_proposal_handler.d.ts.map +0 -1
- package/dest/block_proposal_handler.js +0 -545
- 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 -18
- 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 -554
- 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 -135
package/src/validator.ts
CHANGED
|
@@ -1,61 +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
|
|
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
|
-
|
|
20
|
+
ITxProvider,
|
|
29
21
|
Validator,
|
|
30
22
|
ValidatorClientFullConfig,
|
|
31
23
|
WorldStateSynchronizer,
|
|
32
24
|
} from '@aztec/stdlib/interfaces/server';
|
|
33
25
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
34
|
-
import
|
|
35
|
-
BlockProposal,
|
|
36
|
-
BlockProposalOptions,
|
|
37
|
-
CheckpointAttestation,
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
import {
|
|
27
|
+
type BlockProposal,
|
|
28
|
+
type BlockProposalOptions,
|
|
29
|
+
type CheckpointAttestation,
|
|
30
|
+
CheckpointProposal,
|
|
31
|
+
type CheckpointProposalCore,
|
|
32
|
+
type CheckpointProposalOptions,
|
|
40
33
|
} from '@aztec/stdlib/p2p';
|
|
41
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
42
34
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
43
|
-
import type { BlockHeader,
|
|
35
|
+
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
|
|
44
36
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
45
37
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
46
|
-
import {
|
|
47
|
-
|
|
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';
|
|
48
45
|
|
|
49
46
|
import { EventEmitter } from 'events';
|
|
50
47
|
import type { TypedDataDefinition } from 'viem';
|
|
51
48
|
|
|
52
|
-
import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
|
|
53
49
|
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
54
50
|
import { ValidationService } from './duties/validation_service.js';
|
|
55
51
|
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
56
52
|
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
57
53
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
58
54
|
import { ValidatorMetrics } from './metrics.js';
|
|
55
|
+
import { type BlockProposalValidationFailureReason, ProposalHandler } from './proposal_handler.js';
|
|
59
56
|
|
|
60
57
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
61
58
|
// Just cap the set to avoid unbounded growth.
|
|
@@ -75,34 +72,37 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
75
72
|
private validationService: ValidationService;
|
|
76
73
|
private metrics: ValidatorMetrics;
|
|
77
74
|
private log: Logger;
|
|
78
|
-
|
|
79
75
|
// Whether it has already registered handlers on the p2p client
|
|
80
76
|
private hasRegisteredHandlers = false;
|
|
81
77
|
|
|
82
|
-
|
|
83
|
-
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;
|
|
84
83
|
|
|
85
84
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
86
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();
|
|
87
88
|
|
|
88
89
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
89
90
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
93
|
-
private validatedBlockSlots: Set<SlotNumber> = new Set();
|
|
91
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
92
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
94
93
|
|
|
95
94
|
protected constructor(
|
|
96
95
|
private keyStore: ExtendedValidatorKeyStore,
|
|
97
96
|
private epochCache: EpochCache,
|
|
98
97
|
private p2pClient: P2P,
|
|
99
|
-
private
|
|
98
|
+
private proposalHandler: ProposalHandler,
|
|
100
99
|
private blockSource: L2BlockSource,
|
|
101
100
|
private checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
102
101
|
private worldState: WorldStateSynchronizer,
|
|
103
102
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
104
103
|
private config: ValidatorClientFullConfig,
|
|
105
104
|
private blobClient: BlobClientInterface,
|
|
105
|
+
private slashingProtectionSigner: ValidatorHASigner,
|
|
106
106
|
private dateProvider: DateProvider = new DateProvider(),
|
|
107
107
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
108
108
|
log = createLogger('validator'),
|
|
@@ -156,6 +156,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
156
156
|
this.log.trace(`No committee found for slot`);
|
|
157
157
|
return;
|
|
158
158
|
}
|
|
159
|
+
this.metrics.setCurrentEpoch(epoch);
|
|
159
160
|
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
160
161
|
const me = this.getValidatorAddresses();
|
|
161
162
|
const committeeSet = new Set(committee.map(v => v.toString()));
|
|
@@ -184,17 +185,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
184
185
|
p2pClient: P2P,
|
|
185
186
|
blockSource: L2BlockSource & L2BlockSink,
|
|
186
187
|
l1ToL2MessageSource: L1ToL2MessageSource,
|
|
187
|
-
txProvider:
|
|
188
|
+
txProvider: ITxProvider,
|
|
188
189
|
keyStoreManager: KeystoreManager,
|
|
189
190
|
blobClient: BlobClientInterface,
|
|
190
191
|
dateProvider: DateProvider = new DateProvider(),
|
|
191
192
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
193
|
+
slashingProtectionDb?: SlashingProtectionDatabase,
|
|
192
194
|
) {
|
|
193
195
|
const metrics = new ValidatorMetrics(telemetry);
|
|
194
196
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
195
197
|
txsPermitted: !config.disableTransactions,
|
|
198
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
196
199
|
});
|
|
197
|
-
const
|
|
200
|
+
const proposalHandler = new ProposalHandler(
|
|
198
201
|
checkpointsBuilder,
|
|
199
202
|
worldState,
|
|
200
203
|
blockSource,
|
|
@@ -203,33 +206,54 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
203
206
|
blockProposalValidator,
|
|
204
207
|
epochCache,
|
|
205
208
|
config,
|
|
209
|
+
blobClient,
|
|
206
210
|
metrics,
|
|
207
211
|
dateProvider,
|
|
208
212
|
telemetry,
|
|
213
|
+
undefined,
|
|
209
214
|
);
|
|
210
215
|
|
|
211
|
-
|
|
212
|
-
|
|
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.
|
|
213
226
|
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
214
227
|
const haConfig = {
|
|
215
228
|
...config,
|
|
216
229
|
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
|
|
217
230
|
};
|
|
218
|
-
|
|
219
|
-
|
|
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
|
+
}));
|
|
220
242
|
}
|
|
243
|
+
const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
|
|
221
244
|
|
|
222
245
|
const validator = new ValidatorClient(
|
|
223
246
|
validatorKeyStore,
|
|
224
247
|
epochCache,
|
|
225
248
|
p2pClient,
|
|
226
|
-
|
|
249
|
+
proposalHandler,
|
|
227
250
|
blockSource,
|
|
228
251
|
checkpointsBuilder,
|
|
229
252
|
worldState,
|
|
230
253
|
l1ToL2MessageSource,
|
|
231
254
|
config,
|
|
232
255
|
blobClient,
|
|
256
|
+
slashingProtectionSigner,
|
|
233
257
|
dateProvider,
|
|
234
258
|
telemetry,
|
|
235
259
|
);
|
|
@@ -243,8 +267,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
243
267
|
.filter(addr => !this.config.disabledValidators.some(disabled => disabled.equals(addr)));
|
|
244
268
|
}
|
|
245
269
|
|
|
246
|
-
public
|
|
247
|
-
return this.
|
|
270
|
+
public getProposalHandler() {
|
|
271
|
+
return this.proposalHandler;
|
|
248
272
|
}
|
|
249
273
|
|
|
250
274
|
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition, context: SigningContext) {
|
|
@@ -267,6 +291,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
267
291
|
this.config = { ...this.config, ...config };
|
|
268
292
|
}
|
|
269
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
|
+
|
|
270
300
|
public async start() {
|
|
271
301
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
272
302
|
this.log.warn(`Validator client already started`);
|
|
@@ -311,7 +341,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
311
341
|
checkpoint: CheckpointProposalCore,
|
|
312
342
|
proposalSender: PeerId,
|
|
313
343
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
314
|
-
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
|
+
});
|
|
315
355
|
|
|
316
356
|
const myAddresses = this.getValidatorAddresses();
|
|
317
357
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
@@ -340,6 +380,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
340
380
|
return false;
|
|
341
381
|
}
|
|
342
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
|
+
|
|
343
391
|
// Check if we're in the committee (for metrics purposes)
|
|
344
392
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
345
393
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -353,25 +401,25 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
353
401
|
|
|
354
402
|
// Reexecute txs if we are part of the committee, or if slashing is enabled, or if we are configured to always reexecute.
|
|
355
403
|
// In fisherman mode, we always reexecute to validate proposals.
|
|
356
|
-
const {
|
|
357
|
-
this.config;
|
|
404
|
+
const { slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
|
|
358
405
|
const shouldReexecute =
|
|
359
406
|
fishermanMode ||
|
|
360
|
-
|
|
361
|
-
|
|
407
|
+
slashBroadcastedInvalidBlockPenalty > 0n ||
|
|
408
|
+
partOfCommittee ||
|
|
362
409
|
alwaysReexecuteBlockProposals ||
|
|
363
410
|
this.blobClient.canUpload();
|
|
364
411
|
|
|
365
|
-
const validationResult = await this.
|
|
412
|
+
const validationResult = await this.proposalHandler.handleBlockProposal(
|
|
366
413
|
proposal,
|
|
367
414
|
proposalSender,
|
|
368
415
|
!!shouldReexecute && !escapeHatchOpen,
|
|
369
416
|
);
|
|
370
417
|
|
|
371
418
|
if (!validationResult.isValid) {
|
|
372
|
-
this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
373
|
-
|
|
374
419
|
const reason = validationResult.reason || 'unknown';
|
|
420
|
+
|
|
421
|
+
this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
|
|
422
|
+
|
|
375
423
|
// Classify failure reason: bad proposal vs node issue
|
|
376
424
|
const badProposalReasons: BlockProposalValidationFailureReason[] = [
|
|
377
425
|
'invalid_proposal',
|
|
@@ -413,10 +461,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
413
461
|
return false;
|
|
414
462
|
}
|
|
415
463
|
|
|
416
|
-
// TODO(palla/mbps): Remove this once checkpoint validation is stable.
|
|
417
|
-
// Track that we successfully validated a block for this slot, so we can attest to checkpoint proposals for it.
|
|
418
|
-
this.validatedBlockSlots.add(slotNumber);
|
|
419
|
-
|
|
420
464
|
return true;
|
|
421
465
|
}
|
|
422
466
|
|
|
@@ -430,59 +474,51 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
430
474
|
proposal: CheckpointProposalCore,
|
|
431
475
|
_proposalSender: PeerId,
|
|
432
476
|
): Promise<CheckpointAttestation[] | undefined> {
|
|
433
|
-
const
|
|
477
|
+
const proposalSlotNumber = proposal.slotNumber;
|
|
434
478
|
const proposer = proposal.getSender();
|
|
435
479
|
|
|
436
480
|
// If escape hatch is open for this slot's epoch, do not attest.
|
|
437
|
-
if (await this.epochCache.isEscapeHatchOpenAtSlot(
|
|
438
|
-
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`);
|
|
439
483
|
return undefined;
|
|
440
484
|
}
|
|
441
485
|
|
|
442
|
-
//
|
|
443
|
-
if (
|
|
444
|
-
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
|
+
});
|
|
445
492
|
return undefined;
|
|
446
493
|
}
|
|
447
494
|
|
|
448
|
-
// Check that I have any address in
|
|
449
|
-
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());
|
|
450
497
|
const partOfCommittee = inCommittee.length > 0;
|
|
451
498
|
|
|
452
499
|
const proposalInfo = {
|
|
453
|
-
|
|
500
|
+
proposalSlotNumber,
|
|
454
501
|
archive: proposal.archive.toString(),
|
|
455
|
-
proposer: proposer
|
|
456
|
-
txCount: proposal.txHashes.length,
|
|
502
|
+
proposer: proposer?.toString(),
|
|
457
503
|
};
|
|
458
|
-
this.log.info(`Received checkpoint proposal for slot ${
|
|
504
|
+
this.log.info(`Received checkpoint proposal for slot ${proposalSlotNumber}`, {
|
|
459
505
|
...proposalInfo,
|
|
460
|
-
txHashes: proposal.txHashes.map(t => t.toString()),
|
|
461
506
|
fishermanMode: this.config.fishermanMode || false,
|
|
462
507
|
});
|
|
463
508
|
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
472
|
-
// TODO(palla/mbps): Change default to false once checkpoint validation is stable.
|
|
473
|
-
if (this.config.skipCheckpointProposalValidation !== false) {
|
|
474
|
-
this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
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;
|
|
512
|
+
if (this.config.skipCheckpointProposalValidation) {
|
|
513
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
|
|
514
|
+
checkpointNumber = CheckpointNumber(0);
|
|
475
515
|
} else {
|
|
476
|
-
const validationResult = await this.
|
|
516
|
+
const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
|
|
477
517
|
if (!validationResult.isValid) {
|
|
478
518
|
this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
479
519
|
return undefined;
|
|
480
520
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
// Upload blobs to filestore if we can (fire and forget)
|
|
484
|
-
if (this.blobClient.canUpload()) {
|
|
485
|
-
void this.uploadBlobsForCheckpoint(proposal, proposalInfo);
|
|
521
|
+
checkpointNumber = validationResult.checkpointNumber;
|
|
486
522
|
}
|
|
487
523
|
|
|
488
524
|
// Check that I have any address in current committee before attesting
|
|
@@ -493,14 +529,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
493
529
|
}
|
|
494
530
|
|
|
495
531
|
// Provided all of the above checks pass, we can attest to the proposal
|
|
496
|
-
this.log.info(
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
+
);
|
|
501
540
|
|
|
502
541
|
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
503
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
|
+
|
|
504
554
|
// Determine which validators should attest
|
|
505
555
|
let attestors: EthAddress[];
|
|
506
556
|
if (partOfCommittee) {
|
|
@@ -519,167 +569,60 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
519
569
|
|
|
520
570
|
if (this.config.fishermanMode) {
|
|
521
571
|
// bail out early and don't save attestations to the pool in fisherman mode
|
|
522
|
-
this.log.info(`Creating checkpoint attestations for slot ${
|
|
572
|
+
this.log.info(`Creating checkpoint attestations for slot ${proposalSlotNumber}`, {
|
|
523
573
|
...proposalInfo,
|
|
524
574
|
attestors: attestors.map(a => a.toString()),
|
|
525
575
|
});
|
|
526
576
|
return undefined;
|
|
527
577
|
}
|
|
528
578
|
|
|
529
|
-
return this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
private async createCheckpointAttestationsFromProposal(
|
|
533
|
-
proposal: CheckpointProposalCore,
|
|
534
|
-
attestors: EthAddress[] = [],
|
|
535
|
-
): Promise<CheckpointAttestation[]> {
|
|
536
|
-
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
537
|
-
await this.p2pClient.addCheckpointAttestations(attestations);
|
|
538
|
-
return attestations;
|
|
579
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
|
|
539
580
|
}
|
|
540
581
|
|
|
541
582
|
/**
|
|
542
|
-
*
|
|
543
|
-
* @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
|
|
544
585
|
*/
|
|
545
|
-
private
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const timeoutSeconds = 10;
|
|
586
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
587
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
588
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
551
591
|
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
async () => {
|
|
557
|
-
await this.blockSource.syncImmediate();
|
|
558
|
-
return this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
559
|
-
},
|
|
560
|
-
`waiting for block with archive ${proposal.archive.toString()} for slot ${slot}`,
|
|
561
|
-
timeoutSeconds,
|
|
562
|
-
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}`,
|
|
563
596
|
);
|
|
564
|
-
|
|
565
|
-
if (err instanceof TimeoutError) {
|
|
566
|
-
this.log.warn(`Timed out waiting for block with archive matching checkpoint proposal`, proposalInfo);
|
|
567
|
-
return { isValid: false, reason: 'last_block_not_found' };
|
|
568
|
-
}
|
|
569
|
-
this.log.error(`Error fetching last block for checkpoint proposal`, err, proposalInfo);
|
|
570
|
-
return { isValid: false, reason: 'block_fetch_error' };
|
|
597
|
+
return false;
|
|
571
598
|
}
|
|
572
599
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
return { isValid: false, reason: 'last_block_not_found' };
|
|
576
|
-
}
|
|
600
|
+
return true;
|
|
601
|
+
}
|
|
577
602
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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;
|
|
583
611
|
}
|
|
584
612
|
|
|
585
|
-
this.
|
|
586
|
-
...proposalInfo,
|
|
587
|
-
blockNumbers: blocks.map(b => b.number),
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
// Get checkpoint constants from first block
|
|
591
|
-
const firstBlock = blocks[0];
|
|
592
|
-
const constants = this.extractCheckpointConstants(firstBlock);
|
|
593
|
-
const checkpointNumber = firstBlock.checkpointNumber;
|
|
594
|
-
|
|
595
|
-
// Get L1-to-L2 messages for this checkpoint
|
|
596
|
-
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
597
|
-
|
|
598
|
-
// Compute the previous checkpoint out hashes for the epoch.
|
|
599
|
-
// TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
|
|
600
|
-
// actual checkpoints and the blocks/txs in them.
|
|
601
|
-
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
602
|
-
const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch))
|
|
603
|
-
.filter(b => b.number < checkpointNumber)
|
|
604
|
-
.sort((a, b) => a.number - b.number);
|
|
605
|
-
const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
|
|
606
|
-
|
|
607
|
-
// Fork world state at the block before the first block
|
|
608
|
-
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
609
|
-
const fork = await this.worldState.fork(parentBlockNumber);
|
|
613
|
+
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
|
|
610
614
|
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
614
|
-
checkpointNumber,
|
|
615
|
-
constants,
|
|
616
|
-
l1ToL2Messages,
|
|
617
|
-
previousCheckpointOutHashes,
|
|
618
|
-
fork,
|
|
619
|
-
blocks,
|
|
620
|
-
);
|
|
621
|
-
|
|
622
|
-
// Complete the checkpoint to get computed values
|
|
623
|
-
const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
|
|
624
|
-
|
|
625
|
-
// Compare checkpoint header with proposal
|
|
626
|
-
if (!computedCheckpoint.header.equals(proposal.checkpointHeader)) {
|
|
627
|
-
this.log.warn(`Checkpoint header mismatch`, {
|
|
628
|
-
...proposalInfo,
|
|
629
|
-
computed: computedCheckpoint.header.toInspect(),
|
|
630
|
-
proposal: proposal.checkpointHeader.toInspect(),
|
|
631
|
-
});
|
|
632
|
-
return { isValid: false, reason: 'checkpoint_header_mismatch' };
|
|
633
|
-
}
|
|
615
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
616
|
+
this.lastAttestedProposal = proposal;
|
|
634
617
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
this.log.warn(`Archive root mismatch`, {
|
|
638
|
-
...proposalInfo,
|
|
639
|
-
computed: computedCheckpoint.archive.root.toString(),
|
|
640
|
-
proposal: proposal.archive.toString(),
|
|
641
|
-
});
|
|
642
|
-
return { isValid: false, reason: 'archive_mismatch' };
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// Check that the accumulated out hash matches the value in the proposal.
|
|
646
|
-
const computedOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
647
|
-
const proposalOutHash = proposal.checkpointHeader.epochOutHash;
|
|
648
|
-
if (!computedOutHash.equals(proposalOutHash)) {
|
|
649
|
-
this.log.warn(`Epoch out hash mismatch`, {
|
|
650
|
-
proposalOutHash: proposalOutHash.toString(),
|
|
651
|
-
computedOutHash: computedOutHash.toString(),
|
|
652
|
-
...proposalInfo,
|
|
653
|
-
});
|
|
654
|
-
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
658
|
-
return { isValid: true };
|
|
659
|
-
} finally {
|
|
660
|
-
await fork.close();
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* Extract checkpoint global variables from a block.
|
|
666
|
-
*/
|
|
667
|
-
private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
|
|
668
|
-
const gv = block.header.globalVariables;
|
|
669
|
-
return {
|
|
670
|
-
chainId: gv.chainId,
|
|
671
|
-
version: gv.version,
|
|
672
|
-
slotNumber: gv.slotNumber,
|
|
673
|
-
coinbase: gv.coinbase,
|
|
674
|
-
feeRecipient: gv.feeRecipient,
|
|
675
|
-
gasFees: gv.gasFees,
|
|
676
|
-
};
|
|
618
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
619
|
+
return attestations;
|
|
677
620
|
}
|
|
678
621
|
|
|
679
622
|
/**
|
|
680
623
|
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
681
624
|
*/
|
|
682
|
-
|
|
625
|
+
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
683
626
|
try {
|
|
684
627
|
const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
685
628
|
if (!lastBlockHeader) {
|
|
@@ -694,7 +637,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
694
637
|
}
|
|
695
638
|
|
|
696
639
|
const blobFields = blocks.flatMap(b => b.toBlobFields());
|
|
697
|
-
const blobs: Blob[] = getBlobsPerL1Block(blobFields);
|
|
640
|
+
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
698
641
|
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
699
642
|
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
700
643
|
...proposalInfo,
|
|
@@ -732,26 +675,82 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
732
675
|
]);
|
|
733
676
|
}
|
|
734
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
|
+
|
|
735
724
|
async createBlockProposal(
|
|
736
725
|
blockHeader: BlockHeader,
|
|
726
|
+
checkpointNumber: CheckpointNumber,
|
|
737
727
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
738
728
|
inHash: Fr,
|
|
739
729
|
archive: Fr,
|
|
740
730
|
txs: Tx[],
|
|
741
731
|
proposerAddress: EthAddress | undefined,
|
|
742
|
-
options: BlockProposalOptions,
|
|
732
|
+
options: BlockProposalOptions = {},
|
|
743
733
|
): Promise<BlockProposal> {
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
+
}
|
|
749
747
|
|
|
750
748
|
this.log.info(
|
|
751
749
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
752
750
|
);
|
|
753
751
|
const newProposal = await this.validationService.createBlockProposal(
|
|
754
752
|
blockHeader,
|
|
753
|
+
checkpointNumber,
|
|
755
754
|
indexWithinCheckpoint,
|
|
756
755
|
inHash,
|
|
757
756
|
archive,
|
|
@@ -762,25 +761,44 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
762
761
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
763
762
|
},
|
|
764
763
|
);
|
|
765
|
-
this.
|
|
764
|
+
this.lastProposedBlock = newProposal;
|
|
766
765
|
return newProposal;
|
|
767
766
|
}
|
|
768
767
|
|
|
769
768
|
async createCheckpointProposal(
|
|
770
769
|
checkpointHeader: CheckpointHeader,
|
|
771
770
|
archive: Fr,
|
|
772
|
-
|
|
771
|
+
checkpointNumber: CheckpointNumber,
|
|
772
|
+
feeAssetPriceModifier: bigint,
|
|
773
|
+
lastBlockProposal: BlockProposal | undefined,
|
|
773
774
|
proposerAddress: EthAddress | undefined,
|
|
774
|
-
options: CheckpointProposalOptions,
|
|
775
|
+
options: CheckpointProposalOptions = {},
|
|
775
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
|
+
|
|
776
790
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
777
|
-
|
|
791
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
778
792
|
checkpointHeader,
|
|
779
793
|
archive,
|
|
780
|
-
|
|
794
|
+
checkpointNumber,
|
|
795
|
+
feeAssetPriceModifier,
|
|
796
|
+
lastBlockProposal,
|
|
781
797
|
proposerAddress,
|
|
782
798
|
options,
|
|
783
799
|
);
|
|
800
|
+
this.lastProposedCheckpoint = newProposal;
|
|
801
|
+
return newProposal;
|
|
784
802
|
}
|
|
785
803
|
|
|
786
804
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -791,16 +809,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
791
809
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
792
810
|
proposer: EthAddress,
|
|
793
811
|
slot: SlotNumber,
|
|
794
|
-
|
|
812
|
+
checkpointNumber: CheckpointNumber,
|
|
795
813
|
): Promise<Signature> {
|
|
796
|
-
return await this.validationService.signAttestationsAndSigners(
|
|
814
|
+
return await this.validationService.signAttestationsAndSigners(
|
|
815
|
+
attestationsAndSigners,
|
|
816
|
+
proposer,
|
|
817
|
+
slot,
|
|
818
|
+
checkpointNumber,
|
|
819
|
+
);
|
|
797
820
|
}
|
|
798
821
|
|
|
799
|
-
async collectOwnAttestations(
|
|
822
|
+
async collectOwnAttestations(
|
|
823
|
+
proposal: CheckpointProposal,
|
|
824
|
+
checkpointNumber: CheckpointNumber,
|
|
825
|
+
): Promise<CheckpointAttestation[]> {
|
|
800
826
|
const slot = proposal.slotNumber;
|
|
801
827
|
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
802
828
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
803
|
-
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
829
|
+
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
|
|
830
|
+
|
|
831
|
+
if (!attestations) {
|
|
832
|
+
return [];
|
|
833
|
+
}
|
|
804
834
|
|
|
805
835
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
806
836
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
@@ -815,6 +845,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
815
845
|
proposal: CheckpointProposal,
|
|
816
846
|
required: number,
|
|
817
847
|
deadline: Date,
|
|
848
|
+
checkpointNumber: CheckpointNumber,
|
|
818
849
|
): Promise<CheckpointAttestation[]> {
|
|
819
850
|
// Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
|
|
820
851
|
const slot = proposal.slotNumber;
|
|
@@ -827,7 +858,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
827
858
|
throw new AttestationTimeoutError(0, required, slot);
|
|
828
859
|
}
|
|
829
860
|
|
|
830
|
-
await this.collectOwnAttestations(proposal);
|
|
861
|
+
await this.collectOwnAttestations(proposal, checkpointNumber);
|
|
831
862
|
|
|
832
863
|
const proposalId = proposal.archive.toString();
|
|
833
864
|
const myAddresses = this.getValidatorAddresses();
|