@aztec/validator-client 0.0.1-commit.1142ef1 → 0.0.1-commit.1bea0213
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 +41 -15
- package/dest/block_proposal_handler.d.ts +7 -6
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +23 -29
- package/dest/checkpoint_builder.d.ts +18 -21
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +17 -12
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +6 -11
- package/dest/duties/validation_service.d.ts +19 -6
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +72 -19
- package/dest/factory.d.ts +2 -2
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +1 -1
- package/dest/key_store/ha_key_store.d.ts +99 -0
- package/dest/key_store/ha_key_store.d.ts.map +1 -0
- package/dest/key_store/ha_key_store.js +208 -0
- package/dest/key_store/index.d.ts +2 -1
- package/dest/key_store/index.d.ts.map +1 -1
- package/dest/key_store/index.js +1 -0
- package/dest/key_store/interface.d.ts +36 -6
- package/dest/key_store/interface.d.ts.map +1 -1
- package/dest/key_store/local_key_store.d.ts +10 -5
- package/dest/key_store/local_key_store.d.ts.map +1 -1
- package/dest/key_store/local_key_store.js +8 -4
- package/dest/key_store/node_keystore_adapter.d.ts +18 -5
- package/dest/key_store/node_keystore_adapter.d.ts.map +1 -1
- package/dest/key_store/node_keystore_adapter.js +18 -4
- package/dest/key_store/web3signer_key_store.d.ts +10 -5
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
- package/dest/key_store/web3signer_key_store.js +8 -4
- package/dest/tx_validator/tx_validator_factory.d.ts +1 -1
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.js +2 -1
- package/dest/validator.d.ts +9 -8
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +68 -60
- package/package.json +19 -17
- package/src/block_proposal_handler.ts +34 -36
- package/src/checkpoint_builder.ts +37 -20
- package/src/config.ts +5 -10
- package/src/duties/validation_service.ts +91 -23
- package/src/factory.ts +1 -0
- package/src/key_store/ha_key_store.ts +269 -0
- package/src/key_store/index.ts +1 -0
- package/src/key_store/interface.ts +44 -5
- package/src/key_store/local_key_store.ts +13 -4
- package/src/key_store/node_keystore_adapter.ts +27 -4
- package/src/key_store/web3signer_key_store.ts +17 -4
- package/src/tx_validator/tx_validator_factory.ts +2 -0
- package/src/validator.ts +85 -69
package/dest/validator.js
CHANGED
|
@@ -8,11 +8,15 @@ import { sleep } from '@aztec/foundation/sleep';
|
|
|
8
8
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
9
9
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
10
10
|
import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
|
|
11
|
+
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
11
12
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
12
13
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
14
|
+
import { createHASigner } from '@aztec/validator-ha-signer/factory';
|
|
15
|
+
import { DutyType } from '@aztec/validator-ha-signer/types';
|
|
13
16
|
import { EventEmitter } from 'events';
|
|
14
17
|
import { BlockProposalHandler } from './block_proposal_handler.js';
|
|
15
18
|
import { ValidationService } from './duties/validation_service.js';
|
|
19
|
+
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
16
20
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
17
21
|
import { ValidatorMetrics } from './metrics.js';
|
|
18
22
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
@@ -107,13 +111,23 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
107
111
|
this.log.error(`Error updating epoch committee`, err);
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
|
-
static new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
|
|
114
|
+
static async new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
|
|
111
115
|
const metrics = new ValidatorMetrics(telemetry);
|
|
112
116
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
113
117
|
txsPermitted: !config.disableTransactions
|
|
114
118
|
});
|
|
115
|
-
const blockProposalHandler = new BlockProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, config, metrics, dateProvider, telemetry);
|
|
116
|
-
|
|
119
|
+
const blockProposalHandler = new BlockProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, metrics, dateProvider, telemetry);
|
|
120
|
+
let validatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
121
|
+
if (config.haSigningEnabled) {
|
|
122
|
+
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
123
|
+
const haConfig = {
|
|
124
|
+
...config,
|
|
125
|
+
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000
|
|
126
|
+
};
|
|
127
|
+
const { signer } = await createHASigner(haConfig);
|
|
128
|
+
validatorKeyStore = new HAKeyStore(validatorKeyStore, signer);
|
|
129
|
+
}
|
|
130
|
+
const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider, telemetry);
|
|
117
131
|
return validator;
|
|
118
132
|
}
|
|
119
133
|
getValidatorAddresses() {
|
|
@@ -122,8 +136,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
122
136
|
getBlockProposalHandler() {
|
|
123
137
|
return this.blockProposalHandler;
|
|
124
138
|
}
|
|
125
|
-
signWithAddress(addr, msg) {
|
|
126
|
-
return this.keyStore.signTypedDataWithAddress(addr, msg);
|
|
139
|
+
signWithAddress(addr, msg, context) {
|
|
140
|
+
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
127
141
|
}
|
|
128
142
|
getCoinbaseForAttestor(attestor) {
|
|
129
143
|
return this.keyStore.getCoinbaseAddress(attestor);
|
|
@@ -145,6 +159,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
145
159
|
this.log.warn(`Validator client already started`);
|
|
146
160
|
return;
|
|
147
161
|
}
|
|
162
|
+
await this.keyStore.start();
|
|
148
163
|
await this.registerHandlers();
|
|
149
164
|
const myAddresses = this.getValidatorAddresses();
|
|
150
165
|
const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
|
|
@@ -157,6 +172,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
157
172
|
}
|
|
158
173
|
async stop() {
|
|
159
174
|
await this.epochCacheUpdateLoop.stop();
|
|
175
|
+
await this.keyStore.stop();
|
|
160
176
|
}
|
|
161
177
|
/** Register handlers on the p2p client */ async registerHandlers() {
|
|
162
178
|
if (!this.hasRegisteredHandlers) {
|
|
@@ -181,6 +197,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
181
197
|
* @returns true if the proposal is valid, false otherwise
|
|
182
198
|
*/ async validateBlockProposal(proposal, proposalSender) {
|
|
183
199
|
const slotNumber = proposal.slotNumber;
|
|
200
|
+
// Note: During escape hatch, we still want to "validate" proposals for observability,
|
|
201
|
+
// but we intentionally reject them and disable slashing invalid block and attestation flow.
|
|
202
|
+
const escapeHatchOpen = await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber);
|
|
184
203
|
const proposer = proposal.getSender();
|
|
185
204
|
// Reject proposals with invalid signatures
|
|
186
205
|
if (!proposer) {
|
|
@@ -203,7 +222,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
203
222
|
// In fisherman mode, we always reexecute to validate proposals.
|
|
204
223
|
const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
|
|
205
224
|
const shouldReexecute = fishermanMode || slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute || partOfCommittee && validatorReexecute || alwaysReexecuteBlockProposals || this.blobClient.canUpload();
|
|
206
|
-
const validationResult = await this.blockProposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute);
|
|
225
|
+
const validationResult = await this.blockProposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute && !escapeHatchOpen);
|
|
207
226
|
if (!validationResult.isValid) {
|
|
208
227
|
this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
209
228
|
const reason = validationResult.reason || 'unknown';
|
|
@@ -222,7 +241,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
222
241
|
this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
|
|
223
242
|
}
|
|
224
243
|
// Slash invalid block proposals (can happen even when not in committee)
|
|
225
|
-
if (validationResult.reason && SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) && slashBroadcastedInvalidBlockPenalty > 0n) {
|
|
244
|
+
if (!escapeHatchOpen && validationResult.reason && SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) && slashBroadcastedInvalidBlockPenalty > 0n) {
|
|
226
245
|
this.log.warn(`Slashing proposer for invalid block proposal`, proposalInfo);
|
|
227
246
|
this.slashInvalidBlock(proposal);
|
|
228
247
|
}
|
|
@@ -231,8 +250,13 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
231
250
|
this.log.info(`Validated block proposal for slot ${slotNumber}`, {
|
|
232
251
|
...proposalInfo,
|
|
233
252
|
inCommittee: partOfCommittee,
|
|
234
|
-
fishermanMode: this.config.fishermanMode || false
|
|
253
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
254
|
+
escapeHatchOpen
|
|
235
255
|
});
|
|
256
|
+
if (escapeHatchOpen) {
|
|
257
|
+
this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
236
260
|
// TODO(palla/mbps): Remove this once checkpoint validation is stable.
|
|
237
261
|
// Track that we successfully validated a block for this slot, so we can attest to checkpoint proposals for it.
|
|
238
262
|
this.validatedBlockSlots.add(slotNumber);
|
|
@@ -246,6 +270,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
246
270
|
*/ async attestToCheckpointProposal(proposal, _proposalSender) {
|
|
247
271
|
const slotNumber = proposal.slotNumber;
|
|
248
272
|
const proposer = proposal.getSender();
|
|
273
|
+
// If escape hatch is open for this slot's epoch, do not attest.
|
|
274
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber)) {
|
|
275
|
+
this.log.warn(`Escape hatch open for slot ${slotNumber}, skipping checkpoint attestation handling`);
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
249
278
|
// Reject proposals with invalid signatures
|
|
250
279
|
if (!proposer) {
|
|
251
280
|
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
|
|
@@ -362,18 +391,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
362
391
|
reason: 'last_block_not_found'
|
|
363
392
|
};
|
|
364
393
|
}
|
|
365
|
-
// Get the last full block to determine checkpoint number
|
|
366
|
-
const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
|
|
367
|
-
if (!lastBlock) {
|
|
368
|
-
this.log.warn(`Last block ${lastBlockHeader.getBlockNumber()} not found`, proposalInfo);
|
|
369
|
-
return {
|
|
370
|
-
isValid: false,
|
|
371
|
-
reason: 'last_block_not_found'
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
const checkpointNumber = lastBlock.checkpointNumber;
|
|
375
394
|
// Get all full blocks for the slot and checkpoint
|
|
376
|
-
const blocks = await this.getBlocksForSlot(slot
|
|
395
|
+
const blocks = await this.blockSource.getBlocksForSlot(slot);
|
|
377
396
|
if (blocks.length === 0) {
|
|
378
397
|
this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
|
|
379
398
|
return {
|
|
@@ -388,14 +407,21 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
388
407
|
// Get checkpoint constants from first block
|
|
389
408
|
const firstBlock = blocks[0];
|
|
390
409
|
const constants = this.extractCheckpointConstants(firstBlock);
|
|
410
|
+
const checkpointNumber = firstBlock.checkpointNumber;
|
|
391
411
|
// Get L1-to-L2 messages for this checkpoint
|
|
392
412
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
413
|
+
// Compute the previous checkpoint out hashes for the epoch.
|
|
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.
|
|
416
|
+
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
417
|
+
const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch)).filter((b)=>b.number < checkpointNumber).sort((a, b)=>a.number - b.number);
|
|
418
|
+
const previousCheckpointOutHashes = previousCheckpoints.map((c)=>c.getCheckpointOutHash());
|
|
393
419
|
// Fork world state at the block before the first block
|
|
394
420
|
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
395
421
|
const fork = await this.worldState.fork(parentBlockNumber);
|
|
396
422
|
try {
|
|
397
423
|
// Create checkpoint builder with all existing blocks
|
|
398
|
-
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, fork, blocks);
|
|
424
|
+
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks);
|
|
399
425
|
// Complete the checkpoint to get computed values
|
|
400
426
|
const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
|
|
401
427
|
// Compare checkpoint header with proposal
|
|
@@ -422,6 +448,20 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
422
448
|
reason: 'archive_mismatch'
|
|
423
449
|
};
|
|
424
450
|
}
|
|
451
|
+
// Check that the accumulated out hash matches the value in the proposal.
|
|
452
|
+
const computedOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
453
|
+
const proposalOutHash = proposal.checkpointHeader.epochOutHash;
|
|
454
|
+
if (!computedOutHash.equals(proposalOutHash)) {
|
|
455
|
+
this.log.warn(`Epoch out hash mismatch`, {
|
|
456
|
+
proposalOutHash: proposalOutHash.toString(),
|
|
457
|
+
computedOutHash: computedOutHash.toString(),
|
|
458
|
+
...proposalInfo
|
|
459
|
+
});
|
|
460
|
+
return {
|
|
461
|
+
isValid: false,
|
|
462
|
+
reason: 'out_hash_mismatch'
|
|
463
|
+
};
|
|
464
|
+
}
|
|
425
465
|
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
426
466
|
return {
|
|
427
467
|
isValid: true
|
|
@@ -431,36 +471,6 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
431
471
|
}
|
|
432
472
|
}
|
|
433
473
|
/**
|
|
434
|
-
* Get all full blocks for a given slot and checkpoint by walking backwards from the last block.
|
|
435
|
-
* Returns blocks in ascending order (earliest to latest).
|
|
436
|
-
* TODO(palla/mbps): Add getL2BlocksForSlot() to L2BlockSource interface for efficiency.
|
|
437
|
-
*/ async getBlocksForSlot(slot, lastBlockHeader, checkpointNumber) {
|
|
438
|
-
const blocks = [];
|
|
439
|
-
let currentHeader = lastBlockHeader;
|
|
440
|
-
const { genesisArchiveRoot } = await this.blockSource.getGenesisValues();
|
|
441
|
-
while(currentHeader.getSlot() === slot){
|
|
442
|
-
const block = await this.blockSource.getL2BlockNew(currentHeader.getBlockNumber());
|
|
443
|
-
if (!block) {
|
|
444
|
-
this.log.warn(`Block ${currentHeader.getBlockNumber()} not found while getting blocks for slot ${slot}`);
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
if (block.checkpointNumber !== checkpointNumber) {
|
|
448
|
-
break;
|
|
449
|
-
}
|
|
450
|
-
blocks.unshift(block);
|
|
451
|
-
const prevArchive = currentHeader.lastArchive.root;
|
|
452
|
-
if (prevArchive.equals(genesisArchiveRoot)) {
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
|
-
const prevHeader = await this.blockSource.getBlockHeaderByArchive(prevArchive);
|
|
456
|
-
if (!prevHeader || prevHeader.getSlot() !== slot) {
|
|
457
|
-
break;
|
|
458
|
-
}
|
|
459
|
-
currentHeader = prevHeader;
|
|
460
|
-
}
|
|
461
|
-
return blocks;
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
474
|
* Extract checkpoint global variables from a block.
|
|
465
475
|
*/ extractCheckpointConstants(block) {
|
|
466
476
|
const gv = block.header.globalVariables;
|
|
@@ -482,13 +492,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
482
492
|
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
483
493
|
return;
|
|
484
494
|
}
|
|
485
|
-
|
|
486
|
-
const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
|
|
487
|
-
if (!lastBlock) {
|
|
488
|
-
this.log.warn(`Failed to get last block for blob upload`, proposalInfo);
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
const blocks = await this.getBlocksForSlot(proposal.slotNumber, lastBlockHeader, lastBlock.checkpointNumber);
|
|
495
|
+
const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
|
|
492
496
|
if (blocks.length === 0) {
|
|
493
497
|
this.log.warn(`No blocks found for blob upload`, proposalInfo);
|
|
494
498
|
return;
|
|
@@ -547,8 +551,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
547
551
|
async broadcastBlockProposal(proposal) {
|
|
548
552
|
await this.p2pClient.broadcastProposal(proposal);
|
|
549
553
|
}
|
|
550
|
-
async signAttestationsAndSigners(attestationsAndSigners, proposer) {
|
|
551
|
-
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
|
|
554
|
+
async signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber) {
|
|
555
|
+
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
|
|
552
556
|
}
|
|
553
557
|
async collectOwnAttestations(proposal) {
|
|
554
558
|
const slot = proposal.slotNumber;
|
|
@@ -630,7 +634,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
630
634
|
return Buffer.alloc(0);
|
|
631
635
|
}
|
|
632
636
|
const payloadToSign = authRequest.getPayloadToSign();
|
|
633
|
-
|
|
637
|
+
// AUTH_REQUEST doesn't require HA protection - multiple signatures are safe
|
|
638
|
+
const context = {
|
|
639
|
+
dutyType: DutyType.AUTH_REQUEST
|
|
640
|
+
};
|
|
641
|
+
const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign, context);
|
|
634
642
|
const authResponse = new AuthResponse(statusMessage, signature);
|
|
635
643
|
return authResponse.toBuffer();
|
|
636
644
|
}
|
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.1bea0213",
|
|
4
4
|
"main": "dest/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -64,31 +64,33 @@
|
|
|
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.
|
|
67
|
+
"@aztec/blob-client": "0.0.1-commit.1bea0213",
|
|
68
|
+
"@aztec/blob-lib": "0.0.1-commit.1bea0213",
|
|
69
|
+
"@aztec/constants": "0.0.1-commit.1bea0213",
|
|
70
|
+
"@aztec/epoch-cache": "0.0.1-commit.1bea0213",
|
|
71
|
+
"@aztec/ethereum": "0.0.1-commit.1bea0213",
|
|
72
|
+
"@aztec/foundation": "0.0.1-commit.1bea0213",
|
|
73
|
+
"@aztec/node-keystore": "0.0.1-commit.1bea0213",
|
|
74
|
+
"@aztec/noir-protocol-circuits-types": "0.0.1-commit.1bea0213",
|
|
75
|
+
"@aztec/p2p": "0.0.1-commit.1bea0213",
|
|
76
|
+
"@aztec/protocol-contracts": "0.0.1-commit.1bea0213",
|
|
77
|
+
"@aztec/prover-client": "0.0.1-commit.1bea0213",
|
|
78
|
+
"@aztec/simulator": "0.0.1-commit.1bea0213",
|
|
79
|
+
"@aztec/slasher": "0.0.1-commit.1bea0213",
|
|
80
|
+
"@aztec/stdlib": "0.0.1-commit.1bea0213",
|
|
81
|
+
"@aztec/telemetry-client": "0.0.1-commit.1bea0213",
|
|
82
|
+
"@aztec/validator-ha-signer": "0.0.1-commit.1bea0213",
|
|
82
83
|
"koa": "^2.16.1",
|
|
83
84
|
"koa-router": "^13.1.1",
|
|
84
85
|
"tslib": "^2.4.0",
|
|
85
86
|
"viem": "npm:@aztec/viem@2.38.2"
|
|
86
87
|
},
|
|
87
88
|
"devDependencies": {
|
|
89
|
+
"@electric-sql/pglite": "^0.3.14",
|
|
88
90
|
"@jest/globals": "^30.0.0",
|
|
89
91
|
"@types/jest": "^30.0.0",
|
|
90
92
|
"@types/node": "^22.15.17",
|
|
91
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
93
|
+
"@typescript/native-preview": "7.0.0-dev.20260113.1",
|
|
92
94
|
"jest": "^30.0.0",
|
|
93
95
|
"jest-mock-extended": "^4.0.0",
|
|
94
96
|
"ts-node": "^10.9.1",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
2
|
+
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
3
|
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
4
|
+
import { chunkBy } from '@aztec/foundation/collection';
|
|
3
5
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
4
6
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
5
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
@@ -8,10 +10,14 @@ import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
|
8
10
|
import type { P2P, PeerId } from '@aztec/p2p';
|
|
9
11
|
import { TxProvider } from '@aztec/p2p';
|
|
10
12
|
import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
|
|
11
|
-
import type {
|
|
12
|
-
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
13
|
+
import type { L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
14
|
+
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
13
15
|
import type { ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
14
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
type L1ToL2MessageSource,
|
|
18
|
+
computeCheckpointOutHash,
|
|
19
|
+
computeInHashFromL1ToL2Messages,
|
|
20
|
+
} from '@aztec/stdlib/messaging';
|
|
15
21
|
import type { BlockProposal } from '@aztec/stdlib/p2p';
|
|
16
22
|
import { BlockHeader, type CheckpointGlobalVariables, type FailedTx, type Tx } from '@aztec/stdlib/tx';
|
|
17
23
|
import {
|
|
@@ -39,7 +45,7 @@ export type BlockProposalValidationFailureReason =
|
|
|
39
45
|
| 'unknown_error';
|
|
40
46
|
|
|
41
47
|
type ReexecuteTransactionsResult = {
|
|
42
|
-
block:
|
|
48
|
+
block: L2Block;
|
|
43
49
|
failedTxs: FailedTx[];
|
|
44
50
|
reexecutionTimeMs: number;
|
|
45
51
|
totalManaUsed: number;
|
|
@@ -74,6 +80,7 @@ export class BlockProposalHandler {
|
|
|
74
80
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
75
81
|
private txProvider: TxProvider,
|
|
76
82
|
private blockProposalValidator: BlockProposalValidator,
|
|
83
|
+
private epochCache: EpochCache,
|
|
77
84
|
private config: ValidatorClientFullConfig,
|
|
78
85
|
private metrics?: ValidatorMetrics,
|
|
79
86
|
private dateProvider: DateProvider = new DateProvider(),
|
|
@@ -140,8 +147,8 @@ export class BlockProposalHandler {
|
|
|
140
147
|
|
|
141
148
|
// Check that the proposal is from the current proposer, or the next proposer
|
|
142
149
|
// This should have been handled by the p2p layer, but we double check here out of caution
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
150
|
+
const validationResult = await this.blockProposalValidator.validate(proposal);
|
|
151
|
+
if (validationResult.result !== 'accept') {
|
|
145
152
|
this.log.warn(`Proposal is not valid, skipping processing`, proposalInfo);
|
|
146
153
|
return { isValid: false, reason: 'invalid_proposal' };
|
|
147
154
|
}
|
|
@@ -212,6 +219,18 @@ export class BlockProposalHandler {
|
|
|
212
219
|
// Try re-executing the transactions in the proposal if needed
|
|
213
220
|
let reexecutionResult;
|
|
214
221
|
if (shouldReexecute) {
|
|
222
|
+
// Compute the previous checkpoint out hashes for the epoch.
|
|
223
|
+
// TODO(leila/mbps): There can be a more efficient way to get the previous checkpoint out
|
|
224
|
+
// hashes without having to fetch all the blocks.
|
|
225
|
+
const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
|
|
226
|
+
const checkpointedBlocks = (await this.blockSource.getCheckpointedBlocksForEpoch(epoch))
|
|
227
|
+
.filter(b => b.block.number < blockNumber)
|
|
228
|
+
.sort((a, b) => a.block.number - b.block.number);
|
|
229
|
+
const blocksByCheckpoint = chunkBy(checkpointedBlocks, b => b.checkpointNumber);
|
|
230
|
+
const previousCheckpointOutHashes = blocksByCheckpoint.map(checkpointBlocks =>
|
|
231
|
+
computeCheckpointOutHash(checkpointBlocks.map(b => b.block.body.txEffects.map(tx => tx.l2ToL1Msgs))),
|
|
232
|
+
);
|
|
233
|
+
|
|
215
234
|
try {
|
|
216
235
|
this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
|
|
217
236
|
reexecutionResult = await this.reexecuteTransactions(
|
|
@@ -220,6 +239,7 @@ export class BlockProposalHandler {
|
|
|
220
239
|
checkpointNumber,
|
|
221
240
|
txs,
|
|
222
241
|
l1ToL2Messages,
|
|
242
|
+
previousCheckpointOutHashes,
|
|
223
243
|
);
|
|
224
244
|
} catch (error) {
|
|
225
245
|
this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
|
|
@@ -229,7 +249,6 @@ export class BlockProposalHandler {
|
|
|
229
249
|
}
|
|
230
250
|
|
|
231
251
|
// If we succeeded, push this block into the archiver (unless disabled)
|
|
232
|
-
// TODO(palla/mbps): Change default to false once block sync is stable.
|
|
233
252
|
if (reexecutionResult?.block && this.config.skipPushProposedBlocksToArchiver === false) {
|
|
234
253
|
await this.blockSource.addBlock(reexecutionResult?.block);
|
|
235
254
|
}
|
|
@@ -297,7 +316,7 @@ export class BlockProposalHandler {
|
|
|
297
316
|
// TODO(palla/mbps): The block header should include the checkpoint number to avoid this lookup,
|
|
298
317
|
// or at least the L2BlockSource should return a different struct that includes it.
|
|
299
318
|
const parentBlockNumber = parentBlockHeader.getBlockNumber();
|
|
300
|
-
const parentBlock = await this.blockSource.
|
|
319
|
+
const parentBlock = await this.blockSource.getL2Block(parentBlockNumber);
|
|
301
320
|
if (!parentBlock) {
|
|
302
321
|
this.log.warn(`Parent block ${parentBlockNumber} not found in archiver`, proposalInfo);
|
|
303
322
|
return { reason: 'invalid_proposal' };
|
|
@@ -338,7 +357,7 @@ export class BlockProposalHandler {
|
|
|
338
357
|
*/
|
|
339
358
|
private validateNonFirstBlockInCheckpoint(
|
|
340
359
|
proposal: BlockProposal,
|
|
341
|
-
parentBlock:
|
|
360
|
+
parentBlock: L2Block,
|
|
342
361
|
proposalInfo: object,
|
|
343
362
|
): CheckpointComputationResult | undefined {
|
|
344
363
|
const proposalGlobals = proposal.blockHeader.globalVariables;
|
|
@@ -414,31 +433,7 @@ export class BlockProposalHandler {
|
|
|
414
433
|
|
|
415
434
|
private getReexecutionDeadline(slot: SlotNumber, config: { l1GenesisTime: bigint; slotDuration: number }): Date {
|
|
416
435
|
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
417
|
-
|
|
418
|
-
return new Date(nextSlotTimestampSeconds * 1000 - msNeededForPropagationAndPublishing);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Gets all prior blocks in the same checkpoint (same slot and checkpoint number) up to but not including upToBlockNumber.
|
|
423
|
-
*/
|
|
424
|
-
private async getBlocksInCheckpoint(
|
|
425
|
-
slot: SlotNumber,
|
|
426
|
-
upToBlockNumber: BlockNumber,
|
|
427
|
-
checkpointNumber: CheckpointNumber,
|
|
428
|
-
): Promise<L2BlockNew[]> {
|
|
429
|
-
const blocks: L2BlockNew[] = [];
|
|
430
|
-
let currentBlockNumber = BlockNumber(upToBlockNumber - 1);
|
|
431
|
-
|
|
432
|
-
while (currentBlockNumber >= INITIAL_L2_BLOCK_NUM) {
|
|
433
|
-
const block = await this.blockSource.getL2BlockNew(currentBlockNumber);
|
|
434
|
-
if (!block || block.header.getSlot() !== slot || block.checkpointNumber !== checkpointNumber) {
|
|
435
|
-
break;
|
|
436
|
-
}
|
|
437
|
-
blocks.unshift(block);
|
|
438
|
-
currentBlockNumber = BlockNumber(currentBlockNumber - 1);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return blocks;
|
|
436
|
+
return new Date(nextSlotTimestampSeconds * 1000);
|
|
442
437
|
}
|
|
443
438
|
|
|
444
439
|
private getReexecuteFailureReason(err: any) {
|
|
@@ -459,6 +454,7 @@ export class BlockProposalHandler {
|
|
|
459
454
|
checkpointNumber: CheckpointNumber,
|
|
460
455
|
txs: Tx[],
|
|
461
456
|
l1ToL2Messages: Fr[],
|
|
457
|
+
previousCheckpointOutHashes: Fr[],
|
|
462
458
|
): Promise<ReexecuteTransactionsResult> {
|
|
463
459
|
const { blockHeader, txHashes } = proposal;
|
|
464
460
|
|
|
@@ -473,8 +469,9 @@ export class BlockProposalHandler {
|
|
|
473
469
|
const slot = proposal.slotNumber;
|
|
474
470
|
const config = this.checkpointsBuilder.getConfig();
|
|
475
471
|
|
|
476
|
-
// Get prior blocks in this checkpoint (same slot
|
|
477
|
-
const
|
|
472
|
+
// Get prior blocks in this checkpoint (same slot before current block)
|
|
473
|
+
const allBlocksInSlot = await this.blockSource.getBlocksForSlot(slot);
|
|
474
|
+
const priorBlocks = allBlocksInSlot.filter(b => b.number < blockNumber && b.header.getSlot() === slot);
|
|
478
475
|
|
|
479
476
|
// Fork before the block to be built
|
|
480
477
|
const parentBlockNumber = BlockNumber(blockNumber - 1);
|
|
@@ -495,6 +492,7 @@ export class BlockProposalHandler {
|
|
|
495
492
|
checkpointNumber,
|
|
496
493
|
constants,
|
|
497
494
|
l1ToL2Messages,
|
|
495
|
+
previousCheckpointOutHashes,
|
|
498
496
|
fork,
|
|
499
497
|
priorBlocks,
|
|
500
498
|
);
|
|
@@ -12,39 +12,42 @@ import {
|
|
|
12
12
|
PublicProcessor,
|
|
13
13
|
createPublicTxSimulatorForBlockBuilding,
|
|
14
14
|
} from '@aztec/simulator/server';
|
|
15
|
-
import {
|
|
15
|
+
import { L2Block } from '@aztec/stdlib/block';
|
|
16
16
|
import { Checkpoint } from '@aztec/stdlib/checkpoint';
|
|
17
17
|
import type { ContractDataSource } from '@aztec/stdlib/contract';
|
|
18
|
+
import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
|
|
18
19
|
import { Gas } from '@aztec/stdlib/gas';
|
|
19
20
|
import {
|
|
21
|
+
type BuildBlockInCheckpointResult,
|
|
20
22
|
type FullNodeBlockBuilderConfig,
|
|
21
23
|
FullNodeBlockBuilderConfigKeys,
|
|
24
|
+
type ICheckpointBlockBuilder,
|
|
25
|
+
type ICheckpointsBuilder,
|
|
22
26
|
type MerkleTreeWriteOperations,
|
|
23
27
|
type PublicProcessorLimits,
|
|
28
|
+
type WorldStateSynchronizer,
|
|
24
29
|
} from '@aztec/stdlib/interfaces/server';
|
|
25
30
|
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
26
|
-
import { type CheckpointGlobalVariables,
|
|
31
|
+
import { type CheckpointGlobalVariables, GlobalVariables, StateReference, Tx } from '@aztec/stdlib/tx';
|
|
27
32
|
import { type TelemetryClient, getTelemetryClient } from '@aztec/telemetry-client';
|
|
28
33
|
|
|
29
34
|
import { createValidatorForBlockBuilding } from './tx_validator/tx_validator_factory.js';
|
|
30
35
|
|
|
36
|
+
// Re-export for backward compatibility
|
|
37
|
+
export type { BuildBlockInCheckpointResult } from '@aztec/stdlib/interfaces/server';
|
|
38
|
+
|
|
31
39
|
const log = createLogger('checkpoint-builder');
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
publicGas: Gas;
|
|
36
|
-
publicProcessorDuration: number;
|
|
37
|
-
numTxs: number;
|
|
38
|
-
failedTxs: FailedTx[];
|
|
41
|
+
/** Result of building a block within a checkpoint. Extends the base interface with timer. */
|
|
42
|
+
export interface BuildBlockInCheckpointResultWithTimer extends BuildBlockInCheckpointResult {
|
|
39
43
|
blockBuildingTimer: Timer;
|
|
40
|
-
usedTxs: Tx[];
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
/**
|
|
44
47
|
* Builder for a single checkpoint. Handles building blocks within the checkpoint
|
|
45
48
|
* and completing it.
|
|
46
49
|
*/
|
|
47
|
-
export class CheckpointBuilder {
|
|
50
|
+
export class CheckpointBuilder implements ICheckpointBlockBuilder {
|
|
48
51
|
constructor(
|
|
49
52
|
private checkpointBuilder: LightweightCheckpointBuilder,
|
|
50
53
|
private fork: MerkleTreeWriteOperations,
|
|
@@ -66,11 +69,16 @@ export class CheckpointBuilder {
|
|
|
66
69
|
blockNumber: BlockNumber,
|
|
67
70
|
timestamp: bigint,
|
|
68
71
|
opts: PublicProcessorLimits & { expectedEndState?: StateReference },
|
|
69
|
-
): Promise<
|
|
72
|
+
): Promise<BuildBlockInCheckpointResultWithTimer> {
|
|
70
73
|
const blockBuildingTimer = new Timer();
|
|
71
74
|
const slot = this.checkpointBuilder.constants.slotNumber;
|
|
72
75
|
|
|
73
|
-
log.verbose(`Building block ${blockNumber} for slot ${slot} within checkpoint`, {
|
|
76
|
+
log.verbose(`Building block ${blockNumber} for slot ${slot} within checkpoint`, {
|
|
77
|
+
slot,
|
|
78
|
+
blockNumber,
|
|
79
|
+
...opts,
|
|
80
|
+
currentTime: new Date(this.dateProvider.now()),
|
|
81
|
+
});
|
|
74
82
|
|
|
75
83
|
const constants = this.checkpointBuilder.constants;
|
|
76
84
|
const globalVariables = GlobalVariables.from({
|
|
@@ -85,7 +93,7 @@ export class CheckpointBuilder {
|
|
|
85
93
|
});
|
|
86
94
|
const { processor, validator } = await this.makeBlockBuilderDeps(globalVariables, this.fork);
|
|
87
95
|
|
|
88
|
-
const [publicProcessorDuration, [processedTxs, failedTxs, usedTxs]] = await elapsed(() =>
|
|
96
|
+
const [publicProcessorDuration, [processedTxs, failedTxs, usedTxs, _, usedTxBlobFields]] = await elapsed(() =>
|
|
89
97
|
processor.process(pendingTxs, opts, validator),
|
|
90
98
|
);
|
|
91
99
|
|
|
@@ -105,6 +113,7 @@ export class CheckpointBuilder {
|
|
|
105
113
|
failedTxs,
|
|
106
114
|
blockBuildingTimer,
|
|
107
115
|
usedTxs,
|
|
116
|
+
usedTxBlobFields,
|
|
108
117
|
};
|
|
109
118
|
log.debug('Built block within checkpoint', res.block.header);
|
|
110
119
|
return res;
|
|
@@ -165,12 +174,11 @@ export class CheckpointBuilder {
|
|
|
165
174
|
}
|
|
166
175
|
}
|
|
167
176
|
|
|
168
|
-
/**
|
|
169
|
-
|
|
170
|
-
*/
|
|
171
|
-
export class FullNodeCheckpointsBuilder {
|
|
177
|
+
/** Factory for creating checkpoint builders. */
|
|
178
|
+
export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder {
|
|
172
179
|
constructor(
|
|
173
|
-
private config: FullNodeBlockBuilderConfig,
|
|
180
|
+
private config: FullNodeBlockBuilderConfig & Pick<L1RollupConstants, 'l1GenesisTime' | 'slotDuration'>,
|
|
181
|
+
private worldState: WorldStateSynchronizer,
|
|
174
182
|
private contractDataSource: ContractDataSource,
|
|
175
183
|
private dateProvider: DateProvider,
|
|
176
184
|
private telemetryClient: TelemetryClient = getTelemetryClient(),
|
|
@@ -191,6 +199,7 @@ export class FullNodeCheckpointsBuilder {
|
|
|
191
199
|
checkpointNumber: CheckpointNumber,
|
|
192
200
|
constants: CheckpointGlobalVariables,
|
|
193
201
|
l1ToL2Messages: Fr[],
|
|
202
|
+
previousCheckpointOutHashes: Fr[],
|
|
194
203
|
fork: MerkleTreeWriteOperations,
|
|
195
204
|
): Promise<CheckpointBuilder> {
|
|
196
205
|
const stateReference = await fork.getStateReference();
|
|
@@ -208,6 +217,7 @@ export class FullNodeCheckpointsBuilder {
|
|
|
208
217
|
checkpointNumber,
|
|
209
218
|
constants,
|
|
210
219
|
l1ToL2Messages,
|
|
220
|
+
previousCheckpointOutHashes,
|
|
211
221
|
fork,
|
|
212
222
|
);
|
|
213
223
|
|
|
@@ -228,14 +238,15 @@ export class FullNodeCheckpointsBuilder {
|
|
|
228
238
|
checkpointNumber: CheckpointNumber,
|
|
229
239
|
constants: CheckpointGlobalVariables,
|
|
230
240
|
l1ToL2Messages: Fr[],
|
|
241
|
+
previousCheckpointOutHashes: Fr[],
|
|
231
242
|
fork: MerkleTreeWriteOperations,
|
|
232
|
-
existingBlocks:
|
|
243
|
+
existingBlocks: L2Block[] = [],
|
|
233
244
|
): Promise<CheckpointBuilder> {
|
|
234
245
|
const stateReference = await fork.getStateReference();
|
|
235
246
|
const archiveTree = await fork.getTreeInfo(MerkleTreeId.ARCHIVE);
|
|
236
247
|
|
|
237
248
|
if (existingBlocks.length === 0) {
|
|
238
|
-
return this.startCheckpoint(checkpointNumber, constants, l1ToL2Messages, fork);
|
|
249
|
+
return this.startCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork);
|
|
239
250
|
}
|
|
240
251
|
|
|
241
252
|
log.verbose(`Resuming checkpoint ${checkpointNumber} with ${existingBlocks.length} existing blocks`, {
|
|
@@ -251,6 +262,7 @@ export class FullNodeCheckpointsBuilder {
|
|
|
251
262
|
checkpointNumber,
|
|
252
263
|
constants,
|
|
253
264
|
l1ToL2Messages,
|
|
265
|
+
previousCheckpointOutHashes,
|
|
254
266
|
fork,
|
|
255
267
|
existingBlocks,
|
|
256
268
|
);
|
|
@@ -264,4 +276,9 @@ export class FullNodeCheckpointsBuilder {
|
|
|
264
276
|
this.telemetryClient,
|
|
265
277
|
);
|
|
266
278
|
}
|
|
279
|
+
|
|
280
|
+
/** Returns a fork of the world state at the given block number. */
|
|
281
|
+
getFork(blockNumber: BlockNumber): Promise<MerkleTreeWriteOperations> {
|
|
282
|
+
return this.worldState.fork(blockNumber);
|
|
283
|
+
}
|
|
267
284
|
}
|