@nomicfoundation/ethereumjs-statemanager
Version:
An Ethereum statemanager implementation
1,146 lines (1,040 loc) • 38.8 kB
text/typescript
import { Chain, Common } from '@nomicfoundation/ethereumjs-common'
import { RLP } from '@nomicfoundation/ethereumjs-rlp'
import { Trie } from '@nomicfoundation/ethereumjs-trie'
import {
Account,
Address,
KECCAK256_NULL,
KECCAK256_NULL_S,
KECCAK256_RLP,
KECCAK256_RLP_S,
bigIntToHex,
bytesToBigInt,
bytesToHex,
bytesToUnprefixedHex,
concatBytes,
equalsBytes,
hexToBytes,
setLengthLeft,
short,
toBytes,
unpadBytes,
unprefixedHexToBytes,
utf8ToBytes,
} from '@nomicfoundation/ethereumjs-util'
import debugDefault from 'debug'
import { keccak256 as bufferKeccak256 } from 'ethereum-cryptography/keccak.js'
import { AccountCache, CacheType, CodeCache, StorageCache } from './cache/index.js'
import { OriginalStorageCache } from './cache/originalStorageCache.js'
import type {
AccountFields,
EVMStateManagerInterface,
StorageDump,
} from '@nomicfoundation/ethereumjs-common'
import type { StorageRange } from '@nomicfoundation/ethereumjs-common/src'
import type { DB, PrefixedHexString } from '@nomicfoundation/ethereumjs-util'
import type { Debugger } from 'debug'
const { debug: createDebugLogger } = debugDefault
function keccak256(msg: Uint8Array): Uint8Array {
return new Uint8Array(bufferKeccak256(Buffer.from(msg)))
}
export type StorageProof = {
key: PrefixedHexString
proof: PrefixedHexString[]
value: PrefixedHexString
}
export type Proof = {
address: PrefixedHexString
balance: PrefixedHexString
codeHash: PrefixedHexString
nonce: PrefixedHexString
storageHash: PrefixedHexString
accountProof: PrefixedHexString[]
storageProof: StorageProof[]
}
type CacheOptions = {
/**
* Allows for cache deactivation
*
* Depending on the use case and underlying datastore (and eventual concurrent cache
* mechanisms there), usage with or without cache can be faster
*
* Default: false
*/
deactivate?: boolean
/**
* Cache type to use.
*
* Available options:
*
* ORDERED_MAP: Cache with no fixed upper bound and dynamic allocation,
* use for dynamic setups like testing or similar.
*
* LRU: LRU cache with pre-allocation of memory and a fixed size.
* Use for larger and more persistent caches.
*/
type?: CacheType
/**
* Size of the cache (only for LRU cache)
*
* Default: 100000 (account cache) / 20000 (storage cache) / 20000 (code cache)
*
* Note: the cache/trie interplay mechanism is designed in a way that
* the theoretical number of max modified accounts between two flush operations
* should be smaller than the cache size, otherwise the cache will "forget" the
* old modifications resulting in an incomplete set of trie-flushed accounts.
*/
size?: number
}
type CacheSettings = {
deactivate: boolean
type: CacheType
size: number
}
/**
* Prefix to distinguish between a contract deployed with code `0x80`
* and `RLP([])` (also having the value `0x80`).
*
* Otherwise the creation of the code hash for the `0x80` contract
* will be the same as the hash of the empty trie which leads to
* misbehaviour in the underlying trie library.
*/
export const CODEHASH_PREFIX = utf8ToBytes('c')
/**
* Options for constructing a {@link StateManager}.
*/
export interface DefaultStateManagerOpts {
/**
* A {@link Trie} instance
*/
trie?: Trie
/**
* Option to prefix codehashes in the database. This defaults to `true`.
* If this is disabled, note that it is possible to corrupt the trie, by deploying code
* which code is equal to the preimage of a trie-node.
* E.g. by putting the code `0x80` into the empty trie, will lead to a corrupted trie.
*/
prefixCodeHashes?: boolean
/**
* Option to prefix the keys for the storage tries with the first 7 bytes from the
* associated account address. Activating this option gives a noticeable performance
* boost for storage DB reads when operating on larger tries.
*
* Note: Activating/deactivating this option causes continued state reads to be
* incompatible with existing databases.
*
* Default: false (for backwards compatibility reasons)
*/
prefixStorageTrieKeys?: boolean
accountCacheOpts?: CacheOptions
storageCacheOpts?: CacheOptions
codeCacheOpts?: CacheOptions
/**
* The common to use
*/
common?: Common
}
/**
* Default StateManager implementation for the VM.
*
* The state manager abstracts from the underlying data store
* by providing higher level access to accounts, contract code
* and storage slots.
*
* The default state manager implementation uses a
* `@ethereumjs/trie` trie as a data backend.
*/
export class DefaultStateManager implements EVMStateManagerInterface {
protected _debug: Debugger
protected _accountCache?: AccountCache
protected _storageCache?: StorageCache
protected _codeCache?: CodeCache
originalStorageCache: OriginalStorageCache
protected _trie: Trie
protected _storageTries: { [key: string]: Trie }
protected readonly _prefixCodeHashes: boolean
protected readonly _prefixStorageTrieKeys: boolean
protected readonly _accountCacheSettings: CacheSettings
protected readonly _storageCacheSettings: CacheSettings
protected readonly _codeCacheSettings: CacheSettings
public readonly common: Common
protected _checkpointCount: number
protected _proofTrie: Trie
private keccakFunction: Function
/**
* StateManager is run in DEBUG mode (default: false)
* Taken from DEBUG environment variable
*
* Safeguards on debug() calls are added for
* performance reasons to avoid string literal evaluation
* @hidden
*/
protected readonly DEBUG: boolean = false
/**
* Instantiate the StateManager interface.
*/
constructor(opts: DefaultStateManagerOpts = {}) {
// Skip DEBUG calls unless 'ethjs' included in environmental DEBUG variables
// Additional window check is to prevent vite browser bundling (and potentially other) to break
this.DEBUG =
typeof window === 'undefined' ? process?.env?.DEBUG?.includes('ethjs') ?? false : false
this._debug = createDebugLogger('statemanager:statemanager')
this.common = opts.common ?? new Common({ chain: Chain.Mainnet })
this._proofTrie = new Trie({ useKeyHashing: true, common: this.common })
this._checkpointCount = 0
this._trie = opts.trie ?? new Trie({ useKeyHashing: true, common: this.common })
this._storageTries = {}
this.keccakFunction = opts.common?.customCrypto.keccak256 ?? keccak256
this.originalStorageCache = new OriginalStorageCache(this.getContractStorage.bind(this))
this._prefixCodeHashes = opts.prefixCodeHashes ?? true
this._prefixStorageTrieKeys = opts.prefixStorageTrieKeys ?? false
this._accountCacheSettings = {
deactivate:
(opts.accountCacheOpts?.deactivate === true || opts.accountCacheOpts?.size === 0) ?? false,
type: opts.accountCacheOpts?.type ?? CacheType.ORDERED_MAP,
size: opts.accountCacheOpts?.size ?? 100000,
}
if (!this._accountCacheSettings.deactivate) {
this._accountCache = new AccountCache({
size: this._accountCacheSettings.size,
type: this._accountCacheSettings.type,
})
}
this._storageCacheSettings = {
deactivate:
(opts.storageCacheOpts?.deactivate === true || opts.storageCacheOpts?.size === 0) ?? false,
type: opts.storageCacheOpts?.type ?? CacheType.ORDERED_MAP,
size: opts.storageCacheOpts?.size ?? 20000,
}
if (!this._storageCacheSettings.deactivate) {
this._storageCache = new StorageCache({
size: this._storageCacheSettings.size,
type: this._storageCacheSettings.type,
})
}
this._codeCacheSettings = {
deactivate:
(opts.codeCacheOpts?.deactivate === true || opts.codeCacheOpts?.size === 0) ?? false,
type: opts.codeCacheOpts?.type ?? CacheType.ORDERED_MAP,
size: opts.codeCacheOpts?.size ?? 20000,
}
if (!this._codeCacheSettings.deactivate) {
this._codeCache = new CodeCache({
size: this._codeCacheSettings.size,
type: this._codeCacheSettings.type,
})
}
}
/**
* Gets the account associated with `address` or `undefined` if account does not exist
* @param address - Address of the `account` to get
*/
async getAccount(address: Address): Promise<Account | undefined> {
if (!this._accountCacheSettings.deactivate) {
const elem = this._accountCache!.get(address)
if (elem !== undefined) {
return elem.accountRLP !== undefined
? Account.fromRlpSerializedAccount(elem.accountRLP)
: undefined
}
}
const rlp = await this._trie.get(address.bytes)
const account = rlp !== null ? Account.fromRlpSerializedAccount(rlp) : undefined
if (this.DEBUG) {
this._debug(`Get account ${address} from DB (${account ? 'exists' : 'non-existent'})`)
}
this._accountCache?.put(address, account)
return account
}
/**
* Saves an account into state under the provided `address`.
* @param address - Address under which to store `account`
* @param account - The account to store or undefined if to be deleted
*/
async putAccount(address: Address, account: Account | undefined): Promise<void> {
if (this.DEBUG) {
this._debug(
`Save account address=${address} nonce=${account?.nonce} balance=${
account?.balance
} contract=${account && account.isContract() ? 'yes' : 'no'} empty=${
account && account.isEmpty() ? 'yes' : 'no'
}`
)
}
if (this._accountCacheSettings.deactivate) {
const trie = this._trie
if (account !== undefined) {
await trie.put(address.bytes, account.serialize())
} else {
await trie.del(address.bytes)
}
} else {
if (account !== undefined) {
this._accountCache!.put(address, account)
} else {
this._accountCache!.del(address)
}
}
}
/**
* Gets the account associated with `address`, modifies the given account
* fields, then saves the account into state. Account fields can include
* `nonce`, `balance`, `storageRoot`, and `codeHash`.
* @param address - Address of the account to modify
* @param accountFields - Object containing account fields and values to modify
*/
async modifyAccountFields(address: Address, accountFields: AccountFields): Promise<void> {
let account = await this.getAccount(address)
if (!account) {
account = new Account()
}
account.nonce = accountFields.nonce ?? account.nonce
account.balance = accountFields.balance ?? account.balance
account.storageRoot = accountFields.storageRoot ?? account.storageRoot
account.codeHash = accountFields.codeHash ?? account.codeHash
await this.putAccount(address, account)
}
/**
* Deletes an account from state under the provided `address`.
* @param address - Address of the account which should be deleted
*/
async deleteAccount(address: Address) {
if (this.DEBUG) {
this._debug(`Delete account ${address}`)
}
this._codeCache?.del(address)
if (this._accountCacheSettings.deactivate) {
await this._trie.del(address.bytes)
} else {
this._accountCache!.del(address)
}
if (!this._storageCacheSettings.deactivate) {
this._storageCache?.clearContractStorage(address)
}
}
/**
* Adds `value` to the state trie as code, and sets `codeHash` on the account
* corresponding to `address` to reference this.
* @param address - Address of the `account` to add the `code` for
* @param value - The value of the `code`
*/
async putContractCode(address: Address, value: Uint8Array): Promise<void> {
this._codeCache?.put(address, value)
const codeHash = this.keccakFunction(value)
if (equalsBytes(codeHash, KECCAK256_NULL)) {
return
}
if (this.DEBUG) {
this._debug(`Update codeHash (-> ${short(codeHash)}) for account ${address}`)
}
if ((await this.getAccount(address)) === undefined) {
await this.putAccount(address, new Account())
}
await this.modifyAccountFields(address, { codeHash })
}
/**
* Gets the code corresponding to the provided `address`.
* @param address - Address to get the `code` for
* @returns {Promise<Uint8Array>} - Resolves with the code corresponding to the provided address.
* Returns an empty `Uint8Array` if the account has no associated code.
*/
async getContractCode(address: Address): Promise<Uint8Array> {
if (!this._codeCacheSettings.deactivate) {
const elem = this._codeCache?.get(address)
if (elem !== undefined) {
return elem.code ?? new Uint8Array(0)
}
}
const account = await this.getAccount(address)
if (!account) {
return new Uint8Array(0)
}
if (!account.isContract()) {
return new Uint8Array(0)
}
const key = this._prefixCodeHashes
? concatBytes(CODEHASH_PREFIX, account.codeHash)
: account.codeHash
const code = (await this._trie.database().get(key)) ?? new Uint8Array(0)
if (!this._codeCacheSettings.deactivate) {
this._codeCache!.put(address, code)
}
return code
}
/**
* Gets the storage trie for an account from the storage
* cache or does a lookup.
* @private
*/
// TODO PR: have a better interface for hashed address pull?
protected _getStorageTrie(addressOrHash: Address | Uint8Array, account?: Account): Trie {
// use hashed key for lookup from storage cache
const addressHex = bytesToUnprefixedHex(
addressOrHash instanceof Address ? this.keccakFunction(addressOrHash.bytes) : addressOrHash
)
let storageTrie = this._storageTries[addressHex]
if (storageTrie === undefined) {
const keyPrefix = this._prefixStorageTrieKeys
? (addressOrHash instanceof Address
? this.keccakFunction(addressOrHash.bytes)
: addressOrHash
).slice(0, 7)
: undefined
storageTrie = this._trie.shallowCopy(false, { keyPrefix })
if (account !== undefined) {
storageTrie.root(account.storageRoot)
} else {
storageTrie.root(storageTrie.EMPTY_TRIE_ROOT)
}
storageTrie.flushCheckpoints()
this._storageTries[addressHex] = storageTrie
}
return storageTrie
}
/**
* Gets the storage trie for an account from the storage
* cache or does a lookup.
* @private
*/
protected _getAccountTrie(): Trie {
return this._trie
}
/**
* Gets the storage trie for an account from the storage
* cache or does a lookup.
* @private
*/
protected _getCodeDB(): DB {
return this._trie.database()
}
/**
* Gets the storage value associated with the provided `address` and `key`. This method returns
* the shortest representation of the stored value.
* @param address - Address of the account to get the storage for
* @param key - Key in the account's storage to get the value for. Must be 32 bytes long.
* @returns - The storage value for the account
* corresponding to the provided address at the provided key.
* If this does not exist an empty `Uint8Array` is returned.
*/
async getContractStorage(address: Address, key: Uint8Array): Promise<Uint8Array> {
if (key.length !== 32) {
throw new Error('Storage key must be 32 bytes long')
}
if (!this._storageCacheSettings.deactivate) {
const value = this._storageCache!.get(address, key)
if (value !== undefined) {
const decoded = RLP.decode(value ?? new Uint8Array(0)) as Uint8Array
return decoded
}
}
const account = await this.getAccount(address)
if (!account) {
throw new Error('getContractStorage() called on non-existing account')
}
const trie = this._getStorageTrie(address, account)
const value = await trie.get(key)
if (!this._storageCacheSettings.deactivate) {
this._storageCache?.put(address, key, value ?? hexToBytes('0x80'))
}
const decoded = RLP.decode(value ?? new Uint8Array(0)) as Uint8Array
return decoded
}
/**
* Modifies the storage trie of an account.
* @private
* @param address - Address of the account whose storage is to be modified
* @param modifyTrie - Function to modify the storage trie of the account
*/
protected async _modifyContractStorage(
address: Address,
account: Account,
modifyTrie: (storageTrie: Trie, done: Function) => void
): Promise<void> {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
const storageTrie = this._getStorageTrie(address, account)
modifyTrie(storageTrie, async () => {
// update storage cache
const addressHex = bytesToUnprefixedHex(address.bytes)
this._storageTries[addressHex] = storageTrie
// update contract storageRoot
account.storageRoot = storageTrie.root()
await this.putAccount(address, account)
resolve()
})
})
}
protected async _writeContractStorage(
address: Address,
account: Account,
key: Uint8Array,
value: Uint8Array
) {
await this._modifyContractStorage(address, account, async (storageTrie, done) => {
if (value instanceof Uint8Array && value.length) {
// format input
const encodedValue = RLP.encode(value)
if (this.DEBUG) {
this._debug(`Update contract storage for account ${address} to ${short(value)}`)
}
await storageTrie.put(key, encodedValue)
} else {
// deleting a value
if (this.DEBUG) {
this._debug(`Delete contract storage for account`)
}
await storageTrie.del(key)
}
done()
})
}
/**
* Adds value to the state trie for the `account`
* corresponding to `address` at the provided `key`.
* @param address - Address to set a storage value for
* @param key - Key to set the value at. Must be 32 bytes long.
* @param value - Value to set at `key` for account corresponding to `address`.
* Cannot be more than 32 bytes. Leading zeros are stripped.
* If it is a empty or filled with zeros, deletes the value.
*/
async putContractStorage(address: Address, key: Uint8Array, value: Uint8Array): Promise<void> {
if (key.length !== 32) {
throw new Error('Storage key must be 32 bytes long')
}
if (value.length > 32) {
throw new Error('Storage value cannot be longer than 32 bytes')
}
const account = await this.getAccount(address)
if (!account) {
throw new Error('putContractStorage() called on non-existing account')
}
value = unpadBytes(value)
if (!this._storageCacheSettings.deactivate) {
const encodedValue = RLP.encode(value)
this._storageCache!.put(address, key, encodedValue)
} else {
await this._writeContractStorage(address, account, key, value)
}
}
/**
* Clears all storage entries for the account corresponding to `address`.
* @param address - Address to clear the storage of
*/
async clearContractStorage(address: Address): Promise<void> {
let account = await this.getAccount(address)
if (!account) {
account = new Account()
}
this._storageCache?.clearContractStorage(address)
await this._modifyContractStorage(address, account, (storageTrie, done) => {
storageTrie.root(storageTrie.EMPTY_TRIE_ROOT)
done()
})
}
/**
* Checkpoints the current state of the StateManager instance.
* State changes that follow can then be committed by calling
* `commit` or `reverted` by calling rollback.
*/
async checkpoint(): Promise<void> {
this._trie.checkpoint()
this._storageCache?.checkpoint()
this._accountCache?.checkpoint()
this._codeCache?.checkpoint()
this._checkpointCount++
}
/**
* Commits the current change-set to the instance since the
* last call to checkpoint.
*/
async commit(): Promise<void> {
// setup trie checkpointing
await this._trie.commit()
this._storageCache?.commit()
this._accountCache?.commit()
this._codeCache?.commit()
this._checkpointCount--
if (this._checkpointCount === 0) {
await this.flush()
this.originalStorageCache.clear()
}
if (this.DEBUG) {
this._debug(`state checkpoint committed`)
}
}
/**
* Reverts the current change-set to the instance since the
* last call to checkpoint.
*/
async revert(): Promise<void> {
// setup trie checkpointing
await this._trie.revert()
this._storageCache?.revert()
this._accountCache?.revert()
this._codeCache?.revert()
this._storageTries = {}
this._checkpointCount--
if (this._checkpointCount === 0) {
await this.flush()
this.originalStorageCache.clear()
}
}
/**
* Writes all cache items to the trie
*/
async flush(): Promise<void> {
if (!this._codeCacheSettings.deactivate) {
const items = this._codeCache!.flush()
for (const item of items) {
const addr = Address.fromString(`0x${item[0]}`)
const code = item[1].code
if (code === undefined) {
continue
}
// update code in database
const codeHash = this.keccakFunction(code)
const key = this._prefixCodeHashes ? concatBytes(CODEHASH_PREFIX, codeHash) : codeHash
await this._getCodeDB().put(key, code)
// update code root of associated account
if ((await this.getAccount(addr)) === undefined) {
await this.putAccount(addr, new Account())
}
await this.modifyAccountFields(addr, { codeHash })
}
}
if (!this._storageCacheSettings.deactivate) {
const items = this._storageCache!.flush()
for (const item of items) {
const address = Address.fromString(`0x${item[0]}`)
const keyHex = item[1]
const keyBytes = unprefixedHexToBytes(keyHex)
const value = item[2]
const decoded = RLP.decode(value ?? new Uint8Array(0)) as Uint8Array
const account = await this.getAccount(address)
if (account) {
await this._writeContractStorage(address, account, keyBytes, decoded)
}
}
}
if (!this._accountCacheSettings.deactivate) {
const items = this._accountCache!.flush()
for (const item of items) {
const addressHex = item[0]
const addressBytes = unprefixedHexToBytes(addressHex)
const elem = item[1]
if (elem.accountRLP === undefined) {
const trie = this._trie
await trie.del(addressBytes)
} else {
const trie = this._trie
await trie.put(addressBytes, elem.accountRLP)
}
}
}
}
/**
* Get an EIP-1186 proof
* @param address address to get proof of
* @param storageSlots storage slots to get proof of
*/
async getProof(address: Address, storageSlots: Uint8Array[] = []): Promise<Proof> {
await this.flush()
const account = await this.getAccount(address)
if (!account) {
// throw new Error(`getProof() can only be called for an existing account`)
const returnValue: Proof = {
address: address.toString(),
balance: '0x0',
codeHash: KECCAK256_NULL_S,
nonce: '0x0',
storageHash: KECCAK256_RLP_S,
accountProof: (await this._trie.createProof(address.bytes)).map((p) => bytesToHex(p)),
storageProof: [],
}
return returnValue
}
const accountProof: PrefixedHexString[] = (await this._trie.createProof(address.bytes)).map(
(p) => bytesToHex(p)
)
const storageProof: StorageProof[] = []
const storageTrie = this._getStorageTrie(address, account)
for (const storageKey of storageSlots) {
const proof = (await storageTrie.createProof(storageKey)).map((p) => bytesToHex(p))
const value = bytesToHex(await this.getContractStorage(address, storageKey))
const proofItem: StorageProof = {
key: bytesToHex(storageKey),
value: value === '0x' ? '0x0' : value, // Return '0x' values as '0x0' since this is a JSON RPC response
proof,
}
storageProof.push(proofItem)
}
const returnValue: Proof = {
address: address.toString(),
balance: bigIntToHex(account.balance),
codeHash: bytesToHex(account.codeHash),
nonce: bigIntToHex(account.nonce),
storageHash: bytesToHex(account.storageRoot),
accountProof,
storageProof,
}
return returnValue
}
/**
* Create a StateManager and initialize this with proof(s) gotten previously from getProof
* This generates a (partial) StateManager where one can retrieve all items from the proof
* @param proof Either a proof retrieved from `getProof`, or an array of those proofs
* @param safe Wether or not to verify that the roots of the proof items match the reported roots
* @param verifyRoot verify that all proof root nodes match statemanager's stateroot - should be
* set to `false` when constructing a state manager where the underlying trie has proof nodes from different state roots
* @returns A new DefaultStateManager with elements from the given proof included in its backing state trie
*/
static async fromProof(
proof: Proof | Proof[],
safe: boolean = false,
opts: DefaultStateManagerOpts = {}
): Promise<DefaultStateManager> {
if (Array.isArray(proof)) {
if (proof.length === 0) {
return new DefaultStateManager(opts)
} else {
const trie =
opts.trie ??
(await Trie.createTrieFromProof(
proof[0].accountProof.map((e) => hexToBytes(e)),
{ useKeyHashing: true }
))
const sm = new DefaultStateManager({ ...opts, trie })
const address = Address.fromString(proof[0].address)
await sm.addStorageProof(proof[0].storageProof, proof[0].storageHash, address, safe)
for (let i = 1; i < proof.length; i++) {
const proofItem = proof[i]
await sm.addProofData(proofItem, true)
}
await sm.flush() // TODO verify if this is necessary
return sm
}
} else {
return DefaultStateManager.fromProof([proof])
}
}
/**
* Adds a storage proof to the state manager
* @param storageProof The storage proof
* @param storageHash The root hash of the storage trie
* @param address The address
* @param safe Whether or not to verify if the reported roots match the current storage root
*/
private async addStorageProof(
storageProof: StorageProof[],
storageHash: string,
address: Address,
safe: boolean = false
) {
const trie = this._getStorageTrie(address)
trie.root(hexToBytes(storageHash))
for (let i = 0; i < storageProof.length; i++) {
await trie.updateTrieFromProof(
storageProof[i].proof.map((e) => hexToBytes(e)),
safe
)
}
}
/**
* Add proof(s) into an already existing trie
* @param proof The proof(s) retrieved from `getProof`
* @param verifyRoot verify that all proof root nodes match statemanager's stateroot - should be
* set to `false` when constructing a state manager where the underlying trie has proof nodes from different state roots
*/
async addProofData(proof: Proof | Proof[], safe: boolean = false) {
if (Array.isArray(proof)) {
for (let i = 0; i < proof.length; i++) {
await this._trie.updateTrieFromProof(
proof[i].accountProof.map((e) => hexToBytes(e)),
safe
)
await this.addStorageProof(
proof[i].storageProof,
proof[i].storageHash,
Address.fromString(proof[i].address),
safe
)
}
} else {
await this.addProofData([proof], safe)
}
}
/**
* Verify an EIP-1186 proof. Throws if proof is invalid, otherwise returns true.
* @param proof the proof to prove
*/
async verifyProof(proof: Proof): Promise<boolean> {
const rootHash = this.keccakFunction(hexToBytes(proof.accountProof[0]))
const key = hexToBytes(proof.address)
const accountProof = proof.accountProof.map((rlpString: PrefixedHexString) =>
hexToBytes(rlpString)
)
// This returns the account if the proof is valid.
// Verify that it matches the reported account.
const value = await this._proofTrie.verifyProof(rootHash, key, accountProof)
if (value === null) {
// Verify that the account is empty in the proof.
const emptyBytes = new Uint8Array(0)
const notEmptyErrorMsg = 'Invalid proof provided: account is not empty'
const nonce = unpadBytes(hexToBytes(proof.nonce))
if (!equalsBytes(nonce, emptyBytes)) {
throw new Error(`${notEmptyErrorMsg} (nonce is not zero)`)
}
const balance = unpadBytes(hexToBytes(proof.balance))
if (!equalsBytes(balance, emptyBytes)) {
throw new Error(`${notEmptyErrorMsg} (balance is not zero)`)
}
const storageHash = hexToBytes(proof.storageHash)
if (!equalsBytes(storageHash, KECCAK256_RLP)) {
throw new Error(`${notEmptyErrorMsg} (storageHash does not equal KECCAK256_RLP)`)
}
const codeHash = hexToBytes(proof.codeHash)
if (!equalsBytes(codeHash, KECCAK256_NULL)) {
throw new Error(`${notEmptyErrorMsg} (codeHash does not equal KECCAK256_NULL)`)
}
} else {
const account = Account.fromRlpSerializedAccount(value)
const { nonce, balance, storageRoot, codeHash } = account
const invalidErrorMsg = 'Invalid proof provided:'
if (nonce !== BigInt(proof.nonce)) {
throw new Error(`${invalidErrorMsg} nonce does not match`)
}
if (balance !== BigInt(proof.balance)) {
throw new Error(`${invalidErrorMsg} balance does not match`)
}
if (!equalsBytes(storageRoot, hexToBytes(proof.storageHash))) {
throw new Error(`${invalidErrorMsg} storageHash does not match`)
}
if (!equalsBytes(codeHash, hexToBytes(proof.codeHash))) {
throw new Error(`${invalidErrorMsg} codeHash does not match`)
}
}
const storageRoot = hexToBytes(proof.storageHash)
for (const stProof of proof.storageProof) {
const storageProof = stProof.proof.map((value: PrefixedHexString) => hexToBytes(value))
const storageValue = setLengthLeft(hexToBytes(stProof.value), 32)
const storageKey = hexToBytes(stProof.key)
const proofValue = await this._proofTrie.verifyProof(storageRoot, storageKey, storageProof)
const reportedValue = setLengthLeft(
RLP.decode(proofValue ?? new Uint8Array(0)) as Uint8Array,
32
)
if (!equalsBytes(reportedValue, storageValue)) {
throw new Error('Reported trie value does not match storage')
}
}
return true
}
/**
* Gets the state-root of the Merkle-Patricia trie representation
* of the state of this StateManager. Will error if there are uncommitted
* checkpoints on the instance.
* @returns {Promise<Uint8Array>} - Returns the state-root of the `StateManager`
*/
async getStateRoot(): Promise<Uint8Array> {
await this.flush()
return this._trie.root()
}
/**
* Sets the state of the instance to that represented
* by the provided `stateRoot`. Will error if there are uncommitted
* checkpoints on the instance or if the state root does not exist in
* the state trie.
* @param stateRoot - The state-root to reset the instance to
*/
async setStateRoot(stateRoot: Uint8Array, clearCache: boolean = true): Promise<void> {
await this.flush()
if (!equalsBytes(stateRoot, this._trie.EMPTY_TRIE_ROOT)) {
const hasRoot = await this._trie.checkRoot(stateRoot)
if (!hasRoot) {
throw new Error('State trie does not contain state root')
}
}
this._trie.root(stateRoot)
if (this._accountCache !== undefined && clearCache) {
this._accountCache.clear()
}
if (this._storageCache !== undefined && clearCache) {
this._storageCache.clear()
}
if (this._codeCache !== undefined && clearCache) {
this._codeCache!.clear()
}
this._storageTries = {}
}
/**
* Dumps the RLP-encoded storage values for an `account` specified by `address`.
* @param address - The address of the `account` to return storage for
* @returns {Promise<StorageDump>} - The state of the account as an `Object` map.
* Keys are are the storage keys, values are the storage values as strings.
* Both are represented as hex strings without the `0x` prefix.
*/
async dumpStorage(address: Address): Promise<StorageDump> {
await this.flush()
const account = await this.getAccount(address)
if (!account) {
throw new Error(`dumpStorage f() can only be called for an existing account`)
}
const trie = this._getStorageTrie(address, account)
const storage: StorageDump = {}
const stream = trie.createAsyncReadStream()
for await (const chunk of stream) {
storage[bytesToHex(chunk.key)] = bytesToHex(chunk.value)
}
return storage
}
/**
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<StorageRange>} - 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<StorageRange> {
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.`)
}
const trie = this._getStorageTrie(address, account)
let inRange = false
let i = 0
/** Object conforming to {@link StorageRange.storage}. */
const storageMap: StorageRange['storage'] = {}
const stream = trie.createAsyncReadStream()
for await (const chunk of stream) {
if (!inRange) {
// Check if the key is already in the correct range.
if (bytesToBigInt(chunk.key) >= startKey) {
inRange = true
} else {
continue
}
}
if (i < limit) {
storageMap[bytesToHex(chunk.key)] = { key: null, value: bytesToHex(chunk.value) }
i++
} else if (i === limit) {
return {
storage: storageMap,
nextKey: bytesToHex(chunk.key),
}
}
}
return {
storage: storageMap,
nextKey: null,
}
}
/**
* Initializes the provided genesis state into the state trie.
* Will error if there are uncommitted checkpoints on the instance.
* @param initState address -> balance | [balance, code, storage]
*/
async generateCanonicalGenesis(initState: any): Promise<void> {
if (this._checkpointCount !== 0) {
throw new Error('Cannot create genesis state with uncommitted checkpoints')
}
if (this.DEBUG) {
this._debug(`Save genesis state into the state trie`)
}
const addresses = Object.keys(initState)
for (const address of addresses) {
const addr = Address.fromString(address)
const state = initState[address]
if (!Array.isArray(state)) {
// Prior format: address -> balance
const account = Account.fromAccountData({ balance: state })
await this.putAccount(addr, account)
} else {
// New format: address -> [balance, code, storage]
const [balance, code, storage, nonce] = state
const account = Account.fromAccountData({ balance, nonce })
await this.putAccount(addr, account)
if (code !== undefined) {
await this.putContractCode(addr, toBytes(code))
}
if (storage !== undefined) {
for (const [key, value] of storage) {
await this.putContractStorage(addr, toBytes(key), toBytes(value))
}
}
}
}
await this.flush()
}
/**
* Checks whether there is a state corresponding to a stateRoot
*/
async hasStateRoot(root: Uint8Array): Promise<boolean> {
return this._trie.checkRoot(root)
}
/**
* Copies the current instance of the `StateManager`
* at the last fully committed point, i.e. as if all current
* checkpoints were reverted.
*
* Caches are downleveled (so: adopted for short-term usage)
* by default.
*
* This means in particular:
* 1. For caches instantiated as an LRU cache type
* the copy() method will instantiate with an ORDERED_MAP cache
* instead, since copied instantances are mostly used in
* short-term usage contexts and LRU cache instantation would create
* a large overhead here.
* 2. The underlying trie object is initialized with 0 cache size
*
* Both adoptions can be deactivated by setting `downlevelCaches` to
* `false`.
*
* Cache values are generally not copied along regardless of the
* `downlevelCaches` setting.
*/
shallowCopy(downlevelCaches = true): DefaultStateManager {
const common = this.common.copy()
common.setHardfork(this.common.hardfork())
const cacheSize = !downlevelCaches ? this._trie['_opts']['cacheSize'] : 0
const trie = this._trie.shallowCopy(false, { cacheSize })
const prefixCodeHashes = this._prefixCodeHashes
const prefixStorageTrieKeys = this._prefixStorageTrieKeys
let accountCacheOpts = { ...this._accountCacheSettings }
if (downlevelCaches && !this._accountCacheSettings.deactivate) {
accountCacheOpts = { ...accountCacheOpts, type: CacheType.ORDERED_MAP }
}
let storageCacheOpts = { ...this._storageCacheSettings }
if (downlevelCaches && !this._storageCacheSettings.deactivate) {
storageCacheOpts = { ...storageCacheOpts, type: CacheType.ORDERED_MAP }
}
let codeCacheOpts = { ...this._codeCacheSettings }
if (!this._codeCacheSettings.deactivate) {
codeCacheOpts = { ...codeCacheOpts, type: CacheType.ORDERED_MAP }
}
return new DefaultStateManager({
common,
trie,
prefixStorageTrieKeys,
prefixCodeHashes,
accountCacheOpts,
storageCacheOpts,
codeCacheOpts,
})
}
/**
* Clears all underlying caches
*/
clearCaches() {
this._accountCache?.clear()
this._storageCache?.clear()
this._codeCache?.clear()
}
}