@synthesizer-libs/statemanager
Version:
An Ethereum statemanager implementation
367 lines (332 loc) • 11.1 kB
text/typescript
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
}
}