@aztec/validator-client 0.0.1-commit.96bb3f7 → 0.0.1-commit.993d240
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 +93 -24
- package/dest/checkpoint_builder.d.ts +35 -26
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +144 -48
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +44 -20
- package/dest/duties/validation_service.d.ts +26 -14
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +79 -38
- package/dest/factory.d.ts +11 -5
- 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 +99 -0
- package/dest/key_store/ha_key_store.d.ts.map +1 -0
- package/dest/key_store/ha_key_store.js +208 -0
- package/dest/key_store/index.d.ts +2 -1
- package/dest/key_store/index.d.ts.map +1 -1
- package/dest/key_store/index.js +1 -0
- package/dest/key_store/interface.d.ts +36 -6
- package/dest/key_store/interface.d.ts.map +1 -1
- package/dest/key_store/local_key_store.d.ts +10 -5
- package/dest/key_store/local_key_store.d.ts.map +1 -1
- package/dest/key_store/local_key_store.js +8 -4
- package/dest/key_store/node_keystore_adapter.d.ts +18 -5
- package/dest/key_store/node_keystore_adapter.d.ts.map +1 -1
- package/dest/key_store/node_keystore_adapter.js +18 -4
- package/dest/key_store/web3signer_key_store.d.ts +10 -5
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
- package/dest/key_store/web3signer_key_store.js +8 -4
- package/dest/metrics.d.ts +16 -3
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +58 -5
- package/dest/proposal_handler.d.ts +135 -0
- package/dest/proposal_handler.d.ts.map +1 -0
- package/dest/proposal_handler.js +1111 -0
- package/dest/validator.d.ts +60 -25
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +393 -257
- package/package.json +21 -17
- package/src/checkpoint_builder.ts +211 -61
- package/src/config.ts +43 -18
- package/src/duties/validation_service.ts +119 -46
- package/src/factory.ts +21 -3
- package/src/index.ts +1 -2
- package/src/key_store/ha_key_store.ts +269 -0
- package/src/key_store/index.ts +1 -0
- package/src/key_store/interface.ts +44 -5
- package/src/key_store/local_key_store.ts +13 -4
- package/src/key_store/node_keystore_adapter.ts +27 -4
- package/src/key_store/web3signer_key_store.ts +17 -4
- package/src/metrics.ts +81 -6
- package/src/proposal_handler.ts +1207 -0
- package/src/validator.ts +529 -307
- 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 -551
- 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 -53
- package/src/block_proposal_handler.ts +0 -556
- 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 -133
package/src/validator.ts
CHANGED
|
@@ -1,61 +1,111 @@
|
|
|
1
1
|
import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
2
2
|
import { type Blob, getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
3
3
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
4
|
-
import {
|
|
4
|
+
import { CheckpointNumber, EpochNumber, IndexWithinCheckpoint, SlotNumber } from '@aztec/foundation/branded-types';
|
|
5
5
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
6
|
-
import { TimeoutError } from '@aztec/foundation/error';
|
|
7
6
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
8
7
|
import type { Signature } from '@aztec/foundation/eth-signature';
|
|
8
|
+
import { FifoSet } from '@aztec/foundation/fifo-set';
|
|
9
9
|
import { type LogData, type Logger, createLogger } from '@aztec/foundation/log';
|
|
10
|
-
import { retryUntil } from '@aztec/foundation/retry';
|
|
11
10
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
12
11
|
import { sleep } from '@aztec/foundation/sleep';
|
|
13
12
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
14
13
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
15
|
-
import type { P2P, PeerId
|
|
14
|
+
import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
16
15
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
17
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
OffenseType,
|
|
18
|
+
WANT_TO_CLEAR_SLASH_EVENT,
|
|
19
|
+
WANT_TO_SLASH_EVENT,
|
|
20
|
+
type Watcher,
|
|
21
|
+
type WatcherEmitter,
|
|
22
|
+
getOffenseTypeName,
|
|
23
|
+
} from '@aztec/slasher';
|
|
18
24
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
19
|
-
import type { CommitteeAttestationsAndSigners,
|
|
25
|
+
import type { CommitteeAttestationsAndSigners, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
26
|
+
import type { CheckpointReexecutionTracker } from '@aztec/stdlib/checkpoint';
|
|
27
|
+
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
20
28
|
import type {
|
|
21
|
-
|
|
29
|
+
ITxProvider,
|
|
22
30
|
Validator,
|
|
23
31
|
ValidatorClientFullConfig,
|
|
24
32
|
WorldStateSynchronizer,
|
|
25
33
|
} from '@aztec/stdlib/interfaces/server';
|
|
26
34
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
27
|
-
import
|
|
28
|
-
BlockProposal,
|
|
29
|
-
BlockProposalOptions,
|
|
30
|
-
CheckpointAttestation,
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
import {
|
|
36
|
+
type BlockProposal,
|
|
37
|
+
type BlockProposalOptions,
|
|
38
|
+
type CheckpointAttestation,
|
|
39
|
+
CheckpointProposal,
|
|
40
|
+
type CheckpointProposalCore,
|
|
41
|
+
type CheckpointProposalOptions,
|
|
42
|
+
type CoordinationSignatureContext,
|
|
33
43
|
} from '@aztec/stdlib/p2p';
|
|
34
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
35
44
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
36
|
-
import type { BlockHeader,
|
|
45
|
+
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
|
|
37
46
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
38
47
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
48
|
+
import {
|
|
49
|
+
createHASigner,
|
|
50
|
+
createLocalSignerWithProtection,
|
|
51
|
+
createSignerFromSharedDb,
|
|
52
|
+
} from '@aztec/validator-ha-signer/factory';
|
|
53
|
+
import { DutyType, type SigningContext, type SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
|
|
54
|
+
import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
|
|
39
55
|
|
|
40
56
|
import { EventEmitter } from 'events';
|
|
41
57
|
import type { TypedDataDefinition } from 'viem';
|
|
42
58
|
|
|
43
|
-
import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
|
|
44
59
|
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
45
60
|
import { ValidationService } from './duties/validation_service.js';
|
|
61
|
+
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
62
|
+
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
46
63
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
47
64
|
import { ValidatorMetrics } from './metrics.js';
|
|
65
|
+
import {
|
|
66
|
+
type BlockProposalValidationFailureReason,
|
|
67
|
+
type CheckpointProposalValidationFailureReason,
|
|
68
|
+
type CheckpointProposalValidationFailureResult,
|
|
69
|
+
ProposalHandler,
|
|
70
|
+
} from './proposal_handler.js';
|
|
48
71
|
|
|
49
72
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
50
73
|
// Just cap the set to avoid unbounded growth.
|
|
51
74
|
const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
75
|
+
const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
|
|
76
|
+
const MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS = 1000;
|
|
77
|
+
const MAX_TRACKED_BAD_ATTESTATIONS = 10_000;
|
|
52
78
|
|
|
53
79
|
// What errors from the block proposal handler result in slashing
|
|
54
80
|
const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT: BlockProposalValidationFailureReason[] = [
|
|
55
81
|
'state_mismatch',
|
|
56
82
|
'failed_txs',
|
|
83
|
+
'global_variables_mismatch',
|
|
84
|
+
'invalid_proposal',
|
|
85
|
+
'parent_block_wrong_slot',
|
|
86
|
+
'in_hash_mismatch',
|
|
57
87
|
];
|
|
58
88
|
|
|
89
|
+
const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT: Record<CheckpointProposalValidationFailureReason, boolean> = {
|
|
90
|
+
// enabled
|
|
91
|
+
['invalid_fee_asset_price_modifier']: true,
|
|
92
|
+
['checkpoint_header_mismatch']: true,
|
|
93
|
+
// These late mismatches should normally be caught by earlier checks, but if reached after validating the local
|
|
94
|
+
// checkpoint inputs, the proposer-signed payload disagrees with deterministic recomputation.
|
|
95
|
+
['archive_mismatch']: true,
|
|
96
|
+
['out_hash_mismatch']: true,
|
|
97
|
+
['no_blocks_for_slot']: true,
|
|
98
|
+
['too_many_blocks_in_checkpoint']: true,
|
|
99
|
+
['checkpoint_validation_failed']: true,
|
|
100
|
+
['last_block_archive_mismatch']: true,
|
|
101
|
+
|
|
102
|
+
// disabled
|
|
103
|
+
['invalid_signature']: false,
|
|
104
|
+
['last_block_not_found']: false,
|
|
105
|
+
['block_fetch_error']: false,
|
|
106
|
+
['checkpoint_already_published']: false,
|
|
107
|
+
};
|
|
108
|
+
|
|
59
109
|
/**
|
|
60
110
|
* Validator Client
|
|
61
111
|
*/
|
|
@@ -64,34 +114,41 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
64
114
|
private validationService: ValidationService;
|
|
65
115
|
private metrics: ValidatorMetrics;
|
|
66
116
|
private log: Logger;
|
|
67
|
-
|
|
68
117
|
// Whether it has already registered handlers on the p2p client
|
|
69
118
|
private hasRegisteredHandlers = false;
|
|
70
119
|
|
|
71
|
-
|
|
72
|
-
private
|
|
120
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
|
|
121
|
+
private lastProposedBlock?: BlockProposal;
|
|
122
|
+
|
|
123
|
+
/** Tracks the last checkpoint proposal we created. */
|
|
124
|
+
private lastProposedCheckpoint?: CheckpointProposal;
|
|
73
125
|
|
|
74
126
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
75
127
|
private epochCacheUpdateLoop: RunningPromise;
|
|
128
|
+
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
|
|
129
|
+
private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
|
|
76
130
|
|
|
77
|
-
private proposersOfInvalidBlocks
|
|
131
|
+
private proposersOfInvalidBlocks = FifoSet.withLimit<string>(MAX_PROPOSERS_OF_INVALID_BLOCKS);
|
|
132
|
+
private slotsWithInvalidProposals = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
|
|
133
|
+
private invalidCheckpointProposalOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS);
|
|
134
|
+
private badAttestationOffenseKeys = FifoSet.withLimit<string>(MAX_TRACKED_BAD_ATTESTATIONS);
|
|
135
|
+
private slotsWithProposalEquivocation = FifoSet.withLimit<SlotNumber>(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
|
|
78
136
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
82
|
-
private validatedBlockSlots: Set<SlotNumber> = new Set();
|
|
137
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
138
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
83
139
|
|
|
84
140
|
protected constructor(
|
|
85
|
-
private keyStore:
|
|
141
|
+
private keyStore: ExtendedValidatorKeyStore,
|
|
86
142
|
private epochCache: EpochCache,
|
|
87
143
|
private p2pClient: P2P,
|
|
88
|
-
private
|
|
144
|
+
private proposalHandler: ProposalHandler,
|
|
89
145
|
private blockSource: L2BlockSource,
|
|
90
146
|
private checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
91
147
|
private worldState: WorldStateSynchronizer,
|
|
92
148
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
93
149
|
private config: ValidatorClientFullConfig,
|
|
94
150
|
private blobClient: BlobClientInterface,
|
|
151
|
+
private slashingProtectionSigner: ValidatorHASigner,
|
|
95
152
|
private dateProvider: DateProvider = new DateProvider(),
|
|
96
153
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
97
154
|
log = createLogger('validator'),
|
|
@@ -104,11 +161,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
104
161
|
this.tracer = telemetry.getTracer('Validator');
|
|
105
162
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
106
163
|
|
|
107
|
-
this.validationService = new ValidationService(
|
|
164
|
+
this.validationService = new ValidationService(
|
|
165
|
+
keyStore,
|
|
166
|
+
this.getSignatureContext(),
|
|
167
|
+
this.log.createChild('validation-service'),
|
|
168
|
+
);
|
|
169
|
+
this.proposalHandler.setCheckpointProposalValidationFailureCallback((proposal, result, proposalInfo) =>
|
|
170
|
+
this.handleInvalidCheckpointProposal(proposal, result, proposalInfo),
|
|
171
|
+
);
|
|
108
172
|
|
|
109
173
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
110
174
|
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
|
|
111
|
-
|
|
112
175
|
const myAddresses = this.getValidatorAddresses();
|
|
113
176
|
this.log.verbose(`Initialized validator with addresses: ${myAddresses.map(a => a.toString()).join(', ')}`);
|
|
114
177
|
}
|
|
@@ -145,6 +208,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
145
208
|
this.log.trace(`No committee found for slot`);
|
|
146
209
|
return;
|
|
147
210
|
}
|
|
211
|
+
this.metrics.setCurrentEpoch(epoch);
|
|
148
212
|
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
149
213
|
const me = this.getValidatorAddresses();
|
|
150
214
|
const committeeSet = new Set(committee.map(v => v.toString()));
|
|
@@ -165,7 +229,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
165
229
|
}
|
|
166
230
|
}
|
|
167
231
|
|
|
168
|
-
static new(
|
|
232
|
+
static async new(
|
|
169
233
|
config: ValidatorClientFullConfig,
|
|
170
234
|
checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
171
235
|
worldState: WorldStateSynchronizer,
|
|
@@ -173,40 +237,83 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
173
237
|
p2pClient: P2P,
|
|
174
238
|
blockSource: L2BlockSource & L2BlockSink,
|
|
175
239
|
l1ToL2MessageSource: L1ToL2MessageSource,
|
|
176
|
-
txProvider:
|
|
240
|
+
txProvider: ITxProvider,
|
|
177
241
|
keyStoreManager: KeystoreManager,
|
|
178
242
|
blobClient: BlobClientInterface,
|
|
243
|
+
reexecutionTracker: CheckpointReexecutionTracker,
|
|
179
244
|
dateProvider: DateProvider = new DateProvider(),
|
|
180
245
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
246
|
+
slashingProtectionDb?: SlashingProtectionDatabase,
|
|
181
247
|
) {
|
|
182
248
|
const metrics = new ValidatorMetrics(telemetry);
|
|
183
249
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
184
250
|
txsPermitted: !config.disableTransactions,
|
|
251
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
252
|
+
maxBlocksPerCheckpoint: config.maxBlocksPerCheckpoint,
|
|
253
|
+
skipSlotValidation: config.skipProposalSlotValidation,
|
|
254
|
+
signatureContext: {
|
|
255
|
+
chainId: config.l1ChainId,
|
|
256
|
+
rollupAddress: config.rollupAddress,
|
|
257
|
+
},
|
|
185
258
|
});
|
|
186
|
-
const
|
|
259
|
+
const proposalHandler = new ProposalHandler(
|
|
187
260
|
checkpointsBuilder,
|
|
188
261
|
worldState,
|
|
189
262
|
blockSource,
|
|
190
263
|
l1ToL2MessageSource,
|
|
191
264
|
txProvider,
|
|
192
265
|
blockProposalValidator,
|
|
266
|
+
epochCache,
|
|
193
267
|
config,
|
|
268
|
+
blobClient,
|
|
269
|
+
reexecutionTracker,
|
|
194
270
|
metrics,
|
|
195
271
|
dateProvider,
|
|
196
272
|
telemetry,
|
|
273
|
+
undefined,
|
|
197
274
|
);
|
|
198
275
|
|
|
276
|
+
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
277
|
+
let slashingProtectionSigner: ValidatorHASigner;
|
|
278
|
+
if (slashingProtectionDb) {
|
|
279
|
+
// Shared database mode: use a pre-existing database (e.g. for testing HA setups).
|
|
280
|
+
({ signer: slashingProtectionSigner } = createSignerFromSharedDb(slashingProtectionDb, config, {
|
|
281
|
+
telemetryClient: telemetry,
|
|
282
|
+
dateProvider,
|
|
283
|
+
}));
|
|
284
|
+
} else if (config.haSigningEnabled) {
|
|
285
|
+
// Multi-node HA mode: use PostgreSQL-backed distributed locking.
|
|
286
|
+
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
287
|
+
const haConfig = {
|
|
288
|
+
...config,
|
|
289
|
+
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
|
|
290
|
+
};
|
|
291
|
+
({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
|
|
292
|
+
telemetryClient: telemetry,
|
|
293
|
+
dateProvider,
|
|
294
|
+
}));
|
|
295
|
+
} else {
|
|
296
|
+
// Single-node mode: use LMDB-backed local signing protection.
|
|
297
|
+
// This prevents double-signing if the node crashes and restarts mid-proposal.
|
|
298
|
+
({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
|
|
299
|
+
telemetryClient: telemetry,
|
|
300
|
+
dateProvider,
|
|
301
|
+
}));
|
|
302
|
+
}
|
|
303
|
+
const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
|
|
304
|
+
|
|
199
305
|
const validator = new ValidatorClient(
|
|
200
|
-
|
|
306
|
+
validatorKeyStore,
|
|
201
307
|
epochCache,
|
|
202
308
|
p2pClient,
|
|
203
|
-
|
|
309
|
+
proposalHandler,
|
|
204
310
|
blockSource,
|
|
205
311
|
checkpointsBuilder,
|
|
206
312
|
worldState,
|
|
207
313
|
l1ToL2MessageSource,
|
|
208
314
|
config,
|
|
209
315
|
blobClient,
|
|
316
|
+
slashingProtectionSigner,
|
|
210
317
|
dateProvider,
|
|
211
318
|
telemetry,
|
|
212
319
|
);
|
|
@@ -220,12 +327,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
220
327
|
.filter(addr => !this.config.disabledValidators.some(disabled => disabled.equals(addr)));
|
|
221
328
|
}
|
|
222
329
|
|
|
223
|
-
public
|
|
224
|
-
return this.
|
|
330
|
+
public getProposalHandler() {
|
|
331
|
+
return this.proposalHandler;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition, context: SigningContext) {
|
|
335
|
+
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
225
336
|
}
|
|
226
337
|
|
|
227
|
-
|
|
228
|
-
return
|
|
338
|
+
private getSignatureContext(): CoordinationSignatureContext {
|
|
339
|
+
return {
|
|
340
|
+
chainId: this.config.l1ChainId,
|
|
341
|
+
rollupAddress: this.config.rollupAddress,
|
|
342
|
+
};
|
|
229
343
|
}
|
|
230
344
|
|
|
231
345
|
public getCoinbaseForAttestor(attestor: EthAddress): EthAddress {
|
|
@@ -240,8 +354,27 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
240
354
|
return this.config;
|
|
241
355
|
}
|
|
242
356
|
|
|
357
|
+
public hasProposalEquivocation(slotNumber: SlotNumber): boolean {
|
|
358
|
+
return this.slotsWithProposalEquivocation.has(slotNumber);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
public hasInvalidProposals(slotNumber: SlotNumber): boolean {
|
|
362
|
+
return this.slotsWithInvalidProposals.has(slotNumber);
|
|
363
|
+
}
|
|
364
|
+
|
|
243
365
|
public updateConfig(config: Partial<ValidatorClientFullConfig>) {
|
|
244
366
|
this.config = { ...this.config, ...config };
|
|
367
|
+
this.proposalHandler.updateConfig(config);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
public reloadKeystore(newManager: KeystoreManager): void {
|
|
371
|
+
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
372
|
+
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
373
|
+
this.validationService = new ValidationService(
|
|
374
|
+
this.keyStore,
|
|
375
|
+
this.getSignatureContext(),
|
|
376
|
+
this.log.createChild('validation-service'),
|
|
377
|
+
);
|
|
245
378
|
}
|
|
246
379
|
|
|
247
380
|
public async start() {
|
|
@@ -250,6 +383,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
250
383
|
return;
|
|
251
384
|
}
|
|
252
385
|
|
|
386
|
+
await this.keyStore.start();
|
|
387
|
+
|
|
253
388
|
await this.registerHandlers();
|
|
254
389
|
|
|
255
390
|
const myAddresses = this.getValidatorAddresses();
|
|
@@ -265,6 +400,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
265
400
|
|
|
266
401
|
public async stop() {
|
|
267
402
|
await this.epochCacheUpdateLoop.stop();
|
|
403
|
+
await this.keyStore.stop();
|
|
268
404
|
}
|
|
269
405
|
|
|
270
406
|
/** Register handlers on the p2p client */
|
|
@@ -285,7 +421,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
285
421
|
checkpoint: CheckpointProposalCore,
|
|
286
422
|
proposalSender: PeerId,
|
|
287
423
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
288
|
-
this.p2pClient.
|
|
424
|
+
this.p2pClient.registerValidatorCheckpointProposalHandler(checkpointHandler);
|
|
425
|
+
|
|
426
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
427
|
+
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
428
|
+
this.handleDuplicateProposal(info);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
432
|
+
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
433
|
+
this.handleDuplicateAttestation(info);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
this.p2pClient.registerCheckpointAttestationCallback((attestation: CheckpointAttestation) => {
|
|
437
|
+
this.handleCheckpointAttestation(attestation);
|
|
438
|
+
});
|
|
289
439
|
|
|
290
440
|
const myAddresses = this.getValidatorAddresses();
|
|
291
441
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
@@ -301,6 +451,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
301
451
|
*/
|
|
302
452
|
async validateBlockProposal(proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> {
|
|
303
453
|
const slotNumber = proposal.slotNumber;
|
|
454
|
+
|
|
455
|
+
// Note: During escape hatch, we still want to "validate" proposals for observability,
|
|
456
|
+
// but we intentionally reject them and disable slashing invalid block and attestation flow.
|
|
457
|
+
const escapeHatchOpen = await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber);
|
|
458
|
+
|
|
304
459
|
const proposer = proposal.getSender();
|
|
305
460
|
|
|
306
461
|
// Reject proposals with invalid signatures
|
|
@@ -309,6 +464,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
309
464
|
return false;
|
|
310
465
|
}
|
|
311
466
|
|
|
467
|
+
// Log self-proposals from HA peers (same validator key on different nodes)
|
|
468
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
469
|
+
this.log.verbose(`Processing block proposal from HA peer for slot ${slotNumber}`, {
|
|
470
|
+
proposer: proposer.toString(),
|
|
471
|
+
slotNumber,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
312
475
|
// Check if we're in the committee (for metrics purposes)
|
|
313
476
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
314
477
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -320,27 +483,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
320
483
|
fishermanMode: this.config.fishermanMode || false,
|
|
321
484
|
});
|
|
322
485
|
|
|
323
|
-
// Reexecute
|
|
324
|
-
|
|
325
|
-
const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } =
|
|
326
|
-
this.config;
|
|
327
|
-
const shouldReexecute =
|
|
328
|
-
fishermanMode ||
|
|
329
|
-
(slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute) ||
|
|
330
|
-
(partOfCommittee && validatorReexecute) ||
|
|
331
|
-
alwaysReexecuteBlockProposals ||
|
|
332
|
-
this.blobClient.canUpload();
|
|
333
|
-
|
|
334
|
-
const validationResult = await this.blockProposalHandler.handleBlockProposal(
|
|
335
|
-
proposal,
|
|
336
|
-
proposalSender,
|
|
337
|
-
!!shouldReexecute,
|
|
338
|
-
);
|
|
486
|
+
// Reexecute outside the escape hatch so slashing observers can detect invalid proposals even when penalties are 0.
|
|
487
|
+
const validationResult = await this.proposalHandler.handleBlockProposal(proposal, proposalSender, !escapeHatchOpen);
|
|
339
488
|
|
|
340
489
|
if (!validationResult.isValid) {
|
|
341
|
-
this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
342
|
-
|
|
343
490
|
const reason = validationResult.reason || 'unknown';
|
|
491
|
+
|
|
492
|
+
this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
|
|
493
|
+
|
|
344
494
|
// Classify failure reason: bad proposal vs node issue
|
|
345
495
|
const badProposalReasons: BlockProposalValidationFailureReason[] = [
|
|
346
496
|
'invalid_proposal',
|
|
@@ -357,14 +507,18 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
357
507
|
this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
|
|
358
508
|
}
|
|
359
509
|
|
|
360
|
-
// Slash invalid block proposals (can happen even when not in committee)
|
|
361
510
|
if (
|
|
511
|
+
!escapeHatchOpen &&
|
|
362
512
|
validationResult.reason &&
|
|
363
|
-
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)
|
|
364
|
-
slashBroadcastedInvalidBlockPenalty > 0n
|
|
513
|
+
SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)
|
|
365
514
|
) {
|
|
366
|
-
this.log.
|
|
515
|
+
this.log.info(`Detected invalid block proposal offense`, {
|
|
516
|
+
...proposalInfo,
|
|
517
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
518
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL),
|
|
519
|
+
});
|
|
367
520
|
this.slashInvalidBlock(proposal);
|
|
521
|
+
this.markInvalidProposalSlot(proposal.slotNumber);
|
|
368
522
|
}
|
|
369
523
|
return false;
|
|
370
524
|
}
|
|
@@ -373,11 +527,13 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
373
527
|
...proposalInfo,
|
|
374
528
|
inCommittee: partOfCommittee,
|
|
375
529
|
fishermanMode: this.config.fishermanMode || false,
|
|
530
|
+
escapeHatchOpen,
|
|
376
531
|
});
|
|
377
532
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
533
|
+
if (escapeHatchOpen) {
|
|
534
|
+
this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
381
537
|
|
|
382
538
|
return true;
|
|
383
539
|
}
|
|
@@ -392,53 +548,56 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
392
548
|
proposal: CheckpointProposalCore,
|
|
393
549
|
_proposalSender: PeerId,
|
|
394
550
|
): Promise<CheckpointAttestation[] | undefined> {
|
|
395
|
-
const
|
|
551
|
+
const proposalSlotNumber = proposal.slotNumber;
|
|
396
552
|
const proposer = proposal.getSender();
|
|
397
553
|
|
|
398
|
-
//
|
|
399
|
-
if (
|
|
400
|
-
this.log.warn(`
|
|
554
|
+
// If escape hatch is open for this slot's epoch, do not attest.
|
|
555
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(proposalSlotNumber)) {
|
|
556
|
+
this.log.warn(`Escape hatch open for slot ${proposalSlotNumber}, skipping checkpoint attestation handling`);
|
|
401
557
|
return undefined;
|
|
402
558
|
}
|
|
403
559
|
|
|
404
|
-
//
|
|
405
|
-
|
|
560
|
+
// Early-out for equivocation: refuses if we've already attested to a higher slot.
|
|
561
|
+
if (!this.shouldAttestToSlot(proposalSlotNumber)) {
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
566
|
+
if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
567
|
+
this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
|
|
568
|
+
proposer: proposer.toString(),
|
|
569
|
+
proposalSlotNumber,
|
|
570
|
+
});
|
|
571
|
+
return undefined;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Check that I have any address in the committee where this checkpoint will land before attesting
|
|
575
|
+
const inCommittee = await this.epochCache.filterInCommittee(proposalSlotNumber, this.getValidatorAddresses());
|
|
406
576
|
const partOfCommittee = inCommittee.length > 0;
|
|
407
577
|
|
|
408
578
|
const proposalInfo = {
|
|
409
|
-
|
|
579
|
+
proposalSlotNumber,
|
|
410
580
|
archive: proposal.archive.toString(),
|
|
411
|
-
proposer: proposer
|
|
412
|
-
txCount: proposal.txHashes.length,
|
|
581
|
+
proposer: proposer?.toString(),
|
|
413
582
|
};
|
|
414
|
-
this.log.info(`Received checkpoint proposal for slot ${
|
|
583
|
+
this.log.info(`Received checkpoint proposal for slot ${proposalSlotNumber}`, {
|
|
415
584
|
...proposalInfo,
|
|
416
|
-
txHashes: proposal.txHashes.map(t => t.toString()),
|
|
417
585
|
fishermanMode: this.config.fishermanMode || false,
|
|
418
586
|
});
|
|
419
587
|
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
428
|
-
// TODO(palla/mbps): Change default to false once checkpoint validation is stable.
|
|
429
|
-
if (this.config.skipCheckpointProposalValidation !== false) {
|
|
430
|
-
this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
588
|
+
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
|
|
589
|
+
// Uses the cached result from the all-nodes callback if available (avoids double validation).
|
|
590
|
+
let checkpointNumber: CheckpointNumber;
|
|
591
|
+
if (this.config.skipCheckpointProposalValidation) {
|
|
592
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
|
|
593
|
+
checkpointNumber = CheckpointNumber(0);
|
|
431
594
|
} else {
|
|
432
|
-
const validationResult = await this.
|
|
595
|
+
const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
|
|
433
596
|
if (!validationResult.isValid) {
|
|
434
597
|
this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
435
598
|
return undefined;
|
|
436
599
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
// Upload blobs to filestore if we can (fire and forget)
|
|
440
|
-
if (this.blobClient.canUpload()) {
|
|
441
|
-
void this.uploadBlobsForCheckpoint(proposal, proposalInfo);
|
|
600
|
+
checkpointNumber = validationResult.checkpointNumber;
|
|
442
601
|
}
|
|
443
602
|
|
|
444
603
|
// Check that I have any address in current committee before attesting
|
|
@@ -449,14 +608,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
449
608
|
}
|
|
450
609
|
|
|
451
610
|
// Provided all of the above checks pass, we can attest to the proposal
|
|
452
|
-
this.log.info(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
611
|
+
this.log.info(
|
|
612
|
+
`${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${proposalSlotNumber}`,
|
|
613
|
+
{
|
|
614
|
+
...proposalInfo,
|
|
615
|
+
inCommittee: partOfCommittee,
|
|
616
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
617
|
+
},
|
|
618
|
+
);
|
|
457
619
|
|
|
458
620
|
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
459
621
|
|
|
622
|
+
// Track epoch participation per attester: count each (attester, epoch) pair at most once
|
|
623
|
+
const proposalEpoch = getEpochAtSlot(proposalSlotNumber, this.epochCache.getL1Constants());
|
|
624
|
+
for (const attester of inCommittee) {
|
|
625
|
+
const key = attester.toString();
|
|
626
|
+
const lastEpoch = this.lastAttestedEpochByAttester.get(key);
|
|
627
|
+
if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
|
|
628
|
+
this.lastAttestedEpochByAttester.set(key, proposalEpoch);
|
|
629
|
+
this.metrics.incAttestedEpochCount(attester);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
460
633
|
// Determine which validators should attest
|
|
461
634
|
let attestors: EthAddress[];
|
|
462
635
|
if (partOfCommittee) {
|
|
@@ -475,214 +648,75 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
475
648
|
|
|
476
649
|
if (this.config.fishermanMode) {
|
|
477
650
|
// bail out early and don't save attestations to the pool in fisherman mode
|
|
478
|
-
this.log.info(`Creating checkpoint attestations for slot ${
|
|
651
|
+
this.log.info(`Creating checkpoint attestations for slot ${proposalSlotNumber}`, {
|
|
479
652
|
...proposalInfo,
|
|
480
653
|
attestors: attestors.map(a => a.toString()),
|
|
481
654
|
});
|
|
482
655
|
return undefined;
|
|
483
656
|
}
|
|
484
657
|
|
|
485
|
-
return this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
private async createCheckpointAttestationsFromProposal(
|
|
489
|
-
proposal: CheckpointProposalCore,
|
|
490
|
-
attestors: EthAddress[] = [],
|
|
491
|
-
): Promise<CheckpointAttestation[]> {
|
|
492
|
-
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
493
|
-
await this.p2pClient.addCheckpointAttestations(attestations);
|
|
494
|
-
return attestations;
|
|
658
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
|
|
495
659
|
}
|
|
496
660
|
|
|
497
661
|
/**
|
|
498
|
-
*
|
|
499
|
-
* @returns
|
|
662
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
663
|
+
* @returns true if we should attest, false if we should skip
|
|
500
664
|
*/
|
|
501
|
-
private
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const slot = proposal.slotNumber;
|
|
506
|
-
const timeoutSeconds = 10;
|
|
507
|
-
|
|
508
|
-
// Wait for last block to sync by archive
|
|
509
|
-
let lastBlockHeader: BlockHeader | undefined;
|
|
510
|
-
try {
|
|
511
|
-
lastBlockHeader = await retryUntil(
|
|
512
|
-
async () => {
|
|
513
|
-
await this.blockSource.syncImmediate();
|
|
514
|
-
return this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
515
|
-
},
|
|
516
|
-
`waiting for block with archive ${proposal.archive.toString()} for slot ${slot}`,
|
|
517
|
-
timeoutSeconds,
|
|
518
|
-
0.5,
|
|
519
|
-
);
|
|
520
|
-
} catch (err) {
|
|
521
|
-
if (err instanceof TimeoutError) {
|
|
522
|
-
this.log.warn(`Timed out waiting for block with archive matching checkpoint proposal`, proposalInfo);
|
|
523
|
-
return { isValid: false, reason: 'last_block_not_found' };
|
|
524
|
-
}
|
|
525
|
-
this.log.error(`Error fetching last block for checkpoint proposal`, err, proposalInfo);
|
|
526
|
-
return { isValid: false, reason: 'block_fetch_error' };
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (!lastBlockHeader) {
|
|
530
|
-
this.log.warn(`Last block not found for checkpoint proposal`, proposalInfo);
|
|
531
|
-
return { isValid: false, reason: 'last_block_not_found' };
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Get the last full block to determine checkpoint number
|
|
535
|
-
const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
|
|
536
|
-
if (!lastBlock) {
|
|
537
|
-
this.log.warn(`Last block ${lastBlockHeader.getBlockNumber()} not found`, proposalInfo);
|
|
538
|
-
return { isValid: false, reason: 'last_block_not_found' };
|
|
539
|
-
}
|
|
540
|
-
const checkpointNumber = lastBlock.checkpointNumber;
|
|
541
|
-
|
|
542
|
-
// Get all full blocks for the slot and checkpoint
|
|
543
|
-
const blocks = await this.getBlocksForSlot(slot, lastBlockHeader, checkpointNumber);
|
|
544
|
-
if (blocks.length === 0) {
|
|
545
|
-
this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
|
|
546
|
-
return { isValid: false, reason: 'no_blocks_for_slot' };
|
|
665
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
666
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
667
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
668
|
+
return true;
|
|
547
669
|
}
|
|
548
670
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
// Get checkpoint constants from first block
|
|
555
|
-
const firstBlock = blocks[0];
|
|
556
|
-
const constants = this.extractCheckpointConstants(firstBlock);
|
|
557
|
-
|
|
558
|
-
// Get L1-to-L2 messages for this checkpoint
|
|
559
|
-
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
560
|
-
|
|
561
|
-
// Fork world state at the block before the first block
|
|
562
|
-
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
563
|
-
const fork = await this.worldState.fork(parentBlockNumber);
|
|
564
|
-
|
|
565
|
-
try {
|
|
566
|
-
// Create checkpoint builder with all existing blocks
|
|
567
|
-
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
568
|
-
checkpointNumber,
|
|
569
|
-
constants,
|
|
570
|
-
l1ToL2Messages,
|
|
571
|
-
fork,
|
|
572
|
-
blocks,
|
|
671
|
+
// Check if incoming slot is strictly greater than last attested
|
|
672
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
673
|
+
this.log.warn(
|
|
674
|
+
`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
|
|
573
675
|
);
|
|
574
|
-
|
|
575
|
-
// Complete the checkpoint to get computed values
|
|
576
|
-
const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
|
|
577
|
-
|
|
578
|
-
// Compare checkpoint header with proposal
|
|
579
|
-
if (!computedCheckpoint.header.equals(proposal.checkpointHeader)) {
|
|
580
|
-
this.log.warn(`Checkpoint header mismatch`, {
|
|
581
|
-
...proposalInfo,
|
|
582
|
-
computed: computedCheckpoint.header.toInspect(),
|
|
583
|
-
proposal: proposal.checkpointHeader.toInspect(),
|
|
584
|
-
});
|
|
585
|
-
return { isValid: false, reason: 'checkpoint_header_mismatch' };
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Compare archive root with proposal
|
|
589
|
-
if (!computedCheckpoint.archive.root.equals(proposal.archive)) {
|
|
590
|
-
this.log.warn(`Archive root mismatch`, {
|
|
591
|
-
...proposalInfo,
|
|
592
|
-
computed: computedCheckpoint.archive.root.toString(),
|
|
593
|
-
proposal: proposal.archive.toString(),
|
|
594
|
-
});
|
|
595
|
-
return { isValid: false, reason: 'archive_mismatch' };
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
599
|
-
return { isValid: true };
|
|
600
|
-
} finally {
|
|
601
|
-
await fork.close();
|
|
676
|
+
return false;
|
|
602
677
|
}
|
|
678
|
+
|
|
679
|
+
return true;
|
|
603
680
|
}
|
|
604
681
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
* TODO(palla/mbps): Add getL2BlocksForSlot() to L2BlockSource interface for efficiency.
|
|
609
|
-
*/
|
|
610
|
-
private async getBlocksForSlot(
|
|
611
|
-
slot: SlotNumber,
|
|
612
|
-
lastBlockHeader: BlockHeader,
|
|
682
|
+
private async createCheckpointAttestationsFromProposal(
|
|
683
|
+
proposal: CheckpointProposalCore,
|
|
684
|
+
attestors: EthAddress[] = [],
|
|
613
685
|
checkpointNumber: CheckpointNumber,
|
|
614
|
-
): Promise<
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
while (currentHeader.getSlot() === slot) {
|
|
620
|
-
const block = await this.blockSource.getL2BlockNew(currentHeader.getBlockNumber());
|
|
621
|
-
if (!block) {
|
|
622
|
-
this.log.warn(`Block ${currentHeader.getBlockNumber()} not found while getting blocks for slot ${slot}`);
|
|
623
|
-
break;
|
|
624
|
-
}
|
|
625
|
-
if (block.checkpointNumber !== checkpointNumber) {
|
|
626
|
-
break;
|
|
627
|
-
}
|
|
628
|
-
blocks.unshift(block);
|
|
629
|
-
|
|
630
|
-
const prevArchive = currentHeader.lastArchive.root;
|
|
631
|
-
if (prevArchive.equals(genesisArchiveRoot)) {
|
|
632
|
-
break;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
const prevHeader = await this.blockSource.getBlockHeaderByArchive(prevArchive);
|
|
636
|
-
if (!prevHeader || prevHeader.getSlot() !== slot) {
|
|
637
|
-
break;
|
|
638
|
-
}
|
|
639
|
-
currentHeader = prevHeader;
|
|
686
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
687
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
688
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
689
|
+
return undefined;
|
|
640
690
|
}
|
|
641
691
|
|
|
642
|
-
|
|
643
|
-
}
|
|
692
|
+
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
|
|
644
693
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
return {
|
|
651
|
-
chainId: gv.chainId,
|
|
652
|
-
version: gv.version,
|
|
653
|
-
slotNumber: gv.slotNumber,
|
|
654
|
-
coinbase: gv.coinbase,
|
|
655
|
-
feeRecipient: gv.feeRecipient,
|
|
656
|
-
gasFees: gv.gasFees,
|
|
657
|
-
};
|
|
694
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
695
|
+
this.lastAttestedProposal = proposal;
|
|
696
|
+
|
|
697
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
698
|
+
return attestations;
|
|
658
699
|
}
|
|
659
700
|
|
|
660
701
|
/**
|
|
661
702
|
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
662
703
|
*/
|
|
663
|
-
|
|
704
|
+
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
664
705
|
try {
|
|
665
|
-
const lastBlockHeader = await this.blockSource.
|
|
706
|
+
const lastBlockHeader = (await this.blockSource.getBlockData({ archive: proposal.archive }))?.header;
|
|
666
707
|
if (!lastBlockHeader) {
|
|
667
708
|
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
668
709
|
return;
|
|
669
710
|
}
|
|
670
711
|
|
|
671
|
-
|
|
672
|
-
const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
|
|
673
|
-
if (!lastBlock) {
|
|
674
|
-
this.log.warn(`Failed to get last block for blob upload`, proposalInfo);
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
const blocks = await this.getBlocksForSlot(proposal.slotNumber, lastBlockHeader, lastBlock.checkpointNumber);
|
|
712
|
+
const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
|
|
679
713
|
if (blocks.length === 0) {
|
|
680
714
|
this.log.warn(`No blocks found for blob upload`, proposalInfo);
|
|
681
715
|
return;
|
|
682
716
|
}
|
|
683
717
|
|
|
684
718
|
const blobFields = blocks.flatMap(b => b.toBlobFields());
|
|
685
|
-
const blobs: Blob[] = getBlobsPerL1Block(blobFields);
|
|
719
|
+
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
686
720
|
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
687
721
|
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
688
722
|
...proposalInfo,
|
|
@@ -702,12 +736,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
702
736
|
return;
|
|
703
737
|
}
|
|
704
738
|
|
|
705
|
-
// Trim the set if it's too big.
|
|
706
|
-
if (this.proposersOfInvalidBlocks.size > MAX_PROPOSERS_OF_INVALID_BLOCKS) {
|
|
707
|
-
// remove oldest proposer. `values` is guaranteed to be in insertion order.
|
|
708
|
-
this.proposersOfInvalidBlocks.delete(this.proposersOfInvalidBlocks.values().next().value!);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
739
|
this.proposersOfInvalidBlocks.add(proposer.toString());
|
|
712
740
|
|
|
713
741
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
@@ -720,26 +748,186 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
720
748
|
]);
|
|
721
749
|
}
|
|
722
750
|
|
|
751
|
+
private handleInvalidCheckpointProposal(
|
|
752
|
+
proposal: CheckpointProposalCore,
|
|
753
|
+
result: CheckpointProposalValidationFailureResult,
|
|
754
|
+
proposalInfo: LogData,
|
|
755
|
+
): void {
|
|
756
|
+
if (!SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT[result.reason]) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
this.markInvalidProposalSlot(proposal.slotNumber);
|
|
761
|
+
|
|
762
|
+
if (this.slashInvalidCheckpointProposal(proposal)) {
|
|
763
|
+
this.log.info(`Detected invalid checkpoint proposal offense`, {
|
|
764
|
+
...proposalInfo,
|
|
765
|
+
reason: result.reason,
|
|
766
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
767
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL),
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
private slashInvalidCheckpointProposal(proposal: CheckpointProposalCore): boolean {
|
|
773
|
+
const proposer = proposal.getSender();
|
|
774
|
+
if (!proposer) {
|
|
775
|
+
this.log.warn(`Cannot slash checkpoint proposal with invalid signature`, {
|
|
776
|
+
slotNumber: proposal.slotNumber,
|
|
777
|
+
archive: proposal.archive.toString(),
|
|
778
|
+
});
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const offenseType = OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL;
|
|
783
|
+
const offenseKey = `${proposer.toString()}:${offenseType}:${proposal.slotNumber}`;
|
|
784
|
+
if (!this.invalidCheckpointProposalOffenseKeys.addIfAbsent(offenseKey)) {
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
789
|
+
{
|
|
790
|
+
validator: proposer,
|
|
791
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
792
|
+
offenseType,
|
|
793
|
+
epochOrSlot: BigInt(proposal.slotNumber),
|
|
794
|
+
},
|
|
795
|
+
]);
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private markInvalidProposalSlot(slotNumber: SlotNumber): void {
|
|
800
|
+
this.slotsWithInvalidProposals.add(slotNumber);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private handleCheckpointAttestation(attestation: CheckpointAttestation): void {
|
|
804
|
+
const slotNumber = attestation.slotNumber;
|
|
805
|
+
if (!this.slotsWithInvalidProposals.has(slotNumber) || this.slotsWithProposalEquivocation.has(slotNumber)) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const attester = attestation.getSender();
|
|
810
|
+
if (!attester) {
|
|
811
|
+
this.log.warn(`Cannot slash checkpoint attestation with invalid signature`, {
|
|
812
|
+
slotNumber,
|
|
813
|
+
archive: attestation.archive.toString(),
|
|
814
|
+
});
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
this.slashAttestedToInvalidCheckpointProposal(slotNumber, attester);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
private slashAttestedToInvalidCheckpointProposal(slotNumber: SlotNumber, attester: EthAddress): void {
|
|
822
|
+
const offenseKey = `${attester.toString()}:${OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL}:${slotNumber}`;
|
|
823
|
+
if (!this.badAttestationOffenseKeys.addIfAbsent(offenseKey)) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
this.log.info(`Detected attestation to invalid checkpoint proposal offense`, {
|
|
828
|
+
attester: attester.toString(),
|
|
829
|
+
slotNumber,
|
|
830
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
831
|
+
offenseType: getOffenseTypeName(OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL),
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
835
|
+
{
|
|
836
|
+
validator: attester,
|
|
837
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
838
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
839
|
+
epochOrSlot: BigInt(slotNumber),
|
|
840
|
+
},
|
|
841
|
+
]);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
846
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
847
|
+
*/
|
|
848
|
+
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
849
|
+
const { slot, proposer, type } = info;
|
|
850
|
+
this.slotsWithProposalEquivocation.add(slot);
|
|
851
|
+
|
|
852
|
+
this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
853
|
+
proposer: proposer.toString(),
|
|
854
|
+
slot,
|
|
855
|
+
type,
|
|
856
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
857
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_PROPOSAL),
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
861
|
+
{
|
|
862
|
+
validator: proposer,
|
|
863
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
864
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
865
|
+
epochOrSlot: BigInt(slot),
|
|
866
|
+
},
|
|
867
|
+
]);
|
|
868
|
+
|
|
869
|
+
this.emit(WANT_TO_CLEAR_SLASH_EVENT, [
|
|
870
|
+
{
|
|
871
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
872
|
+
epochOrSlot: BigInt(slot),
|
|
873
|
+
},
|
|
874
|
+
]);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
879
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
880
|
+
*/
|
|
881
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
882
|
+
const { slot, attester } = info;
|
|
883
|
+
|
|
884
|
+
this.log.info(`Detected duplicate attestation offense from ${attester.toString()} at slot ${slot}`, {
|
|
885
|
+
attester: attester.toString(),
|
|
886
|
+
slot,
|
|
887
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
888
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_ATTESTATION),
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
892
|
+
{
|
|
893
|
+
validator: attester,
|
|
894
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
895
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
896
|
+
epochOrSlot: BigInt(slot),
|
|
897
|
+
},
|
|
898
|
+
]);
|
|
899
|
+
}
|
|
900
|
+
|
|
723
901
|
async createBlockProposal(
|
|
724
902
|
blockHeader: BlockHeader,
|
|
725
|
-
|
|
903
|
+
checkpointNumber: CheckpointNumber,
|
|
904
|
+
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
726
905
|
inHash: Fr,
|
|
727
906
|
archive: Fr,
|
|
728
907
|
txs: Tx[],
|
|
729
908
|
proposerAddress: EthAddress | undefined,
|
|
730
|
-
options: BlockProposalOptions,
|
|
909
|
+
options: BlockProposalOptions = {},
|
|
731
910
|
): Promise<BlockProposal> {
|
|
732
|
-
//
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
911
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
912
|
+
if (this.lastProposedBlock) {
|
|
913
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
914
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
915
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
916
|
+
|
|
917
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
918
|
+
throw new Error(
|
|
919
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
920
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
737
924
|
|
|
738
925
|
this.log.info(
|
|
739
926
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
740
927
|
);
|
|
741
928
|
const newProposal = await this.validationService.createBlockProposal(
|
|
742
929
|
blockHeader,
|
|
930
|
+
checkpointNumber,
|
|
743
931
|
indexWithinCheckpoint,
|
|
744
932
|
inHash,
|
|
745
933
|
archive,
|
|
@@ -747,28 +935,55 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
747
935
|
proposerAddress,
|
|
748
936
|
{
|
|
749
937
|
...options,
|
|
750
|
-
broadcastInvalidBlockProposal:
|
|
938
|
+
broadcastInvalidBlockProposal:
|
|
939
|
+
options.broadcastInvalidBlockProposal || this.config.broadcastInvalidBlockProposal,
|
|
751
940
|
},
|
|
752
941
|
);
|
|
753
|
-
this.
|
|
942
|
+
this.lastProposedBlock = newProposal;
|
|
754
943
|
return newProposal;
|
|
755
944
|
}
|
|
756
945
|
|
|
757
946
|
async createCheckpointProposal(
|
|
758
947
|
checkpointHeader: CheckpointHeader,
|
|
759
948
|
archive: Fr,
|
|
760
|
-
|
|
949
|
+
checkpointNumber: CheckpointNumber,
|
|
950
|
+
feeAssetPriceModifier: bigint,
|
|
951
|
+
lastBlockProposal: BlockProposal | undefined,
|
|
761
952
|
proposerAddress: EthAddress | undefined,
|
|
762
|
-
options: CheckpointProposalOptions,
|
|
953
|
+
options: CheckpointProposalOptions = {},
|
|
763
954
|
): Promise<CheckpointProposal> {
|
|
955
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
956
|
+
if (this.lastProposedCheckpoint) {
|
|
957
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
958
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
959
|
+
|
|
960
|
+
if (newSlot <= lastSlot) {
|
|
961
|
+
throw new Error(
|
|
962
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
963
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
764
968
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
765
|
-
|
|
969
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
766
970
|
checkpointHeader,
|
|
767
971
|
archive,
|
|
768
|
-
|
|
972
|
+
checkpointNumber,
|
|
973
|
+
feeAssetPriceModifier,
|
|
974
|
+
lastBlockProposal,
|
|
769
975
|
proposerAddress,
|
|
770
976
|
options,
|
|
771
977
|
);
|
|
978
|
+
this.lastProposedCheckpoint = newProposal;
|
|
979
|
+
// Self-record this slot's outcome on the re-execution tracker. Proposers don't run their
|
|
980
|
+
// own proposals through `handleCheckpointProposal`, so without this call the proposer's
|
|
981
|
+
// sentinel would see no outcome for slots it proposed and would mis-attribute itself as
|
|
982
|
+
// inactive. We pass the locally-computed `archive` (not `newProposal.archive`, which may
|
|
983
|
+
// be intentionally corrupted under test-only flags); from the proposer's local-view
|
|
984
|
+
// perspective the work it just completed is valid by definition.
|
|
985
|
+
this.proposalHandler.recordOwnCheckpointProposalAsValid(checkpointHeader.slotNumber, archive, checkpointNumber);
|
|
986
|
+
return newProposal;
|
|
772
987
|
}
|
|
773
988
|
|
|
774
989
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -778,15 +993,29 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
778
993
|
async signAttestationsAndSigners(
|
|
779
994
|
attestationsAndSigners: CommitteeAttestationsAndSigners,
|
|
780
995
|
proposer: EthAddress,
|
|
996
|
+
slot: SlotNumber,
|
|
997
|
+
checkpointNumber: CheckpointNumber,
|
|
781
998
|
): Promise<Signature> {
|
|
782
|
-
return await this.validationService.signAttestationsAndSigners(
|
|
999
|
+
return await this.validationService.signAttestationsAndSigners(
|
|
1000
|
+
attestationsAndSigners,
|
|
1001
|
+
proposer,
|
|
1002
|
+
slot,
|
|
1003
|
+
checkpointNumber,
|
|
1004
|
+
);
|
|
783
1005
|
}
|
|
784
1006
|
|
|
785
|
-
async collectOwnAttestations(
|
|
1007
|
+
async collectOwnAttestations(
|
|
1008
|
+
proposal: CheckpointProposal,
|
|
1009
|
+
checkpointNumber: CheckpointNumber,
|
|
1010
|
+
): Promise<CheckpointAttestation[]> {
|
|
786
1011
|
const slot = proposal.slotNumber;
|
|
787
1012
|
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
788
1013
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
789
|
-
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
1014
|
+
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
|
|
1015
|
+
|
|
1016
|
+
if (!attestations) {
|
|
1017
|
+
return [];
|
|
1018
|
+
}
|
|
790
1019
|
|
|
791
1020
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
792
1021
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
@@ -801,6 +1030,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
801
1030
|
proposal: CheckpointProposal,
|
|
802
1031
|
required: number,
|
|
803
1032
|
deadline: Date,
|
|
1033
|
+
checkpointNumber: CheckpointNumber,
|
|
804
1034
|
): Promise<CheckpointAttestation[]> {
|
|
805
1035
|
// Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
|
|
806
1036
|
const slot = proposal.slotNumber;
|
|
@@ -813,33 +1043,23 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
813
1043
|
throw new AttestationTimeoutError(0, required, slot);
|
|
814
1044
|
}
|
|
815
1045
|
|
|
816
|
-
await this.collectOwnAttestations(proposal);
|
|
1046
|
+
await this.collectOwnAttestations(proposal, checkpointNumber);
|
|
817
1047
|
|
|
818
|
-
const
|
|
1048
|
+
const proposalPayloadHash = proposal.getPayloadHash();
|
|
819
1049
|
const myAddresses = this.getValidatorAddresses();
|
|
820
1050
|
|
|
821
1051
|
let attestations: CheckpointAttestation[] = [];
|
|
822
1052
|
while (true) {
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
if (!attestation.archive.equals(proposal.archive)) {
|
|
828
|
-
this.log.warn(
|
|
829
|
-
`Received attestation for slot ${slot} with mismatched archive from ${attestation.getSender()?.toString()}`,
|
|
830
|
-
{ attestationArchive: attestation.archive.toString(), proposalArchive: proposal.archive.toString() },
|
|
831
|
-
);
|
|
832
|
-
return false;
|
|
833
|
-
}
|
|
834
|
-
return true;
|
|
835
|
-
},
|
|
836
|
-
);
|
|
1053
|
+
// The pool already filters by proposal payload hash; if any attestation slips through with a
|
|
1054
|
+
// mismatched payload hash, drop it defensively. Equivocations are emitted as separate slash
|
|
1055
|
+
// events from libp2p_service.
|
|
1056
|
+
const collectedAttestations = await this.p2pClient.getCheckpointAttestationsForSlot(slot, proposalPayloadHash);
|
|
837
1057
|
|
|
838
1058
|
// Log new attestations we collected
|
|
839
1059
|
const oldSenders = attestations.map(attestation => attestation.getSender());
|
|
840
1060
|
for (const collected of collectedAttestations) {
|
|
841
1061
|
const collectedSender = collected.getSender();
|
|
842
|
-
// Skip attestations with invalid signatures
|
|
1062
|
+
// Skip attestations with invalid signatures. Should not happen as we don't add invalid attestations to our pool.
|
|
843
1063
|
if (!collectedSender) {
|
|
844
1064
|
this.log.warn(`Skipping attestation with invalid signature for slot ${slot}`);
|
|
845
1065
|
continue;
|
|
@@ -886,7 +1106,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
886
1106
|
}
|
|
887
1107
|
|
|
888
1108
|
const payloadToSign = authRequest.getPayloadToSign();
|
|
889
|
-
|
|
1109
|
+
// AUTH_REQUEST doesn't require HA protection - multiple signatures are safe
|
|
1110
|
+
const context: SigningContext = { dutyType: DutyType.AUTH_REQUEST };
|
|
1111
|
+
const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign, context);
|
|
890
1112
|
const authResponse = new AuthResponse(statusMessage, signature);
|
|
891
1113
|
return authResponse.toBuffer();
|
|
892
1114
|
}
|