@aztec/validator-client 0.0.1-commit.42ee6df9b → 0.0.1-commit.431c48d
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 -9
- package/dest/checkpoint_builder.d.ts +12 -4
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +14 -3
- package/dest/config.d.ts +9 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +23 -5
- 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 +5 -2
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +6 -6
- package/dest/key_store/web3signer_key_store.d.ts +10 -2
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
- package/dest/key_store/web3signer_key_store.js +32 -41
- package/dest/metrics.d.ts +5 -1
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +15 -1
- package/dest/proposal_handler.d.ts +84 -18
- package/dest/proposal_handler.d.ts.map +1 -1
- package/dest/proposal_handler.js +510 -171
- package/dest/validator.d.ts +32 -13
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +208 -65
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +16 -5
- package/src/config.ts +31 -7
- package/src/duties/validation_service.ts +51 -47
- package/src/factory.ts +12 -5
- package/src/key_store/web3signer_key_store.ts +43 -59
- package/src/metrics.ts +20 -0
- package/src/proposal_handler.ts +590 -199
- package/src/validator.ts +280 -88
package/dest/validator.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
2
|
+
import { CheckpointNumber } from '@aztec/foundation/branded-types';
|
|
3
|
+
import { FifoSet } from '@aztec/foundation/fifo-set';
|
|
2
4
|
import { createLogger } from '@aztec/foundation/log';
|
|
3
5
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
4
6
|
import { sleep } from '@aztec/foundation/sleep';
|
|
5
7
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
6
|
-
import { AuthRequest, AuthResponse,
|
|
7
|
-
import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
|
|
8
|
+
import { AuthRequest, AuthResponse, ReqRespSubProtocol } from '@aztec/p2p';
|
|
9
|
+
import { OffenseType, WANT_TO_CLEAR_SLASH_EVENT, WANT_TO_SLASH_EVENT, getOffenseTypeName } from '@aztec/slasher';
|
|
8
10
|
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
11
|
+
import { ConsensusTimetable } from '@aztec/stdlib/timetable';
|
|
9
12
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
10
13
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
11
14
|
import { createHASigner, createLocalSignerWithProtection, createSignerFromSharedDb } from '@aztec/validator-ha-signer/factory';
|
|
@@ -15,15 +18,12 @@ import { ValidationService } from './duties/validation_service.js';
|
|
|
15
18
|
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
16
19
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
17
20
|
import { ValidatorMetrics } from './metrics.js';
|
|
18
|
-
import { ProposalHandler } from './proposal_handler.js';
|
|
21
|
+
import { ProposalHandler, SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT, SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT } from './proposal_handler.js';
|
|
19
22
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
20
23
|
// Just cap the set to avoid unbounded growth.
|
|
21
24
|
const MAX_PROPOSERS_OF_INVALID_BLOCKS = 1000;
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
'state_mismatch',
|
|
25
|
-
'failed_txs'
|
|
26
|
-
];
|
|
25
|
+
const MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS = 1000;
|
|
26
|
+
const MAX_TRACKED_BAD_ATTESTATIONS = 10_000;
|
|
27
27
|
/**
|
|
28
28
|
* Validator Client
|
|
29
29
|
*/ export class ValidatorClient extends EventEmitter {
|
|
@@ -51,14 +51,18 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
51
51
|
epochCacheUpdateLoop;
|
|
52
52
|
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */ lastAttestedEpochByAttester;
|
|
53
53
|
proposersOfInvalidBlocks;
|
|
54
|
+
invalidCheckpointProposalOffenseKeys;
|
|
55
|
+
oversizedProposalOffenseKeys;
|
|
56
|
+
badAttestationOffenseKeys;
|
|
54
57
|
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */ lastAttestedProposal;
|
|
55
58
|
constructor(keyStore, epochCache, p2pClient, proposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, slashingProtectionSigner, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
56
|
-
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 =
|
|
59
|
+
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.invalidCheckpointProposalOffenseKeys = FifoSet.withLimit(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS), this.oversizedProposalOffenseKeys = FifoSet.withLimit(MAX_TRACKED_INVALID_CHECKPOINT_PROPOSALS), this.badAttestationOffenseKeys = FifoSet.withLimit(MAX_TRACKED_BAD_ATTESTATIONS);
|
|
57
60
|
// Create child logger with fisherman prefix if in fisherman mode
|
|
58
61
|
this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
|
|
59
62
|
this.tracer = telemetry.getTracer('Validator');
|
|
60
63
|
this.metrics = new ValidatorMetrics(telemetry);
|
|
61
|
-
this.validationService = new ValidationService(keyStore, this.log.createChild('validation-service'));
|
|
64
|
+
this.validationService = new ValidationService(keyStore, this.getSignatureContext(), this.log.createChild('validation-service'));
|
|
65
|
+
this.proposalHandler.setCheckpointProposalValidationFailureCallback((proposal, result, proposalInfo)=>this.handleInvalidCheckpointProposal(proposal, result, proposalInfo));
|
|
62
66
|
// Refresh epoch cache every second to trigger alert if participation in committee changes
|
|
63
67
|
this.epochCacheUpdateLoop = new RunningPromise(this.handleEpochCommitteeUpdate.bind(this), this.log, 1000);
|
|
64
68
|
const myAddresses = this.getValidatorAddresses();
|
|
@@ -108,13 +112,13 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
108
112
|
this.log.error(`Error updating epoch committee`, err);
|
|
109
113
|
}
|
|
110
114
|
}
|
|
111
|
-
static async new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), slashingProtectionDb) {
|
|
115
|
+
static async new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, reexecutionTracker, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), slashingProtectionDb) {
|
|
112
116
|
const metrics = new ValidatorMetrics(telemetry);
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
117
|
+
const consensusTimetable = new ConsensusTimetable({
|
|
118
|
+
l1Constants: epochCache.getL1Constants(),
|
|
119
|
+
blockDuration: config.blockDurationMs / 1000
|
|
116
120
|
});
|
|
117
|
-
const proposalHandler = new ProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider,
|
|
121
|
+
const proposalHandler = new ProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, epochCache, consensusTimetable, config, blobClient, reexecutionTracker, metrics, dateProvider, telemetry, undefined);
|
|
118
122
|
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
119
123
|
let slashingProtectionSigner;
|
|
120
124
|
if (slashingProtectionDb) {
|
|
@@ -155,6 +159,12 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
155
159
|
signWithAddress(addr, msg, context) {
|
|
156
160
|
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
157
161
|
}
|
|
162
|
+
getSignatureContext() {
|
|
163
|
+
return {
|
|
164
|
+
chainId: this.config.l1ChainId,
|
|
165
|
+
rollupAddress: this.config.rollupAddress
|
|
166
|
+
};
|
|
167
|
+
}
|
|
158
168
|
getCoinbaseForAttestor(attestor) {
|
|
159
169
|
return this.keyStore.getCoinbaseAddress(attestor);
|
|
160
170
|
}
|
|
@@ -164,16 +174,23 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
164
174
|
getConfig() {
|
|
165
175
|
return this.config;
|
|
166
176
|
}
|
|
177
|
+
hasProposalEquivocation(slotNumber) {
|
|
178
|
+
return this.proposalHandler.hasProposalEquivocation(slotNumber);
|
|
179
|
+
}
|
|
180
|
+
hasInvalidProposals(slotNumber) {
|
|
181
|
+
return this.proposalHandler.hasInvalidProposals(slotNumber);
|
|
182
|
+
}
|
|
167
183
|
updateConfig(config) {
|
|
168
184
|
this.config = {
|
|
169
185
|
...this.config,
|
|
170
186
|
...config
|
|
171
187
|
};
|
|
188
|
+
this.proposalHandler.updateConfig(config);
|
|
172
189
|
}
|
|
173
190
|
reloadKeystore(newManager) {
|
|
174
191
|
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
175
192
|
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
176
|
-
this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
|
|
193
|
+
this.validationService = new ValidationService(this.keyStore, this.getSignatureContext(), this.log.createChild('validation-service'));
|
|
177
194
|
}
|
|
178
195
|
async start() {
|
|
179
196
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
@@ -211,10 +228,17 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
211
228
|
this.p2pClient.registerDuplicateProposalCallback((info)=>{
|
|
212
229
|
this.handleDuplicateProposal(info);
|
|
213
230
|
});
|
|
231
|
+
// Oversized proposal handler - triggers slashing for proposals beyond the per-checkpoint block limit
|
|
232
|
+
this.p2pClient.registerOversizedProposalCallback((info)=>{
|
|
233
|
+
this.handleOversizedProposal(info);
|
|
234
|
+
});
|
|
214
235
|
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
215
236
|
this.p2pClient.registerDuplicateAttestationCallback((info)=>{
|
|
216
237
|
this.handleDuplicateAttestation(info);
|
|
217
238
|
});
|
|
239
|
+
this.p2pClient.registerCheckpointAttestationCallback((attestation)=>{
|
|
240
|
+
this.handleCheckpointAttestation(attestation);
|
|
241
|
+
});
|
|
218
242
|
const myAddresses = this.getValidatorAddresses();
|
|
219
243
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
220
244
|
await this.p2pClient.addReqRespSubProtocol(ReqRespSubProtocol.AUTH, this.handleAuthRequest.bind(this));
|
|
@@ -254,11 +278,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
254
278
|
txHashes: proposal.txHashes.map((t)=>t.toString()),
|
|
255
279
|
fishermanMode: this.config.fishermanMode || false
|
|
256
280
|
});
|
|
257
|
-
// Reexecute
|
|
258
|
-
|
|
259
|
-
const { slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
|
|
260
|
-
const shouldReexecute = fishermanMode || slashBroadcastedInvalidBlockPenalty > 0n || partOfCommittee || alwaysReexecuteBlockProposals || this.blobClient.canUpload();
|
|
261
|
-
const validationResult = await this.proposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute && !escapeHatchOpen);
|
|
281
|
+
// Reexecute outside the escape hatch so slashing observers can detect invalid proposals even when penalties are 0.
|
|
282
|
+
const validationResult = await this.proposalHandler.handleBlockProposal(proposal, proposalSender, !escapeHatchOpen);
|
|
262
283
|
if (!validationResult.isValid) {
|
|
263
284
|
const reason = validationResult.reason || 'unknown';
|
|
264
285
|
this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
|
|
@@ -268,7 +289,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
268
289
|
'state_mismatch',
|
|
269
290
|
'failed_txs',
|
|
270
291
|
'in_hash_mismatch',
|
|
271
|
-
'parent_block_wrong_slot'
|
|
292
|
+
'parent_block_wrong_slot',
|
|
293
|
+
'duplicate_txs',
|
|
294
|
+
'invalid_embedded_txs'
|
|
272
295
|
];
|
|
273
296
|
if (badProposalReasons.includes(reason)) {
|
|
274
297
|
this.metrics.incFailedAttestationsBadProposal(1, reason, partOfCommittee);
|
|
@@ -276,10 +299,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
276
299
|
// Node issues so we can't validate
|
|
277
300
|
this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
|
|
278
301
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
302
|
+
if (!escapeHatchOpen && validationResult.reason && SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason)) {
|
|
303
|
+
this.log.info(`Detected invalid block proposal offense`, {
|
|
304
|
+
...proposalInfo,
|
|
305
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
306
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL)
|
|
307
|
+
});
|
|
282
308
|
this.slashInvalidBlock(proposal);
|
|
309
|
+
this.markInvalidProposalSlot(proposal.slotNumber);
|
|
283
310
|
}
|
|
284
311
|
return false;
|
|
285
312
|
}
|
|
@@ -308,9 +335,13 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
308
335
|
this.log.warn(`Escape hatch open for slot ${proposalSlotNumber}, skipping checkpoint attestation handling`);
|
|
309
336
|
return undefined;
|
|
310
337
|
}
|
|
338
|
+
// Early-out for equivocation: refuses if we've already attested to a higher slot.
|
|
339
|
+
if (!this.shouldAttestToSlot(proposalSlotNumber)) {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
311
342
|
// Ignore proposals from ourselves (may happen in HA setups)
|
|
312
343
|
if (proposer && this.getValidatorAddresses().some((addr)=>addr.equals(proposer))) {
|
|
313
|
-
this.log.debug(`
|
|
344
|
+
this.log.debug(`Not attesting to block proposal from self for slot ${proposalSlotNumber}`, {
|
|
314
345
|
proposer: proposer.toString(),
|
|
315
346
|
proposalSlotNumber
|
|
316
347
|
});
|
|
@@ -330,14 +361,17 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
330
361
|
});
|
|
331
362
|
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
|
|
332
363
|
// Uses the cached result from the all-nodes callback if available (avoids double validation).
|
|
364
|
+
let checkpointNumber;
|
|
333
365
|
if (this.config.skipCheckpointProposalValidation) {
|
|
334
366
|
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
|
|
367
|
+
checkpointNumber = CheckpointNumber(0);
|
|
335
368
|
} else {
|
|
336
369
|
const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
|
|
337
370
|
if (!validationResult.isValid) {
|
|
338
371
|
this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
339
372
|
return undefined;
|
|
340
373
|
}
|
|
374
|
+
checkpointNumber = validationResult.checkpointNumber;
|
|
341
375
|
}
|
|
342
376
|
// Check that I have any address in current committee before attesting
|
|
343
377
|
// In fisherman mode, we still create attestations for validation even if not in committee
|
|
@@ -384,7 +418,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
384
418
|
});
|
|
385
419
|
return undefined;
|
|
386
420
|
}
|
|
387
|
-
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
421
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors, checkpointNumber);
|
|
388
422
|
}
|
|
389
423
|
/**
|
|
390
424
|
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
@@ -401,12 +435,12 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
401
435
|
}
|
|
402
436
|
return true;
|
|
403
437
|
}
|
|
404
|
-
async createCheckpointAttestationsFromProposal(proposal, attestors = []) {
|
|
438
|
+
async createCheckpointAttestationsFromProposal(proposal, attestors = [], checkpointNumber) {
|
|
405
439
|
// Equivocation check: must happen right before signing to minimize the race window
|
|
406
440
|
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
407
441
|
return undefined;
|
|
408
442
|
}
|
|
409
|
-
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
443
|
+
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors, checkpointNumber);
|
|
410
444
|
// Track the proposal we attested to (to prevent equivocation)
|
|
411
445
|
this.lastAttestedProposal = proposal;
|
|
412
446
|
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
@@ -416,7 +450,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
416
450
|
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
417
451
|
*/ async uploadBlobsForCheckpoint(proposal, proposalInfo) {
|
|
418
452
|
try {
|
|
419
|
-
const lastBlockHeader = await this.blockSource.
|
|
453
|
+
const lastBlockHeader = (await this.blockSource.getBlockData({
|
|
454
|
+
archive: proposal.archive
|
|
455
|
+
}))?.header;
|
|
420
456
|
if (!lastBlockHeader) {
|
|
421
457
|
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
422
458
|
return;
|
|
@@ -444,11 +480,6 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
444
480
|
this.log.warn(`Cannot slash proposal with invalid signature`);
|
|
445
481
|
return;
|
|
446
482
|
}
|
|
447
|
-
// Trim the set if it's too big.
|
|
448
|
-
if (this.proposersOfInvalidBlocks.size > MAX_PROPOSERS_OF_INVALID_BLOCKS) {
|
|
449
|
-
// remove oldest proposer. `values` is guaranteed to be in insertion order.
|
|
450
|
-
this.proposersOfInvalidBlocks.delete(this.proposersOfInvalidBlocks.values().next().value);
|
|
451
|
-
}
|
|
452
483
|
this.proposersOfInvalidBlocks.add(proposer.toString());
|
|
453
484
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
454
485
|
{
|
|
@@ -459,17 +490,122 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
459
490
|
}
|
|
460
491
|
]);
|
|
461
492
|
}
|
|
493
|
+
handleInvalidCheckpointProposal(proposal, result, proposalInfo) {
|
|
494
|
+
if (!SLASHABLE_CHECKPOINT_PROPOSAL_VALIDATION_RESULT[result.reason]) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
// The slot is already marked invalid by the all-nodes checkpoint handler that invokes this callback,
|
|
498
|
+
// so we only emit the proposer slash event here.
|
|
499
|
+
if (this.slashInvalidCheckpointProposal(proposal)) {
|
|
500
|
+
this.log.info(`Detected invalid checkpoint proposal offense`, {
|
|
501
|
+
...proposalInfo,
|
|
502
|
+
reason: result.reason,
|
|
503
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
504
|
+
offenseType: getOffenseTypeName(OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL)
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
slashInvalidCheckpointProposal(proposal) {
|
|
509
|
+
const proposer = proposal.getSender();
|
|
510
|
+
if (!proposer) {
|
|
511
|
+
this.log.warn(`Cannot slash checkpoint proposal with invalid signature`, {
|
|
512
|
+
slotNumber: proposal.slotNumber,
|
|
513
|
+
archive: proposal.archive.toString()
|
|
514
|
+
});
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
const offenseType = OffenseType.BROADCASTED_INVALID_CHECKPOINT_PROPOSAL;
|
|
518
|
+
const offenseKey = `${proposer.toString()}:${offenseType}:${proposal.slotNumber}`;
|
|
519
|
+
if (!this.invalidCheckpointProposalOffenseKeys.addIfAbsent(offenseKey)) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
523
|
+
{
|
|
524
|
+
validator: proposer,
|
|
525
|
+
amount: this.config.slashBroadcastedInvalidCheckpointProposalPenalty,
|
|
526
|
+
offenseType,
|
|
527
|
+
epochOrSlot: BigInt(proposal.slotNumber)
|
|
528
|
+
}
|
|
529
|
+
]);
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
markInvalidProposalSlot(slotNumber) {
|
|
533
|
+
this.proposalHandler.markInvalidProposalSlot(slotNumber);
|
|
534
|
+
}
|
|
535
|
+
handleCheckpointAttestation(attestation) {
|
|
536
|
+
const slotNumber = attestation.slotNumber;
|
|
537
|
+
if (!this.proposalHandler.hasInvalidProposals(slotNumber) || this.proposalHandler.hasProposalEquivocation(slotNumber)) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const attester = attestation.getSender();
|
|
541
|
+
if (!attester) {
|
|
542
|
+
this.log.warn(`Cannot slash checkpoint attestation with invalid signature`, {
|
|
543
|
+
slotNumber,
|
|
544
|
+
archive: attestation.archive.toString()
|
|
545
|
+
});
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
this.slashAttestedToInvalidCheckpointProposal(slotNumber, attester);
|
|
549
|
+
}
|
|
550
|
+
slashAttestedToInvalidCheckpointProposal(slotNumber, attester) {
|
|
551
|
+
const offenseKey = `${attester.toString()}:${OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL}:${slotNumber}`;
|
|
552
|
+
if (!this.badAttestationOffenseKeys.addIfAbsent(offenseKey)) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
this.log.info(`Detected attestation to invalid checkpoint proposal offense`, {
|
|
556
|
+
attester: attester.toString(),
|
|
557
|
+
slotNumber,
|
|
558
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
559
|
+
offenseType: getOffenseTypeName(OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL)
|
|
560
|
+
});
|
|
561
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
562
|
+
{
|
|
563
|
+
validator: attester,
|
|
564
|
+
amount: this.config.slashAttestInvalidCheckpointProposalPenalty,
|
|
565
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
566
|
+
epochOrSlot: BigInt(slotNumber)
|
|
567
|
+
}
|
|
568
|
+
]);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Handle detection of an oversized block proposal: one whose index within its checkpoint lands at or
|
|
572
|
+
* beyond the consensus per-checkpoint block limit. A single signed proposal at an illegal index is
|
|
573
|
+
* self-contained evidence, so emit an invalid-block-proposal slash event for the proposer, deduped per
|
|
574
|
+
* (proposer, slot) since the p2p layer reports every oversized proposal it stores.
|
|
575
|
+
*/ handleOversizedProposal(info) {
|
|
576
|
+
const { slot, proposer } = info;
|
|
577
|
+
const offenseType = OffenseType.BROADCASTED_INVALID_BLOCK_PROPOSAL;
|
|
578
|
+
if (!this.oversizedProposalOffenseKeys.addIfAbsent(`${proposer.toString()}:${offenseType}:${slot}`)) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
this.log.info(`Detected oversized block proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
582
|
+
proposer: proposer.toString(),
|
|
583
|
+
slot,
|
|
584
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
585
|
+
offenseType: getOffenseTypeName(offenseType)
|
|
586
|
+
});
|
|
587
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
588
|
+
{
|
|
589
|
+
validator: proposer,
|
|
590
|
+
amount: this.config.slashBroadcastedInvalidBlockPenalty,
|
|
591
|
+
offenseType,
|
|
592
|
+
epochOrSlot: BigInt(slot)
|
|
593
|
+
}
|
|
594
|
+
]);
|
|
595
|
+
}
|
|
462
596
|
/**
|
|
463
597
|
* Handle detection of a duplicate proposal (equivocation).
|
|
464
598
|
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
465
599
|
*/ handleDuplicateProposal(info) {
|
|
466
600
|
const { slot, proposer, type } = info;
|
|
467
|
-
this.
|
|
601
|
+
this.proposalHandler.markProposalEquivocation(slot);
|
|
602
|
+
this.log.info(`Detected duplicate ${type} proposal offense from ${proposer.toString()} at slot ${slot}`, {
|
|
468
603
|
proposer: proposer.toString(),
|
|
469
604
|
slot,
|
|
470
|
-
type
|
|
605
|
+
type,
|
|
606
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
607
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_PROPOSAL)
|
|
471
608
|
});
|
|
472
|
-
// Emit slash event
|
|
473
609
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
474
610
|
{
|
|
475
611
|
validator: proposer,
|
|
@@ -478,15 +614,23 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
478
614
|
epochOrSlot: BigInt(slot)
|
|
479
615
|
}
|
|
480
616
|
]);
|
|
617
|
+
this.emit(WANT_TO_CLEAR_SLASH_EVENT, [
|
|
618
|
+
{
|
|
619
|
+
offenseType: OffenseType.ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL,
|
|
620
|
+
epochOrSlot: BigInt(slot)
|
|
621
|
+
}
|
|
622
|
+
]);
|
|
481
623
|
}
|
|
482
624
|
/**
|
|
483
625
|
* Handle detection of a duplicate attestation (equivocation).
|
|
484
626
|
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
485
627
|
*/ handleDuplicateAttestation(info) {
|
|
486
628
|
const { slot, attester } = info;
|
|
487
|
-
this.log.
|
|
629
|
+
this.log.info(`Detected duplicate attestation offense from ${attester.toString()} at slot ${slot}`, {
|
|
488
630
|
attester: attester.toString(),
|
|
489
|
-
slot
|
|
631
|
+
slot,
|
|
632
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
633
|
+
offenseType: getOffenseTypeName(OffenseType.DUPLICATE_ATTESTATION)
|
|
490
634
|
});
|
|
491
635
|
this.emit(WANT_TO_SLASH_EVENT, [
|
|
492
636
|
{
|
|
@@ -497,7 +641,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
497
641
|
}
|
|
498
642
|
]);
|
|
499
643
|
}
|
|
500
|
-
async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
|
|
644
|
+
async createBlockProposal(blockHeader, checkpointNumber, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
|
|
501
645
|
// Validate that we're not creating a proposal for an older or equal position
|
|
502
646
|
if (this.lastProposedBlock) {
|
|
503
647
|
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
@@ -508,14 +652,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
508
652
|
}
|
|
509
653
|
}
|
|
510
654
|
this.log.info(`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`);
|
|
511
|
-
const newProposal = await this.validationService.createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, {
|
|
655
|
+
const newProposal = await this.validationService.createBlockProposal(blockHeader, checkpointNumber, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, {
|
|
512
656
|
...options,
|
|
513
|
-
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal
|
|
657
|
+
broadcastInvalidBlockProposal: options.broadcastInvalidBlockProposal || this.config.broadcastInvalidBlockProposal
|
|
514
658
|
});
|
|
515
659
|
this.lastProposedBlock = newProposal;
|
|
516
660
|
return newProposal;
|
|
517
661
|
}
|
|
518
|
-
async createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier,
|
|
662
|
+
async createCheckpointProposal(checkpointHeader, archive, checkpointNumber, feeAssetPriceModifier, lastBlockProposal, proposerAddress, options = {}) {
|
|
519
663
|
// Validate that we're not creating a proposal for an older or equal slot
|
|
520
664
|
if (this.lastProposedCheckpoint) {
|
|
521
665
|
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
@@ -525,23 +669,30 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
525
669
|
}
|
|
526
670
|
}
|
|
527
671
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
528
|
-
const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier,
|
|
672
|
+
const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, checkpointNumber, feeAssetPriceModifier, lastBlockProposal, proposerAddress, options);
|
|
529
673
|
this.lastProposedCheckpoint = newProposal;
|
|
674
|
+
// Self-record this slot's outcome on the re-execution tracker. Proposers don't run their
|
|
675
|
+
// own proposals through `handleCheckpointProposal`, so without this call the proposer's
|
|
676
|
+
// sentinel would see no outcome for slots it proposed and would mis-attribute itself as
|
|
677
|
+
// inactive. We pass the locally-computed `archive` (not `newProposal.archive`, which may
|
|
678
|
+
// be intentionally corrupted under test-only flags); from the proposer's local-view
|
|
679
|
+
// perspective the work it just completed is valid by definition.
|
|
680
|
+
this.proposalHandler.recordOwnCheckpointProposalAsValid(checkpointHeader.slotNumber, archive, checkpointNumber);
|
|
530
681
|
return newProposal;
|
|
531
682
|
}
|
|
532
683
|
async broadcastBlockProposal(proposal) {
|
|
533
684
|
await this.p2pClient.broadcastProposal(proposal);
|
|
534
685
|
}
|
|
535
|
-
async signAttestationsAndSigners(attestationsAndSigners, proposer, slot,
|
|
536
|
-
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot,
|
|
686
|
+
async signAttestationsAndSigners(attestationsAndSigners, proposer, slot, checkpointNumber) {
|
|
687
|
+
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, checkpointNumber);
|
|
537
688
|
}
|
|
538
|
-
async collectOwnAttestations(proposal) {
|
|
689
|
+
async collectOwnAttestations(proposal, checkpointNumber) {
|
|
539
690
|
const slot = proposal.slotNumber;
|
|
540
691
|
const inCommittee = await this.epochCache.filterInCommittee(slot, this.getValidatorAddresses());
|
|
541
692
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, {
|
|
542
693
|
inCommittee
|
|
543
694
|
});
|
|
544
|
-
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
695
|
+
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee, checkpointNumber);
|
|
545
696
|
if (!attestations) {
|
|
546
697
|
return [];
|
|
547
698
|
}
|
|
@@ -553,7 +704,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
553
704
|
});
|
|
554
705
|
return attestations;
|
|
555
706
|
}
|
|
556
|
-
async collectAttestations(proposal, required, deadline) {
|
|
707
|
+
async collectAttestations(proposal, required, deadline, checkpointNumber) {
|
|
557
708
|
// Wait and poll the p2pClient's attestation pool for this checkpoint until we have enough attestations
|
|
558
709
|
const slot = proposal.slotNumber;
|
|
559
710
|
this.log.debug(`Collecting ${required} attestations for slot ${slot} with deadline ${deadline.toISOString()}`);
|
|
@@ -561,28 +712,20 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
561
712
|
this.log.error(`Deadline ${deadline.toISOString()} for collecting ${required} attestations for slot ${slot} is in the past`);
|
|
562
713
|
throw new AttestationTimeoutError(0, required, slot);
|
|
563
714
|
}
|
|
564
|
-
await this.collectOwnAttestations(proposal);
|
|
565
|
-
const
|
|
715
|
+
await this.collectOwnAttestations(proposal, checkpointNumber);
|
|
716
|
+
const proposalPayloadHash = proposal.getPayloadHash();
|
|
566
717
|
const myAddresses = this.getValidatorAddresses();
|
|
567
718
|
let attestations = [];
|
|
568
719
|
while(true){
|
|
569
|
-
//
|
|
570
|
-
//
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
this.log.warn(`Received attestation for slot ${slot} with mismatched archive from ${attestation.getSender()?.toString()}`, {
|
|
574
|
-
attestationArchive: attestation.archive.toString(),
|
|
575
|
-
proposalArchive: proposal.archive.toString()
|
|
576
|
-
});
|
|
577
|
-
return false;
|
|
578
|
-
}
|
|
579
|
-
return true;
|
|
580
|
-
});
|
|
720
|
+
// The pool already filters by proposal payload hash; if any attestation slips through with a
|
|
721
|
+
// mismatched payload hash, drop it defensively. Equivocations are emitted as separate slash
|
|
722
|
+
// events from libp2p_service.
|
|
723
|
+
const collectedAttestations = await this.p2pClient.getCheckpointAttestationsForSlot(slot, proposalPayloadHash);
|
|
581
724
|
// Log new attestations we collected
|
|
582
725
|
const oldSenders = attestations.map((attestation)=>attestation.getSender());
|
|
583
726
|
for (const collected of collectedAttestations){
|
|
584
727
|
const collectedSender = collected.getSender();
|
|
585
|
-
// Skip attestations with invalid signatures
|
|
728
|
+
// Skip attestations with invalid signatures. Should not happen as we don't add invalid attestations to our pool.
|
|
586
729
|
if (!collectedSender) {
|
|
587
730
|
this.log.warn(`Skipping attestation with invalid signature for slot ${slot}`);
|
|
588
731
|
continue;
|
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.431c48d",
|
|
4
4
|
"main": "dest/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -64,30 +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.431c48d",
|
|
68
|
+
"@aztec/blob-lib": "0.0.1-commit.431c48d",
|
|
69
|
+
"@aztec/constants": "0.0.1-commit.431c48d",
|
|
70
|
+
"@aztec/epoch-cache": "0.0.1-commit.431c48d",
|
|
71
|
+
"@aztec/ethereum": "0.0.1-commit.431c48d",
|
|
72
|
+
"@aztec/foundation": "0.0.1-commit.431c48d",
|
|
73
|
+
"@aztec/node-keystore": "0.0.1-commit.431c48d",
|
|
74
|
+
"@aztec/noir-protocol-circuits-types": "0.0.1-commit.431c48d",
|
|
75
|
+
"@aztec/p2p": "0.0.1-commit.431c48d",
|
|
76
|
+
"@aztec/protocol-contracts": "0.0.1-commit.431c48d",
|
|
77
|
+
"@aztec/prover-client": "0.0.1-commit.431c48d",
|
|
78
|
+
"@aztec/simulator": "0.0.1-commit.431c48d",
|
|
79
|
+
"@aztec/slasher": "0.0.1-commit.431c48d",
|
|
80
|
+
"@aztec/stdlib": "0.0.1-commit.431c48d",
|
|
81
|
+
"@aztec/telemetry-client": "0.0.1-commit.431c48d",
|
|
82
|
+
"@aztec/validator-ha-signer": "0.0.1-commit.431c48d",
|
|
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.
|
|
90
|
-
"@aztec/world-state": "0.0.1-commit.
|
|
89
|
+
"@aztec/archiver": "0.0.1-commit.431c48d",
|
|
90
|
+
"@aztec/world-state": "0.0.1-commit.431c48d",
|
|
91
91
|
"@electric-sql/pglite": "^0.3.14",
|
|
92
92
|
"@jest/globals": "^30.0.0",
|
|
93
93
|
"@types/jest": "^30.0.0",
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
PublicProcessor,
|
|
15
15
|
createPublicTxSimulatorForBlockBuilding,
|
|
16
16
|
} from '@aztec/simulator/server';
|
|
17
|
-
import { L2Block } from '@aztec/stdlib/block';
|
|
17
|
+
import { type BlockHash, L2Block } from '@aztec/stdlib/block';
|
|
18
18
|
import { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
19
19
|
import type { ContractDataSource } from '@aztec/stdlib/contract';
|
|
20
20
|
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
|
|
@@ -219,10 +219,12 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
|
219
219
|
if (opts.isBuildingProposal) {
|
|
220
220
|
const remainingBlocks = Math.max(1, opts.maxBlocksPerCheckpoint - existingBlocks.length);
|
|
221
221
|
const multiplier = opts.perBlockAllocationMultiplier;
|
|
222
|
+
// DA gas and blob fields use a higher multiplier so the largest contract class deploy fits a block.
|
|
223
|
+
const daMultiplier = opts.perBlockDAAllocationMultiplier ?? multiplier;
|
|
222
224
|
|
|
223
225
|
cappedL2Gas = Math.min(cappedL2Gas, Math.ceil((remainingMana / remainingBlocks) * multiplier));
|
|
224
|
-
cappedDAGas = Math.min(cappedDAGas, Math.ceil((remainingDAGas / remainingBlocks) *
|
|
225
|
-
cappedBlobFields = Math.min(cappedBlobFields, Math.ceil((maxBlobFieldsForTxs / remainingBlocks) *
|
|
226
|
+
cappedDAGas = Math.min(cappedDAGas, Math.ceil((remainingDAGas / remainingBlocks) * daMultiplier));
|
|
227
|
+
cappedBlobFields = Math.min(cappedBlobFields, Math.ceil((maxBlobFieldsForTxs / remainingBlocks) * daMultiplier));
|
|
226
228
|
cappedMaxTransactions = Math.min(cappedMaxTransactions, Math.ceil((remainingTxs / remainingBlocks) * multiplier));
|
|
227
229
|
}
|
|
228
230
|
|
|
@@ -410,8 +412,17 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder {
|
|
|
410
412
|
);
|
|
411
413
|
}
|
|
412
414
|
|
|
413
|
-
/**
|
|
414
|
-
|
|
415
|
+
/**
|
|
416
|
+
* Syncs world state to the given block number and returns a fork of it at that block.
|
|
417
|
+
*
|
|
418
|
+
* Syncing first is required: the block source (archiver) can already hold a block while world state
|
|
419
|
+
* still trails it, and forking a not-yet-applied block throws a raw "initialize from future block"
|
|
420
|
+
* tree error. syncImmediate blocks until world state reaches the block, or throws a typed error if it
|
|
421
|
+
* genuinely cannot. When `blockHash` is provided it is verified against the synced block, triggering a
|
|
422
|
+
* resync on mismatch (reorg detection).
|
|
423
|
+
*/
|
|
424
|
+
async getFork(blockNumber: BlockNumber, blockHash?: BlockHash): Promise<MerkleTreeWriteOperations> {
|
|
425
|
+
await this.worldState.syncImmediate(blockNumber, blockHash);
|
|
415
426
|
return this.worldState.fork(blockNumber);
|
|
416
427
|
}
|
|
417
428
|
}
|