@ignored/statemanager
Version:
An Ethereum statemanager implementation
200 lines (177 loc) • 5.04 kB
text/typescript
import { Account, isFalsy, isTruthy } from '@ignored/util'
import type { Address } from '@ignored/util'
const Tree = require('functional-red-black-tree')
export type getCb = (address: Address) => Promise<Account | undefined>
export type putCb = (keyBuf: Buffer, accountRlp: Buffer) => Promise<void>
export type deleteCb = (keyBuf: Buffer) => Promise<void>
export interface CacheOpts {
getCb: getCb
putCb: putCb
deleteCb: deleteCb
}
/**
* @ignore
*/
export class Cache {
_cache: any
_checkpoints: any[]
_getCb: getCb
_putCb: putCb
_deleteCb: deleteCb
constructor(opts: CacheOpts) {
this._cache = Tree()
this._getCb = opts.getCb
this._putCb = opts.putCb
this._deleteCb = opts.deleteCb
this._checkpoints = []
}
/**
* Puts account to cache under its address.
* @param key - Address of account
* @param val - Account
*/
put(key: Address, val: Account, fromTrie: boolean = false): void {
const modified = !fromTrie
this._update(key, val, modified, false)
}
/**
* Returns the queried account or an empty account.
* @param key - Address of account
*/
get(key: Address): Account {
const account = this.lookup(key)
return account ?? new Account()
}
/**
* Returns the queried account or undefined.
* @param key - Address of account
*/
lookup(key: Address): Account | undefined {
const keyStr = key.buf.toString('hex')
const it = this._cache.find(keyStr)
if (isTruthy(it.node)) {
const rlp = it.value.val
const account = Account.fromRlpSerializedAccount(rlp)
;(account as any).virtual = it.value.virtual
return account
}
}
/**
* Returns true if the key was deleted and thus existed in the cache earlier
* @param key - trie key to lookup
*/
keyIsDeleted(key: Address): boolean {
const keyStr = key.buf.toString('hex')
const it = this._cache.find(keyStr)
if (isTruthy(it.node)) {
return it.value.deleted
}
return false
}
/**
* Looks up address in cache, if not found, looks it up
* in the underlying trie.
* @param key - Address of account
*/
async getOrLoad(address: Address): Promise<Account> {
let account = this.lookup(address)
if (!account) {
account = await this._getCb(address)
if (account) {
this._update(address, account, false, false, false)
} else {
account = new Account()
;(account as any).virtual = true
this._update(address, account, false, false, true)
}
}
return account
}
/**
* Flushes cache by updating accounts that have been modified
* and removing accounts that have been deleted.
*/
async flush(): Promise<void> {
const it = this._cache.begin
let next = true
while (next) {
if (isTruthy(it.value) && isTruthy(it.value.modified) && isFalsy(it.value.deleted)) {
it.value.modified = false
const accountRlp = it.value.val
const keyBuf = Buffer.from(it.key, 'hex')
await this._putCb(keyBuf, accountRlp)
next = it.hasNext
it.next()
} else if (isTruthy(it.value) && isTruthy(it.value.modified) && isTruthy(it.value.deleted)) {
it.value.modified = false
it.value.deleted = true
it.value.virtual = true
it.value.val = new Account().serialize()
const keyBuf = Buffer.from(it.key, 'hex')
await this._deleteCb(keyBuf)
next = it.hasNext
it.next()
} else {
next = it.hasNext
it.next()
}
}
}
/**
* Marks current state of cache as checkpoint, which can
* later on be reverted or commited.
*/
checkpoint(): void {
this._checkpoints.push(this._cache)
}
/**
* Revert changes to cache last checkpoint (no effect on trie).
*/
revert(): void {
this._cache = this._checkpoints.pop()
}
/**
* Commits to current state of cache (no effect on trie).
*/
commit(): void {
this._checkpoints.pop()
}
/**
* Clears cache.
*/
clear(): void {
this._cache = Tree()
}
/**
* Marks address as deleted in cache.
* @param key - Address
*/
del(key: Address): void {
this._update(key, new Account(), true, true, true)
}
/**
* Generic cache update helper function
*
* @param key
* @param value
* @param modified - Has the value been modfied or is it coming unchanged from the trie (also used for deleted accounts)
* @param deleted - Delete operation on an account
* @param virtual - Account doesn't exist in the underlying trie
*/
_update(
key: Address,
value: Account,
modified: boolean,
deleted: boolean,
virtual = false
): void {
const keyHex = key.buf.toString('hex')
const it = this._cache.find(keyHex)
const val = value.serialize()
if (isTruthy(it.node)) {
this._cache = it.update({ val, modified, deleted, virtual })
} else {
this._cache = this._cache.insert(keyHex, { val, modified, deleted, virtual })
}
}
}