@aztec/validator-client 5.0.0-private.20260319 → 5.0.0-rc.1
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 +12 -11
- package/dest/checkpoint_builder.d.ts +1 -1
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +4 -2
- package/dest/config.d.ts +9 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +23 -10
- package/dest/duties/validation_service.d.ts +12 -13
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +32 -38
- package/dest/factory.d.ts +8 -4
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +18 -6
- package/dest/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/metrics.d.ts +6 -2
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +12 -0
- package/dest/proposal_handler.d.ts +142 -0
- package/dest/proposal_handler.d.ts.map +1 -0
- package/dest/proposal_handler.js +1081 -0
- package/dest/validator.d.ts +27 -19
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +219 -245
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +4 -2
- package/src/config.ts +31 -12
- package/src/duties/validation_service.ts +51 -47
- package/src/factory.ts +25 -4
- package/src/index.ts +1 -1
- package/src/metrics.ts +19 -1
- package/src/proposal_handler.ts +1160 -0
- package/src/validator.ts +278 -272
- package/dest/block_proposal_handler.d.ts +0 -64
- package/dest/block_proposal_handler.d.ts.map +0 -1
- package/dest/block_proposal_handler.js +0 -614
- package/src/block_proposal_handler.ts +0 -632
package/dest/validator.js
CHANGED
|
@@ -1,42 +1,65 @@
|
|
|
1
1
|
import { getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { TimeoutError } from '@aztec/foundation/error';
|
|
2
|
+
import { CheckpointNumber } from '@aztec/foundation/branded-types';
|
|
3
|
+
import { FifoSet } from '@aztec/foundation/fifo-set';
|
|
5
4
|
import { createLogger } from '@aztec/foundation/log';
|
|
6
|
-
import { retryUntil } from '@aztec/foundation/retry';
|
|
7
5
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
8
6
|
import { sleep } from '@aztec/foundation/sleep';
|
|
9
7
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
10
8
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
11
|
-
import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
9
|
+
import { OffenseType, WANT_TO_CLEAR_SLASH_EVENT, WANT_TO_SLASH_EVENT, getOffenseTypeName } from '@aztec/slasher';
|
|
10
|
+
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
11
|
+
import { ConsensusTimetable } from '@aztec/stdlib/timetable';
|
|
15
12
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
16
13
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
17
14
|
import { createHASigner, createLocalSignerWithProtection, createSignerFromSharedDb } from '@aztec/validator-ha-signer/factory';
|
|
18
15
|
import { DutyType } from '@aztec/validator-ha-signer/types';
|
|
19
16
|
import { EventEmitter } from 'events';
|
|
20
|
-
import {
|
|
17
|
+
import { DEFAULT_MAX_GOSSIP_CLOCK_DISPARITY_MS } from './config.js';
|
|
21
18
|
import { ValidationService } from './duties/validation_service.js';
|
|
22
19
|
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
23
20
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
24
21
|
import { ValidatorMetrics } from './metrics.js';
|
|
22
|
+
import { ProposalHandler } from './proposal_handler.js';
|
|
25
23
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
26
24
|
// Just cap the set to avoid unbounded growth.
|
|
27
25
|
const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
26
|
+
const MAX_TRACKED_INVALID_PROPOSAL_SLOTS = 1000;
|
|
27
|
+
const MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS = 1000;
|
|
28
|
+
const MAX_TRACKED_BAD_ATTESTATIONS = 10_000;
|
|
28
29
|
// What errors from the block proposal handler result in slashing
|
|
29
30
|
const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
30
31
|
'state_mismatch',
|
|
31
|
-
'failed_txs'
|
|
32
|
+
'failed_txs',
|
|
33
|
+
'global_variables_mismatch',
|
|
34
|
+
'invalid_proposal',
|
|
35
|
+
'parent_block_wrong_slot',
|
|
36
|
+
'in_hash_mismatch'
|
|
32
37
|
];
|
|
38
|
+
const SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT = {
|
|
39
|
+
// enabled
|
|
40
|
+
['invalid_fee_asset_price_modifier']: true,
|
|
41
|
+
['checkpoint_header_mismatch']: true,
|
|
42
|
+
// These late mismatches should normally be caught by earlier checks, but if reached after validating the local
|
|
43
|
+
// checkpoint inputs, the proposer-signed payload disagrees with deterministic recomputation.
|
|
44
|
+
['archive_mismatch']: true,
|
|
45
|
+
['out_hash_mismatch']: true,
|
|
46
|
+
['no_blocks_for_slot']: true,
|
|
47
|
+
['too_many_blocks_in_checkpoint']: true,
|
|
48
|
+
['checkpoint_validation_failed']: true,
|
|
49
|
+
['last_block_archive_mismatch']: true,
|
|
50
|
+
// disabled
|
|
51
|
+
['invalid_signature']: false,
|
|
52
|
+
['last_block_not_found']: false,
|
|
53
|
+
['block_fetch_error']: false,
|
|
54
|
+
['checkpoint_already_published']: false
|
|
55
|
+
};
|
|
33
56
|
/**
|
|
34
57
|
* Validator Client
|
|
35
58
|
*/ export class ValidatorClient extends EventEmitter {
|
|
36
59
|
keyStore;
|
|
37
60
|
epochCache;
|
|
38
61
|
p2pClient;
|
|
39
|
-
|
|
62
|
+
proposalHandler;
|
|
40
63
|
blockSource;
|
|
41
64
|
checkpointsBuilder;
|
|
42
65
|
worldState;
|
|
@@ -57,14 +80,19 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
57
80
|
epochCacheUpdateLoop;
|
|
58
81
|
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */ lastAttestedEpochByAttester;
|
|
59
82
|
proposersOfInvalidBlocks;
|
|
83
|
+
slotsWithInvalidProposals;
|
|
84
|
+
invalidCheckpointProposalOffenseKeys;
|
|
85
|
+
badAttestationOffenseKeys;
|
|
86
|
+
slotsWithProposalEquivocation;
|
|
60
87
|
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */ lastAttestedProposal;
|
|
61
|
-
constructor(keyStore, epochCache, p2pClient,
|
|
62
|
-
super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.
|
|
88
|
+
constructor(keyStore, epochCache, p2pClient, proposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, slashingProtectionSigner, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
89
|
+
super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.proposalHandler = proposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.slashingProtectionSigner = slashingProtectionSigner, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.lastAttestedEpochByAttester = new Map(), this.proposersOfInvalidBlocks = FifoSet.withLimit(MAX_PROPOSERS_OF_INVALID_BLOCKS), this.slotsWithInvalidProposals = FifoSet.withLimit(MAX_TRACKED_INVALID_PROPOSAL_SLOTS), this.invalidCheckpointProposalOffenseKeys = FifoSet.withLimit(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS), this.badAttestationOffenseKeys = FifoSet.withLimit(MAX_TRACKED_BAD_ATTESTATIONS), this.slotsWithProposalEquivocation = FifoSet.withLimit(MAX_TRACKED_INVALID_PROPOSAL_SLOTS);
|
|
63
90
|
// Create child logger with fisherman prefix if in fisherman mode
|
|
64
91
|
this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
|
|
65
92
|
this.tracer = telemetry.getTracer('Validator');
|
|
66
93
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
67
|
-
this.validationService = new ValidationService(keyStore, this.log.createChild('validation-service'));
|
|
94
|
+
this.validationService = new ValidationService(keyStore, this.getSignatureContext(), this.log.createChild('validation-service'));
|
|
95
|
+
this.proposalHandler.setCheckpointProposalValidationFailureCallback((proposal, result, proposalInfo)=>this.handleInvalidCheckpointProposal(proposal, result, proposalInfo));
|
|
68
96
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
69
97
|
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
|
|
70
98
|
const myAddresses = this.getValidatorAddresses();
|
|
@@ -114,13 +142,24 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
114
142
|
this.log.error(`Error updating epoch committee`, err);
|
|
115
143
|
}
|
|
116
144
|
}
|
|
117
|
-
static async new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), slashingProtectionDb) {
|
|
145
|
+
static async new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, reexecutionTracker, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), slashingProtectionDb) {
|
|
118
146
|
const metrics = new ValidatorMetrics(telemetry);
|
|
119
|
-
const
|
|
147
|
+
const consensusTimetable = new ConsensusTimetable({
|
|
148
|
+
l1Constants: epochCache.getL1Constants(),
|
|
149
|
+
blockDuration: config.blockDurationMs / 1000
|
|
150
|
+
});
|
|
151
|
+
const blockProposalValidator = new BlockProposalValidator(epochCache, consensusTimetable, {
|
|
120
152
|
txsPermitted: !config.disableTransactions,
|
|
121
|
-
maxTxsPerBlock: config.validateMaxTxsPerBlock
|
|
153
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
154
|
+
maxBlocksPerCheckpoint: config.maxBlocksPerCheckpoint,
|
|
155
|
+
skipSlotValidation: config.skipProposalSlotValidation,
|
|
156
|
+
signatureContext: {
|
|
157
|
+
chainId: config.l1ChainId,
|
|
158
|
+
rollupAddress: config.rollupAddress
|
|
159
|
+
},
|
|
160
|
+
clockDisparityMs: config.maxGossipClockDisparityMs ?? DEFAULT_MAX_GOSSIP_CLOCK_DISPARITY_MS
|
|
122
161
|
});
|
|
123
|
-
const
|
|
162
|
+
const proposalHandler = new ProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, consensusTimetable, config, blobClient, reexecutionTracker, metrics, dateProvider, telemetry, undefined);
|
|
124
163
|
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
125
164
|
let slashingProtectionSigner;
|
|
126
165
|
if (slashingProtectionDb) {
|
|
@@ -149,18 +188,24 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
149
188
|
}));
|
|
150
189
|
}
|
|
151
190
|
const validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
|
|
152
|
-
const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient,
|
|
191
|
+
const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, proposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, slashingProtectionSigner, dateProvider, telemetry);
|
|
153
192
|
return validator;
|
|
154
193
|
}
|
|
155
194
|
getValidatorAddresses() {
|
|
156
195
|
return this.keyStore.getAddresses().filter((addr)=>!this.config.disabledValidators.some((disabled)=>disabled.equals(addr)));
|
|
157
196
|
}
|
|
158
|
-
|
|
159
|
-
return this.
|
|
197
|
+
getProposalHandler() {
|
|
198
|
+
return this.proposalHandler;
|
|
160
199
|
}
|
|
161
200
|
signWithAddress(addr, msg, context) {
|
|
162
201
|
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
163
202
|
}
|
|
203
|
+
getSignatureContext() {
|
|
204
|
+
return {
|
|
205
|
+
chainId: this.config.l1ChainId,
|
|
206
|
+
rollupAddress: this.config.rollupAddress
|
|
207
|
+
};
|
|
208
|
+
}
|
|
164
209
|
getCoinbaseForAttestor(attestor) {
|
|
165
210
|
return this.keyStore.getCoinbaseAddress(attestor);
|
|
166
211
|
}
|
|
@@ -170,16 +215,23 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
170
215
|
getConfig() {
|
|
171
216
|
return this.config;
|
|
172
217
|
}
|
|
218
|
+
hasProposalEquivocation(slotNumber) {
|
|
219
|
+
return this.slotsWithProposalEquivocation.has(slotNumber);
|
|
220
|
+
}
|
|
221
|
+
hasInvalidProposals(slotNumber) {
|
|
222
|
+
return this.slotsWithInvalidProposals.has(slotNumber);
|
|
223
|
+
}
|
|
173
224
|
updateConfig(config) {
|
|
174
225
|
this.config = {
|
|
175
226
|
...this.config,
|
|
176
227
|
...config
|
|
177
228
|
};
|
|
229
|
+
this.proposalHandler.updateConfig(config);
|
|
178
230
|
}
|
|
179
231
|
reloadKeystore(newManager) {
|
|
180
232
|
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
181
233
|
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
182
|
-
this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
|
|
234
|
+
this.validationService = new ValidationService(this.keyStore, this.getSignatureContext(), this.log.createChild('validation-service'));
|
|
183
235
|
}
|
|
184
236
|
async start() {
|
|
185
237
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
@@ -212,7 +264,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
212
264
|
// The checkpoint is received as CheckpointProposalCore since the lastBlock is extracted
|
|
213
265
|
// and processed separately via the block handler above.
|
|
214
266
|
const checkpointHandler = (checkpoint, proposalSender)=>this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
215
|
-
this.p2pClient.
|
|
267
|
+
this.p2pClient.registerValidatorCheckpointProposalHandler(checkpointHandler);
|
|
216
268
|
// Duplicate proposal handler - triggers slashing for equivocation
|
|
217
269
|
this.p2pClient.registerDuplicateProposalCallback((info)=>{
|
|
218
270
|
this.handleDuplicateProposal(info);
|
|
@@ -221,6 +273,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
221
273
|
this.p2pClient.registerDuplicateAttestationCallback((info)=>{
|
|
222
274
|
this.handleDuplicateAttestation(info);
|
|
223
275
|
});
|
|
276
|
+
this.p2pClient.registerCheckpointAttestationCallback((attestation)=>{
|
|
277
|
+
this.handleCheckpointAttestation(attestation);
|
|
278
|
+
});
|
|
224
279
|
const myAddresses = this.getValidatorAddresses();
|
|
225
280
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
226
281
|
await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
|
|
@@ -260,11 +315,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
260
315
|
txHashes: proposal.txHashes.map((t)=>t.toString()),
|
|
261
316
|
fishermanMode: this.config.fishermanMode || false
|
|
262
317
|
});
|
|
263
|
-
// Reexecute
|
|
264
|
-
|
|
265
|
-
const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
|
|
266
|
-
const shouldReexecute = fishermanMode || slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute || partOfCommittee && validatorReexecute || alwaysReexecuteBlockProposals || this.blobClient.canUpload();
|
|
267
|
-
const validationResult = await this.blockProposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute && !escapeHatchOpen);
|
|
318
|
+
// Reexecute outside the escape hatch so slashing observers can detect invalid proposals even when penalties are 0.
|
|
319
|
+
const validationResult = await this.proposalHandler.handleBlockProposal(proposal, proposalSender, !escapeHatchOpen);
|
|
268
320
|
if (!validationResult.isValid) {
|
|
269
321
|
const reason = validationResult.reason || 'unknown';
|
|
270
322
|
this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
|
|
@@ -282,10 +334,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
282
334
|
// Node issues so we can't validate
|
|
283
335
|
this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
|
|
284
336
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
337
|
+
if (!escapeHatchOpen && validationResult.reason && SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)) {
|
|
338
|
+
this.log.info(`Detected invalid block proposal offense`, {
|
|
339
|
+
...proposalInfo,
|
|
340
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
341
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL)
|
|
342
|
+
});
|
|
288
343
|
this.slashInvalidBlock(proposal);
|
|
344
|
+
this.markInvalidProposalSlot(proposal.slotNumber);
|
|
289
345
|
}
|
|
290
346
|
return false;
|
|
291
347
|
}
|
|
@@ -314,49 +370,43 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
314
370
|
this.log.warn(`Escape hatch open for slot ${proposalSlotNumber}, skipping checkpoint attestation handling`);
|
|
315
371
|
return undefined;
|
|
316
372
|
}
|
|
317
|
-
//
|
|
318
|
-
if (!
|
|
319
|
-
this.log.warn(`Received checkpoint proposal with invalid signature for proposal slot ${proposalSlotNumber}`);
|
|
373
|
+
// Early-out for equivocation: refuses if we've already attested to a higher slot.
|
|
374
|
+
if (!this.shouldAttestToSlot(proposalSlotNumber)) {
|
|
320
375
|
return undefined;
|
|
321
376
|
}
|
|
322
377
|
// Ignore proposals from ourselves (may happen in HA setups)
|
|
323
|
-
if (this.getValidatorAddresses().some((addr)=>addr.equals(proposer))) {
|
|
324
|
-
this.log.debug(`
|
|
378
|
+
if (proposer && this.getValidatorAddresses().some((addr)=>addr.equals(proposer))) {
|
|
379
|
+
this.log.debug(`Not attesting to block proposal from self for slot ${proposalSlotNumber}`, {
|
|
325
380
|
proposer: proposer.toString(),
|
|
326
381
|
proposalSlotNumber
|
|
327
382
|
});
|
|
328
383
|
return undefined;
|
|
329
384
|
}
|
|
330
|
-
// Validate fee asset price modifier is within allowed range
|
|
331
|
-
if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
332
|
-
this.log.warn(`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${proposalSlotNumber}`);
|
|
333
|
-
return undefined;
|
|
334
|
-
}
|
|
335
385
|
// Check that I have any address in the committee where this checkpoint will land before attesting
|
|
336
386
|
const inCommittee = await this.epochCache.filterInCommittee(proposalSlotNumber, this.getValidatorAddresses());
|
|
337
387
|
const partOfCommittee = inCommittee.length > 0;
|
|
338
388
|
const proposalInfo = {
|
|
339
389
|
proposalSlotNumber,
|
|
340
390
|
archive: proposal.archive.toString(),
|
|
341
|
-
proposer: proposer
|
|
391
|
+
proposer: proposer?.toString()
|
|
342
392
|
};
|
|
343
393
|
this.log.info(`Received checkpoint proposal for slot ${proposalSlotNumber}`, {
|
|
344
394
|
...proposalInfo,
|
|
345
395
|
fishermanMode: this.config.fishermanMode || false
|
|
346
396
|
});
|
|
347
|
-
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
397
|
+
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
|
|
398
|
+
// Uses the cached result from the all-nodes callback if available (avoids double validation).
|
|
399
|
+
let checkpointNumber;
|
|
348
400
|
if (this.config.skipCheckpointProposalValidation) {
|
|
349
401
|
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
|
|
402
|
+
checkpointNumber = CheckpointNumber(0);
|
|
350
403
|
} else {
|
|
351
|
-
const validationResult = await this.
|
|
404
|
+
const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
|
|
352
405
|
if (!validationResult.isValid) {
|
|
353
406
|
this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
354
407
|
return undefined;
|
|
355
408
|
}
|
|
356
|
-
|
|
357
|
-
// Upload blobs to filestore if we can (fire and forget)
|
|
358
|
-
if (this.blobClient.canUpload()) {
|
|
359
|
-
void this.uploadBlobsForCheckpoint(proposal, proposalInfo);
|
|
409
|
+
checkpointNumber = validationResult.checkpointNumber;
|
|
360
410
|
}
|
|
361
411
|
// Check that I have any address in current committee before attesting
|
|
362
412
|
// In fisherman mode, we still create attestations for validation even if not in committee
|
|
@@ -403,7 +453,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
403
453
|
});
|
|
404
454
|
return undefined;
|
|
405
455
|
}
|
|
406
|
-
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
456
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
|
|
407
457
|
}
|
|
408
458
|
/**
|
|
409
459
|
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
@@ -420,180 +470,24 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
420
470
|
}
|
|
421
471
|
return true;
|
|
422
472
|
}
|
|
423
|
-
async createCheckpointAttestationsFromProposal(proposal, attestors = []) {
|
|
473
|
+
async createCheckpointAttestationsFromProposal(proposal, attestors = [], checkpointNumber) {
|
|
424
474
|
// Equivocation check: must happen right before signing to minimize the race window
|
|
425
475
|
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
426
476
|
return undefined;
|
|
427
477
|
}
|
|
428
|
-
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
478
|
+
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
|
|
429
479
|
// Track the proposal we attested to (to prevent equivocation)
|
|
430
480
|
this.lastAttestedProposal = proposal;
|
|
431
481
|
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
432
482
|
return attestations;
|
|
433
483
|
}
|
|
434
484
|
/**
|
|
435
|
-
* Validates a checkpoint proposal by building the full checkpoint and comparing it with the proposal.
|
|
436
|
-
* @returns Validation result with isValid flag and reason if invalid.
|
|
437
|
-
*/ async validateCheckpointProposal(proposal, proposalInfo) {
|
|
438
|
-
const slot = proposal.slotNumber;
|
|
439
|
-
// Timeout block syncing at the start of the next slot
|
|
440
|
-
const config = this.checkpointsBuilder.getConfig();
|
|
441
|
-
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
442
|
-
const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
|
|
443
|
-
// Wait for last block to sync by archive
|
|
444
|
-
let lastBlockHeader;
|
|
445
|
-
try {
|
|
446
|
-
lastBlockHeader = await retryUntil(async ()=>{
|
|
447
|
-
await this.blockSource.syncImmediate();
|
|
448
|
-
return this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
449
|
-
}, `waiting for block with archive ${proposal.archive.toString()} for slot ${slot}`, timeoutSeconds, 0.5);
|
|
450
|
-
} catch (err) {
|
|
451
|
-
if (err instanceof TimeoutError) {
|
|
452
|
-
this.log.warn(`Timed out waiting for block with archive matching checkpoint proposal`, proposalInfo);
|
|
453
|
-
return {
|
|
454
|
-
isValid: false,
|
|
455
|
-
reason: 'last_block_not_found'
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
this.log.error(`Error fetching last block for checkpoint proposal`, err, proposalInfo);
|
|
459
|
-
return {
|
|
460
|
-
isValid: false,
|
|
461
|
-
reason: 'block_fetch_error'
|
|
462
|
-
};
|
|
463
|
-
}
|
|
464
|
-
if (!lastBlockHeader) {
|
|
465
|
-
this.log.warn(`Last block not found for checkpoint proposal`, proposalInfo);
|
|
466
|
-
return {
|
|
467
|
-
isValid: false,
|
|
468
|
-
reason: 'last_block_not_found'
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
// Get all full blocks for the slot and checkpoint
|
|
472
|
-
const blocks = await this.blockSource.getBlocksForSlot(slot);
|
|
473
|
-
if (blocks.length === 0) {
|
|
474
|
-
this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
|
|
475
|
-
return {
|
|
476
|
-
isValid: false,
|
|
477
|
-
reason: 'no_blocks_for_slot'
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
// Ensure the last block for this slot matches the archive in the checkpoint proposal
|
|
481
|
-
if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
|
|
482
|
-
this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
|
|
483
|
-
return {
|
|
484
|
-
isValid: false,
|
|
485
|
-
reason: 'last_block_archive_mismatch'
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
|
|
489
|
-
...proposalInfo,
|
|
490
|
-
blockNumbers: blocks.map((b)=>b.number)
|
|
491
|
-
});
|
|
492
|
-
// Get checkpoint constants from first block
|
|
493
|
-
const firstBlock = blocks[0];
|
|
494
|
-
const constants = this.extractCheckpointConstants(firstBlock);
|
|
495
|
-
const checkpointNumber = firstBlock.checkpointNumber;
|
|
496
|
-
// Get L1-to-L2 messages for this checkpoint
|
|
497
|
-
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
498
|
-
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
499
|
-
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
500
|
-
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
|
|
501
|
-
// Fork world state at the block before the first block
|
|
502
|
-
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
503
|
-
const fork = await this.worldState.fork(parentBlockNumber);
|
|
504
|
-
try {
|
|
505
|
-
// Create checkpoint builder with all existing blocks
|
|
506
|
-
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, proposal.feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
|
|
507
|
-
// Complete the checkpoint to get computed values
|
|
508
|
-
const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
|
|
509
|
-
// Compare checkpoint header with proposal
|
|
510
|
-
if (!computedCheckpoint.header.equals(proposal.checkpointHeader)) {
|
|
511
|
-
this.log.warn(`Checkpoint header mismatch`, {
|
|
512
|
-
...proposalInfo,
|
|
513
|
-
computed: computedCheckpoint.header.toInspect(),
|
|
514
|
-
proposal: proposal.checkpointHeader.toInspect()
|
|
515
|
-
});
|
|
516
|
-
return {
|
|
517
|
-
isValid: false,
|
|
518
|
-
reason: 'checkpoint_header_mismatch'
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
// Compare archive root with proposal
|
|
522
|
-
if (!computedCheckpoint.archive.root.equals(proposal.archive)) {
|
|
523
|
-
this.log.warn(`Archive root mismatch`, {
|
|
524
|
-
...proposalInfo,
|
|
525
|
-
computed: computedCheckpoint.archive.root.toString(),
|
|
526
|
-
proposal: proposal.archive.toString()
|
|
527
|
-
});
|
|
528
|
-
return {
|
|
529
|
-
isValid: false,
|
|
530
|
-
reason: 'archive_mismatch'
|
|
531
|
-
};
|
|
532
|
-
}
|
|
533
|
-
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
534
|
-
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
535
|
-
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
536
|
-
const computedEpochOutHash = accumulateCheckpointOutHashes([
|
|
537
|
-
...previousCheckpointOutHashes,
|
|
538
|
-
checkpointOutHash
|
|
539
|
-
]);
|
|
540
|
-
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
541
|
-
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
542
|
-
this.log.warn(`Epoch out hash mismatch`, {
|
|
543
|
-
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
544
|
-
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
545
|
-
checkpointOutHash: checkpointOutHash.toString(),
|
|
546
|
-
previousCheckpointOutHashes: previousCheckpointOutHashes.map((h)=>h.toString()),
|
|
547
|
-
...proposalInfo
|
|
548
|
-
});
|
|
549
|
-
return {
|
|
550
|
-
isValid: false,
|
|
551
|
-
reason: 'out_hash_mismatch'
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
// Final round of validations on the checkpoint, just in case.
|
|
555
|
-
try {
|
|
556
|
-
validateCheckpoint(computedCheckpoint, {
|
|
557
|
-
rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
|
|
558
|
-
maxDABlockGas: this.config.validateMaxDABlockGas,
|
|
559
|
-
maxL2BlockGas: this.config.validateMaxL2BlockGas,
|
|
560
|
-
maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
|
|
561
|
-
maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint
|
|
562
|
-
});
|
|
563
|
-
} catch (err) {
|
|
564
|
-
this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
|
|
565
|
-
return {
|
|
566
|
-
isValid: false,
|
|
567
|
-
reason: 'checkpoint_validation_failed'
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
571
|
-
return {
|
|
572
|
-
isValid: true
|
|
573
|
-
};
|
|
574
|
-
} finally{
|
|
575
|
-
await fork.close();
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* Extract checkpoint global variables from a block.
|
|
580
|
-
*/ extractCheckpointConstants(block) {
|
|
581
|
-
const gv = block.header.globalVariables;
|
|
582
|
-
return {
|
|
583
|
-
chainId: gv.chainId,
|
|
584
|
-
version: gv.version,
|
|
585
|
-
slotNumber: gv.slotNumber,
|
|
586
|
-
timestamp: gv.timestamp,
|
|
587
|
-
coinbase: gv.coinbase,
|
|
588
|
-
feeRecipient: gv.feeRecipient,
|
|
589
|
-
gasFees: gv.gasFees
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
485
|
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
594
486
|
*/ async uploadBlobsForCheckpoint(proposal, proposalInfo) {
|
|
595
487
|
try {
|
|
596
|
-
const lastBlockHeader = await this.blockSource.
|
|
488
|
+
const lastBlockHeader = (await this.blockSource.getBlockData({
|
|
489
|
+
archive: proposal.archive
|
|
490
|
+
}))?.header;
|
|
597
491
|
if (!lastBlockHeader) {
|
|
598
492
|
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
599
493
|
return;
|
|
@@ -621,11 +515,6 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
621
515
|
this.log.warn(`Cannot slash proposal with invalid signature`);
|
|
622
516
|
return;
|
|
623
517
|
}
|
|
624
|
-
// Trim the set if it's too big.
|
|
625
|
-
if (this.proposersOfInvalidBlocks.size > MAX_PROPOSERS_OF_INVALID_BLOCKS) {
|
|
626
|
-
// remove oldest proposer. `values` is guaranteed to be in insertion order.
|
|
627
|
-
this.proposersOfInvalidBlocks.delete(this.proposersOfInvalidBlocks.values().next().value);
|
|
628
|
-
}
|
|
629
518
|
this.proposersOfInvalidBlocks.add(proposer.toString());
|
|
630
519
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
631
520
|
{
|
|
@@ -636,17 +525,95 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
636
525
|
}
|
|
637
526
|
]);
|
|
638
527
|
}
|
|
528
|
+
handleInvalidCheckpointProposal(proposal, result, proposalInfo) {
|
|
529
|
+
if (!SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT[result.reason]) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
this.markInvalidProposalSlot(proposal.slotNumber);
|
|
533
|
+
if (this.slashInvalidCheckpointProposal(proposal)) {
|
|
534
|
+
this.log.info(`Detected invalid checkpoint proposal offense`, {
|
|
535
|
+
...proposalInfo,
|
|
536
|
+
reason: result.reason,
|
|
537
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
538
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL)
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
slashInvalidCheckpointProposal(proposal) {
|
|
543
|
+
const proposer = proposal.getSender();
|
|
544
|
+
if (!proposer) {
|
|
545
|
+
this.log.warn(`Cannot slash checkpoint proposal with invalid signature`, {
|
|
546
|
+
slotNumber: proposal.slotNumber,
|
|
547
|
+
archive: proposal.archive.toString()
|
|
548
|
+
});
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
const offenseType = OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL;
|
|
552
|
+
const offenseKey = `${proposer.toString()}:${offenseType}:${proposal.slotNumber}`;
|
|
553
|
+
if (!this.invalidCheckpointProposalOffenseKeys.addIfAbsent(offenseKey)) {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
557
|
+
{
|
|
558
|
+
validator: proposer,
|
|
559
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
560
|
+
offenseType,
|
|
561
|
+
epochOrSlot: BigInt(proposal.slotNumber)
|
|
562
|
+
}
|
|
563
|
+
]);
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
markInvalidProposalSlot(slotNumber) {
|
|
567
|
+
this.slotsWithInvalidProposals.add(slotNumber);
|
|
568
|
+
}
|
|
569
|
+
handleCheckpointAttestation(attestation) {
|
|
570
|
+
const slotNumber = attestation.slotNumber;
|
|
571
|
+
if (!this.slotsWithInvalidProposals.has(slotNumber) || this.slotsWithProposalEquivocation.has(slotNumber)) {
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const attester = attestation.getSender();
|
|
575
|
+
if (!attester) {
|
|
576
|
+
this.log.warn(`Cannot slash checkpoint attestation with invalid signature`, {
|
|
577
|
+
slotNumber,
|
|
578
|
+
archive: attestation.archive.toString()
|
|
579
|
+
});
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
this.slashAttestedToInvalidCheckpointProposal(slotNumber, attester);
|
|
583
|
+
}
|
|
584
|
+
slashAttestedToInvalidCheckpointProposal(slotNumber, attester) {
|
|
585
|
+
const offenseKey = `${attester.toString()}:${OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL}:${slotNumber}`;
|
|
586
|
+
if (!this.badAttestationOffenseKeys.addIfAbsent(offenseKey)) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
this.log.info(`Detected attestation to invalid checkpoint proposal offense`, {
|
|
590
|
+
attester: attester.toString(),
|
|
591
|
+
slotNumber,
|
|
592
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
593
|
+
offenseType: getOffenseTypeName(OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL)
|
|
594
|
+
});
|
|
595
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
596
|
+
{
|
|
597
|
+
validator: attester,
|
|
598
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
599
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
600
|
+
epochOrSlot: BigInt(slotNumber)
|
|
601
|
+
}
|
|
602
|
+
]);
|
|
603
|
+
}
|
|
639
604
|
/**
|
|
640
605
|
* Handle detection of a duplicate proposal (equivocation).
|
|
641
606
|
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
642
607
|
*/ handleDuplicateProposal(info) {
|
|
643
608
|
const { slot, proposer, type } = info;
|
|
644
|
-
this.
|
|
609
|
+
this.slotsWithProposalEquivocation.add(slot);
|
|
610
|
+
this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
645
611
|
proposer: proposer.toString(),
|
|
646
612
|
slot,
|
|
647
|
-
type
|
|
613
|
+
type,
|
|
614
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
615
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_PROPOSAL)
|
|
648
616
|
});
|
|
649
|
-
// Emit slash event
|
|
650
617
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
651
618
|
{
|
|
652
619
|
validator: proposer,
|
|
@@ -655,15 +622,23 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
655
622
|
epochOrSlot: BigInt(slot)
|
|
656
623
|
}
|
|
657
624
|
]);
|
|
625
|
+
this.emit(WANT_TO_CLEAR_SLASH_EVENT, [
|
|
626
|
+
{
|
|
627
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
628
|
+
epochOrSlot: BigInt(slot)
|
|
629
|
+
}
|
|
630
|
+
]);
|
|
658
631
|
}
|
|
659
632
|
/**
|
|
660
633
|
* Handle detection of a duplicate attestation (equivocation).
|
|
661
634
|
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
662
635
|
*/ handleDuplicateAttestation(info) {
|
|
663
636
|
const { slot, attester } = info;
|
|
664
|
-
this.log.
|
|
637
|
+
this.log.info(`Detected duplicate attestation offense from ${attester.toString()} at slot ${slot}`, {
|
|
665
638
|
attester: attester.toString(),
|
|
666
|
-
slot
|
|
639
|
+
slot,
|
|
640
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
641
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_ATTESTATION)
|
|
667
642
|
});
|
|
668
643
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
669
644
|
{
|
|
@@ -674,7 +649,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
674
649
|
}
|
|
675
650
|
]);
|
|
676
651
|
}
|
|
677
|
-
async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
|
|
652
|
+
async createBlockProposal(blockHeader, checkpointNumber, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
|
|
678
653
|
// Validate that we're not creating a proposal for an older or equal position
|
|
679
654
|
if (this.lastProposedBlock) {
|
|
680
655
|
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
@@ -685,14 +660,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
685
660
|
}
|
|
686
661
|
}
|
|
687
662
|
this.log.info(`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`);
|
|
688
|
-
const newProposal = await this.validationService.createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, {
|
|
663
|
+
const newProposal = await this.validationService.createBlockProposal(blockHeader, checkpointNumber, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, {
|
|
689
664
|
...options,
|
|
690
|
-
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
|
|
665
|
+
broadcastInvalidBlockProposal: options.broadcastInvalidBlockProposal || this.config.broadcastInvalidBlockProposal
|
|
691
666
|
});
|
|
692
667
|
this.lastProposedBlock = newProposal;
|
|
693
668
|
return newProposal;
|
|
694
669
|
}
|
|
695
|
-
async createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier,
|
|
670
|
+
async createCheckpointProposal(checkpointHeader, archive, checkpointNumber, feeAssetPriceModifier, lastBlockProposal, proposerAddress, options = {}) {
|
|
696
671
|
// Validate that we're not creating a proposal for an older or equal slot
|
|
697
672
|
if (this.lastProposedCheckpoint) {
|
|
698
673
|
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
@@ -702,23 +677,30 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
702
677
|
}
|
|
703
678
|
}
|
|
704
679
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
705
|
-
const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier,
|
|
680
|
+
const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, checkpointNumber, feeAssetPriceModifier, lastBlockProposal, proposerAddress, options);
|
|
706
681
|
this.lastProposedCheckpoint = newProposal;
|
|
682
|
+
// Self-record this slot's outcome on the re-execution tracker. Proposers don't run their
|
|
683
|
+
// own proposals through `handleCheckpointProposal`, so without this call the proposer's
|
|
684
|
+
// sentinel would see no outcome for slots it proposed and would mis-attribute itself as
|
|
685
|
+
// inactive. We pass the locally-computed `archive` (not `newProposal.archive`, which may
|
|
686
|
+
// be intentionally corrupted under test-only flags); from the proposer's local-view
|
|
687
|
+
// perspective the work it just completed is valid by definition.
|
|
688
|
+
this.proposalHandler.recordOwnCheckpointProposalAsValid(checkpointHeader.slotNumber, archive, checkpointNumber);
|
|
707
689
|
return newProposal;
|
|
708
690
|
}
|
|
709
691
|
async broadcastBlockProposal(proposal) {
|
|
710
692
|
await this.p2pClient.broadcastProposal(proposal);
|
|
711
693
|
}
|
|
712
|
-
async signAttestationsAndSigners(attestationsAndSigners, proposer, slot,
|
|
713
|
-
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot,
|
|
694
|
+
async signAttestationsAndSigners(attestationsAndSigners, proposer, slot, checkpointNumber) {
|
|
695
|
+
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, checkpointNumber);
|
|
714
696
|
}
|
|
715
|
-
async collectOwnAttestations(proposal) {
|
|
697
|
+
async collectOwnAttestations(proposal, checkpointNumber) {
|
|
716
698
|
const slot = proposal.slotNumber;
|
|
717
699
|
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
718
700
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, {
|
|
719
701
|
inCommittee
|
|
720
702
|
});
|
|
721
|
-
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
703
|
+
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
|
|
722
704
|
if (!attestations) {
|
|
723
705
|
return [];
|
|
724
706
|
}
|
|
@@ -730,7 +712,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
730
712
|
});
|
|
731
713
|
return attestations;
|
|
732
714
|
}
|
|
733
|
-
async collectAttestations(proposal, required, deadline) {
|
|
715
|
+
async collectAttestations(proposal, required, deadline, checkpointNumber) {
|
|
734
716
|
// Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
|
|
735
717
|
const slot = proposal.slotNumber;
|
|
736
718
|
this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
|
|
@@ -738,28 +720,20 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
738
720
|
this.log.error(`Deadline ${deadline.toISOString()} for collecting ${required} attestations for slot ${slot} is in the past`);
|
|
739
721
|
throw new AttestationTimeoutError(0, required, slot);
|
|
740
722
|
}
|
|
741
|
-
await this.collectOwnAttestations(proposal);
|
|
742
|
-
const
|
|
723
|
+
await this.collectOwnAttestations(proposal, checkpointNumber);
|
|
724
|
+
const proposalPayloadHash = proposal.getPayloadHash();
|
|
743
725
|
const myAddresses = this.getValidatorAddresses();
|
|
744
726
|
let attestations = [];
|
|
745
727
|
while(true){
|
|
746
|
-
//
|
|
747
|
-
//
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
this.log.warn(`Received attestation for slot ${slot} with mismatched archive from ${attestation.getSender()?.toString()}`, {
|
|
751
|
-
attestationArchive: attestation.archive.toString(),
|
|
752
|
-
proposalArchive: proposal.archive.toString()
|
|
753
|
-
});
|
|
754
|
-
return false;
|
|
755
|
-
}
|
|
756
|
-
return true;
|
|
757
|
-
});
|
|
728
|
+
// The pool already filters by proposal payload hash; if any attestation slips through with a
|
|
729
|
+
// mismatched payload hash, drop it defensively. Equivocations are emitted as separate slash
|
|
730
|
+
// events from libp2p_service.
|
|
731
|
+
const collectedAttestations = await this.p2pClient.getCheckpointAttestationsForSlot(slot, proposalPayloadHash);
|
|
758
732
|
// Log new attestations we collected
|
|
759
733
|
const oldSenders = attestations.map((attestation)=>attestation.getSender());
|
|
760
734
|
for (const collected of collectedAttestations){
|
|
761
735
|
const collectedSender = collected.getSender();
|
|
762
|
-
// Skip attestations with invalid signatures
|
|
736
|
+
// Skip attestations with invalid signatures. Should not happen as we don't add invalid attestations to our pool.
|
|
763
737
|
if (!collectedSender) {
|
|
764
738
|
this.log.warn(`Skipping attestation with invalid signature for slot ${slot}`);
|
|
765
739
|
continue;
|