UNPKG

@nomicfoundation/ethereumjs-statemanager

Version:
655 lines (548 loc) 21.6 kB
import { Account, KECCAK256_NULL_S, bigIntToBytes, bytesToBigInt, bytesToHex, bytesToInt32, concatBytes, equalsBytes, hexToBytes, padToEven, setLengthRight, short, toBytes, zeros, } from '@nomicfoundation/ethereumjs-util' import { getKey, getStem, verifyUpdate } from '@nomicfoundation/ethereumjs-verkle' import debugDefault from 'debug' import { keccak256 as bufferKeccak256 } from 'ethereum-cryptography/keccak.js' import { AccountCache, CacheType, StorageCache } from './cache/index.js' import { OriginalStorageCache } from './cache/originalStorageCache.js' import type { DefaultStateManager } from './stateManager.js' import type { VerkleExecutionWitness } from '@nomicfoundation/ethereumjs-block' import type { AccountFields, Common, EVMStateManagerInterface, Proof, StorageDump, StorageRange, } from '@nomicfoundation/ethereumjs-common' import type { Address, PrefixedHexString } from '@nomicfoundation/ethereumjs-util' function keccak256(msg: Uint8Array): Uint8Array { return new Uint8Array(bufferKeccak256(Buffer.from(msg))) } const { debug: createDebugLogger } = debugDefault const debug = createDebugLogger('statemanager:verkle') export interface VerkleState { [key: PrefixedHexString]: PrefixedHexString } export interface EncodedVerkleProof { [key: PrefixedHexString]: PrefixedHexString } 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) * * 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 } /** * Options dictionary. */ export interface StatelessVerkleStateManagerOpts { accountCacheOpts?: CacheOptions /** * The common to use */ common?: Common storageCacheOpts?: CacheOptions } /** * Tree key constants. */ const VERSION_LEAF_KEY = toBytes(0) const BALANCE_LEAF_KEY = toBytes(1) const NONCE_LEAF_KEY = toBytes(2) const CODE_KECCAK_LEAF_KEY = toBytes(3) const CODE_SIZE_LEAF_KEY = toBytes(4) const HEADER_STORAGE_OFFSET = 64 const CODE_OFFSET = 128 const VERKLE_NODE_WIDTH = 256 const MAIN_STORAGE_OFFSET = 256 ** 31 const PUSH_OFFSET = 95 // eslint-disable-next-line @typescript-eslint/no-unused-vars const PUSH1 = PUSH_OFFSET + 1 // eslint-disable-next-line @typescript-eslint/no-unused-vars const PUSH32 = PUSH_OFFSET + 32 /** * Stateless Verkle StateManager implementation for the VM. * * Experimental. * * This State Manager enables stateless block execution by building a * temporary (1-block) state from the verkle block witness. * The Stateless Verkle State Manager then uses that populated state * to fetch data requested by the the VM. * */ export class StatelessVerkleStateManager implements EVMStateManagerInterface { _accountCache?: AccountCache _storageCache?: StorageCache _codeCache: { [key: string]: Uint8Array } originalStorageCache: OriginalStorageCache protected readonly _accountCacheSettings: CacheSettings protected readonly _storageCacheSettings: CacheSettings /** * 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 private _executionWitness?: VerkleExecutionWitness private _proof: Uint8Array | undefined // State along execution (should update) private _state: VerkleState = {} // Post-state provided from the executionWitness. // Should not update. Used for comparing our computed post-state with the canonical one. private _postState: VerkleState = {} // Checkpointing private _checkpoints: VerkleState[] = [] private keccakFunction: Function /** * Instantiate the StateManager interface. */ constructor(opts: StatelessVerkleStateManagerOpts = {}) { this._accountCacheSettings = { deactivate: opts.accountCacheOpts?.deactivate ?? 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 ?? 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.originalStorageCache = new OriginalStorageCache(this.getContractStorage.bind(this)) this._codeCache = {} this.keccakFunction = opts.common?.customCrypto.keccak256 ?? keccak256 // 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 /* * For a custom StateManager implementation adopt these * callbacks passed to the `Cache` instantiated to perform * the `get`, `put` and `delete` operations with the * desired backend. */ // const getCb: get = async (address) => { // return undefined // } // const putCb: put = async (keyBuf, accountRlp) => {} // const deleteCb = async (keyBuf: Uint8Array) => {} // this._cache = new Cache({ get, putCb, deleteCb }) } async getTransitionStateRoot(_: DefaultStateManager, __: Uint8Array): Promise<Uint8Array> { throw Error('not implemented') } public initVerkleExecutionWitness(executionWitness?: VerkleExecutionWitness | null) { if (executionWitness === null || executionWitness === undefined) { throw Error(`Invalid executionWitness=${executionWitness} for initVerkleExecutionWitness`) } this._executionWitness = executionWitness this._proof = executionWitness.verkleProof as unknown as Uint8Array // Populate the pre-state and post-state from the executionWitness const preStateRaw = executionWitness.stateDiff.flatMap(({ stem, suffixDiffs }) => { const suffixDiffPairs = suffixDiffs.map(({ currentValue, suffix }) => { const key = `${stem}${padToEven(suffix.toString(16))}` // TODO: Evaluate if we should store and handle null in a special way // Currently we are replacing `null` with 0x00..00 (32 bytes) [expect for codeHash, suffix 3, where we use the empty codeHash] for simplifying handling and comparisons // Also, test data has been inconsistent in this regard, so this simplifies things while things get more standardized if (Number(suffix) === 3) { return { [key]: currentValue ?? KECCAK256_NULL_S } } return { [key]: currentValue ?? bytesToHex(zeros(32)), } }) return suffixDiffPairs }) const preState = preStateRaw.reduce((prevValue, currentValue) => { const acc = { ...prevValue, ...currentValue } return acc }, {}) this._state = preState const postStateRaw = executionWitness.stateDiff.flatMap(({ stem, suffixDiffs }) => { const suffixDiffPairs = suffixDiffs.map(({ newValue, suffix }) => { const key = `${stem}${padToEven(suffix.toString(16))}` // A postState value of null means there was no change from the preState. // In this implementation, we therefore replace null with the preState. const value = newValue ?? this._state[key] return { [key]: value, } }) return suffixDiffPairs }) const postState = postStateRaw.reduce((prevValue, currentValue) => { const acc = { ...prevValue, ...currentValue } return acc }, {}) this._postState = postState debug('initVerkleExecutionWitness preState', this._state) debug('initVerkleExecutionWitness postState', this._postState) } getTreeKeyForVersion(stem: Uint8Array) { return getKey(stem, VERSION_LEAF_KEY) } getTreeKeyForBalance(stem: Uint8Array) { return getKey(stem, BALANCE_LEAF_KEY) } getTreeKeyForNonce(stem: Uint8Array) { return getKey(stem, NONCE_LEAF_KEY) } getTreeKeyForCodeHash(stem: Uint8Array) { return getKey(stem, CODE_KECCAK_LEAF_KEY) } getTreeKeyForCodeSize(stem: Uint8Array) { return getKey(stem, CODE_SIZE_LEAF_KEY) } getTreeKeyForCodeChunk(address: Address, chunkId: number) { return getKey( getStem(address, Math.floor((CODE_OFFSET + chunkId) / VERKLE_NODE_WIDTH)), toBytes((CODE_OFFSET + chunkId) % VERKLE_NODE_WIDTH) ) } chunkifyCode(code: Uint8Array) { // Pad code to multiple of 31 bytes if (code.length % 31 !== 0) { const paddingLength = 31 - (code.length % 31) code = setLengthRight(code, code.length + paddingLength) } throw new Error('Not implemented') } getTreeKeyForStorageSlot(address: Address, storageKey: number) { let position: number if (storageKey < CODE_OFFSET - HEADER_STORAGE_OFFSET) { position = HEADER_STORAGE_OFFSET + storageKey } else { position = MAIN_STORAGE_OFFSET + storageKey } return getKey( getStem(address, Math.floor(position / VERKLE_NODE_WIDTH)), toBytes(position % VERKLE_NODE_WIDTH) ) } /** * Copies the current instance of the `StateManager` * at the last fully committed point, i.e. as if all current * checkpoints were reverted. */ shallowCopy(): EVMStateManagerInterface { const stateManager = new StatelessVerkleStateManager() stateManager.initVerkleExecutionWitness(this._executionWitness!) return stateManager } /** * 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> { const stem = getStem(address, 0) const codeHashKey = this.getTreeKeyForCodeHash(stem) const codeHash = bytesToHex(this.keccakFunction(value)) this._state[bytesToHex(codeHashKey)] = codeHash if (this.DEBUG) { debug(`putContractCode address=${address.toString()} value=${short(value)}`) } if (KECCAK256_NULL_S === codeHash) { // If the code hash is the null hash, no code has to be stored return } // TODO: Slice the code into chunks and add them to the state throw new Error('Not implemented') } /** * 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.DEBUG) { debug(`getContractCode address=${address.toString()}`) } // Get the contract code size const codeSizeKey = this.getTreeKeyForCodeSize(getStem(address, 0)) const codeSizeLE = hexToBytes(this._state[bytesToHex(codeSizeKey)] ?? '0x') // Calculate number of chunks const chunks = Math.ceil(bytesToInt32(codeSizeLE, true) / 32) const retrievedChunks: Uint8Array[] = [] // Retrieve all code chunks for (let chunkId = 0; chunkId < chunks; chunkId++) { retrievedChunks.push(this.getTreeKeyForCodeChunk(address, chunkId)) } // Aggregate code chunks const code = concatBytes(...retrievedChunks) // Return code chunks return code } /** * 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 {Promise<Uint8Array>} - 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> { const storageKey = this.getTreeKeyForStorageSlot(address, Number(bytesToHex(key))) const storage = toBytes(this._state[bytesToHex(storageKey)]) return storage } /** * Adds value to the state 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> { const storageKey = this.getTreeKeyForStorageSlot(address, Number(bytesToHex(key))) this._state[bytesToHex(storageKey)] = bytesToHex(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> { const stem = getStem(address, 0) const codeHashKey = this.getTreeKeyForCodeHash(stem) // Update codeHash to `c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470` this._state[bytesToHex(codeHashKey)] = KECCAK256_NULL_S // TODO: Clear all storage slots (how?) } async getAccount(address: Address): Promise<Account> { const stem = getStem(address, 0) const balanceKey = this.getTreeKeyForBalance(stem) const nonceKey = this.getTreeKeyForNonce(stem) const codeHashKey = this.getTreeKeyForCodeHash(stem) const balanceRaw = this._state[bytesToHex(balanceKey)] const nonceRaw = this._state[bytesToHex(nonceKey)] const codeHash = this._state[bytesToHex(codeHashKey)] const account = Account.fromAccountData({ balance: typeof balanceRaw === 'string' ? bytesToBigInt(hexToBytes(balanceRaw), true) : undefined, nonce: typeof nonceRaw === 'string' ? bytesToBigInt(hexToBytes(nonceRaw), true) : undefined, codeHash, }) if (this.DEBUG) { debug( `getAccount address=${address.toString()} stem=${short(stem)} balance=${ account.balance } nonce=${account.nonce} codeHash=${short(account.codeHash)} storageHash=${short( account.storageRoot )}` ) } return account } async putAccount(address: Address, account: Account): Promise<void> { const stem = getStem(address, 0) const balanceKey = this.getTreeKeyForBalance(stem) const nonceKey = this.getTreeKeyForNonce(stem) const codeHashKey = this.getTreeKeyForCodeHash(stem) const balanceBuf = setLengthRight(bigIntToBytes(account.balance, true), 32) const nonceBuf = setLengthRight(bigIntToBytes(account.nonce, true), 32) this._state[bytesToHex(balanceKey)] = bytesToHex(balanceBuf) this._state[bytesToHex(nonceKey)] = bytesToHex(nonceBuf) this._state[bytesToHex(codeHashKey)] = bytesToHex(account.codeHash) if (this.DEBUG) { debug( `putAccount address=${address.toString()} stem=${short(stem)} balance=${ account.balance } nonce=${account.nonce} codeHash=${short(account.codeHash)} storageHash=${short( account.storageRoot )}` ) } } /** * Deletes an account from state under the provided `address`. * @param address - Address of the account which should be deleted */ async deleteAccount(address: Address) { this._accountCache!.del(address) if (!this._storageCacheSettings.deactivate) { this._storageCache?.clearContractStorage(address) } } async modifyAccountFields(address: Address, accountFields: AccountFields): Promise<void> { const account = await this.getAccount(address) 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) } getProof(_: Address, __: Uint8Array[] = []): Promise<Proof> { throw new Error('Not implemented yet') } async verifyProof(parentVerkleRoot: Uint8Array): Promise<boolean> { // Implementation: https://github.com/crate-crypto/rust-verkle-wasm/blob/master/src/lib.rs#L45 // The root is the root of the current (un-updated) trie // The proof is proof of membership of all of the accessed values // keys_values is a map from the key of the accessed value to a tuple // the tuple contains the old value and the updated value // // This function returns the new root when all of the updated values are applied const updatedStateRoot: Uint8Array = verifyUpdate( parentVerkleRoot, this._proof!, // TODO: Convert this into a Uint8Array ingestible by the method new Map() // TODO: Generate the keys_values map from the old to the updated value ) // TODO: Not sure if this should return the updated state Root (current block) or the un-updated one (parent block) const verkleRoot = await this.getStateRoot() // Verify that updatedStateRoot matches the state root of the block return equalsBytes(updatedStateRoot, verkleRoot) } // Verifies that the witness post-state matches the computed post-state verifyPostState(): boolean { for (const [key, canonicalValue] of Object.entries(this._postState)) { const computedValue = this._state[key] if (canonicalValue !== computedValue) { debug( `verifyPostState failed for key ${key}. Canonical value: ${canonicalValue}, computed value: ${computedValue}` ) return false } } return true } /** * 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._checkpoints.push(this._state) this._accountCache?.checkpoint() this._storageCache?.checkpoint() } /** * Commits the current change-set to the instance since the * last call to checkpoint. */ async commit(): Promise<void> { this._checkpoints.pop() this._accountCache!.commit() } // TODO async hasStateRoot(_: Uint8Array): Promise<boolean> { return true } /** * Reverts the current change-set to the instance since the * last call to checkpoint. */ async revert(): Promise<void> { // setup trie checkpointing this._accountCache?.revert() this._storageCache?.revert() this._codeCache = {} } /** * Writes all cache items to the trie */ async flush(): Promise<void> {} /** * Gets the verkle root. * NOTE: this needs some examination in the code where this is needed * and if we have the verkle root present * @returns {Promise<Uint8Array>} - Returns the verkle root of the `StateManager` */ async getStateRoot(): Promise<Uint8Array> { return new Uint8Array(0) } /** * TODO: needed? * Maybe in this context: reset to original pre state suffice * @param stateRoot - The verkle root to reset the instance to */ async setStateRoot(_: Uint8Array): Promise<void> {} /** * 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): Promise<StorageDump> { throw Error('not implemented') } dumpStorageRange(_: Address, __: bigint, ___: number): Promise<StorageRange> { throw Error('not implemented') } /** * Clears all underlying caches */ clearCaches() { this._accountCache?.clear() this._storageCache?.clear() } generateCanonicalGenesis(_initState: any): Promise<void> { return Promise.resolve() } }