UNPKG

@ethereumjs/statemanager

Version:
574 lines 28.6 kB
import { BinaryTree } from '@ethereumjs/binarytree'; import { BinaryTreeAccessedStateType } from '@ethereumjs/common'; import { RLP } from '@ethereumjs/rlp'; import { Account, BINARY_TREE_CODE_CHUNK_SIZE, BINARY_TREE_CODE_OFFSET, BINARY_TREE_NODE_WIDTH, BinaryTreeLeafType, EthereumJSErrorWithoutCode, KECCAK256_NULL, MapDB, bigIntToBytes, bytesToBigInt, bytesToHex, chunkifyBinaryTreeCode, createAddressFromString, createPartialAccount, createPartialAccountFromRLP, decodeBinaryTreeLeafBasicData, encodeBinaryTreeLeafBasicData, equalsBytes, generateBinaryTreeChunkSuffixes, generateBinaryTreeCodeStems, getBinaryTreeKeyForStorageSlot, getBinaryTreeStem, hexToBigInt, hexToBytes, padToEven, setLengthLeft, setLengthRight, short, unprefixedHexToBytes, } from '@ethereumjs/util'; import { blake3 } from '@noble/hashes/blake3'; import debugDefault from 'debug'; import { keccak256 } from 'ethereum-cryptography/keccak.js'; import { OriginalStorageCache } from "./cache/originalStorageCache.js"; import { modifyAccountFields } from "./util.js"; const ZEROVALUE = '0x0000000000000000000000000000000000000000000000000000000000000000'; export class StatefulBinaryTreeStateManager { constructor(opts) { // Post-state provided from the executionWitness. // Should not update. Used for comparing our computed post-state with the canonical one. this._postState = {}; /** * 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 */ this.DEBUG = false; /** * Gets the account associated with `address` or `undefined` if account does not exist * @param address - Address of the `account` to get */ this.getAccount = async (address) => { const elem = this._caches?.account?.get(address); if (elem !== undefined) { return elem.accountRLP !== undefined ? createPartialAccountFromRLP(elem.accountRLP) : undefined; } const stem = getBinaryTreeStem(this.hashFunction, address, 0); // First retrieve the account "header" values from the trie const accountValues = await this._tree.get(stem, [ BinaryTreeLeafType.BasicData, BinaryTreeLeafType.CodeHash, ]); let account; if (accountValues[0] !== null && accountValues[0] !== undefined) { const basicData = decodeBinaryTreeLeafBasicData(accountValues[0]); account = createPartialAccount({ version: basicData.version, balance: basicData.balance, nonce: basicData.nonce, // Codehash is either untouched (i.e. null) or deleted (i.e. overwritten with zeros) codeHash: accountValues[1] === null || accountValues[1] === undefined || equalsBytes(accountValues[1], new Uint8Array(32)) ? KECCAK256_NULL : accountValues[1], codeSize: basicData.codeSize, storageRoot: KECCAK256_NULL, }); } else if (accountValues[1] === undefined || accountValues[1] === null) { // account does not exist if both basic fields and codehash are undefined if (this.DEBUG) { this._debug(`getAccount address=${address.toString()} from DB (non-existent)`); } this._caches?.account?.put(address, account); } if (this.DEBUG) { this._debug(`getAccount address=${address.toString()} stem=${short(stem)}`); } 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 */ this.putAccount = async (address, account) => { if (this.DEBUG) { this._debug(`putAccount address=${address} nonce=${account?.nonce} balance=${account?.balance} contract=${account && account.isContract() ? 'yes' : 'no'} empty=${account && account.isEmpty() ? 'yes' : 'no'}`); } if (this._caches?.account === undefined) { if (account !== undefined) { const stem = getBinaryTreeStem(this.hashFunction, address, 0); const basicDataBytes = encodeBinaryTreeLeafBasicData(account); await this._tree.put(stem, [BinaryTreeLeafType.BasicData, BinaryTreeLeafType.CodeHash], [basicDataBytes, account.codeHash]); } else { // Delete account await this.deleteAccount(address); } } else { if (account !== undefined) { this._caches?.account?.put(address, account, true); } else { this._caches?.account?.del(address); } } }; /** * Deletes an account from state under the provided `address`. * @param address - Address of the account which should be deleted */ this.deleteAccount = async (address) => { if (this.DEBUG) { this._debug(`Delete account ${address}`); } this._caches?.deleteAccount(address); if (this._caches?.account === undefined) { const stem = getBinaryTreeStem(this.hashFunction, address); // Special instance where we delete the account and revert the trie value to untouched await this._tree.put(stem, [BinaryTreeLeafType.BasicData, BinaryTreeLeafType.CodeHash], [null, null]); } }; this.modifyAccountFields = async (address, accountFields) => { await modifyAccountFields(this, address, accountFields); }; this.putCode = async (address, value) => { if (this.DEBUG) { this._debug(`putCode address=${address.toString()} value=${short(value)}`); } this._caches?.code?.put(address, value); const codeHash = keccak256(value); if (equalsBytes(codeHash, KECCAK256_NULL)) { // If the code hash is the null hash, no code has to be stored return; } if ((await this.getAccount(address)) === undefined) { await this.putAccount(address, new Account()); } if (this.DEBUG) { this._debug(`Update codeHash (-> ${short(codeHash)}) for account ${address}`); } const codeChunks = chunkifyBinaryTreeCode(value); const chunkStems = generateBinaryTreeCodeStems(codeChunks.length, address, this.hashFunction); const chunkSuffixes = generateBinaryTreeChunkSuffixes(codeChunks.length); // Put the code chunks corresponding to the first stem (up to 128 chunks) await this._tree.put(chunkStems[0], chunkSuffixes.slice(0, chunkSuffixes.length <= BINARY_TREE_CODE_OFFSET ? chunkSuffixes.length : BINARY_TREE_CODE_OFFSET), codeChunks.slice(0, codeChunks.length <= BINARY_TREE_CODE_OFFSET ? codeChunks.length : BINARY_TREE_CODE_OFFSET)); // Put additional chunks under additional stems as applicable for (let stem = 1; stem < chunkStems.length; stem++) { const sliceStart = BINARY_TREE_CODE_OFFSET + BINARY_TREE_NODE_WIDTH * (stem - 1); const sliceEnd = value.length <= BINARY_TREE_CODE_OFFSET + BINARY_TREE_NODE_WIDTH * stem ? value.length : BINARY_TREE_CODE_OFFSET + BINARY_TREE_NODE_WIDTH * stem; await this._tree.put(chunkStems[stem], chunkSuffixes.slice(sliceStart, sliceEnd), codeChunks.slice(sliceStart, sliceEnd)); } await this.modifyAccountFields(address, { codeHash, codeSize: value.length, }); }; this.getCode = async (address) => { if (this.DEBUG) { this._debug(`getCode address=${address.toString()}`); } const elem = this._caches?.code?.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); } // allocate the code const codeSize = account.codeSize; const stems = generateBinaryTreeCodeStems(Math.ceil(codeSize / BINARY_TREE_CODE_CHUNK_SIZE), address, this.hashFunction); const chunkSuffixes = generateBinaryTreeChunkSuffixes(Math.ceil(codeSize / BINARY_TREE_CODE_CHUNK_SIZE)); const chunksByStem = new Array(stems.length); // Retrieve the code chunks stored in the first leaf node chunksByStem[0] = await this._tree.get(stems[0], chunkSuffixes.slice(0, codeSize <= BINARY_TREE_CODE_OFFSET ? codeSize : BINARY_TREE_CODE_OFFSET)); // Retrieve code chunks on any additional stems for (let stem = 1; stem < stems.length; stem++) { const sliceStart = BINARY_TREE_CODE_OFFSET + BINARY_TREE_NODE_WIDTH * (stem - 1); const sliceEnd = codeSize <= BINARY_TREE_CODE_OFFSET + BINARY_TREE_NODE_WIDTH * stem ? codeSize : BINARY_TREE_CODE_OFFSET + BINARY_TREE_NODE_WIDTH * stem; chunksByStem[stem] = await this._tree.get(stems[stem], chunkSuffixes.slice(sliceStart, sliceEnd)); } const chunks = chunksByStem.flat(); const code = new Uint8Array(codeSize); // Insert code chunks into final array (skipping PUSHDATA overflow indicator byte) for (let x = 0; x < chunks.length; x++) { if (chunks[x] === undefined) throw EthereumJSErrorWithoutCode(`expected code chunk at index ${x}, got undefined`); let lastChunkByteIndex = BINARY_TREE_CODE_CHUNK_SIZE; // Determine code ending byte (if we're on the last chunk) if (x === chunks.length - 1) { // On the last chunk, the slice either ends on a partial chunk (if codeSize doesn't exactly fit in full chunks), or a full chunk lastChunkByteIndex = codeSize % BINARY_TREE_CODE_CHUNK_SIZE || BINARY_TREE_CODE_CHUNK_SIZE; } code.set(chunks[x].slice(1, lastChunkByteIndex + 1), code.byteOffset + x * BINARY_TREE_CODE_CHUNK_SIZE); } this._caches?.code?.put(address, code); return code; }; this.getCodeSize = async (address) => { const accountBytes = (await this._tree.get(getBinaryTreeStem(this.hashFunction, address), [ BinaryTreeLeafType.BasicData, ]))[0]; if (accountBytes === null) return 0; return decodeBinaryTreeLeafBasicData(accountBytes).codeSize; }; this.getStorage = async (address, key) => { if (key.length !== 32) { throw EthereumJSErrorWithoutCode('Storage key must be 32 bytes long'); } const cachedValue = this._caches?.storage?.get(address, key); if (cachedValue !== undefined) { const decoded = RLP.decode(cachedValue ?? new Uint8Array(0)); return decoded; } const account = await this.getAccount(address); if (!account) { return new Uint8Array(); } const storageKey = getBinaryTreeKeyForStorageSlot(address, bytesToBigInt(key), this.hashFunction); const value = await this._tree.get(storageKey.slice(0, 31), [storageKey[31]]); this._caches?.storage?.put(address, key, value[0] ?? hexToBytes('0x80')); const decoded = (value[0] ?? new Uint8Array(0)); return setLengthLeft(decoded, 32); }; this.putStorage = async (address, key, value) => { this._caches?.storage?.put(address, key, RLP.encode(value)); if (this._caches?.storage === undefined) { const storageKey = getBinaryTreeKeyForStorageSlot(address, bytesToBigInt(key), this.hashFunction); await this._tree.put(storageKey.slice(0, 31), [storageKey[31]], [setLengthLeft(value, 32)]); } }; this.clearStorage = async (address) => { this._caches?.storage?.clearStorage(address); }; this.checkpoint = async () => { this._tree.checkpoint(); this._caches?.checkpoint(); this._checkpointCount++; }; this.commit = async () => { await this._tree.commit(); this._caches?.commit(); this._checkpointCount--; if (this._checkpointCount === 0) { await this.flush(); this.originalStorageCache.clear(); } if (this.DEBUG) { this._debug(`state checkpoint committed`); } }; this.revert = async () => { await this._tree.revert(); this._caches?.revert(); this._checkpointCount--; if (this._checkpointCount === 0) { await this.flush(); this.originalStorageCache.clear(); } }; this.flush = async () => { const codeItems = this._caches?.code?.flush() ?? []; for (const item of codeItems) { const addr = createAddressFromString(`0x${item[0]}`); const code = item[1].code; if (code === undefined) { continue; } await this.putCode(addr, code); } const storageItems = this._caches?.storage?.flush() ?? []; for (const item of storageItems) { const address = createAddressFromString(`0x${item[0]}`); const keyHex = item[1]; const keyBytes = unprefixedHexToBytes(keyHex); const value = item[2]; const decoded = RLP.decode(value ?? new Uint8Array(0)); const account = await this.getAccount(address); if (account) { await this.putStorage(address, keyBytes, decoded); } } const accountItems = this._caches?.account?.flush() ?? []; for (const item of accountItems) { const address = createAddressFromString(`0x${item[0]}`); const elem = item[1]; if (elem.accountRLP === undefined) { await this.deleteAccount(address); } else { const account = createPartialAccountFromRLP(elem.accountRLP); await this.putAccount(address, account); } } }; // 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._checkpointCount = 0; if (opts.common?.isActivatedEIP(7864) === false) { throw EthereumJSErrorWithoutCode('EIP-7864 required for binary tree state management'); } this.hashFunction = opts.hashFunction ?? blake3; this._tree = opts.tree ?? new BinaryTree({ hashFunction: this.hashFunction, db: new MapDB(), useRootPersistence: false, cacheSize: 0, }); this._debug = debugDefault('statemanager:binarytree'); this.originalStorageCache = new OriginalStorageCache(this.getStorage.bind(this)); this._caches = opts.caches; this.keccakFunction = keccak256; this.preStateRoot = new Uint8Array(32); // Initial state root is zeroes } initBinaryTreeExecutionWitness(_blockNum, executionWitness) { if (executionWitness === null || executionWitness === undefined) { const errorMsg = `Invalid executionWitness=${executionWitness} for initBinaryTreeExecutionWitness`; this._debug(errorMsg); throw Error(errorMsg); } this.preStateRoot = hexToBytes(executionWitness.parentStateRoot); // set prestate root // Populate the post-state from the executionWitness const postStateRaw = executionWitness.stateDiff.flatMap(({ stem, suffixDiffs }) => { const suffixDiffPairs = suffixDiffs.map(({ newValue, currentValue, suffix }) => { const key = `${stem}${padToEven(Number(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 ?? currentValue; return { [key]: value, }; }); return suffixDiffPairs; }); const postState = postStateRaw.reduce((prevValue, currentValue) => { const acc = { ...prevValue, ...currentValue }; return acc; }, {}); this._postState = postState; this._debug(`initBinaryTreeExecutionWitness postState=${JSON.stringify(this._postState)}`); } async getComputedValue(accessedState) { const { address, type } = accessedState; switch (type) { case BinaryTreeAccessedStateType.BasicData: { if (this._caches === undefined) { const accountData = await this.getAccount(address); if (accountData === undefined) { return null; } const basicDataBytes = encodeBinaryTreeLeafBasicData(accountData); return bytesToHex(basicDataBytes); } else { const encodedAccount = this._caches?.account?.get(address)?.accountRLP; if (encodedAccount === undefined) { return null; } const basicDataBytes = encodeBinaryTreeLeafBasicData(createPartialAccountFromRLP(encodedAccount)); return bytesToHex(basicDataBytes); } } case BinaryTreeAccessedStateType.CodeHash: { if (this._caches === undefined) { const accountData = await this.getAccount(address); if (accountData === undefined) { return null; } return bytesToHex(accountData.codeHash); } else { const encodedAccount = this._caches?.account?.get(address)?.accountRLP; if (encodedAccount === undefined) { return null; } return bytesToHex(createPartialAccountFromRLP(encodedAccount).codeHash); } } case BinaryTreeAccessedStateType.Code: { const { codeOffset } = accessedState; let code = null; if (this._caches === undefined) { code = await this.getCode(address); if (code === undefined) { return null; } } else { code = this._caches?.code?.get(address)?.code; if (code === undefined) { return null; } } // we can only compare the actual code because to compare the first byte would // be very tricky and impossible in certain scenarios like when the previous code chunk // was not accessed and hence not even provided in the witness // We are left-padding with two zeroes to get a 32-byte length, but these bytes should not be considered reliable return bytesToHex(setLengthLeft(setLengthRight(code.slice(codeOffset, codeOffset + BINARY_TREE_CODE_CHUNK_SIZE), BINARY_TREE_CODE_CHUNK_SIZE), BINARY_TREE_CODE_CHUNK_SIZE + 1)); } case BinaryTreeAccessedStateType.Storage: { const { slot } = accessedState; const key = setLengthLeft(bigIntToBytes(slot), 32); let storage = null; if (this._caches === undefined) { storage = await this.getStorage(address, key); if (storage === undefined) { return null; } } else { storage = this._caches?.storage?.get(address, key); } if (storage === undefined) { return null; } return bytesToHex(setLengthLeft(storage, 32)); } } } // Verifies that the witness post-state matches the computed post-state async verifyBinaryTreePostState(accessWitness) { // track what all chunks were accessed so as to compare in the end if any chunks were missed // in access while comparing against the provided poststate in the execution witness const accessedChunks = new Map(); // switch to false if postVerify fails let postFailures = 0; for (const accessedState of accessWitness?.accesses() ?? []) { const { address, type } = accessedState; let extraMeta = ''; if (accessedState.type === BinaryTreeAccessedStateType.Code) { extraMeta = `codeOffset=${accessedState.codeOffset}`; } else if (accessedState.type === BinaryTreeAccessedStateType.Storage) { extraMeta = `slot=${accessedState.slot}`; } const { chunkKey } = accessedState; accessedChunks.set(chunkKey, true); let computedValue = await this.getComputedValue(accessedState); if (computedValue === undefined) { this.DEBUG && this._debug(`Missing computed value for address=${address} type=${type} ${extraMeta} chunkKey=${chunkKey}`); postFailures++; continue; } let canonicalValue = this._postState[chunkKey]; if (canonicalValue === undefined) { this.DEBUG && this._debug(`Block accesses missing from postState for address=${address} type=${type} ${extraMeta} chunkKey=${chunkKey}`); postFailures++; continue; } // if the access type is code, then we can't match the first byte because since the computed value // doesn't has the first byte for push data since previous chunk code itself might not be available if (accessedState.type === BinaryTreeAccessedStateType.Code) { computedValue = computedValue !== null ? `0x${computedValue.slice(4)}` : null; canonicalValue = canonicalValue !== null ? `0x${canonicalValue.slice(4)}` : null; } else if (accessedState.type === BinaryTreeAccessedStateType.Storage && canonicalValue === null && computedValue === ZEROVALUE) { canonicalValue = ZEROVALUE; } this._debug(`computed ${computedValue} canonical ${canonicalValue}`); if (computedValue !== canonicalValue) { if (type === BinaryTreeAccessedStateType.BasicData) { this.DEBUG && this._debug(`canonical value: `, canonicalValue === null ? null : decodeBinaryTreeLeafBasicData(hexToBytes(canonicalValue))); this.DEBUG && this._debug(`computed value: `, computedValue === null ? null : decodeBinaryTreeLeafBasicData(hexToBytes(computedValue))); } this.DEBUG && this._debug(`Block accesses mismatch address=${address} type=${type} ${extraMeta} chunkKey=${chunkKey}`); this.DEBUG && this._debug(`expected=${canonicalValue}`); this.DEBUG && this._debug(`computed=${computedValue}`); postFailures++; } } for (const canChunkKey of Object.keys(this._postState)) { if (accessedChunks.get(canChunkKey) === undefined) { this.DEBUG && this._debug(`Missing chunk access for canChunkKey=${canChunkKey}`); postFailures++; } } const verifyPassed = postFailures === 0; this.DEBUG && this._debug(`verifyBinaryTreePostState verifyPassed=${verifyPassed} postFailures=${postFailures}`); return verifyPassed; } getStateRoot() { return Promise.resolve(this._tree.root()); } setStateRoot(stateRoot, clearCache) { this._tree.root(stateRoot); clearCache === true && this.clearCaches(); return Promise.resolve(); } hasStateRoot(root) { return this._tree.checkRoot(root); } dumpStorage(_address) { throw EthereumJSErrorWithoutCode('Method not implemented.'); } dumpStorageRange(_address, _startKey, _limit) { throw EthereumJSErrorWithoutCode('Method not implemented.'); } clearCaches() { this._caches?.clear(); } shallowCopy(_downlevelCaches) { throw EthereumJSErrorWithoutCode('Method not implemented.'); } async checkChunkWitnessPresent(_address, _codeOffset) { throw EthereumJSErrorWithoutCode('Method not implemented.'); } async generateCanonicalGenesis(genesisState) { await this._tree.createRootNode(); await this.checkpoint(); for (const addressStr of Object.keys(genesisState)) { const addrState = genesisState[addressStr]; let nonce; let balance; let code; let storage = []; if (Array.isArray(addrState)) { ; [balance, code, storage, nonce] = addrState; } else { balance = hexToBigInt(addrState); nonce = '0x1'; code = '0x'; } const address = createAddressFromString(addressStr); await this.putAccount(address, new Account()); const codeBuf = hexToBytes(code ?? '0x'); const codeHash = this.keccakFunction(codeBuf); // Set contract storage if (storage !== undefined) { for (const [storageKey, valHex] of storage) { const val = hexToBytes(valHex); if (['0x', '0x00'].includes(bytesToHex(val))) { continue; } const key = setLengthLeft(hexToBytes(storageKey), 32); await this.putStorage(address, key, val); } } // Put contract code await this.putCode(address, codeBuf); // Put account data const account = createPartialAccount({ nonce, balance, codeHash, codeSize: codeBuf.byteLength, }); await this.putAccount(address, account); } await this.commit(); await this.flush(); } } //# sourceMappingURL=statefulBinaryTreeStateManager.js.map