@synthesizer-libs/statemanager
Version:
An Ethereum statemanager implementation
275 lines (249 loc) • 7.2 kB
text/typescript
import { bytesToUnprefixedHex } 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'
/**
* Represents a cached code element.
*/
type CodeCacheElement = {
code: Uint8Array | undefined
}
export class CodeCache extends Cache {
_lruCache: LRUCache<string, CodeCacheElement> | undefined
_orderedMapCache: OrderedMap<string, CodeCacheElement> | 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 code), the element didn't exist in the cache
* before.
*/
_diffCache: Map<string, CodeCacheElement | undefined>[] = []
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<string, CodeCacheElement | undefined>())
this._debug = debugDefault('statemanager:cache:code')
}
/**
* Saves the state of the code cache before making changes to it.
*
* @param cacheKeyHex Account key for which code is being modified.
*/
_saveCachePreState(cacheKeyHex: string) {
const diffMap = this._diffCache[this._checkpoints]
if (!diffMap.has(cacheKeyHex)) {
let oldElem: CodeCacheElement | undefined
if (this._lruCache) {
oldElem = this._lruCache.get(cacheKeyHex)
} else {
oldElem = this._orderedMapCache!.getElementByKey(cacheKeyHex)
}
diffMap.set(cacheKeyHex, oldElem)
}
}
/**
* Puts code into the cache under its hash.
*
* @param address - Address of account code is being modified for.
* @param code - Bytecode or undefined if code doesn't exist.
*/
put(address: Address, code: Uint8Array | undefined): void {
const addressHex = bytesToUnprefixedHex(address.bytes)
this._saveCachePreState(addressHex)
const elem = {
code,
}
if (this.DEBUG) {
this._debug(`Put code ${addressHex}`)
}
if (this._lruCache) {
this._lruCache.set(addressHex, elem)
} else {
this._orderedMapCache!.setElement(addressHex, elem)
}
this._stats.writes += 1
}
/**
* Returns the queried code or undefined if it doesn't exist.
*
* @param address - Account address for which code is being fetched.
*/
get(address: Address): CodeCacheElement | undefined {
const addressHex = bytesToUnprefixedHex(address.bytes)
if (this.DEBUG) {
this._debug(`Get code ${addressHex}`)
}
let elem: CodeCacheElement | undefined
if (this._lruCache) {
elem = this._lruCache.get(addressHex)
} else {
elem = this._orderedMapCache!.getElementByKey(addressHex)
}
this._stats.reads += 1
if (elem) {
this._stats.hits += 1
}
return elem
}
/**
* Marks code as deleted in the cache.
*
* @param address - Account address for which code is being fetched.
*/
del(address: Address): void {
const addressHex = bytesToUnprefixedHex(address.bytes)
this._saveCachePreState(addressHex)
if (this.DEBUG) {
this._debug(`Delete code ${addressHex}`)
}
if (this._lruCache) {
this._lruCache.set(addressHex, {
code: undefined,
})
} else {
this._orderedMapCache!.setElement(addressHex, {
code: undefined,
})
}
this._stats.deletions += 1
}
/**
* Flushes the cache by returning codes that have been modified
* or deleted and resetting the diff cache (at checkpoint height).
*/
flush(): [string, CodeCacheElement][] {
if (this.DEBUG) {
this._debug(`Flushing cache on checkpoint ${this._checkpoints}`)
}
const diffMap = this._diffCache[this._checkpoints]
const items: [string, CodeCacheElement][] = []
for (const entry of diffMap.entries()) {
const cacheKeyHex = entry[0]
let elem: CodeCacheElement | undefined
if (this._lruCache) {
elem = this._lruCache.get(cacheKeyHex)
} else {
elem = this._orderedMapCache!.getElementByKey(cacheKeyHex)
}
if (elem !== undefined) {
items.push([cacheKeyHex, elem])
}
}
this._diffCache[this._checkpoints] = new Map<string, CodeCacheElement | undefined>()
return items
}
/**
* Revert changes to the cache to the 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 elem = entry[1]
if (elem === undefined) {
if (this._lruCache) {
this._lruCache.delete(addressHex)
} else {
this._orderedMapCache!.eraseElementByKey(addressHex)
}
} else {
if (this._lruCache) {
this._lruCache.set(addressHex, elem)
} else {
this._orderedMapCache!.setElement(addressHex, elem)
}
}
}
}
/**
* Commits the current state of the cache (no effect on trie).
*/
commit(): void {
this._checkpoints -= 1
if (this.DEBUG) {
this._debug(`Commit to checkpoint ${this._checkpoints}`)
}
const diffMap = this._diffCache.pop()!
for (const entry of diffMap.entries()) {
const addressHex = entry[0]
const oldEntry = this._diffCache[this._checkpoints].has(addressHex)
if (!oldEntry) {
const elem = entry[1]
this._diffCache[this._checkpoints].set(addressHex, elem)
}
}
}
/**
* Marks the current state of the cache as a checkpoint, which can
* later be reverted or committed.
*/
checkpoint(): void {
this._checkpoints += 1
if (this.DEBUG) {
this._debug(`New checkpoint ${this._checkpoints}`)
}
this._diffCache.push(new Map<string, CodeCacheElement | undefined>())
}
/**
* Returns the size of the cache
* @returns
*/
size() {
if (this._lruCache) {
return this._lruCache!.size
} else {
return this._orderedMapCache!.size()
}
}
/**
* Returns a dictionary with cache statistics.
*
* @param reset - Whether to reset statistics after retrieval.
* @returns A dictionary with cache statistics.
*/
stats(reset = true): any {
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 the cache.
*/
clear(): void {
if (this.DEBUG) {
this._debug(`Clear cache`)
}
if (this._lruCache) {
this._lruCache.clear()
} else {
this._orderedMapCache!.clear()
}
}
}