UNPKG

@synthesizer-libs/statemanager

Version:
367 lines (332 loc) 11.1 kB
import { bytesToUnprefixedHex, hexToBytes } from '@synthesizer-libs/util' import { OrderedMap } from '@js-sdsl/ordered-map' import debugDefault from 'debug' import { LRUCache } from 'lru-cache' import { Cache } from './cache.js' import { CacheType } from './types.js' import type { CacheOpts } from './types.js' import type { Address } from '@synthesizer-libs/util' /** * key -> storage mapping * * undefined: storage value is known not to exist in the cache */ type DiffStorageCacheMap = Map<string, Uint8Array | undefined> type StorageCacheMap = Map<string, Uint8Array> export class StorageCache extends Cache { _lruCache: LRUCache<string, StorageCacheMap> | undefined _orderedMapCache: OrderedMap<string, StorageCacheMap> | undefined /** * Diff cache collecting the state of the cache * at the beginning of checkpoint height * (respectively: before a first modification) * * If the whole cache element is undefined (in contrast * to the account), the element didn't exist in the cache * before. */ _diffCache: Map<string, DiffStorageCacheMap>[] = [] constructor(opts: CacheOpts) { super() if (opts.type === CacheType.LRU) { this._lruCache = new LRUCache({ max: opts.size, updateAgeOnGet: true, }) } else { this._orderedMapCache = new OrderedMap() } this._diffCache.push(new Map()) if (this.DEBUG) { this._debug = debugDefault('statemanager:cache:storage') } } _saveCachePreState(addressHex: string, keyHex: string) { const addressStoragePreState = this._diffCache[this._checkpoints].get(addressHex) const diffStorageMap: DiffStorageCacheMap = addressStoragePreState ?? new Map() if (!diffStorageMap.has(keyHex)) { let oldStorageMap: StorageCacheMap | undefined let oldStorage: Uint8Array | undefined = undefined if (this._lruCache) { oldStorageMap = this._lruCache!.get(addressHex) if (oldStorageMap) { oldStorage = oldStorageMap.get(keyHex) } } else { oldStorageMap = this._orderedMapCache!.getElementByKey(addressHex) if (oldStorageMap) { oldStorage = oldStorageMap.get(keyHex) } } diffStorageMap.set(keyHex, oldStorage) this._diffCache[this._checkpoints].set(addressHex, diffStorageMap) } } /** * Puts storage value to cache under address_key cache key. * @param address - Account address * @param key - Storage key * @param val - RLP-encoded storage value */ put(address: Address, key: Uint8Array, value: Uint8Array): void { const addressHex = bytesToUnprefixedHex(address.bytes) const keyHex = bytesToUnprefixedHex(key) this._saveCachePreState(addressHex, keyHex) if (this.DEBUG) { this._debug( `Put storage for ${addressHex}: ${keyHex} -> ${ value !== undefined ? bytesToUnprefixedHex(value) : '' }`, ) } if (this._lruCache) { let storageMap = this._lruCache!.get(addressHex) if (!storageMap) { storageMap = new Map() } storageMap.set(keyHex, value) this._lruCache!.set(addressHex, storageMap) } else { let storageMap = this._orderedMapCache!.getElementByKey(addressHex) if (!storageMap) { storageMap = new Map() } storageMap.set(keyHex, value) this._orderedMapCache!.setElement(addressHex, storageMap) } this._stats.writes += 1 } /** * Returns the queried slot as the RLP encoded storage value * hexToBytes('0x80'): slot is known to be empty * undefined: slot is not in cache * @param address - Address of account * @param key - Storage key * @returns Storage value or undefined */ get(address: Address, key: Uint8Array): Uint8Array | undefined { const addressHex = bytesToUnprefixedHex(address.bytes) const keyHex = bytesToUnprefixedHex(key) if (this.DEBUG) { this._debug(`Get storage for ${addressHex}`) } let storageMap: StorageCacheMap | undefined if (this._lruCache) { storageMap = this._lruCache!.get(addressHex) } else { storageMap = this._orderedMapCache!.getElementByKey(addressHex) } this._stats.reads += 1 if (storageMap) { this._stats.hits += 1 return storageMap.get(keyHex) } } /** * Marks storage key for address as deleted in cache. * @param address - Address * @param key - Storage key */ del(address: Address, key: Uint8Array): void { const addressHex = bytesToUnprefixedHex(address.bytes) const keyHex = bytesToUnprefixedHex(key) this._saveCachePreState(addressHex, keyHex) if (this.DEBUG) { this._debug(`Delete storage for ${addressHex}: ${keyHex}`) } if (this._lruCache) { let storageMap = this._lruCache!.get(addressHex) if (!storageMap) { storageMap = new Map() } storageMap.set(keyHex, hexToBytes('0x80')) this._lruCache!.set(addressHex, storageMap) } else { let storageMap = this._orderedMapCache!.getElementByKey(addressHex) if (!storageMap) { storageMap = new Map() } storageMap.set(keyHex, hexToBytes('0x80')) this._orderedMapCache!.setElement(addressHex, storageMap) } this._stats.deletions += 1 } /** * Deletes all storage slots for address from the cache * @param address */ clearStorage(address: Address): void { const addressHex = bytesToUnprefixedHex(address.bytes) if (this._lruCache) { this._lruCache!.set(addressHex, new Map()) } else { this._orderedMapCache!.setElement(addressHex, new Map()) } } /** * Flushes cache by returning storage slots that have been modified * or deleted and resetting the diff cache (at checkpoint height). */ flush(): [string, string, Uint8Array | undefined][] { if (this.DEBUG) { this._debug(`Flushing cache on checkpoint ${this._checkpoints}`) } const diffMap = this._diffCache[this._checkpoints]! const items: [string, string, Uint8Array | undefined][] = [] for (const entry of diffMap.entries()) { const addressHex = entry[0] const diffStorageMap = entry[1] let storageMap: StorageCacheMap | undefined if (this._lruCache) { storageMap = this._lruCache!.get(addressHex) } else { storageMap = this._orderedMapCache!.getElementByKey(addressHex) } if (storageMap !== undefined) { for (const entry of diffStorageMap.entries()) { const keyHex = entry[0] const value = storageMap.get(keyHex) items.push([addressHex, keyHex, value]) } } else { throw new Error('internal error: storage cache map for account should be defined') } } this._diffCache[this._checkpoints] = new Map() return items } /** * Revert changes to cache last checkpoint (no effect on trie). */ revert(): void { this._checkpoints -= 1 if (this.DEBUG) { this._debug(`Revert to checkpoint ${this._checkpoints}`) } const diffMap = this._diffCache.pop()! for (const entry of diffMap.entries()) { const addressHex = entry[0] const diffStorageMap = entry[1] for (const entry of diffStorageMap.entries()) { const keyHex = entry[0] const value = entry[1] if (this._lruCache) { const storageMap = this._lruCache.get(addressHex) ?? new Map() if (value === undefined) { // Value is known not to be in the cache before // -> delete from cache storageMap.delete(keyHex) } else { // Value is known to be in the cache before // (being either some storage value or the RLP-encoded empty Uint8Array) storageMap.set(keyHex, value) } this._lruCache.set(addressHex, storageMap) } else { const storageMap = this._orderedMapCache!.getElementByKey(addressHex) ?? new Map() if (!value) { storageMap.delete(keyHex) } else { storageMap.set(keyHex, value) } this._orderedMapCache!.setElement(addressHex, storageMap) } } } } /** * Commits to current state of cache (no effect on trie). */ commit(): void { this._checkpoints -= 1 if (this.DEBUG) { this._debug(`Commit to checkpoint ${this._checkpoints}`) } const higherHeightDiffMap = this._diffCache.pop()! const lowerHeightDiffMap = this._diffCache[this._checkpoints] // Go through diffMap from the pre-commit checkpoint height. // 1. Iterate through all state pre states // 2. If state pre-state is not in the new (lower) height diff map, take pre commit pre state value // 3. If state is in new map, take this one, since this supersedes subsequent changes for (const entry of higherHeightDiffMap.entries()) { const addressHex = entry[0] const higherHeightStorageDiff = entry[1] const lowerHeightStorageDiff = lowerHeightDiffMap.get(addressHex) ?? new Map() for (const entry of higherHeightStorageDiff.entries()) { const keyHex = entry[0] if (!lowerHeightStorageDiff.has(keyHex)) { const elem = entry[1] lowerHeightStorageDiff.set(keyHex, elem) } } lowerHeightDiffMap.set(addressHex, lowerHeightStorageDiff) } } /** * Marks current state of cache as checkpoint, which can * later on be reverted or committed. */ checkpoint(): void { this._checkpoints += 1 if (this.DEBUG) { this._debug(`New checkpoint ${this._checkpoints}`) } this._diffCache.push(new Map()) } /** * Returns the size of the cache * @returns */ size() { if (this._lruCache) { return this._lruCache!.size } else { return this._orderedMapCache!.size() } } /** * Returns a dict with cache stats * @param reset */ stats(reset = true) { const stats = { ...this._stats } stats.size = this.size() if (reset) { this._stats = { size: 0, reads: 0, hits: 0, writes: 0, deletions: 0, } } return stats } /** * Clears cache. */ clear(): void { if (this.DEBUG) { this._debug(`Clear cache`) } if (this._lruCache) { this._lruCache!.clear() } else { this._orderedMapCache!.clear() } } /** * Dumps the RLP-encoded storage values for an `account` specified by `address`. * @param address - The address of the `account` to return storage for * @returns {StorageCacheMap | undefined} - The storage values for the `account` or undefined if the `account` is not in the cache */ dump(address: Address): StorageCacheMap | undefined { let storageMap if (this._lruCache) { storageMap = this._lruCache!.get(bytesToUnprefixedHex(address.bytes)) } else { storageMap = this._orderedMapCache?.getElementByKey(bytesToUnprefixedHex(address.bytes)) } return storageMap } }