diff --git a/packages/client/src/rpc/modules/debug.ts b/packages/client/src/rpc/modules/debug.ts index b1387ba749..4dae2fc6cc 100644 --- a/packages/client/src/rpc/modules/debug.ts +++ b/packages/client/src/rpc/modules/debug.ts @@ -8,6 +8,7 @@ import type { EthereumClient } from '../..' import type { Chain } from '../../blockchain' import type { FullEthereumService } from '../../service' import type { RpcTx } from '../types' +import type { Block } from '@ethereumjs/block' import type { VM } from '@ethereumjs/vm' export interface tracerOpts { @@ -92,6 +93,13 @@ export class Debug { [validators.transaction()], [validators.blockOption], ]) + this.storageRangeAt = middleware(this.storageRangeAt.bind(this), 5, [ + [validators.blockHash], + [validators.unsignedInteger], + [validators.address], + [validators.uint256], + [validators.unsignedInteger], + ]) } /** @@ -278,4 +286,63 @@ export class Debug { message: err.message.toString(), } } + + /** + * Returns a limited set of storage keys belonging to an account. + * @param params An array of 5 parameters: + * 1. The hash of the block at which to get storage from the state. + * 2. The transaction index of the requested block post which to get the storage. + * 3. The address of the account. + * 4. The starting (hashed) key from which storage will be returned. To include the entire range, pass '0x00'. + * 5. The maximum number of storage values that could be returned. + * @returns A {@link StorageRange} object that will contain at most `limit` entries in its `storage` field. + * The object will also contain `nextKey`, the next (hashed) storage key after the range included in `storage`. + */ + async storageRangeAt(params: [string, number, string, string, number]) { + const [blockHash, txIndex, account, startKey, limit] = params + + if (this.vm === undefined) { + throw new Error('Missing VM.') + } + + let block: Block + try { + // Validator already verified that `blockHash` is properly formatted. + block = await this.chain.getBlock(hexToBytes(blockHash)) + } catch (err: any) { + throw { + code: INTERNAL_ERROR, + message: 'Could not get requested block hash.', + } + } + + if (txIndex >= block.transactions.length) { + throw { + code: INTERNAL_ERROR, + message: 'txIndex cannot be larger than the number of transactions in the block.', + } + } + + try { + const parentBlock = await this.chain.getBlock(block.header.parentHash) + // Copy the VM and run transactions including the relevant transaction. + const vmCopy = await this.vm.shallowCopy() + await vmCopy.stateManager.setStateRoot(parentBlock.header.stateRoot) + for (let i = 0; i <= txIndex; i++) { + await vmCopy.runTx({ tx: block.transactions[i], block }) + } + + return vmCopy.stateManager.dumpStorageRange( + // Validator already verified that `account` and `startKey` are properly formatted. + Address.fromString(account), + BigInt(startKey), + limit + ) + } catch (err: any) { + throw { + code: INTERNAL_ERROR, + message: err.message.toString(), + } + } + } } diff --git a/packages/client/src/rpc/validation.ts b/packages/client/src/rpc/validation.ts index 4032eaed70..7fd1755f3c 100644 --- a/packages/client/src/rpc/validation.ts +++ b/packages/client/src/rpc/validation.ts @@ -193,6 +193,33 @@ export const validators = { return (params: any[], index: number) => bytes(131072, params, index) }, + /** + * Validator to ensure a valid integer [0, Number.MAX_SAFE_INTEGER], represented as a `number`. + * @returns A validator function with parameters: + * - @param params Parameters of the method. + * - @param index The index of the parameter. + */ + get unsignedInteger() { + return (params: any[], index: number) => { + // This check guards against non-number types, decimal numbers, + // numbers that are too large (or small) to be represented exactly, + // NaN, null, and undefined. + if (!Number.isSafeInteger(params[index])) { + return { + code: INVALID_PARAMS, + message: `invalid argument ${index}: argument must be an integer`, + } + } + + if (params[index] < 0) { + return { + code: INVALID_PARAMS, + message: `invalid argument ${index}: argument must be larger than 0`, + } + } + } + }, + /** * hex validator to validate block hash * @param params parameters of method diff --git a/packages/client/test/rpc/debug/storageRangeAt.spec.ts b/packages/client/test/rpc/debug/storageRangeAt.spec.ts new file mode 100644 index 0000000000..891fc6b8c7 --- /dev/null +++ b/packages/client/test/rpc/debug/storageRangeAt.spec.ts @@ -0,0 +1,483 @@ +import { TransactionFactory } from '@ethereumjs/tx' +import { bigIntToHex, bytesToBigInt, bytesToHex, hexToBytes, setLengthLeft } from '@ethereumjs/util' +import { keccak256 } from 'ethereum-cryptography/keccak' +import { assert, beforeEach, describe, it } from 'vitest' + +import { INTERNAL_ERROR, INVALID_PARAMS } from '../../../src/rpc/error-code' +import genesisJSON from '../../testdata/geth-genesis/debug.json' +import { baseRequest, dummy, params, setupChain } from '../helpers' +import { checkError } from '../util' + +import type { Block } from '@ethereumjs/block' +import type { StorageRange } from '@ethereumjs/common/src' +import type { Address } from '@ethereumjs/util' +import type { HttpServer } from 'jayson/promise' + +const method = 'debug_storageRangeAt' + +/* + Contract used to test storageRangeAt(), compiled with solc 0.8.18+commit.87f61d96 + ```sol + pragma solidity ^0.8.0; + + contract Storage { + uint256 public x; + uint256 public y; + uint256 public z; + + constructor() { + x = 0x42; + y = 0x01; + z = 0x02; + } + + function update() public { + x = 0x43; + } + } + ``` +*/ +const storageBytecode: string = + '0x608060405234801561001057600080fd5b5060426000819055506001808190555060028081905550610123806100366000396000f3fe6080604052348015600f57600080fd5b506004361060465760003560e01c80630c55699c14604b578063a2e62045146065578063a56dfe4a14606d578063c5d7802e146087575b600080fd5b605160a1565b604051605c919060d4565b60405180910390f35b606b60a7565b005b607360b1565b604051607e919060d4565b60405180910390f35b608d60b7565b6040516098919060d4565b60405180910390f35b60005481565b6043600081905550565b60015481565b60025481565b6000819050919050565b60ce8160bd565b82525050565b600060208201905060e7600083018460c7565b9291505056fea2646970667358221220702e3426f9487bc4c75cca28733223e1292e723c32bbea553973c1ebeaeeb87d64736f6c63430008120033' +/* + Function selector of the contract's update() function. + */ +const updateBytecode: string = '0xa2e62045' +/* + Contract used to test storageRangeAt(), compiled with solc 0.8.18+commit.87f61d96 + ```sol + pragma solidity ^0.8.0; + + contract Empty { + constructor() { + } + } + ``` +*/ +const noStorageBytecode: string = + '0x6080604052348015600f57600080fd5b50603f80601d6000396000f3fe6080604052600080fdfea26469706673582212202f85c21c604b5e0fde9dca0615b4dd49a586dd18ada5ad8b85aa950462e1e73664736f6c63430008120033' + +describe(method, () => { + /** + * Object that is initialized before each test. + */ + interface TestSetup { + /** + * The server object to query for the tests. + */ + server: HttpServer + /** + * The block that contains the transactions to be tested. + */ + block: Block + /** + * The address of the created dummy contract. + */ + createdAddress: Address + /** + * The address of another created dummy contract that does not contain storage. + */ + createdAddressNoStorage: Address + } + + beforeEach(async (context) => { + // Populate a chain with three transactions: the first one deploys a contract, + // the second one updates a value in that contract, and the third one deploys + // another contract that does not put anything in its storage. + + const { chain, common, execution, server } = await setupChain(genesisJSON, 'post-merge', { + txLookupLimit: 0, + }) + + const firstTx = TransactionFactory.fromTxData( + { + type: 0x2, + gasLimit: 10000000, + maxFeePerGas: 1000000000, + maxPriorityFeePerGas: 1, + value: 0, + data: storageBytecode, + }, + { common, freeze: false } + ).sign(dummy.privKey) + + const vmCopy = await execution.vm.shallowCopy() + const parentBlock = await chain.getCanonicalHeadBlock() + const blockBuilder = await vmCopy.buildBlock({ + parentBlock, + headerData: { + timestamp: parentBlock.header.timestamp + BigInt(1), + }, + blockOpts: { + calcDifficultyFromHeader: parentBlock.header, + putBlockIntoBlockchain: false, + }, + }) + + const result = await blockBuilder.addTransaction(firstTx, { skipHardForkValidation: true }) + + const secondTx = TransactionFactory.fromTxData( + { + to: result.createdAddress, + type: 0x2, + gasLimit: 10000000, + maxFeePerGas: 1000000000, + maxPriorityFeePerGas: 1, + value: 0, + nonce: 1, + data: updateBytecode, + }, + { common, freeze: false } + ).sign(dummy.privKey) + + await blockBuilder.addTransaction(secondTx, { skipHardForkValidation: true }) + + const thirdTx = TransactionFactory.fromTxData( + { + type: 0x2, + gasLimit: 10000000, + maxFeePerGas: 1000000000, + maxPriorityFeePerGas: 1, + value: 0, + nonce: 2, + data: noStorageBytecode, + }, + { common, freeze: false } + ).sign(dummy.privKey) + + const thirdResult = await blockBuilder.addTransaction(thirdTx, { skipHardForkValidation: true }) + + const block = await blockBuilder.build() + await chain.putBlocks([block], true) + + context.server = server + context.block = await chain.getCanonicalHeadBlock() + context.createdAddress = result.createdAddress!! + context.createdAddressNoStorage = thirdResult.createdAddress!! + }) + + it('Should return the correct (number of) key value pairs.', async ({ + server, + block, + createdAddress, + }) => { + const req = params(method, [ + bytesToHex(block.hash()), + 1, + createdAddress.toString(), + '0x00', + 100, + ]) + const expectRes = (res: any) => { + const storageRange: StorageRange = res.body.result + + const firstVariableHash = keccak256(setLengthLeft(hexToBytes('0x00'), 32)) + assert.equal( + storageRange.storage[bytesToHex(firstVariableHash)].value, + '0x43', + 'First variable correctly included.' + ) + + const secondVariableHash = keccak256(setLengthLeft(hexToBytes('0x01'), 32)) + assert.equal( + storageRange.storage[bytesToHex(secondVariableHash)].value, + '0x01', + 'Second variable correctly included.' + ) + + const thirdVariableHash = keccak256(setLengthLeft(hexToBytes('0x02'), 32)) + assert.equal( + storageRange.storage[bytesToHex(thirdVariableHash)].value, + '0x02', + 'Third variable correctly included.' + ) + + assert.equal( + Object.keys(storageRange.storage).length, + 3, + 'Call returned the correct number of key value pairs.' + ) + } + + await baseRequest(server, req, 200, expectRes, true) + }) + + it('Should return an old storage state if requested.', async ({ + server, + block, + createdAddress, + }) => { + // Call the method with txIndex = 0. + const req = params(method, [ + bytesToHex(block.hash()), + 0, + createdAddress.toString(), + '0x00', + 100, + ]) + const expectRes = (res: any) => { + const storageRange: StorageRange = res.body.result + + const hashedKey = keccak256(setLengthLeft(hexToBytes('0x00'), 32)) + assert.equal( + storageRange.storage[bytesToHex(hashedKey)].value, + '0x42', + 'Old value was correctly reported.' + ) + } + + await baseRequest(server, req, 200, expectRes, true) + }) + + it('Should not return too many storage keys.', async ({ + server, + block, + createdAddress, + }) => { + const req = params(method, [bytesToHex(block.hash()), 1, createdAddress.toString(), '0x00', 2]) + const expectRes = (res: any) => { + const storageRange: StorageRange = res.body.result + + assert.equal( + Object.keys(storageRange.storage).length, + 2, + 'Call returned the correct number of key value pairs.' + ) + } + + await baseRequest(server, req, 200, expectRes, true) + }) + + it('Should return an empty result for accounts without storage.', async ({ + server, + block, + createdAddressNoStorage, + }) => { + const req = params(method, [ + bytesToHex(block.hash()), + 2, + createdAddressNoStorage.toString(), + '0x00', + 2, + ]) + const expectRes = (res: any) => { + const storageRange: StorageRange = res.body.result + + assert.equal( + Object.keys(storageRange.storage).length, + 0, + 'Call returned the correct number of key value pairs.' + ) + + assert.isNull(storageRange.nextKey, 'nextKey was correctly set to null.') + } + + await baseRequest(server, req, 200, expectRes, true) + }) + + it('Should not return keys falling outside the requested range.', async ({ + server, + block, + createdAddress, + }) => { + // The lowest hashed key in our example contract corresponds to storage slot 0x00. + const smallestHashedKey = keccak256(setLengthLeft(hexToBytes('0x00'), 32)) + + const req = params(method, [ + bytesToHex(block.hash()), + 1, + createdAddress.toString(), + // Add 1 to the smallest hashed key in our contract storage. + bigIntToHex(bytesToBigInt(smallestHashedKey) + BigInt(1)), + 100, + ]) + + const expectRes = (res: any) => { + const storageRange: StorageRange = res.body.result + + assert.equal( + Object.keys(storageRange.storage).length, + 2, + 'Call returned the correct number of key value pairs.' + ) + + assert.isUndefined( + storageRange.storage[bytesToHex(smallestHashedKey)], + 'Smallest hashed key was correctly excluded from result.' + ) + } + + await baseRequest(server, req, 200, expectRes, true) + }) + + it('Should provide a null value for nextKey if there is no next key.', async ({ + server, + block, + createdAddress, + }) => { + const req = params(method, [ + bytesToHex(block.hash()), + 1, + createdAddress.toString(), + '0x00', + 100, + ]) + const expectRes = (res: any) => { + const storageRange: StorageRange = res.body.result + + assert.isNull(storageRange.nextKey, 'nextKey was correctly set to null.') + } + + await baseRequest(server, req, 200, expectRes, true) + }) + + it('Should provide a valid nextKey if there is one.', async ({ + server, + block, + createdAddress, + }) => { + const req = params(method, [bytesToHex(block.hash()), 1, createdAddress.toString(), '0x00', 2]) + const expectRes = (res: any) => { + // The largest hashed key in our example contract corresponds to storage slot 0x01. + const largestHashedKey = bytesToHex(keccak256(setLengthLeft(hexToBytes('0x01'), 32))) + + const storageRange: StorageRange = res.body.result + + assert.equal(storageRange.nextKey, largestHashedKey, 'nextKey was correctly set.') + } + + await baseRequest(server, req, 200, expectRes, true) + }) + + it('Should provide a null value for preimages (until this is implemented).', async ({ + server, + block, + createdAddress, + }) => { + const req = params(method, [ + bytesToHex(block.hash()), + 1, + createdAddress.toString(), + '0x00', + 100, + ]) + const expectRes = (res: any) => { + const storageRange: StorageRange = res.body.result + + for (const [_, KVPair] of Object.entries(storageRange.storage)) { + assert.isNull(KVPair.key, 'Storage key preimage was intentionally set to null.') + } + } + + await baseRequest(server, req, 200, expectRes, true) + }) + + it('Should throw an error if the requested block does not exist.', async ({ + server, + createdAddress, + }) => { + const req = params(method, [ + bytesToHex(setLengthLeft(hexToBytes('0x00'), 32)), + 1, + createdAddress.toString(), + '0x00', + 100, + ]) + const expectRes = checkError(INTERNAL_ERROR, 'Could not get requested block hash.') + + await baseRequest(server, req, 200, expectRes, true) + }) + + it('Should throw an error if txIndex is too small or too large.', async ({ + server, + block, + createdAddress, + }) => { + let req = params(method, [bytesToHex(block.hash()), -1, createdAddress.toString(), '0x00', 100]) + let expectRes = checkError(INVALID_PARAMS, 'invalid argument 1: argument must be larger than 0') + + await baseRequest(server, req, 200, expectRes, false) + + req = params(method, [bytesToHex(block.hash()), 3, createdAddress.toString(), '0x00', 100]) + expectRes = checkError( + INTERNAL_ERROR, + 'txIndex cannot be larger than the number of transactions in the block.' + ) + + await baseRequest(server, req, 200, expectRes, true) + }) + + it('Should throw an error if the address is not formatted correctly.', async ({ + server, + block, + }) => { + const req = params(method, [bytesToHex(block.hash()), 0, '0xabcd'.toString(), '0x00', 100]) + const expectRes = checkError(INVALID_PARAMS, 'invalid argument 2: invalid address') + + await baseRequest(server, req, 200, expectRes, true) + }) + + it('Should throw an error if the address does not exist.', async ({ + server, + block, + }) => { + // The address is formatted correctly but is not associated with any account. + const req = params(method, [ + bytesToHex(block.hash()), + 0, + '0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd'.toString(), + '0x00', + 100, + ]) + const expectRes = checkError(INTERNAL_ERROR, 'Account does not exist.') + + await baseRequest(server, req, 200, expectRes, true) + }) + + it('Should throw an error if limit is too small.', async ({ + server, + block, + createdAddress, + }) => { + const req = params(method, [bytesToHex(block.hash()), 0, createdAddress.toString(), '0x00', -1]) + const expectRes = checkError( + INVALID_PARAMS, + 'invalid argument 4: argument must be larger than 0' + ) + + await baseRequest(server, req, 200, expectRes, true) + }) + + it("Should throw an error if hex parameters do not start with '0x'.", async ({ + server, + block, + createdAddress, + }) => { + let req = params(method, [ + bytesToHex(block.hash()).slice(2), + 0, + createdAddress.toString(), + '0x00', + -1, + ]) + let expectRes = checkError(INVALID_PARAMS, 'invalid argument 0: hex string without 0x prefix') + + await baseRequest(server, req, 200, expectRes, false) + + req = params(method, [ + bytesToHex(block.hash()), + 0, + createdAddress.toString().slice(2), + '0x00', + -1, + ]) + expectRes = checkError(INVALID_PARAMS, 'invalid argument 2: missing 0x prefix') + + await baseRequest(server, req, 200, expectRes, false) + + req = params(method, [bytesToHex(block.hash()), 0, createdAddress.toString(), '00', -1]) + expectRes = checkError(INVALID_PARAMS, 'invalid argument 3: hex string without 0x prefix') + + await baseRequest(server, req, 200, expectRes, true) + }) +}) diff --git a/packages/client/test/rpc/validation.spec.ts b/packages/client/test/rpc/validation.spec.ts index b3fb1232a8..a77ceebe83 100644 --- a/packages/client/test/rpc/validation.spec.ts +++ b/packages/client/test/rpc/validation.spec.ts @@ -206,6 +206,31 @@ describe(prefix, () => { assert.notOk(validatorResult(validators.bytes256([bytesToUnprefixedHex(randomBytes(256))], 0))) }) + it('unsignedInteger', () => { + assert.ok(validatorResult(validators.unsignedInteger([0], 0))) + assert.ok(validatorResult(validators.unsignedInteger([0.0], 0))) + assert.ok(validatorResult(validators.unsignedInteger([1], 0))) + assert.ok(validatorResult(validators.unsignedInteger([0x01], 0))) + assert.ok(validatorResult(validators.unsignedInteger([Number.MAX_SAFE_INTEGER], 0))) + + assert.notOk(validatorResult(validators.unsignedInteger([-1], 0))) + assert.notOk(validatorResult(validators.unsignedInteger([Number.MAX_SAFE_INTEGER + 1], 0))) + assert.notOk(validatorResult(validators.unsignedInteger([Number.MIN_VALUE], 0))) + assert.notOk(validatorResult(validators.unsignedInteger([Number.MAX_VALUE], 0))) + assert.notOk(validatorResult(validators.unsignedInteger([Number.NEGATIVE_INFINITY], 0))) + assert.notOk(validatorResult(validators.unsignedInteger([Number.POSITIVE_INFINITY], 0))) + assert.notOk(validatorResult(validators.unsignedInteger([Number.NaN], 0))) + assert.notOk(validatorResult(validators.unsignedInteger([Number.EPSILON], 0))) + + assert.notOk(validatorResult(validators.unsignedInteger(['1'], 0))) + assert.notOk(validatorResult(validators.unsignedInteger([0.1], 0))) + assert.notOk(validatorResult(validators.unsignedInteger([BigInt(1)], 0))) + assert.notOk(validatorResult(validators.unsignedInteger([{ number: 1 }], 0))) + + assert.notOk(validatorResult(validators.unsignedInteger([null], 0))) + assert.notOk(validatorResult(validators.unsignedInteger([undefined], 0))) + }) + it('blockHash', () => { // valid assert.ok( diff --git a/packages/common/src/interfaces.ts b/packages/common/src/interfaces.ts index 99fa205d5c..e46c447da7 100644 --- a/packages/common/src/interfaces.ts +++ b/packages/common/src/interfaces.ts @@ -8,6 +8,29 @@ export interface StorageDump { [key: string]: string } +/** + * Object that can contain a set of storage keys associated with an account. + */ +export interface StorageRange { + /** + * A dictionary where the keys are hashed storage keys, and the values are + * objects containing the preimage of the hashed key (in `key`) and the + * storage key (in `value`). Currently, there is no way to retrieve preimages, + * so they are always `null`. + */ + storage: { + [key: string]: { + key: string | null + value: string + } + } + /** + * The next (hashed) storage key after the greatest storage key + * contained in `storage`. + */ + nextKey: string | null +} + export type AccountFields = Partial> export type StorageProof = { @@ -69,6 +92,7 @@ export interface EVMStateManagerInterface extends StateManagerInterface { } dumpStorage(address: Address): Promise // only used in client + dumpStorageRange(address: Address, startKey: bigint, limit: number): Promise // only used in client generateCanonicalGenesis(initState: any): Promise // TODO make input more typesafe getProof(address: Address, storageSlots?: Uint8Array[]): Promise diff --git a/packages/statemanager/src/ethersStateManager.ts b/packages/statemanager/src/ethersStateManager.ts index 152fdd9cdd..cbe36d554b 100644 --- a/packages/statemanager/src/ethersStateManager.ts +++ b/packages/statemanager/src/ethersStateManager.ts @@ -9,6 +9,7 @@ import { OriginalStorageCache } from './cache/originalStorageCache.js' import type { Proof } from './index.js' import type { AccountFields, EVMStateManagerInterface, StorageDump } from '@ethereumjs/common' +import type { StorageRange } from '@ethereumjs/common/src' import type { Address } from '@ethereumjs/util' import type { Debugger } from 'debug' const { debug: createDebugLogger } = debugDefault @@ -191,6 +192,11 @@ export class EthersStateManager implements EVMStateManagerInterface { return Promise.resolve(dump) } + dumpStorageRange(_address: Address, _startKey: bigint, _limit: number): Promise { + // TODO: Implement. + return Promise.reject() + } + /** * Checks if an `account` exists at `address` * @param address - Address of the `account` to check diff --git a/packages/statemanager/src/stateManager.ts b/packages/statemanager/src/stateManager.ts index e51cd0e4da..a8f842dff2 100644 --- a/packages/statemanager/src/stateManager.ts +++ b/packages/statemanager/src/stateManager.ts @@ -9,6 +9,7 @@ import { KECCAK256_RLP, KECCAK256_RLP_S, bigIntToHex, + bytesToBigInt, bytesToHex, bytesToUnprefixedHex, concatBytes, @@ -28,6 +29,7 @@ import { AccountCache, CacheType, StorageCache } from './cache/index.js' import { OriginalStorageCache } from './cache/originalStorageCache.js' import type { AccountFields, EVMStateManagerInterface, StorageDump } from '@ethereumjs/common' +import type { StorageRange } from '@ethereumjs/common/src' import type { PrefixedHexString } from '@ethereumjs/util' import type { Debugger } from 'debug' const { debug: createDebugLogger } = debugDefault @@ -801,6 +803,70 @@ export class DefaultStateManager implements EVMStateManagerInterface { }) } + /** + Dumps a limited number of RLP-encoded storage values for an account specified by `address`, + starting from `startKey` or greater. + @param address - The address of the `account` to return storage for. + @param startKey - The bigint representation of the smallest storage key that will be returned. + @param limit - The maximum number of storage values that will be returned. + @returns {Promise} - A {@link StorageRange} object that will contain at most `limit` entries in its `storage` field. + The object will also contain `nextKey`, the next (hashed) storage key after the range included in `storage`. + */ + async dumpStorageRange(address: Address, startKey: bigint, limit: number): Promise { + if (!Number.isSafeInteger(limit) || limit < 0) { + throw new Error(`Limit is not a proper uint.`) + } + + await this.flush() + const account = await this.getAccount(address) + if (!account) { + throw new Error(`Account does not exist.`) + } + + return new Promise((resolve, reject) => { + this._getStorageTrie(address, account) + .then((trie) => { + let inRange = false + let i = 0 + + /** Object conforming to {@link StorageRange.storage}. */ + const storageMap: StorageRange['storage'] = {} + const stream = trie.createReadStream() + + stream.on('data', (val: any) => { + if (!inRange) { + // Check if the key is already in the correct range. + if (bytesToBigInt(val.key) >= startKey) { + inRange = true + } else { + return + } + } + + if (i < limit) { + storageMap[bytesToHex(val.key)] = { key: null, value: bytesToHex(val.value) } + i++ + } else if (i === limit) { + resolve({ + storage: storageMap, + nextKey: bytesToHex(val.key), + }) + } + }) + + stream.on('end', () => { + resolve({ + storage: storageMap, + nextKey: null, + }) + }) + }) + .catch((e) => { + reject(e) + }) + }) + } + /** * Initializes the provided genesis state into the state trie. * Will error if there are uncommitted checkpoints on the instance.