UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

173 lines 6.17 kB
import { toRootHex } from "@lodestar/utils"; import { LinkedList } from "../../util/array.js"; import { MapTracker } from "./mapMetrics.js"; /** * Given `maxSkipSlots` = 32 and `DEFAULT_EARLIEST_PERMISSIBLE_SLOT_DISTANCE` = 32, lodestar doesn't need to * reload states in order to process a gossip block. * * |-----------------------------------------------|-----------------------------------------------| * maxSkipSlots DEFAULT_EARLIEST_PERMISSIBLE_SLOT_DISTANCE ^ * clock slot */ export const DEFAULT_MAX_BLOCK_STATES = 64; /** * New implementation of BlockStateCache that keeps the most recent n states consistently * - Maintain a linked list (FIFO) with special handling for head state, which is always the first item in the list * - Prune per add() instead of per checkpoint so it only keeps n historical states consistently, prune from tail * - No need to prune per finalized checkpoint * * Given this block tree with Block 11 as head: * ``` Block 10 | +-----+-----+ | | Block 11 Block 12 ^ | | | head Block 13 * ``` * The maintained key order would be: 11 -> 13 -> 12 -> 10, and state 10 will be pruned first. */ export class FIFOBlockStateCache { constructor(opts, { metrics }) { this.maxStates = opts.maxBlockStates ?? DEFAULT_MAX_BLOCK_STATES; this.cache = new MapTracker(metrics?.stateCache); if (metrics) { this.metrics = metrics.stateCache; metrics.stateCache.size.addCollect(() => metrics.stateCache.size.set(this.cache.size)); } this.keyOrder = new LinkedList(); } /** * Set a state as head, happens when importing a block and head block is changed. */ setHeadState(item) { if (item !== null) { this.add(item, true); } } /** * Get a seed state for state reload, this could be any states. The goal is to have the same * base merkle tree for all BeaconState objects across application. * See packages/state-transition/src/util/loadState/loadState.ts for more detail */ getSeedState() { const firstValue = this.cache.values().next(); if (firstValue.done) { // should not happen throw Error("No state in FIFOBlockStateCache"); } const firstState = firstValue.value; // don't transfer cache because consumer only use this cache to reload another state from disc return firstState.clone(true); } /** * Get a state from this cache given a state root hex. */ get(rootHex, opts) { this.metrics?.lookups.inc(); const item = this.cache.get(rootHex); if (!item) { return null; } this.metrics?.hits.inc(); this.metrics?.stateClonedCount.observe(item.clonedCount); return item.clone(opts?.dontTransferCache); } /** * Add a state to this cache. * @param isHead if true, move it to the head of the list. Otherwise add to the 2nd position. * In importBlock() steps, normally it'll call add() with isHead = false first. Then call setHeadState() to set the head. */ add(item, isHead = false) { const key = toRootHex(item.hashTreeRoot()); if (this.cache.get(key) != null) { if (!this.keyOrder.has(key)) { throw Error(`State exists but key not found in keyOrder: ${key}`); } if (isHead) { this.keyOrder.moveToHead(key); } else { this.keyOrder.moveToSecond(key); } // same size, no prune return; } // new state this.metrics?.adds.inc(); this.cache.set(key, item); if (isHead) { this.keyOrder.unshift(key); } else { // insert after head const head = this.keyOrder.first(); if (head == null) { // should not happen, however handle just in case this.keyOrder.unshift(key); } else { this.keyOrder.insertAfter(head, key); } } this.prune(key); } get size() { return this.cache.size; } /** * Prune the cache from tail to keep the most recent n states consistently. * The tail of the list is the oldest state, in case regen adds back the same state, * it should stay next to head so that it won't be pruned right away. * The FIFO cache helps with this. */ prune(lastAddedKey) { while (this.keyOrder.length > this.maxStates) { const key = this.keyOrder.last(); // it does not make sense to prune the last added state // this only happens when max state is 1 in a short period of time if (key === lastAddedKey) { break; } if (!key) { // should not happen throw new Error("No key"); } this.keyOrder.pop(); this.cache.delete(key); } } /** * No need for this implementation * This is only to conform to the old api */ deleteAllBeforeEpoch() { } /** * ONLY FOR DEBUGGING PURPOSES. For lodestar debug API. */ clear() { this.cache.clear(); } /** ONLY FOR DEBUGGING PURPOSES. For lodestar debug API */ dumpSummary() { return Array.from(this.cache.entries()).map(([key, state]) => ({ slot: state.slot, root: toRootHex(state.hashTreeRoot()), reads: this.cache.readCount.get(key) ?? 0, lastRead: this.cache.lastRead.get(key) ?? 0, checkpointState: false, })); } getStates() { return this.cache.values(); } /** * For unit test only. */ dumpKeyOrder() { return this.keyOrder.toArray(); } } //# sourceMappingURL=fifoBlockStateCache.js.map