@aztec/validator-client 0.0.1-commit.5daedc8 → 0.0.1-commit.6201a7b05
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 +324 -0
- package/dest/checkpoint_builder.d.ts +79 -0
- package/dest/checkpoint_builder.d.ts.map +1 -0
- package/dest/checkpoint_builder.js +251 -0
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +44 -14
- package/dest/duties/validation_service.d.ts +43 -15
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +96 -31
- package/dest/factory.d.ts +19 -11
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +10 -5
- package/dest/index.d.ts +3 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +2 -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 +9 -5
- 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 +9 -5
- package/dest/metrics.d.ts +16 -3
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +58 -30
- 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 +74 -23
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +328 -65
- package/package.json +23 -13
- package/src/checkpoint_builder.ts +417 -0
- package/src/config.ts +43 -12
- package/src/duties/validation_service.ts +162 -40
- package/src/factory.ts +31 -11
- package/src/index.ts +2 -1
- 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 +14 -5
- package/src/key_store/node_keystore_adapter.ts +28 -5
- package/src/key_store/web3signer_key_store.ts +18 -5
- package/src/metrics.ts +81 -33
- package/src/proposal_handler.ts +1042 -0
- package/src/validator.ts +514 -100
- package/dest/block_proposal_handler.d.ts +0 -52
- package/dest/block_proposal_handler.d.ts.map +0 -1
- package/dest/block_proposal_handler.js +0 -290
- package/src/block_proposal_handler.ts +0 -341
package/src/validator.ts
CHANGED
|
@@ -1,33 +1,59 @@
|
|
|
1
|
+
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
2
|
+
import { type Blob, getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
1
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import { EpochNumber } from '@aztec/foundation/branded-types';
|
|
4
|
+
import { CheckpointNumber, EpochNumber, IndexWithinCheckpoint, SlotNumber } from '@aztec/foundation/branded-types';
|
|
5
|
+
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
3
6
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
7
|
import type { Signature } from '@aztec/foundation/eth-signature';
|
|
5
|
-
import {
|
|
6
|
-
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
8
|
+
import { type LogData, type Logger, createLogger } from '@aztec/foundation/log';
|
|
7
9
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
8
10
|
import { sleep } from '@aztec/foundation/sleep';
|
|
9
11
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
10
12
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
11
|
-
import type { P2P, PeerId
|
|
13
|
+
import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
12
14
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
13
15
|
import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
|
|
14
16
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
15
|
-
import type { CommitteeAttestationsAndSigners, L2BlockSource } from '@aztec/stdlib/block';
|
|
16
|
-
import
|
|
17
|
+
import type { CommitteeAttestationsAndSigners, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
18
|
+
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
19
|
+
import type {
|
|
20
|
+
ITxProvider,
|
|
21
|
+
Validator,
|
|
22
|
+
ValidatorClientFullConfig,
|
|
23
|
+
WorldStateSynchronizer,
|
|
24
|
+
} from '@aztec/stdlib/interfaces/server';
|
|
17
25
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
18
|
-
import
|
|
26
|
+
import {
|
|
27
|
+
type BlockProposal,
|
|
28
|
+
type BlockProposalOptions,
|
|
29
|
+
type CheckpointAttestation,
|
|
30
|
+
CheckpointProposal,
|
|
31
|
+
type CheckpointProposalCore,
|
|
32
|
+
type CheckpointProposalOptions,
|
|
33
|
+
type CoordinationSignatureContext,
|
|
34
|
+
} from '@aztec/stdlib/p2p';
|
|
19
35
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
20
|
-
import type { Tx } from '@aztec/stdlib/tx';
|
|
36
|
+
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
|
|
21
37
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
22
38
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
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';
|
|
23
46
|
|
|
24
47
|
import { EventEmitter } from 'events';
|
|
25
48
|
import type { TypedDataDefinition } from 'viem';
|
|
26
49
|
|
|
27
|
-
import {
|
|
50
|
+
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
28
51
|
import { ValidationService } from './duties/validation_service.js';
|
|
52
|
+
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
53
|
+
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
29
54
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
30
55
|
import { ValidatorMetrics } from './metrics.js';
|
|
56
|
+
import { type BlockProposalValidationFailureReason, ProposalHandler } from './proposal_handler.js';
|
|
31
57
|
|
|
32
58
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
33
59
|
// Just cap the set to avoid unbounded growth.
|
|
@@ -47,24 +73,37 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
47
73
|
private validationService: ValidationService;
|
|
48
74
|
private metrics: ValidatorMetrics;
|
|
49
75
|
private log: Logger;
|
|
50
|
-
|
|
51
76
|
// Whether it has already registered handlers on the p2p client
|
|
52
77
|
private hasRegisteredHandlers = false;
|
|
53
78
|
|
|
54
|
-
|
|
55
|
-
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;
|
|
56
84
|
|
|
57
85
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
58
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();
|
|
59
89
|
|
|
60
90
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
61
91
|
|
|
92
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
93
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
94
|
+
|
|
62
95
|
protected constructor(
|
|
63
|
-
private keyStore:
|
|
96
|
+
private keyStore: ExtendedValidatorKeyStore,
|
|
64
97
|
private epochCache: EpochCache,
|
|
65
98
|
private p2pClient: P2P,
|
|
66
|
-
private
|
|
99
|
+
private proposalHandler: ProposalHandler,
|
|
100
|
+
private blockSource: L2BlockSource,
|
|
101
|
+
private checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
102
|
+
private worldState: WorldStateSynchronizer,
|
|
103
|
+
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
67
104
|
private config: ValidatorClientFullConfig,
|
|
105
|
+
private blobClient: BlobClientInterface,
|
|
106
|
+
private slashingProtectionSigner: ValidatorHASigner,
|
|
68
107
|
private dateProvider: DateProvider = new DateProvider(),
|
|
69
108
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
70
109
|
log = createLogger('validator'),
|
|
@@ -77,7 +116,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
77
116
|
this.tracer = telemetry.getTracer('Validator');
|
|
78
117
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
79
118
|
|
|
80
|
-
this.validationService = new ValidationService(
|
|
119
|
+
this.validationService = new ValidationService(
|
|
120
|
+
keyStore,
|
|
121
|
+
this.getSignatureContext(),
|
|
122
|
+
this.log.createChild('validation-service'),
|
|
123
|
+
);
|
|
81
124
|
|
|
82
125
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
83
126
|
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
|
|
@@ -118,6 +161,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
118
161
|
this.log.trace(`No committee found for slot`);
|
|
119
162
|
return;
|
|
120
163
|
}
|
|
164
|
+
this.metrics.setCurrentEpoch(epoch);
|
|
121
165
|
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
122
166
|
const me = this.getValidatorAddresses();
|
|
123
167
|
const committeeSet = new Set(committee.map(v => v.toString()));
|
|
@@ -138,40 +182,87 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
138
182
|
}
|
|
139
183
|
}
|
|
140
184
|
|
|
141
|
-
static new(
|
|
185
|
+
static async new(
|
|
142
186
|
config: ValidatorClientFullConfig,
|
|
143
|
-
|
|
187
|
+
checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
188
|
+
worldState: WorldStateSynchronizer,
|
|
144
189
|
epochCache: EpochCache,
|
|
145
190
|
p2pClient: P2P,
|
|
146
|
-
blockSource: L2BlockSource,
|
|
191
|
+
blockSource: L2BlockSource & L2BlockSink,
|
|
147
192
|
l1ToL2MessageSource: L1ToL2MessageSource,
|
|
148
|
-
txProvider:
|
|
193
|
+
txProvider: ITxProvider,
|
|
149
194
|
keyStoreManager: KeystoreManager,
|
|
195
|
+
blobClient: BlobClientInterface,
|
|
150
196
|
dateProvider: DateProvider = new DateProvider(),
|
|
151
197
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
198
|
+
slashingProtectionDb?: SlashingProtectionDatabase,
|
|
152
199
|
) {
|
|
153
200
|
const metrics = new ValidatorMetrics(telemetry);
|
|
154
201
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
155
202
|
txsPermitted: !config.disableTransactions,
|
|
203
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
204
|
+
signatureContext: {
|
|
205
|
+
chainId: config.l1ChainId,
|
|
206
|
+
rollupAddress: config.l1Contracts.rollupAddress,
|
|
207
|
+
},
|
|
156
208
|
});
|
|
157
|
-
const
|
|
158
|
-
|
|
209
|
+
const proposalHandler = new ProposalHandler(
|
|
210
|
+
checkpointsBuilder,
|
|
211
|
+
worldState,
|
|
159
212
|
blockSource,
|
|
160
213
|
l1ToL2MessageSource,
|
|
161
214
|
txProvider,
|
|
162
215
|
blockProposalValidator,
|
|
216
|
+
epochCache,
|
|
163
217
|
config,
|
|
218
|
+
blobClient,
|
|
164
219
|
metrics,
|
|
165
220
|
dateProvider,
|
|
166
221
|
telemetry,
|
|
222
|
+
undefined,
|
|
167
223
|
);
|
|
168
224
|
|
|
225
|
+
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
226
|
+
let slashingProtectionSigner: ValidatorHASigner;
|
|
227
|
+
if (slashingProtectionDb) {
|
|
228
|
+
// Shared database mode: use a pre-existing database (e.g. for testing HA setups).
|
|
229
|
+
({ signer: slashingProtectionSigner } = createSignerFromSharedDb(slashingProtectionDb, config, {
|
|
230
|
+
telemetryClient: telemetry,
|
|
231
|
+
dateProvider,
|
|
232
|
+
}));
|
|
233
|
+
} else if (config.haSigningEnabled) {
|
|
234
|
+
// Multi-node HA mode: use PostgreSQL-backed distributed locking.
|
|
235
|
+
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
236
|
+
const haConfig = {
|
|
237
|
+
...config,
|
|
238
|
+
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
|
|
239
|
+
};
|
|
240
|
+
({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
|
|
241
|
+
telemetryClient: telemetry,
|
|
242
|
+
dateProvider,
|
|
243
|
+
}));
|
|
244
|
+
} else {
|
|
245
|
+
// Single-node mode: use LMDB-backed local signing protection.
|
|
246
|
+
// This prevents double-signing if the node crashes and restarts mid-proposal.
|
|
247
|
+
({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
|
|
248
|
+
telemetryClient: telemetry,
|
|
249
|
+
dateProvider,
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
|
|
253
|
+
|
|
169
254
|
const validator = new ValidatorClient(
|
|
170
|
-
|
|
255
|
+
validatorKeyStore,
|
|
171
256
|
epochCache,
|
|
172
257
|
p2pClient,
|
|
173
|
-
|
|
258
|
+
proposalHandler,
|
|
259
|
+
blockSource,
|
|
260
|
+
checkpointsBuilder,
|
|
261
|
+
worldState,
|
|
262
|
+
l1ToL2MessageSource,
|
|
174
263
|
config,
|
|
264
|
+
blobClient,
|
|
265
|
+
slashingProtectionSigner,
|
|
175
266
|
dateProvider,
|
|
176
267
|
telemetry,
|
|
177
268
|
);
|
|
@@ -185,22 +276,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
185
276
|
.filter(addr => !this.config.disabledValidators.some(disabled => disabled.equals(addr)));
|
|
186
277
|
}
|
|
187
278
|
|
|
188
|
-
public
|
|
189
|
-
return this.
|
|
279
|
+
public getProposalHandler() {
|
|
280
|
+
return this.proposalHandler;
|
|
190
281
|
}
|
|
191
282
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
proposal: BlockProposal,
|
|
195
|
-
blockNumber: number,
|
|
196
|
-
txs: any[],
|
|
197
|
-
l1ToL2Messages: Fr[],
|
|
198
|
-
): Promise<any> {
|
|
199
|
-
return this.blockProposalHandler.reexecuteTransactions(proposal, blockNumber, txs, l1ToL2Messages);
|
|
283
|
+
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition, context: SigningContext) {
|
|
284
|
+
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
200
285
|
}
|
|
201
286
|
|
|
202
|
-
|
|
203
|
-
return
|
|
287
|
+
private getSignatureContext(): CoordinationSignatureContext {
|
|
288
|
+
return {
|
|
289
|
+
chainId: this.config.l1ChainId,
|
|
290
|
+
rollupAddress: this.config.l1Contracts.rollupAddress,
|
|
291
|
+
};
|
|
204
292
|
}
|
|
205
293
|
|
|
206
294
|
public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
|
|
@@ -219,12 +307,24 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
219
307
|
this.config = { ...this.config, ...config };
|
|
220
308
|
}
|
|
221
309
|
|
|
310
|
+
public reloadKeystore(newManager: KeystoreManager): void {
|
|
311
|
+
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
312
|
+
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
313
|
+
this.validationService = new ValidationService(
|
|
314
|
+
this.keyStore,
|
|
315
|
+
this.getSignatureContext(),
|
|
316
|
+
this.log.createChild('validation-service'),
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
222
320
|
public async start() {
|
|
223
321
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
224
322
|
this.log.warn(`Validator client already started`);
|
|
225
323
|
return;
|
|
226
324
|
}
|
|
227
325
|
|
|
326
|
+
await this.keyStore.start();
|
|
327
|
+
|
|
228
328
|
await this.registerHandlers();
|
|
229
329
|
|
|
230
330
|
const myAddresses = this.getValidatorAddresses();
|
|
@@ -240,6 +340,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
240
340
|
|
|
241
341
|
public async stop() {
|
|
242
342
|
await this.epochCacheUpdateLoop.stop();
|
|
343
|
+
await this.keyStore.stop();
|
|
243
344
|
}
|
|
244
345
|
|
|
245
346
|
/** Register handlers on the p2p client */
|
|
@@ -248,9 +349,29 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
248
349
|
this.hasRegisteredHandlers = true;
|
|
249
350
|
this.log.debug(`Registering validator handlers for p2p client`);
|
|
250
351
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
352
|
+
// Block proposal handler - validates but does NOT attest (validators only attest to checkpoints)
|
|
353
|
+
const blockHandler = (block: BlockProposal, proposalSender: PeerId): Promise<boolean> =>
|
|
354
|
+
this.validateBlockProposal(block, proposalSender);
|
|
355
|
+
this.p2pClient.registerBlockProposalHandler(blockHandler);
|
|
356
|
+
|
|
357
|
+
// Checkpoint proposal handler - validates and creates attestations
|
|
358
|
+
// The checkpoint is received as CheckpointProposalCore since the lastBlock is extracted
|
|
359
|
+
// and processed separately via the block handler above.
|
|
360
|
+
const checkpointHandler = (
|
|
361
|
+
checkpoint: CheckpointProposalCore,
|
|
362
|
+
proposalSender: PeerId,
|
|
363
|
+
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
364
|
+
this.p2pClient.registerValidatorCheckpointProposalHandler(checkpointHandler);
|
|
365
|
+
|
|
366
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
367
|
+
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
368
|
+
this.handleDuplicateProposal(info);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
372
|
+
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
373
|
+
this.handleDuplicateAttestation(info);
|
|
374
|
+
});
|
|
254
375
|
|
|
255
376
|
const myAddresses = this.getValidatorAddresses();
|
|
256
377
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
@@ -259,48 +380,66 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
259
380
|
}
|
|
260
381
|
}
|
|
261
382
|
|
|
262
|
-
|
|
383
|
+
/**
|
|
384
|
+
* Validate a block proposal from a peer.
|
|
385
|
+
* Note: Validators do NOT attest to individual blocks - attestations are only for checkpoint proposals.
|
|
386
|
+
* @returns true if the proposal is valid, false otherwise
|
|
387
|
+
*/
|
|
388
|
+
async validateBlockProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> {
|
|
263
389
|
const slotNumber = proposal.slotNumber;
|
|
390
|
+
|
|
391
|
+
// Note: During escape hatch, we still want to "validate" proposals for observability,
|
|
392
|
+
// but we intentionally reject them and disable slashing invalid block and attestation flow.
|
|
393
|
+
const escapeHatchOpen = await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber);
|
|
394
|
+
|
|
264
395
|
const proposer = proposal.getSender();
|
|
265
396
|
|
|
266
397
|
// Reject proposals with invalid signatures
|
|
267
398
|
if (!proposer) {
|
|
268
|
-
this.log.warn(`Received proposal with invalid signature for slot ${slotNumber}`);
|
|
269
|
-
return
|
|
399
|
+
this.log.warn(`Received block proposal with invalid signature for slot ${slotNumber}`);
|
|
400
|
+
return false;
|
|
270
401
|
}
|
|
271
402
|
|
|
272
|
-
//
|
|
403
|
+
// Log self-proposals from HA peers (same validator key on different nodes)
|
|
404
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
405
|
+
this.log.verbose(`Processing block proposal from HA peer for slot ${slotNumber}`, {
|
|
406
|
+
proposer: proposer.toString(),
|
|
407
|
+
slotNumber,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Check if we're in the committee (for metrics purposes)
|
|
273
412
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
274
413
|
const partOfCommittee = inCommittee.length > 0;
|
|
275
414
|
|
|
276
415
|
const proposalInfo = { ...proposal.toBlockInfo(), proposer: proposer.toString() };
|
|
277
|
-
this.log.info(`Received proposal for slot ${slotNumber}`, {
|
|
416
|
+
this.log.info(`Received block proposal for slot ${slotNumber}`, {
|
|
278
417
|
...proposalInfo,
|
|
279
418
|
txHashes: proposal.txHashes.map(t => t.toString()),
|
|
280
419
|
fishermanMode: this.config.fishermanMode || false,
|
|
281
420
|
});
|
|
282
421
|
|
|
283
|
-
// Reexecute txs if we are part of the committee
|
|
284
|
-
// invalid proposals even when not in the committee, or if we are configured to always reexecute for monitoring purposes.
|
|
422
|
+
// Reexecute txs if we are part of the committee, or if slashing is enabled, or if we are configured to always reexecute.
|
|
285
423
|
// In fisherman mode, we always reexecute to validate proposals.
|
|
286
|
-
const {
|
|
287
|
-
this.config;
|
|
424
|
+
const { slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
|
|
288
425
|
const shouldReexecute =
|
|
289
426
|
fishermanMode ||
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
alwaysReexecuteBlockProposals
|
|
427
|
+
slashBroadcastedInvalidBlockPenalty > 0n ||
|
|
428
|
+
partOfCommittee ||
|
|
429
|
+
alwaysReexecuteBlockProposals ||
|
|
430
|
+
this.blobClient.canUpload();
|
|
293
431
|
|
|
294
|
-
const validationResult = await this.
|
|
432
|
+
const validationResult = await this.proposalHandler.handleBlockProposal(
|
|
295
433
|
proposal,
|
|
296
434
|
proposalSender,
|
|
297
|
-
!!shouldReexecute,
|
|
435
|
+
!!shouldReexecute && !escapeHatchOpen,
|
|
298
436
|
);
|
|
299
437
|
|
|
300
438
|
if (!validationResult.isValid) {
|
|
301
|
-
this.log.warn(`Proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
302
|
-
|
|
303
439
|
const reason = validationResult.reason || 'unknown';
|
|
440
|
+
|
|
441
|
+
this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
|
|
442
|
+
|
|
304
443
|
// Classify failure reason: bad proposal vs node issue
|
|
305
444
|
const badProposalReasons: BlockProposalValidationFailureReason[] = [
|
|
306
445
|
'invalid_proposal',
|
|
@@ -313,12 +452,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
313
452
|
if (badProposalReasons.includes(reason as BlockProposalValidationFailureReason)) {
|
|
314
453
|
this.metrics.incFailedAttestationsBadProposal(1, reason, partOfCommittee);
|
|
315
454
|
} else {
|
|
316
|
-
// Node issues so we can't
|
|
455
|
+
// Node issues so we can't validate
|
|
317
456
|
this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
|
|
318
457
|
}
|
|
319
458
|
|
|
320
459
|
// Slash invalid block proposals (can happen even when not in committee)
|
|
321
460
|
if (
|
|
461
|
+
!escapeHatchOpen &&
|
|
322
462
|
validationResult.reason &&
|
|
323
463
|
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) &&
|
|
324
464
|
slashBroadcastedInvalidBlockPenalty > 0n
|
|
@@ -326,9 +466,81 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
326
466
|
this.log.warn(`Slashing proposer for invalid block proposal`, proposalInfo);
|
|
327
467
|
this.slashInvalidBlock(proposal);
|
|
328
468
|
}
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
this.log.info(`Validated block proposal for slot ${slotNumber}`, {
|
|
473
|
+
...proposalInfo,
|
|
474
|
+
inCommittee: partOfCommittee,
|
|
475
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
476
|
+
escapeHatchOpen,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
if (escapeHatchOpen) {
|
|
480
|
+
this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Validate and attest to a checkpoint proposal from a peer.
|
|
489
|
+
* The proposal is received as CheckpointProposalCore (without lastBlock) since
|
|
490
|
+
* the lastBlock is extracted and processed separately via the block handler.
|
|
491
|
+
* @returns Checkpoint attestations if valid, undefined otherwise
|
|
492
|
+
*/
|
|
493
|
+
async attestToCheckpointProposal(
|
|
494
|
+
proposal: CheckpointProposalCore,
|
|
495
|
+
_proposalSender: PeerId,
|
|
496
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
497
|
+
const proposalSlotNumber = proposal.slotNumber;
|
|
498
|
+
const proposer = proposal.getSender();
|
|
499
|
+
|
|
500
|
+
// If escape hatch is open for this slot's epoch, do not attest.
|
|
501
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(proposalSlotNumber)) {
|
|
502
|
+
this.log.warn(`Escape hatch open for slot ${proposalSlotNumber}, skipping checkpoint attestation handling`);
|
|
329
503
|
return undefined;
|
|
330
504
|
}
|
|
331
505
|
|
|
506
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
507
|
+
if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
508
|
+
this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
|
|
509
|
+
proposer: proposer.toString(),
|
|
510
|
+
proposalSlotNumber,
|
|
511
|
+
});
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Check that I have any address in the committee where this checkpoint will land before attesting
|
|
516
|
+
const inCommittee = await this.epochCache.filterInCommittee(proposalSlotNumber, this.getValidatorAddresses());
|
|
517
|
+
const partOfCommittee = inCommittee.length > 0;
|
|
518
|
+
|
|
519
|
+
const proposalInfo = {
|
|
520
|
+
proposalSlotNumber,
|
|
521
|
+
archive: proposal.archive.toString(),
|
|
522
|
+
proposer: proposer?.toString(),
|
|
523
|
+
};
|
|
524
|
+
this.log.info(`Received checkpoint proposal for slot ${proposalSlotNumber}`, {
|
|
525
|
+
...proposalInfo,
|
|
526
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
|
|
530
|
+
// Uses the cached result from the all-nodes callback if available (avoids double validation).
|
|
531
|
+
let checkpointNumber: CheckpointNumber;
|
|
532
|
+
if (this.config.skipCheckpointProposalValidation) {
|
|
533
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
|
|
534
|
+
checkpointNumber = CheckpointNumber(0);
|
|
535
|
+
} else {
|
|
536
|
+
const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
|
|
537
|
+
if (!validationResult.isValid) {
|
|
538
|
+
this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
checkpointNumber = validationResult.checkpointNumber;
|
|
542
|
+
}
|
|
543
|
+
|
|
332
544
|
// Check that I have any address in current committee before attesting
|
|
333
545
|
// In fisherman mode, we still create attestations for validation even if not in committee
|
|
334
546
|
if (!partOfCommittee && !this.config.fishermanMode) {
|
|
@@ -337,15 +549,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
337
549
|
}
|
|
338
550
|
|
|
339
551
|
// Provided all of the above checks pass, we can attest to the proposal
|
|
340
|
-
this.log.info(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
552
|
+
this.log.info(
|
|
553
|
+
`${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${proposalSlotNumber}`,
|
|
554
|
+
{
|
|
555
|
+
...proposalInfo,
|
|
556
|
+
inCommittee: partOfCommittee,
|
|
557
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
558
|
+
},
|
|
559
|
+
);
|
|
345
560
|
|
|
346
561
|
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
347
562
|
|
|
348
|
-
//
|
|
563
|
+
// Track epoch participation per attester: count each (attester, epoch) pair at most once
|
|
564
|
+
const proposalEpoch = getEpochAtSlot(proposalSlotNumber, this.epochCache.getL1Constants());
|
|
565
|
+
for (const attester of inCommittee) {
|
|
566
|
+
const key = attester.toString();
|
|
567
|
+
const lastEpoch = this.lastAttestedEpochByAttester.get(key);
|
|
568
|
+
if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
|
|
569
|
+
this.lastAttestedEpochByAttester.set(key, proposalEpoch);
|
|
570
|
+
this.metrics.incAttestedEpochCount(attester);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
349
574
|
// Determine which validators should attest
|
|
350
575
|
let attestors: EthAddress[];
|
|
351
576
|
if (partOfCommittee) {
|
|
@@ -364,13 +589,83 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
364
589
|
|
|
365
590
|
if (this.config.fishermanMode) {
|
|
366
591
|
// bail out early and don't save attestations to the pool in fisherman mode
|
|
367
|
-
this.log.info(`Creating attestations for
|
|
592
|
+
this.log.info(`Creating checkpoint attestations for slot ${proposalSlotNumber}`, {
|
|
368
593
|
...proposalInfo,
|
|
369
594
|
attestors: attestors.map(a => a.toString()),
|
|
370
595
|
});
|
|
371
596
|
return undefined;
|
|
372
597
|
}
|
|
373
|
-
|
|
598
|
+
|
|
599
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
604
|
+
* @returns true if we should attest, false if we should skip
|
|
605
|
+
*/
|
|
606
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
607
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
608
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
609
|
+
return true;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Check if incoming slot is strictly greater than last attested
|
|
613
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
614
|
+
this.log.warn(
|
|
615
|
+
`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
|
|
616
|
+
);
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private async createCheckpointAttestationsFromProposal(
|
|
624
|
+
proposal: CheckpointProposalCore,
|
|
625
|
+
attestors: EthAddress[] = [],
|
|
626
|
+
checkpointNumber: CheckpointNumber,
|
|
627
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
628
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
629
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
|
|
634
|
+
|
|
635
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
636
|
+
this.lastAttestedProposal = proposal;
|
|
637
|
+
|
|
638
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
639
|
+
return attestations;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
644
|
+
*/
|
|
645
|
+
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
646
|
+
try {
|
|
647
|
+
const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
648
|
+
if (!lastBlockHeader) {
|
|
649
|
+
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
|
|
654
|
+
if (blocks.length === 0) {
|
|
655
|
+
this.log.warn(`No blocks found for blob upload`, proposalInfo);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const blobFields = blocks.flatMap(b => b.toBlobFields());
|
|
660
|
+
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
661
|
+
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
662
|
+
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
663
|
+
...proposalInfo,
|
|
664
|
+
numBlobs: blobs.length,
|
|
665
|
+
});
|
|
666
|
+
} catch (err) {
|
|
667
|
+
this.log.warn(`Failed to upload blobs for checkpoint: ${err}`, proposalInfo);
|
|
668
|
+
}
|
|
374
669
|
}
|
|
375
670
|
|
|
376
671
|
private slashInvalidBlock(proposal: BlockProposal) {
|
|
@@ -400,24 +695,129 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
400
695
|
]);
|
|
401
696
|
}
|
|
402
697
|
|
|
698
|
+
/**
|
|
699
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
700
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
701
|
+
*/
|
|
702
|
+
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
703
|
+
const { slot, proposer, type } = info;
|
|
704
|
+
|
|
705
|
+
this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
|
|
706
|
+
proposer: proposer.toString(),
|
|
707
|
+
slot,
|
|
708
|
+
type,
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// Emit slash event
|
|
712
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
713
|
+
{
|
|
714
|
+
validator: proposer,
|
|
715
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
716
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
717
|
+
epochOrSlot: BigInt(slot),
|
|
718
|
+
},
|
|
719
|
+
]);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
724
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
725
|
+
*/
|
|
726
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
727
|
+
const { slot, attester } = info;
|
|
728
|
+
|
|
729
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
730
|
+
attester: attester.toString(),
|
|
731
|
+
slot,
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
735
|
+
{
|
|
736
|
+
validator: attester,
|
|
737
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
738
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
739
|
+
epochOrSlot: BigInt(slot),
|
|
740
|
+
},
|
|
741
|
+
]);
|
|
742
|
+
}
|
|
743
|
+
|
|
403
744
|
async createBlockProposal(
|
|
404
|
-
|
|
405
|
-
|
|
745
|
+
blockHeader: BlockHeader,
|
|
746
|
+
checkpointNumber: CheckpointNumber,
|
|
747
|
+
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
748
|
+
inHash: Fr,
|
|
406
749
|
archive: Fr,
|
|
407
750
|
txs: Tx[],
|
|
408
751
|
proposerAddress: EthAddress | undefined,
|
|
409
|
-
options: BlockProposalOptions,
|
|
410
|
-
): Promise<BlockProposal
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
752
|
+
options: BlockProposalOptions = {},
|
|
753
|
+
): Promise<BlockProposal> {
|
|
754
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
755
|
+
if (this.lastProposedBlock) {
|
|
756
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
757
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
758
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
759
|
+
|
|
760
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
761
|
+
throw new Error(
|
|
762
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
763
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
414
766
|
}
|
|
415
767
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
768
|
+
this.log.info(
|
|
769
|
+
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
770
|
+
);
|
|
771
|
+
const newProposal = await this.validationService.createBlockProposal(
|
|
772
|
+
blockHeader,
|
|
773
|
+
checkpointNumber,
|
|
774
|
+
indexWithinCheckpoint,
|
|
775
|
+
inHash,
|
|
776
|
+
archive,
|
|
777
|
+
txs,
|
|
778
|
+
proposerAddress,
|
|
779
|
+
{
|
|
780
|
+
...options,
|
|
781
|
+
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
782
|
+
},
|
|
783
|
+
);
|
|
784
|
+
this.lastProposedBlock = newProposal;
|
|
785
|
+
return newProposal;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
async createCheckpointProposal(
|
|
789
|
+
checkpointHeader: CheckpointHeader,
|
|
790
|
+
archive: Fr,
|
|
791
|
+
checkpointNumber: CheckpointNumber,
|
|
792
|
+
feeAssetPriceModifier: bigint,
|
|
793
|
+
lastBlockProposal: BlockProposal | undefined,
|
|
794
|
+
proposerAddress: EthAddress | undefined,
|
|
795
|
+
options: CheckpointProposalOptions = {},
|
|
796
|
+
): Promise<CheckpointProposal> {
|
|
797
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
798
|
+
if (this.lastProposedCheckpoint) {
|
|
799
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
800
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
801
|
+
|
|
802
|
+
if (newSlot <= lastSlot) {
|
|
803
|
+
throw new Error(
|
|
804
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
805
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
811
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
812
|
+
checkpointHeader,
|
|
813
|
+
archive,
|
|
814
|
+
checkpointNumber,
|
|
815
|
+
feeAssetPriceModifier,
|
|
816
|
+
lastBlockProposal,
|
|
817
|
+
proposerAddress,
|
|
818
|
+
options,
|
|
819
|
+
);
|
|
820
|
+
this.lastProposedCheckpoint = newProposal;
|
|
421
821
|
return newProposal;
|
|
422
822
|
}
|
|
423
823
|
|
|
@@ -428,28 +828,47 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
428
828
|
async signAttestationsAndSigners(
|
|
429
829
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
430
830
|
proposer: EthAddress,
|
|
831
|
+
slot: SlotNumber,
|
|
832
|
+
checkpointNumber: CheckpointNumber,
|
|
431
833
|
): Promise<Signature> {
|
|
432
|
-
return await this.validationService.signAttestationsAndSigners(
|
|
834
|
+
return await this.validationService.signAttestationsAndSigners(
|
|
835
|
+
attestationsAndSigners,
|
|
836
|
+
proposer,
|
|
837
|
+
slot,
|
|
838
|
+
checkpointNumber,
|
|
839
|
+
);
|
|
433
840
|
}
|
|
434
841
|
|
|
435
|
-
async collectOwnAttestations(
|
|
436
|
-
|
|
842
|
+
async collectOwnAttestations(
|
|
843
|
+
proposal: CheckpointProposal,
|
|
844
|
+
checkpointNumber: CheckpointNumber,
|
|
845
|
+
): Promise<CheckpointAttestation[]> {
|
|
846
|
+
const slot = proposal.slotNumber;
|
|
437
847
|
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
438
848
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
439
|
-
const attestations = await this.
|
|
849
|
+
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
|
|
850
|
+
|
|
851
|
+
if (!attestations) {
|
|
852
|
+
return [];
|
|
853
|
+
}
|
|
440
854
|
|
|
441
855
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
442
856
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
443
857
|
// due to inactivity for missed attestations.
|
|
444
|
-
void this.p2pClient.
|
|
858
|
+
void this.p2pClient.broadcastCheckpointAttestations(attestations).catch(err => {
|
|
445
859
|
this.log.error(`Failed to broadcast self-attestations for slot ${slot}`, err);
|
|
446
860
|
});
|
|
447
861
|
return attestations;
|
|
448
862
|
}
|
|
449
863
|
|
|
450
|
-
async collectAttestations(
|
|
451
|
-
|
|
452
|
-
|
|
864
|
+
async collectAttestations(
|
|
865
|
+
proposal: CheckpointProposal,
|
|
866
|
+
required: number,
|
|
867
|
+
deadline: Date,
|
|
868
|
+
checkpointNumber: CheckpointNumber,
|
|
869
|
+
): Promise<CheckpointAttestation[]> {
|
|
870
|
+
// Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
|
|
871
|
+
const slot = proposal.slotNumber;
|
|
453
872
|
this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
|
|
454
873
|
|
|
455
874
|
if (+deadline < this.dateProvider.now()) {
|
|
@@ -459,21 +878,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
459
878
|
throw new AttestationTimeoutError(0, required, slot);
|
|
460
879
|
}
|
|
461
880
|
|
|
462
|
-
await this.collectOwnAttestations(proposal);
|
|
881
|
+
await this.collectOwnAttestations(proposal, checkpointNumber);
|
|
463
882
|
|
|
464
883
|
const proposalId = proposal.archive.toString();
|
|
465
884
|
const myAddresses = this.getValidatorAddresses();
|
|
466
885
|
|
|
467
|
-
let attestations:
|
|
886
|
+
let attestations: CheckpointAttestation[] = [];
|
|
468
887
|
while (true) {
|
|
469
|
-
// Filter out attestations with a mismatching
|
|
888
|
+
// Filter out attestations with a mismatching archive. This should NOT happen since we have verified
|
|
470
889
|
// the proposer signature (ie our own) before accepting the attestation into the pool via the p2p client.
|
|
471
|
-
const collectedAttestations = (await this.p2pClient.
|
|
890
|
+
const collectedAttestations = (await this.p2pClient.getCheckpointAttestationsForSlot(slot, proposalId)).filter(
|
|
472
891
|
attestation => {
|
|
473
|
-
if (!attestation.
|
|
892
|
+
if (!attestation.archive.equals(proposal.archive)) {
|
|
474
893
|
this.log.warn(
|
|
475
|
-
`Received attestation for slot ${slot} with mismatched
|
|
476
|
-
|
|
894
|
+
`Received attestation for slot ${slot} with mismatched archive from ${attestation
|
|
895
|
+
.getSender()
|
|
896
|
+
?.toString()}`,
|
|
897
|
+
{ attestationArchive: attestation.archive.toString(), proposalArchive: proposal.archive.toString() },
|
|
477
898
|
);
|
|
478
899
|
return false;
|
|
479
900
|
}
|
|
@@ -514,15 +935,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
514
935
|
}
|
|
515
936
|
}
|
|
516
937
|
|
|
517
|
-
private async createBlockAttestationsFromProposal(
|
|
518
|
-
proposal: BlockProposal,
|
|
519
|
-
attestors: EthAddress[] = [],
|
|
520
|
-
): Promise<BlockAttestation[]> {
|
|
521
|
-
const attestations = await this.validationService.attestToProposal(proposal, attestors);
|
|
522
|
-
await this.p2pClient.addAttestations(attestations);
|
|
523
|
-
return attestations;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
938
|
private async handleAuthRequest(peer: PeerId, msg: Buffer): Promise<Buffer> {
|
|
527
939
|
const authRequest = AuthRequest.fromBuffer(msg);
|
|
528
940
|
const statusMessage = await this.p2pClient.handleAuthRequestFromPeer(authRequest, peer).catch(_ => undefined);
|
|
@@ -541,7 +953,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
541
953
|
}
|
|
542
954
|
|
|
543
955
|
const payloadToSign = authRequest.getPayloadToSign();
|
|
544
|
-
|
|
956
|
+
// AUTH_REQUEST doesn't require HA protection - multiple signatures are safe
|
|
957
|
+
const context: SigningContext = { dutyType: DutyType.AUTH_REQUEST };
|
|
958
|
+
const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign, context);
|
|
545
959
|
const authResponse = new AuthResponse(statusMessage, signature);
|
|
546
960
|
return authResponse.toBuffer();
|
|
547
961
|
}
|