UNPKG

@synthesizer-libs/statemanager

Version:
616 lines 25.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MerkleStateManager = exports.CODEHASH_PREFIX = void 0; const common_1 = require("@synthesizer-libs/common"); const mpt_1 = require("@synthesizer-libs/mpt"); const rlp_1 = require("@ethereumjs/rlp"); const util_1 = require("@synthesizer-libs/util"); const debug_1 = require("debug"); const keccak_js_1 = require("ethereum-cryptography/keccak.js"); const index_js_1 = require("./cache/index.js"); const util_js_1 = require("./util.js"); /** * 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. */ exports.CODEHASH_PREFIX = (0, util_1.utf8ToBytes)('c'); /** * 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 * `@synthesizer-libs/mpt` trie as a data backend. * * Note that there is a `SimpleStateManager` dependency-free state * manager implementation available shipped with the `@synthesizer-libs/statemanager` * package which might be an alternative to this implementation * for many basic use cases. */ class MerkleStateManager { /** * Instantiate the StateManager interface. */ constructor(opts = {}) { /** * 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; // 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 = (0, debug_1.default)('statemanager:merkle'); this.common = opts.common ?? new common_1.Common({ chain: common_1.Mainnet }); this._checkpointCount = 0; this._trie = opts.trie ?? new mpt_1.MerklePatriciaTrie({ useKeyHashing: true, common: this.common }); this._storageTries = {}; this.keccakFunction = opts.common?.customCrypto.keccak256 ?? keccak_js_1.keccak256; this.originalStorageCache = new index_js_1.OriginalStorageCache(this.getStorage.bind(this)); this._prefixCodeHashes = opts.prefixCodeHashes ?? true; this._prefixStorageTrieKeys = opts.prefixStorageTrieKeys ?? false; this._caches = opts.caches; } /** * Gets the account associated with `address` or `undefined` if account does not exist * @param address - Address of the `account` to get */ async getAccount(address) { const elem = this._caches?.account?.get(address); if (elem !== undefined) { return elem.accountRLP !== undefined ? (0, util_1.createAccountFromRLP)(elem.accountRLP) : undefined; } const rlp = await this._trie.get(address.bytes); const account = rlp !== null ? (0, util_1.createAccountFromRLP)(rlp) : undefined; if (this.DEBUG) { this._debug(`Get account ${address} from DB (${account ? 'exists' : 'non-existent'})`); } this._caches?.account?.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, account) { 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._caches?.account === undefined) { 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._caches.account?.put(address, account); } else { this._caches.account?.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, accountFields) { await (0, util_js_1.modifyAccountFields)(this, address, accountFields); } /** * Deletes an account from state under the provided `address`. * @param address - Address of the account which should be deleted */ async deleteAccount(address) { if (this.DEBUG) { this._debug(`Delete account ${address}`); } this._caches?.deleteAccount(address); if (this._caches?.account === undefined) { await this._trie.del(address.bytes); } } /** * 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 putCode(address, value) { const codeHash = this.keccakFunction(value); if (this._caches?.code !== undefined) { this._caches.code.put(address, value); } else { const key = this._prefixCodeHashes ? (0, util_1.concatBytes)(exports.CODEHASH_PREFIX, codeHash) : codeHash; await this._getCodeDB().put(key, value); } if (this.DEBUG) { this._debug(`Update codeHash (-> ${(0, util_1.short)(codeHash)}) for account ${address}`); } if ((await this.getAccount(address)) === undefined) { await this.putAccount(address, new util_1.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 getCode(address) { 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); } const key = this._prefixCodeHashes ? (0, util_1.concatBytes)(exports.CODEHASH_PREFIX, account.codeHash) : account.codeHash; const code = (await this._trie.database().get(key)) ?? new Uint8Array(0); this._caches?.code?.put(address, code); return code; } async getCodeSize(address) { const contractCode = await this.getCode(address); return contractCode.length; } /** * Gets the storage trie for the EVM-internal account identified by the provided address/hash. * If the storage trie is not in the local cache ('this._storageTries'), * generates a new storage trie object based on a lookup (shallow copy from 'this._trie'), * applies the storage root of the provided rootAccount (or an * empty trie root if no rootAccount is provided), and stores the new entry * in the local cache. * @param addressOrHash Address (or other object) with populated 'bytes', or a raw Uint8Array. * Used to identify the requested storage trie in the local cache and define the * prefix used when creating a new storage trie. * @param rootAccount (Optional) Account object whose 'storageRoot' is to be used as * the root of the new storageTrie returned when there is no pre-existing trie. * If left undefined, the EMPTY_TRIE_ROOT will be used as the root instead. * @returns storage MerklePatriciaTrie object * @private */ // TODO PR: have a better interface for hashed address pull? _getStorageTrie(addressOrHash, rootAccount) { // use hashed key for lookup from storage cache const addressBytes = addressOrHash instanceof Uint8Array ? addressOrHash : this.keccakFunction(addressOrHash.bytes); const addressHex = (0, util_1.bytesToUnprefixedHex)(addressBytes); let storageTrie = this._storageTries[addressHex]; if (storageTrie === undefined) { const keyPrefix = this._prefixStorageTrieKeys ? addressBytes.slice(0, 7) : undefined; storageTrie = this._trie.shallowCopy(false, { keyPrefix }); if (rootAccount !== undefined) { storageTrie.root(rootAccount.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 */ _getAccountTrie() { return this._trie; } /** * Gets the storage trie for an account from the storage * cache or does a lookup. * @private */ _getCodeDB() { 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 getStorage(address, key) { if (key.length !== 32) { throw new Error('Storage key must be 32 bytes long'); } const cachedValue = this._caches?.storage?.get(address, key); if (cachedValue !== undefined) { const decoded = rlp_1.RLP.decode(cachedValue ?? new Uint8Array(0)); return decoded; } const account = await this.getAccount(address); if (!account) { return new Uint8Array(); } const trie = this._getStorageTrie(address, account); const value = await trie.get(key); this._caches?.storage?.put(address, key, value ?? (0, util_1.hexToBytes)('0x80')); const decoded = rlp_1.RLP.decode(value ?? new Uint8Array(0)); 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 */ async _modifyContractStorage(address, account, modifyTrie) { // 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 = (0, util_1.bytesToUnprefixedHex)(address.bytes); this._storageTries[addressHex] = storageTrie; // update contract storageRoot account.storageRoot = storageTrie.root(); await this.putAccount(address, account); resolve(); }); }); } async _writeContractStorage(address, account, key, value) { await this._modifyContractStorage(address, account, async (storageTrie, done) => { if (value instanceof Uint8Array && value.length) { // format input const encodedValue = rlp_1.RLP.encode(value); if (this.DEBUG) { this._debug(`Update contract storage for account ${address} to ${(0, util_1.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 putStorage(address, key, value) { 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('putStorage() called on non-existing account'); } value = (0, util_1.unpadBytes)(value); this._caches?.storage?.put(address, key, rlp_1.RLP.encode(value)) ?? (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 clearStorage(address) { let account = await this.getAccount(address); if (!account) { account = new util_1.Account(); } this._caches?.storage?.clearStorage(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() { this._trie.checkpoint(); this._caches?.checkpoint(); this._checkpointCount++; } /** * Commits the current change-set to the instance since the * last call to checkpoint. */ async commit() { // setup trie checkpointing await this._trie.commit(); this._caches?.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() { // setup trie checkpointing await this._trie.revert(); this._caches?.revert(); this._storageTries = {}; this._checkpointCount--; if (this._checkpointCount === 0) { await this.flush(); this.originalStorageCache.clear(); } } /** * Writes all cache items to the trie */ async flush() { const codeItems = this._caches?.code?.flush() ?? []; for (const item of codeItems) { const addr = (0, util_1.createAddressFromString)(`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 ? (0, util_1.concatBytes)(exports.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 util_1.Account()); } await this.modifyAccountFields(addr, { codeHash }); } const storageItems = this._caches?.storage?.flush() ?? []; for (const item of storageItems) { const address = (0, util_1.createAddressFromString)(`0x${item[0]}`); const keyHex = item[1]; const keyBytes = (0, util_1.unprefixedHexToBytes)(keyHex); const value = item[2]; const decoded = rlp_1.RLP.decode(value ?? new Uint8Array(0)); const account = await this.getAccount(address); if (account) { await this._writeContractStorage(address, account, keyBytes, decoded); } } const accountItems = this._caches?.account?.flush() ?? []; for (const item of accountItems) { const addressHex = item[0]; const addressBytes = (0, util_1.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); } } } /** * 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() { 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, clearCache = true) { await this.flush(); if (!(0, util_1.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 (clearCache) { this._caches?.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) { 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); return trie.getValueMap().then((value) => { return value.values; }); } /** 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, startKey, limit) { 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); return trie.getValueMap(startKey, limit).then((value) => { const values = value.values; const dump = Object.create(null); for (const key of Object.keys(values)) { const val = values[key]; dump[key] = { key: null, value: val, }; } return { storage: dump, nextKey: value.nextKey, }; }); } /** * 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) { 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 = (0, util_1.createAddressFromString)(address); const state = initState[address]; if (!Array.isArray(state)) { // Prior format: address -> balance const account = (0, util_1.createAccount)({ balance: state }); await this.putAccount(addr, account); } else { // New format: address -> [balance, code, storage] const [balance, code, storage, nonce] = state; const account = (0, util_1.createAccount)({ balance, nonce }); await this.putAccount(addr, account); if (code !== undefined) { await this.putCode(addr, (0, util_1.toBytes)(code)); } if (storage !== undefined) { for (const [key, value] of storage) { await this.putStorage(addr, (0, util_1.toBytes)(key), (0, util_1.toBytes)(value)); } } } } await this.flush(); } /** * Checks whether there is a state corresponding to a stateRoot */ async hasStateRoot(root) { 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 instances are mostly used in * short-term usage contexts and LRU cache instantiation 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) { 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; return new MerkleStateManager({ common, trie, prefixStorageTrieKeys, prefixCodeHashes, caches: this._caches?.shallowCopy(downlevelCaches), }); } /** * Clears all underlying caches */ clearCaches() { this._caches?.clear(); } /** * Returns the applied key for a given address * Used for saving preimages * @param address - The address to return the applied key * @returns {Uint8Array} - The applied key (e.g. hashed address) */ getAppliedKey(address) { return this._trie['appliedKey'](address); } } exports.MerkleStateManager = MerkleStateManager; //# sourceMappingURL=merkleStateManager.js.map