@aztec/validator-client 0.0.1-commit.29c6b1a3 → 0.0.1-commit.2c0ee1788
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 +137 -46
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +33 -7
- package/dest/duties/validation_service.d.ts +11 -12
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +27 -45
- package/dest/factory.d.ts +7 -4
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +11 -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 +986 -0
- package/dest/validator.d.ts +46 -23
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +205 -200
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +183 -50
- package/src/config.ts +33 -6
- package/src/duties/validation_service.ts +53 -53
- package/src/factory.ts +15 -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 +1052 -0
- package/src/validator.ts +294 -228
- 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 -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,62 +1,59 @@
|
|
|
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,
|
|
33
|
+
type CoordinationSignatureContext,
|
|
41
34
|
} from '@aztec/stdlib/p2p';
|
|
42
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
43
35
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
44
|
-
import type { BlockHeader,
|
|
36
|
+
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
|
|
45
37
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
46
38
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
47
|
-
import {
|
|
48
|
-
|
|
39
|
+
import {
|
|
40
|
+
createHASigner,
|
|
41
|
+
createLocalSignerWithProtection,
|
|
42
|
+
createSignerFromSharedDb,
|
|
43
|
+
} from '@aztec/validator-ha-signer/factory';
|
|
44
|
+
import { DutyType, type SigningContext, type SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
|
|
45
|
+
import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
|
|
49
46
|
|
|
50
47
|
import { EventEmitter } from 'events';
|
|
51
48
|
import type { TypedDataDefinition } from 'viem';
|
|
52
49
|
|
|
53
|
-
import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
|
|
54
50
|
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
55
51
|
import { ValidationService } from './duties/validation_service.js';
|
|
56
52
|
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
57
53
|
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
58
54
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
59
55
|
import { ValidatorMetrics } from './metrics.js';
|
|
56
|
+
import { type BlockProposalValidationFailureReason, ProposalHandler } from './proposal_handler.js';
|
|
60
57
|
|
|
61
58
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
62
59
|
// Just cap the set to avoid unbounded growth.
|
|
@@ -76,29 +73,37 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
76
73
|
private validationService: ValidationService;
|
|
77
74
|
private metrics: ValidatorMetrics;
|
|
78
75
|
private log: Logger;
|
|
79
|
-
|
|
80
76
|
// Whether it has already registered handlers on the p2p client
|
|
81
77
|
private hasRegisteredHandlers = false;
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
private
|
|
79
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
|
|
80
|
+
private lastProposedBlock?: BlockProposal;
|
|
81
|
+
|
|
82
|
+
/** Tracks the last checkpoint proposal we created. */
|
|
83
|
+
private lastProposedCheckpoint?: CheckpointProposal;
|
|
85
84
|
|
|
86
85
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
87
86
|
private epochCacheUpdateLoop: RunningPromise;
|
|
87
|
+
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
|
|
88
|
+
private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
|
|
88
89
|
|
|
89
90
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
90
91
|
|
|
92
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
93
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
94
|
+
|
|
91
95
|
protected constructor(
|
|
92
96
|
private keyStore: ExtendedValidatorKeyStore,
|
|
93
97
|
private epochCache: EpochCache,
|
|
94
98
|
private p2pClient: P2P,
|
|
95
|
-
private
|
|
99
|
+
private proposalHandler: ProposalHandler,
|
|
96
100
|
private blockSource: L2BlockSource,
|
|
97
101
|
private checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
98
102
|
private worldState: WorldStateSynchronizer,
|
|
99
103
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
100
104
|
private config: ValidatorClientFullConfig,
|
|
101
105
|
private blobClient: BlobClientInterface,
|
|
106
|
+
private slashingProtectionSigner: ValidatorHASigner,
|
|
102
107
|
private dateProvider: DateProvider = new DateProvider(),
|
|
103
108
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
104
109
|
log = createLogger('validator'),
|
|
@@ -111,7 +116,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
111
116
|
this.tracer = telemetry.getTracer('Validator');
|
|
112
117
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
113
118
|
|
|
114
|
-
this.validationService = new ValidationService(
|
|
119
|
+
this.validationService = new ValidationService(
|
|
120
|
+
keyStore,
|
|
121
|
+
this.getSignatureContext(),
|
|
122
|
+
this.log.createChild('validation-service'),
|
|
123
|
+
);
|
|
115
124
|
|
|
116
125
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
117
126
|
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
|
|
@@ -152,6 +161,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
152
161
|
this.log.trace(`No committee found for slot`);
|
|
153
162
|
return;
|
|
154
163
|
}
|
|
164
|
+
this.metrics.setCurrentEpoch(epoch);
|
|
155
165
|
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
156
166
|
const me = this.getValidatorAddresses();
|
|
157
167
|
const committeeSet = new Set(committee.map(v => v.toString()));
|
|
@@ -185,12 +195,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
185
195
|
blobClient: BlobClientInterface,
|
|
186
196
|
dateProvider: DateProvider = new DateProvider(),
|
|
187
197
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
198
|
+
slashingProtectionDb?: SlashingProtectionDatabase,
|
|
188
199
|
) {
|
|
189
200
|
const metrics = new ValidatorMetrics(telemetry);
|
|
190
201
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
191
202
|
txsPermitted: !config.disableTransactions,
|
|
203
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
204
|
+
maxBlocksPerCheckpoint: config.maxBlocksPerCheckpoint,
|
|
205
|
+
signatureContext: {
|
|
206
|
+
chainId: config.l1ChainId,
|
|
207
|
+
rollupAddress: config.l1Contracts.rollupAddress,
|
|
208
|
+
},
|
|
192
209
|
});
|
|
193
|
-
const
|
|
210
|
+
const proposalHandler = new ProposalHandler(
|
|
194
211
|
checkpointsBuilder,
|
|
195
212
|
worldState,
|
|
196
213
|
blockSource,
|
|
@@ -199,33 +216,54 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
199
216
|
blockProposalValidator,
|
|
200
217
|
epochCache,
|
|
201
218
|
config,
|
|
219
|
+
blobClient,
|
|
202
220
|
metrics,
|
|
203
221
|
dateProvider,
|
|
204
222
|
telemetry,
|
|
223
|
+
undefined,
|
|
205
224
|
);
|
|
206
225
|
|
|
207
|
-
|
|
208
|
-
|
|
226
|
+
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
227
|
+
let slashingProtectionSigner: ValidatorHASigner;
|
|
228
|
+
if (slashingProtectionDb) {
|
|
229
|
+
// Shared database mode: use a pre-existing database (e.g. for testing HA setups).
|
|
230
|
+
({ signer: slashingProtectionSigner } = createSignerFromSharedDb(slashingProtectionDb, config, {
|
|
231
|
+
telemetryClient: telemetry,
|
|
232
|
+
dateProvider,
|
|
233
|
+
}));
|
|
234
|
+
} else if (config.haSigningEnabled) {
|
|
235
|
+
// Multi-node HA mode: use PostgreSQL-backed distributed locking.
|
|
209
236
|
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
210
237
|
const haConfig = {
|
|
211
238
|
...config,
|
|
212
239
|
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
|
|
213
240
|
};
|
|
214
|
-
|
|
215
|
-
|
|
241
|
+
({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
|
|
242
|
+
telemetryClient: telemetry,
|
|
243
|
+
dateProvider,
|
|
244
|
+
}));
|
|
245
|
+
} else {
|
|
246
|
+
// Single-node mode: use LMDB-backed local signing protection.
|
|
247
|
+
// This prevents double-signing if the node crashes and restarts mid-proposal.
|
|
248
|
+
({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
|
|
249
|
+
telemetryClient: telemetry,
|
|
250
|
+
dateProvider,
|
|
251
|
+
}));
|
|
216
252
|
}
|
|
253
|
+
const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
|
|
217
254
|
|
|
218
255
|
const validator = new ValidatorClient(
|
|
219
256
|
validatorKeyStore,
|
|
220
257
|
epochCache,
|
|
221
258
|
p2pClient,
|
|
222
|
-
|
|
259
|
+
proposalHandler,
|
|
223
260
|
blockSource,
|
|
224
261
|
checkpointsBuilder,
|
|
225
262
|
worldState,
|
|
226
263
|
l1ToL2MessageSource,
|
|
227
264
|
config,
|
|
228
265
|
blobClient,
|
|
266
|
+
slashingProtectionSigner,
|
|
229
267
|
dateProvider,
|
|
230
268
|
telemetry,
|
|
231
269
|
);
|
|
@@ -239,14 +277,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
239
277
|
.filter(addr => !this.config.disabledValidators.some(disabled => disabled.equals(addr)));
|
|
240
278
|
}
|
|
241
279
|
|
|
242
|
-
public
|
|
243
|
-
return this.
|
|
280
|
+
public getProposalHandler() {
|
|
281
|
+
return this.proposalHandler;
|
|
244
282
|
}
|
|
245
283
|
|
|
246
284
|
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition, context: SigningContext) {
|
|
247
285
|
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
248
286
|
}
|
|
249
287
|
|
|
288
|
+
private getSignatureContext(): CoordinationSignatureContext {
|
|
289
|
+
return {
|
|
290
|
+
chainId: this.config.l1ChainId,
|
|
291
|
+
rollupAddress: this.config.l1Contracts.rollupAddress,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
250
295
|
public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
|
|
251
296
|
return this.keyStore.getCoinbaseAddress(attestor);
|
|
252
297
|
}
|
|
@@ -263,6 +308,16 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
263
308
|
this.config = { ...this.config, ...config };
|
|
264
309
|
}
|
|
265
310
|
|
|
311
|
+
public reloadKeystore(newManager: KeystoreManager): void {
|
|
312
|
+
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
313
|
+
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
314
|
+
this.validationService = new ValidationService(
|
|
315
|
+
this.keyStore,
|
|
316
|
+
this.getSignatureContext(),
|
|
317
|
+
this.log.createChild('validation-service'),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
266
321
|
public async start() {
|
|
267
322
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
268
323
|
this.log.warn(`Validator client already started`);
|
|
@@ -307,7 +362,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
307
362
|
checkpoint: CheckpointProposalCore,
|
|
308
363
|
proposalSender: PeerId,
|
|
309
364
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
310
|
-
this.p2pClient.
|
|
365
|
+
this.p2pClient.registerValidatorCheckpointProposalHandler(checkpointHandler);
|
|
366
|
+
|
|
367
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
368
|
+
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
369
|
+
this.handleDuplicateProposal(info);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
373
|
+
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
374
|
+
this.handleDuplicateAttestation(info);
|
|
375
|
+
});
|
|
311
376
|
|
|
312
377
|
const myAddresses = this.getValidatorAddresses();
|
|
313
378
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
@@ -336,6 +401,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
336
401
|
return false;
|
|
337
402
|
}
|
|
338
403
|
|
|
404
|
+
// Log self-proposals from HA peers (same validator key on different nodes)
|
|
405
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
406
|
+
this.log.verbose(`Processing block proposal from HA peer for slot ${slotNumber}`, {
|
|
407
|
+
proposer: proposer.toString(),
|
|
408
|
+
slotNumber,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
339
412
|
// Check if we're in the committee (for metrics purposes)
|
|
340
413
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
341
414
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -349,25 +422,25 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
349
422
|
|
|
350
423
|
// Reexecute txs if we are part of the committee, or if slashing is enabled, or if we are configured to always reexecute.
|
|
351
424
|
// In fisherman mode, we always reexecute to validate proposals.
|
|
352
|
-
const {
|
|
353
|
-
this.config;
|
|
425
|
+
const { slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
|
|
354
426
|
const shouldReexecute =
|
|
355
427
|
fishermanMode ||
|
|
356
|
-
|
|
357
|
-
|
|
428
|
+
slashBroadcastedInvalidBlockPenalty > 0n ||
|
|
429
|
+
partOfCommittee ||
|
|
358
430
|
alwaysReexecuteBlockProposals ||
|
|
359
431
|
this.blobClient.canUpload();
|
|
360
432
|
|
|
361
|
-
const validationResult = await this.
|
|
433
|
+
const validationResult = await this.proposalHandler.handleBlockProposal(
|
|
362
434
|
proposal,
|
|
363
435
|
proposalSender,
|
|
364
436
|
!!shouldReexecute && !escapeHatchOpen,
|
|
365
437
|
);
|
|
366
438
|
|
|
367
439
|
if (!validationResult.isValid) {
|
|
368
|
-
this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
369
|
-
|
|
370
440
|
const reason = validationResult.reason || 'unknown';
|
|
441
|
+
|
|
442
|
+
this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
|
|
443
|
+
|
|
371
444
|
// Classify failure reason: bad proposal vs node issue
|
|
372
445
|
const badProposalReasons: BlockProposalValidationFailureReason[] = [
|
|
373
446
|
'invalid_proposal',
|
|
@@ -422,51 +495,51 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
422
495
|
proposal: CheckpointProposalCore,
|
|
423
496
|
_proposalSender: PeerId,
|
|
424
497
|
): Promise<CheckpointAttestation[] | undefined> {
|
|
425
|
-
const
|
|
498
|
+
const proposalSlotNumber = proposal.slotNumber;
|
|
426
499
|
const proposer = proposal.getSender();
|
|
427
500
|
|
|
428
501
|
// 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 ${
|
|
502
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(proposalSlotNumber)) {
|
|
503
|
+
this.log.warn(`Escape hatch open for slot ${proposalSlotNumber}, skipping checkpoint attestation handling`);
|
|
431
504
|
return undefined;
|
|
432
505
|
}
|
|
433
506
|
|
|
434
|
-
//
|
|
435
|
-
if (
|
|
436
|
-
this.log.
|
|
507
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
508
|
+
if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
509
|
+
this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
|
|
510
|
+
proposer: proposer.toString(),
|
|
511
|
+
proposalSlotNumber,
|
|
512
|
+
});
|
|
437
513
|
return undefined;
|
|
438
514
|
}
|
|
439
515
|
|
|
440
|
-
// Check that I have any address in
|
|
441
|
-
const inCommittee = await this.epochCache.filterInCommittee(
|
|
516
|
+
// Check that I have any address in the committee where this checkpoint will land before attesting
|
|
517
|
+
const inCommittee = await this.epochCache.filterInCommittee(proposalSlotNumber, this.getValidatorAddresses());
|
|
442
518
|
const partOfCommittee = inCommittee.length > 0;
|
|
443
519
|
|
|
444
520
|
const proposalInfo = {
|
|
445
|
-
|
|
521
|
+
proposalSlotNumber,
|
|
446
522
|
archive: proposal.archive.toString(),
|
|
447
|
-
proposer: proposer
|
|
448
|
-
txCount: proposal.txHashes.length,
|
|
523
|
+
proposer: proposer?.toString(),
|
|
449
524
|
};
|
|
450
|
-
this.log.info(`Received checkpoint proposal for slot ${
|
|
525
|
+
this.log.info(`Received checkpoint proposal for slot ${proposalSlotNumber}`, {
|
|
451
526
|
...proposalInfo,
|
|
452
|
-
txHashes: proposal.txHashes.map(t => t.toString()),
|
|
453
527
|
fishermanMode: this.config.fishermanMode || false,
|
|
454
528
|
});
|
|
455
529
|
|
|
456
|
-
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
530
|
+
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
|
|
531
|
+
// Uses the cached result from the all-nodes callback if available (avoids double validation).
|
|
532
|
+
let checkpointNumber: CheckpointNumber;
|
|
457
533
|
if (this.config.skipCheckpointProposalValidation) {
|
|
458
|
-
this.log.warn(`Skipping checkpoint proposal validation for slot ${
|
|
534
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
|
|
535
|
+
checkpointNumber = CheckpointNumber(0);
|
|
459
536
|
} else {
|
|
460
|
-
const validationResult = await this.
|
|
537
|
+
const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
|
|
461
538
|
if (!validationResult.isValid) {
|
|
462
539
|
this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
463
540
|
return undefined;
|
|
464
541
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
// Upload blobs to filestore if we can (fire and forget)
|
|
468
|
-
if (this.blobClient.canUpload()) {
|
|
469
|
-
void this.uploadBlobsForCheckpoint(proposal, proposalInfo);
|
|
542
|
+
checkpointNumber = validationResult.checkpointNumber;
|
|
470
543
|
}
|
|
471
544
|
|
|
472
545
|
// Check that I have any address in current committee before attesting
|
|
@@ -477,14 +550,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
477
550
|
}
|
|
478
551
|
|
|
479
552
|
// Provided all of the above checks pass, we can attest to the proposal
|
|
480
|
-
this.log.info(
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
553
|
+
this.log.info(
|
|
554
|
+
`${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${proposalSlotNumber}`,
|
|
555
|
+
{
|
|
556
|
+
...proposalInfo,
|
|
557
|
+
inCommittee: partOfCommittee,
|
|
558
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
559
|
+
},
|
|
560
|
+
);
|
|
485
561
|
|
|
486
562
|
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
487
563
|
|
|
564
|
+
// Track epoch participation per attester: count each (attester, epoch) pair at most once
|
|
565
|
+
const proposalEpoch = getEpochAtSlot(proposalSlotNumber, this.epochCache.getL1Constants());
|
|
566
|
+
for (const attester of inCommittee) {
|
|
567
|
+
const key = attester.toString();
|
|
568
|
+
const lastEpoch = this.lastAttestedEpochByAttester.get(key);
|
|
569
|
+
if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
|
|
570
|
+
this.lastAttestedEpochByAttester.set(key, proposalEpoch);
|
|
571
|
+
this.metrics.incAttestedEpochCount(attester);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
488
575
|
// Determine which validators should attest
|
|
489
576
|
let attestors: EthAddress[];
|
|
490
577
|
if (partOfCommittee) {
|
|
@@ -503,171 +590,60 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
503
590
|
|
|
504
591
|
if (this.config.fishermanMode) {
|
|
505
592
|
// bail out early and don't save attestations to the pool in fisherman mode
|
|
506
|
-
this.log.info(`Creating checkpoint attestations for slot ${
|
|
593
|
+
this.log.info(`Creating checkpoint attestations for slot ${proposalSlotNumber}`, {
|
|
507
594
|
...proposalInfo,
|
|
508
595
|
attestors: attestors.map(a => a.toString()),
|
|
509
596
|
});
|
|
510
597
|
return undefined;
|
|
511
598
|
}
|
|
512
599
|
|
|
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;
|
|
600
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
|
|
523
601
|
}
|
|
524
602
|
|
|
525
603
|
/**
|
|
526
|
-
*
|
|
527
|
-
* @returns
|
|
604
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
605
|
+
* @returns true if we should attest, false if we should skip
|
|
528
606
|
*/
|
|
529
|
-
private
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const timeoutSeconds = 10; // TODO(palla/mbps): This should map to the timetable settings
|
|
607
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
608
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
609
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
535
612
|
|
|
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,
|
|
613
|
+
// Check if incoming slot is strictly greater than last attested
|
|
614
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
615
|
+
this.log.warn(
|
|
616
|
+
`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
|
|
547
617
|
);
|
|
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' };
|
|
618
|
+
return false;
|
|
555
619
|
}
|
|
556
620
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
return { isValid: false, reason: 'last_block_not_found' };
|
|
560
|
-
}
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
561
623
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
624
|
+
private async createCheckpointAttestationsFromProposal(
|
|
625
|
+
proposal: CheckpointProposalCore,
|
|
626
|
+
attestors: EthAddress[] = [],
|
|
627
|
+
checkpointNumber: CheckpointNumber,
|
|
628
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
629
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
630
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
631
|
+
return undefined;
|
|
567
632
|
}
|
|
568
633
|
|
|
569
|
-
this.
|
|
570
|
-
...proposalInfo,
|
|
571
|
-
blockNumbers: blocks.map(b => b.number),
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
// Get checkpoint constants from first block
|
|
575
|
-
const firstBlock = blocks[0];
|
|
576
|
-
const constants = this.extractCheckpointConstants(firstBlock);
|
|
577
|
-
const checkpointNumber = firstBlock.checkpointNumber;
|
|
578
|
-
|
|
579
|
-
// Get L1-to-L2 messages for this checkpoint
|
|
580
|
-
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
581
|
-
|
|
582
|
-
// Compute the previous checkpoint out hashes for the epoch.
|
|
583
|
-
// TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
|
|
584
|
-
// actual checkpoints and the blocks/txs in them.
|
|
585
|
-
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
586
|
-
const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch))
|
|
587
|
-
.filter(b => b.number < checkpointNumber)
|
|
588
|
-
.sort((a, b) => a.number - b.number);
|
|
589
|
-
const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
|
|
634
|
+
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
|
|
590
635
|
|
|
591
|
-
//
|
|
592
|
-
|
|
593
|
-
const fork = await this.worldState.fork(parentBlockNumber);
|
|
636
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
637
|
+
this.lastAttestedProposal = proposal;
|
|
594
638
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
598
|
-
checkpointNumber,
|
|
599
|
-
constants,
|
|
600
|
-
l1ToL2Messages,
|
|
601
|
-
previousCheckpointOutHashes,
|
|
602
|
-
fork,
|
|
603
|
-
blocks,
|
|
604
|
-
);
|
|
605
|
-
|
|
606
|
-
// Complete the checkpoint to get computed values
|
|
607
|
-
const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
|
|
608
|
-
|
|
609
|
-
// Compare checkpoint header with proposal
|
|
610
|
-
if (!computedCheckpoint.header.equals(proposal.checkpointHeader)) {
|
|
611
|
-
this.log.warn(`Checkpoint header mismatch`, {
|
|
612
|
-
...proposalInfo,
|
|
613
|
-
computed: computedCheckpoint.header.toInspect(),
|
|
614
|
-
proposal: proposal.checkpointHeader.toInspect(),
|
|
615
|
-
});
|
|
616
|
-
return { isValid: false, reason: 'checkpoint_header_mismatch' };
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// Compare archive root with proposal
|
|
620
|
-
if (!computedCheckpoint.archive.root.equals(proposal.archive)) {
|
|
621
|
-
this.log.warn(`Archive root mismatch`, {
|
|
622
|
-
...proposalInfo,
|
|
623
|
-
computed: computedCheckpoint.archive.root.toString(),
|
|
624
|
-
proposal: proposal.archive.toString(),
|
|
625
|
-
});
|
|
626
|
-
return { isValid: false, reason: 'archive_mismatch' };
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
630
|
-
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
631
|
-
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
632
|
-
const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
|
|
633
|
-
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
634
|
-
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
635
|
-
this.log.warn(`Epoch out hash mismatch`, {
|
|
636
|
-
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
637
|
-
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
638
|
-
checkpointOutHash: checkpointOutHash.toString(),
|
|
639
|
-
previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
|
|
640
|
-
...proposalInfo,
|
|
641
|
-
});
|
|
642
|
-
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
646
|
-
return { isValid: true };
|
|
647
|
-
} finally {
|
|
648
|
-
await fork.close();
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* Extract checkpoint global variables from a block.
|
|
654
|
-
*/
|
|
655
|
-
private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
|
|
656
|
-
const gv = block.header.globalVariables;
|
|
657
|
-
return {
|
|
658
|
-
chainId: gv.chainId,
|
|
659
|
-
version: gv.version,
|
|
660
|
-
slotNumber: gv.slotNumber,
|
|
661
|
-
coinbase: gv.coinbase,
|
|
662
|
-
feeRecipient: gv.feeRecipient,
|
|
663
|
-
gasFees: gv.gasFees,
|
|
664
|
-
};
|
|
639
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
640
|
+
return attestations;
|
|
665
641
|
}
|
|
666
642
|
|
|
667
643
|
/**
|
|
668
644
|
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
669
645
|
*/
|
|
670
|
-
|
|
646
|
+
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
671
647
|
try {
|
|
672
648
|
const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
673
649
|
if (!lastBlockHeader) {
|
|
@@ -682,7 +658,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
682
658
|
}
|
|
683
659
|
|
|
684
660
|
const blobFields = blocks.flatMap(b => b.toBlobFields());
|
|
685
|
-
const blobs: Blob[] = getBlobsPerL1Block(blobFields);
|
|
661
|
+
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
686
662
|
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
687
663
|
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
688
664
|
...proposalInfo,
|
|
@@ -720,8 +696,55 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
720
696
|
]);
|
|
721
697
|
}
|
|
722
698
|
|
|
699
|
+
/**
|
|
700
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
701
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
702
|
+
*/
|
|
703
|
+
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
704
|
+
const { slot, proposer, type } = info;
|
|
705
|
+
|
|
706
|
+
this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
|
|
707
|
+
proposer: proposer.toString(),
|
|
708
|
+
slot,
|
|
709
|
+
type,
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Emit slash event
|
|
713
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
714
|
+
{
|
|
715
|
+
validator: proposer,
|
|
716
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
717
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
718
|
+
epochOrSlot: BigInt(slot),
|
|
719
|
+
},
|
|
720
|
+
]);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
725
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
726
|
+
*/
|
|
727
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
728
|
+
const { slot, attester } = info;
|
|
729
|
+
|
|
730
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
731
|
+
attester: attester.toString(),
|
|
732
|
+
slot,
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
736
|
+
{
|
|
737
|
+
validator: attester,
|
|
738
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
739
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
740
|
+
epochOrSlot: BigInt(slot),
|
|
741
|
+
},
|
|
742
|
+
]);
|
|
743
|
+
}
|
|
744
|
+
|
|
723
745
|
async createBlockProposal(
|
|
724
746
|
blockHeader: BlockHeader,
|
|
747
|
+
checkpointNumber: CheckpointNumber,
|
|
725
748
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
726
749
|
inHash: Fr,
|
|
727
750
|
archive: Fr,
|
|
@@ -729,17 +752,26 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
729
752
|
proposerAddress: EthAddress | undefined,
|
|
730
753
|
options: BlockProposalOptions = {},
|
|
731
754
|
): Promise<BlockProposal> {
|
|
732
|
-
//
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
755
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
756
|
+
if (this.lastProposedBlock) {
|
|
757
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
758
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
759
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
760
|
+
|
|
761
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
762
|
+
throw new Error(
|
|
763
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
764
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
737
768
|
|
|
738
769
|
this.log.info(
|
|
739
770
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
740
771
|
);
|
|
741
772
|
const newProposal = await this.validationService.createBlockProposal(
|
|
742
773
|
blockHeader,
|
|
774
|
+
checkpointNumber,
|
|
743
775
|
indexWithinCheckpoint,
|
|
744
776
|
inHash,
|
|
745
777
|
archive,
|
|
@@ -750,25 +782,44 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
750
782
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
751
783
|
},
|
|
752
784
|
);
|
|
753
|
-
this.
|
|
785
|
+
this.lastProposedBlock = newProposal;
|
|
754
786
|
return newProposal;
|
|
755
787
|
}
|
|
756
788
|
|
|
757
789
|
async createCheckpointProposal(
|
|
758
790
|
checkpointHeader: CheckpointHeader,
|
|
759
791
|
archive: Fr,
|
|
760
|
-
|
|
792
|
+
checkpointNumber: CheckpointNumber,
|
|
793
|
+
feeAssetPriceModifier: bigint,
|
|
794
|
+
lastBlockProposal: BlockProposal | undefined,
|
|
761
795
|
proposerAddress: EthAddress | undefined,
|
|
762
796
|
options: CheckpointProposalOptions = {},
|
|
763
797
|
): Promise<CheckpointProposal> {
|
|
798
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
799
|
+
if (this.lastProposedCheckpoint) {
|
|
800
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
801
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
802
|
+
|
|
803
|
+
if (newSlot <= lastSlot) {
|
|
804
|
+
throw new Error(
|
|
805
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
806
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
764
811
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
765
|
-
|
|
812
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
766
813
|
checkpointHeader,
|
|
767
814
|
archive,
|
|
768
|
-
|
|
815
|
+
checkpointNumber,
|
|
816
|
+
feeAssetPriceModifier,
|
|
817
|
+
lastBlockProposal,
|
|
769
818
|
proposerAddress,
|
|
770
819
|
options,
|
|
771
820
|
);
|
|
821
|
+
this.lastProposedCheckpoint = newProposal;
|
|
822
|
+
return newProposal;
|
|
772
823
|
}
|
|
773
824
|
|
|
774
825
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -779,16 +830,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
779
830
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
780
831
|
proposer: EthAddress,
|
|
781
832
|
slot: SlotNumber,
|
|
782
|
-
|
|
833
|
+
checkpointNumber: CheckpointNumber,
|
|
783
834
|
): Promise<Signature> {
|
|
784
|
-
return await this.validationService.signAttestationsAndSigners(
|
|
835
|
+
return await this.validationService.signAttestationsAndSigners(
|
|
836
|
+
attestationsAndSigners,
|
|
837
|
+
proposer,
|
|
838
|
+
slot,
|
|
839
|
+
checkpointNumber,
|
|
840
|
+
);
|
|
785
841
|
}
|
|
786
842
|
|
|
787
|
-
async collectOwnAttestations(
|
|
843
|
+
async collectOwnAttestations(
|
|
844
|
+
proposal: CheckpointProposal,
|
|
845
|
+
checkpointNumber: CheckpointNumber,
|
|
846
|
+
): Promise<CheckpointAttestation[]> {
|
|
788
847
|
const slot = proposal.slotNumber;
|
|
789
848
|
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
790
849
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
791
|
-
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
850
|
+
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
|
|
851
|
+
|
|
852
|
+
if (!attestations) {
|
|
853
|
+
return [];
|
|
854
|
+
}
|
|
792
855
|
|
|
793
856
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
794
857
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
@@ -803,6 +866,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
803
866
|
proposal: CheckpointProposal,
|
|
804
867
|
required: number,
|
|
805
868
|
deadline: Date,
|
|
869
|
+
checkpointNumber: CheckpointNumber,
|
|
806
870
|
): Promise<CheckpointAttestation[]> {
|
|
807
871
|
// Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
|
|
808
872
|
const slot = proposal.slotNumber;
|
|
@@ -815,7 +879,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
815
879
|
throw new AttestationTimeoutError(0, required, slot);
|
|
816
880
|
}
|
|
817
881
|
|
|
818
|
-
await this.collectOwnAttestations(proposal);
|
|
882
|
+
await this.collectOwnAttestations(proposal, checkpointNumber);
|
|
819
883
|
|
|
820
884
|
const proposalId = proposal.archive.toString();
|
|
821
885
|
const myAddresses = this.getValidatorAddresses();
|
|
@@ -828,7 +892,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
828
892
|
attestation => {
|
|
829
893
|
if (!attestation.archive.equals(proposal.archive)) {
|
|
830
894
|
this.log.warn(
|
|
831
|
-
`Received attestation for slot ${slot} with mismatched archive from ${attestation
|
|
895
|
+
`Received attestation for slot ${slot} with mismatched archive from ${attestation
|
|
896
|
+
.getSender()
|
|
897
|
+
?.toString()}`,
|
|
832
898
|
{ attestationArchive: attestation.archive.toString(), proposalArchive: proposal.archive.toString() },
|
|
833
899
|
);
|
|
834
900
|
return false;
|