@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
173 lines • 6.17 kB
JavaScript
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