@aztec/validator-client 0.0.1-commit.e61ad554 → 0.0.1-commit.ec5f612
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 +21 -18
- package/dest/block_proposal_handler.d.ts +4 -5
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +23 -36
- package/dest/checkpoint_builder.d.ts +14 -12
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +51 -31
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +7 -4
- package/dest/duties/validation_service.d.ts +2 -2
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +3 -3
- package/dest/index.d.ts +1 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +0 -1
- package/dest/key_store/ha_key_store.d.ts +1 -1
- package/dest/key_store/ha_key_store.d.ts.map +1 -1
- package/dest/key_store/ha_key_store.js +2 -2
- package/dest/metrics.d.ts +4 -3
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +34 -5
- package/dest/validator.d.ts +38 -14
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +182 -51
- package/package.json +19 -17
- package/src/block_proposal_handler.ts +34 -53
- package/src/checkpoint_builder.ts +74 -30
- package/src/config.ts +7 -4
- package/src/duties/validation_service.ts +9 -2
- package/src/index.ts +0 -1
- package/src/key_store/ha_key_store.ts +2 -2
- package/src/metrics.ts +45 -6
- package/src/validator.ts +236 -63
- package/dest/tx_validator/index.d.ts +0 -3
- package/dest/tx_validator/index.d.ts.map +0 -1
- package/dest/tx_validator/index.js +0 -2
- package/dest/tx_validator/nullifier_cache.d.ts +0 -14
- package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
- package/dest/tx_validator/nullifier_cache.js +0 -24
- package/dest/tx_validator/tx_validator_factory.d.ts +0 -18
- package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
- package/dest/tx_validator/tx_validator_factory.js +0 -54
- package/src/tx_validator/index.ts +0 -2
- package/src/tx_validator/nullifier_cache.ts +0 -30
- package/src/tx_validator/tx_validator_factory.ts +0 -135
package/dest/validator.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
2
|
-
import {
|
|
2
|
+
import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
|
|
3
|
+
import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
3
4
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
4
5
|
import { createLogger } from '@aztec/foundation/log';
|
|
5
6
|
import { retryUntil } from '@aztec/foundation/retry';
|
|
@@ -8,7 +9,8 @@ import { sleep } from '@aztec/foundation/sleep';
|
|
|
8
9
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
9
10
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
10
11
|
import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
|
|
11
|
-
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
12
|
+
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
13
|
+
import { accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
12
14
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
13
15
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
14
16
|
import { createHASigner } from '@aztec/validator-ha-signer/factory';
|
|
@@ -40,6 +42,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
40
42
|
l1ToL2MessageSource;
|
|
41
43
|
config;
|
|
42
44
|
blobClient;
|
|
45
|
+
haSigner;
|
|
43
46
|
dateProvider;
|
|
44
47
|
tracer;
|
|
45
48
|
validationService;
|
|
@@ -47,17 +50,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
47
50
|
log;
|
|
48
51
|
// Whether it has already registered handlers on the p2p client
|
|
49
52
|
hasRegisteredHandlers;
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */ lastProposedBlock;
|
|
54
|
+
/** Tracks the last checkpoint proposal we created. */ lastProposedCheckpoint;
|
|
52
55
|
lastEpochForCommitteeUpdateLoop;
|
|
53
56
|
epochCacheUpdateLoop;
|
|
54
57
|
proposersOfInvalidBlocks;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
validatedBlockSlots;
|
|
59
|
-
constructor(keyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
60
|
-
super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockProposalHandler = blockProposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.proposersOfInvalidBlocks = new Set(), this.validatedBlockSlots = new Set();
|
|
58
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */ lastAttestedProposal;
|
|
59
|
+
constructor(keyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, haSigner, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
60
|
+
super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockProposalHandler = blockProposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.haSigner = haSigner, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.proposersOfInvalidBlocks = new Set();
|
|
61
61
|
// Create child logger with fisherman prefix if in fisherman mode
|
|
62
62
|
this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
|
|
63
63
|
this.tracer = telemetry.getTracer('Validator');
|
|
@@ -117,17 +117,23 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
117
117
|
txsPermitted: !config.disableTransactions
|
|
118
118
|
});
|
|
119
119
|
const blockProposalHandler = new BlockProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, metrics, dateProvider, telemetry);
|
|
120
|
-
|
|
120
|
+
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
121
|
+
let validatorKeyStore = nodeKeystoreAdapter;
|
|
122
|
+
let haSigner;
|
|
121
123
|
if (config.haSigningEnabled) {
|
|
122
124
|
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
123
125
|
const haConfig = {
|
|
124
126
|
...config,
|
|
125
127
|
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000
|
|
126
128
|
};
|
|
127
|
-
const { signer } = await createHASigner(haConfig
|
|
128
|
-
|
|
129
|
+
const { signer } = await createHASigner(haConfig, {
|
|
130
|
+
telemetryClient: telemetry,
|
|
131
|
+
dateProvider
|
|
132
|
+
});
|
|
133
|
+
haSigner = signer;
|
|
134
|
+
validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, signer);
|
|
129
135
|
}
|
|
130
|
-
const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider, telemetry);
|
|
136
|
+
const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, haSigner, dateProvider, telemetry);
|
|
131
137
|
return validator;
|
|
132
138
|
}
|
|
133
139
|
getValidatorAddresses() {
|
|
@@ -154,6 +160,20 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
154
160
|
...config
|
|
155
161
|
};
|
|
156
162
|
}
|
|
163
|
+
reloadKeystore(newManager) {
|
|
164
|
+
if (this.config.haSigningEnabled && !this.haSigner) {
|
|
165
|
+
this.log.warn('HA signing is enabled in config but was not initialized at startup. ' + 'Restart the node to enable HA signing.');
|
|
166
|
+
} else if (!this.config.haSigningEnabled && this.haSigner) {
|
|
167
|
+
this.log.warn('HA signing was disabled via config update but the HA signer is still active. ' + 'Restart the node to fully disable HA signing.');
|
|
168
|
+
}
|
|
169
|
+
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
170
|
+
if (this.haSigner) {
|
|
171
|
+
this.keyStore = new HAKeyStore(newAdapter, this.haSigner);
|
|
172
|
+
} else {
|
|
173
|
+
this.keyStore = newAdapter;
|
|
174
|
+
}
|
|
175
|
+
this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
|
|
176
|
+
}
|
|
157
177
|
async start() {
|
|
158
178
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
159
179
|
this.log.warn(`Validator client already started`);
|
|
@@ -186,6 +206,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
186
206
|
// and processed separately via the block handler above.
|
|
187
207
|
const checkpointHandler = (checkpoint, proposalSender)=>this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
188
208
|
this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
|
|
209
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
210
|
+
this.p2pClient.registerDuplicateProposalCallback((info)=>{
|
|
211
|
+
this.handleDuplicateProposal(info);
|
|
212
|
+
});
|
|
213
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
214
|
+
this.p2pClient.registerDuplicateAttestationCallback((info)=>{
|
|
215
|
+
this.handleDuplicateAttestation(info);
|
|
216
|
+
});
|
|
189
217
|
const myAddresses = this.getValidatorAddresses();
|
|
190
218
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
191
219
|
await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
|
|
@@ -206,6 +234,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
206
234
|
this.log.warn(`Received block proposal with invalid signature for slot ${slotNumber}`);
|
|
207
235
|
return false;
|
|
208
236
|
}
|
|
237
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
238
|
+
if (this.getValidatorAddresses().some((addr)=>addr.equals(proposer))) {
|
|
239
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
240
|
+
proposer: proposer.toString(),
|
|
241
|
+
slotNumber
|
|
242
|
+
});
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
209
245
|
// Check if we're in the committee (for metrics purposes)
|
|
210
246
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
211
247
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -257,9 +293,6 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
257
293
|
this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
|
|
258
294
|
return false;
|
|
259
295
|
}
|
|
260
|
-
// TODO(palla/mbps): Remove this once checkpoint validation is stable.
|
|
261
|
-
// Track that we successfully validated a block for this slot, so we can attest to checkpoint proposals for it.
|
|
262
|
-
this.validatedBlockSlots.add(slotNumber);
|
|
263
296
|
return true;
|
|
264
297
|
}
|
|
265
298
|
/**
|
|
@@ -280,6 +313,19 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
280
313
|
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
|
|
281
314
|
return undefined;
|
|
282
315
|
}
|
|
316
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
317
|
+
if (this.getValidatorAddresses().some((addr)=>addr.equals(proposer))) {
|
|
318
|
+
this.log.warn(`Ignoring block proposal from self for slot ${slotNumber}`, {
|
|
319
|
+
proposer: proposer.toString(),
|
|
320
|
+
slotNumber
|
|
321
|
+
});
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
// Validate fee asset price modifier is within allowed range
|
|
325
|
+
if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
326
|
+
this.log.warn(`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${slotNumber}`);
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
283
329
|
// Check that I have any address in current committee before attesting
|
|
284
330
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
285
331
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -294,16 +340,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
294
340
|
txHashes: proposal.txHashes.map((t)=>t.toString()),
|
|
295
341
|
fishermanMode: this.config.fishermanMode || false
|
|
296
342
|
});
|
|
297
|
-
// TODO(palla/mbps): Remove this once checkpoint validation is stable.
|
|
298
|
-
// Check that we have successfully validated a block for this slot before attesting to the checkpoint.
|
|
299
|
-
if (!this.validatedBlockSlots.has(slotNumber)) {
|
|
300
|
-
this.log.warn(`No validated block found for slot ${slotNumber}, refusing to attest to checkpoint`, proposalInfo);
|
|
301
|
-
return undefined;
|
|
302
|
-
}
|
|
303
343
|
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
344
|
+
if (this.config.skipCheckpointProposalValidation) {
|
|
345
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
307
346
|
} else {
|
|
308
347
|
const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
|
|
309
348
|
if (!validationResult.isValid) {
|
|
@@ -350,11 +389,32 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
350
389
|
});
|
|
351
390
|
return undefined;
|
|
352
391
|
}
|
|
353
|
-
return this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
392
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
396
|
+
* @returns true if we should attest, false if we should skip
|
|
397
|
+
*/ shouldAttestToSlot(slotNumber) {
|
|
398
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
399
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
// Check if incoming slot is strictly greater than last attested
|
|
403
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
404
|
+
this.log.warn(`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`);
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
return true;
|
|
354
408
|
}
|
|
355
409
|
async createCheckpointAttestationsFromProposal(proposal, attestors = []) {
|
|
410
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
411
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
356
414
|
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
357
|
-
|
|
415
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
416
|
+
this.lastAttestedProposal = proposal;
|
|
417
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
358
418
|
return attestations;
|
|
359
419
|
}
|
|
360
420
|
/**
|
|
@@ -362,7 +422,10 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
362
422
|
* @returns Validation result with isValid flag and reason if invalid.
|
|
363
423
|
*/ async validateCheckpointProposal(proposal, proposalInfo) {
|
|
364
424
|
const slot = proposal.slotNumber;
|
|
365
|
-
|
|
425
|
+
// Timeout block syncing at the start of the next slot
|
|
426
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
427
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
428
|
+
const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
|
|
366
429
|
// Wait for last block to sync by archive
|
|
367
430
|
let lastBlockHeader;
|
|
368
431
|
try {
|
|
@@ -400,6 +463,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
400
463
|
reason: 'no_blocks_for_slot'
|
|
401
464
|
};
|
|
402
465
|
}
|
|
466
|
+
// Ensure the last block for this slot matches the archive in the checkpoint proposal
|
|
467
|
+
if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
|
|
468
|
+
this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
|
|
469
|
+
return {
|
|
470
|
+
isValid: false,
|
|
471
|
+
reason: 'last_block_archive_mismatch'
|
|
472
|
+
};
|
|
473
|
+
}
|
|
403
474
|
this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
|
|
404
475
|
...proposalInfo,
|
|
405
476
|
blockNumbers: blocks.map((b)=>b.number)
|
|
@@ -410,18 +481,15 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
410
481
|
const checkpointNumber = firstBlock.checkpointNumber;
|
|
411
482
|
// Get L1-to-L2 messages for this checkpoint
|
|
412
483
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
413
|
-
//
|
|
414
|
-
// TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
|
|
415
|
-
// actual checkpoints and the blocks/txs in them.
|
|
484
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
416
485
|
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
417
|
-
const
|
|
418
|
-
const previousCheckpointOutHashes = previousCheckpoints.map((c)=>c.getCheckpointOutHash());
|
|
486
|
+
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
|
|
419
487
|
// Fork world state at the block before the first block
|
|
420
488
|
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
421
489
|
const fork = await this.worldState.fork(parentBlockNumber);
|
|
422
490
|
try {
|
|
423
491
|
// Create checkpoint builder with all existing blocks
|
|
424
|
-
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks);
|
|
492
|
+
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, proposal.feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
|
|
425
493
|
// Complete the checkpoint to get computed values
|
|
426
494
|
const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
|
|
427
495
|
// Compare checkpoint header with proposal
|
|
@@ -448,13 +516,20 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
448
516
|
reason: 'archive_mismatch'
|
|
449
517
|
};
|
|
450
518
|
}
|
|
451
|
-
// Check that the accumulated out hash matches the value in the proposal.
|
|
452
|
-
|
|
453
|
-
const
|
|
454
|
-
|
|
519
|
+
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
520
|
+
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
521
|
+
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
522
|
+
const computedEpochOutHash = accumulateCheckpointOutHashes([
|
|
523
|
+
...previousCheckpointOutHashes,
|
|
524
|
+
checkpointOutHash
|
|
525
|
+
]);
|
|
526
|
+
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
527
|
+
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
455
528
|
this.log.warn(`Epoch out hash mismatch`, {
|
|
456
|
-
|
|
457
|
-
|
|
529
|
+
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
530
|
+
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
531
|
+
checkpointOutHash: checkpointOutHash.toString(),
|
|
532
|
+
previousCheckpointOutHashes: previousCheckpointOutHashes.map((h)=>h.toString()),
|
|
458
533
|
...proposalInfo
|
|
459
534
|
});
|
|
460
535
|
return {
|
|
@@ -478,6 +553,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
478
553
|
chainId: gv.chainId,
|
|
479
554
|
version: gv.version,
|
|
480
555
|
slotNumber: gv.slotNumber,
|
|
556
|
+
timestamp: gv.timestamp,
|
|
481
557
|
coinbase: gv.coinbase,
|
|
482
558
|
feeRecipient: gv.feeRecipient,
|
|
483
559
|
gasFees: gv.gasFees
|
|
@@ -498,7 +574,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
498
574
|
return;
|
|
499
575
|
}
|
|
500
576
|
const blobFields = blocks.flatMap((b)=>b.toBlobFields());
|
|
501
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
577
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
502
578
|
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
503
579
|
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
504
580
|
...proposalInfo,
|
|
@@ -530,23 +606,75 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
530
606
|
}
|
|
531
607
|
]);
|
|
532
608
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
609
|
+
/**
|
|
610
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
611
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
612
|
+
*/ handleDuplicateProposal(info) {
|
|
613
|
+
const { slot, proposer, type } = info;
|
|
614
|
+
this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
|
|
615
|
+
proposer: proposer.toString(),
|
|
616
|
+
slot,
|
|
617
|
+
type
|
|
618
|
+
});
|
|
619
|
+
// Emit slash event
|
|
620
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
621
|
+
{
|
|
622
|
+
validator: proposer,
|
|
623
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
624
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
625
|
+
epochOrSlot: BigInt(slot)
|
|
626
|
+
}
|
|
627
|
+
]);
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
631
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
632
|
+
*/ handleDuplicateAttestation(info) {
|
|
633
|
+
const { slot, attester } = info;
|
|
634
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
635
|
+
attester: attester.toString(),
|
|
636
|
+
slot
|
|
637
|
+
});
|
|
638
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
639
|
+
{
|
|
640
|
+
validator: attester,
|
|
641
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
642
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
643
|
+
epochOrSlot: BigInt(slot)
|
|
644
|
+
}
|
|
645
|
+
]);
|
|
646
|
+
}
|
|
647
|
+
async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
|
|
648
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
649
|
+
if (this.lastProposedBlock) {
|
|
650
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
651
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
652
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
653
|
+
if (newSlot < lastSlot || newSlot === lastSlot && indexWithinCheckpoint <= lastIndex) {
|
|
654
|
+
throw new Error(`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` + `already proposed block for slot ${lastSlot} index ${lastIndex}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
539
657
|
this.log.info(`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`);
|
|
540
658
|
const newProposal = await this.validationService.createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, {
|
|
541
659
|
...options,
|
|
542
660
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
|
|
543
661
|
});
|
|
544
|
-
this.
|
|
662
|
+
this.lastProposedBlock = newProposal;
|
|
545
663
|
return newProposal;
|
|
546
664
|
}
|
|
547
|
-
async createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options) {
|
|
665
|
+
async createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier, lastBlockInfo, proposerAddress, options = {}) {
|
|
666
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
667
|
+
if (this.lastProposedCheckpoint) {
|
|
668
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
669
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
670
|
+
if (newSlot <= lastSlot) {
|
|
671
|
+
throw new Error(`Cannot create checkpoint proposal for slot ${newSlot}: ` + `already proposed checkpoint for slot ${lastSlot}`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
548
674
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
549
|
-
|
|
675
|
+
const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier, lastBlockInfo, proposerAddress, options);
|
|
676
|
+
this.lastProposedCheckpoint = newProposal;
|
|
677
|
+
return newProposal;
|
|
550
678
|
}
|
|
551
679
|
async broadcastBlockProposal(proposal) {
|
|
552
680
|
await this.p2pClient.broadcastProposal(proposal);
|
|
@@ -561,6 +689,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
561
689
|
inCommittee
|
|
562
690
|
});
|
|
563
691
|
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
692
|
+
if (!attestations) {
|
|
693
|
+
return [];
|
|
694
|
+
}
|
|
564
695
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
565
696
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
566
697
|
// due to inactivity for missed attestations.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/validator-client",
|
|
3
|
-
"version": "0.0.1-commit.
|
|
3
|
+
"version": "0.0.1-commit.ec5f612",
|
|
4
4
|
"main": "dest/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -64,28 +64,30 @@
|
|
|
64
64
|
]
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@aztec/blob-client": "0.0.1-commit.
|
|
68
|
-
"@aztec/blob-lib": "0.0.1-commit.
|
|
69
|
-
"@aztec/constants": "0.0.1-commit.
|
|
70
|
-
"@aztec/epoch-cache": "0.0.1-commit.
|
|
71
|
-
"@aztec/ethereum": "0.0.1-commit.
|
|
72
|
-
"@aztec/foundation": "0.0.1-commit.
|
|
73
|
-
"@aztec/node-keystore": "0.0.1-commit.
|
|
74
|
-
"@aztec/noir-protocol-circuits-types": "0.0.1-commit.
|
|
75
|
-
"@aztec/p2p": "0.0.1-commit.
|
|
76
|
-
"@aztec/protocol-contracts": "0.0.1-commit.
|
|
77
|
-
"@aztec/prover-client": "0.0.1-commit.
|
|
78
|
-
"@aztec/simulator": "0.0.1-commit.
|
|
79
|
-
"@aztec/slasher": "0.0.1-commit.
|
|
80
|
-
"@aztec/stdlib": "0.0.1-commit.
|
|
81
|
-
"@aztec/telemetry-client": "0.0.1-commit.
|
|
82
|
-
"@aztec/validator-ha-signer": "0.0.1-commit.
|
|
67
|
+
"@aztec/blob-client": "0.0.1-commit.ec5f612",
|
|
68
|
+
"@aztec/blob-lib": "0.0.1-commit.ec5f612",
|
|
69
|
+
"@aztec/constants": "0.0.1-commit.ec5f612",
|
|
70
|
+
"@aztec/epoch-cache": "0.0.1-commit.ec5f612",
|
|
71
|
+
"@aztec/ethereum": "0.0.1-commit.ec5f612",
|
|
72
|
+
"@aztec/foundation": "0.0.1-commit.ec5f612",
|
|
73
|
+
"@aztec/node-keystore": "0.0.1-commit.ec5f612",
|
|
74
|
+
"@aztec/noir-protocol-circuits-types": "0.0.1-commit.ec5f612",
|
|
75
|
+
"@aztec/p2p": "0.0.1-commit.ec5f612",
|
|
76
|
+
"@aztec/protocol-contracts": "0.0.1-commit.ec5f612",
|
|
77
|
+
"@aztec/prover-client": "0.0.1-commit.ec5f612",
|
|
78
|
+
"@aztec/simulator": "0.0.1-commit.ec5f612",
|
|
79
|
+
"@aztec/slasher": "0.0.1-commit.ec5f612",
|
|
80
|
+
"@aztec/stdlib": "0.0.1-commit.ec5f612",
|
|
81
|
+
"@aztec/telemetry-client": "0.0.1-commit.ec5f612",
|
|
82
|
+
"@aztec/validator-ha-signer": "0.0.1-commit.ec5f612",
|
|
83
83
|
"koa": "^2.16.1",
|
|
84
84
|
"koa-router": "^13.1.1",
|
|
85
85
|
"tslib": "^2.4.0",
|
|
86
86
|
"viem": "npm:@aztec/viem@2.38.2"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
+
"@aztec/archiver": "0.0.1-commit.ec5f612",
|
|
90
|
+
"@aztec/world-state": "0.0.1-commit.ec5f612",
|
|
89
91
|
"@electric-sql/pglite": "^0.3.14",
|
|
90
92
|
"@jest/globals": "^30.0.0",
|
|
91
93
|
"@types/jest": "^30.0.0",
|