@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
173 lines • 8.11 kB
JavaScript
import { computeEpochShuffling, computeEpochShufflingAsync, } from "@lodestar/state-transition";
import { LodestarError, MapDef, pruneSetToMax } from "@lodestar/utils";
/**
* Same value to CheckpointBalancesCache, with the assumption that we don't have to use it for old epochs. In the worse case:
* - when loading state bytes from disk, we need to compute shuffling for all epochs (~1s as of Sep 2023)
* - don't have shuffling to verify attestations, need to do 1 epoch transition to add shuffling to this cache. This never happens
* with default chain option of maxSkipSlots = 32
**/
const MAX_EPOCHS = 4;
/**
* With default chain option of maxSkipSlots = 32, there should be no shuffling promise. If that happens a lot, it could blow up Lodestar,
* with MAX_EPOCHS = 4, only allow 2 promise at a time. Note that regen already bounds number of concurrent requests at 1 already.
*/
const MAX_PROMISES = 2;
var CacheItemType;
(function (CacheItemType) {
CacheItemType[CacheItemType["shuffling"] = 0] = "shuffling";
CacheItemType[CacheItemType["promise"] = 1] = "promise";
})(CacheItemType || (CacheItemType = {}));
/**
* A shuffling cache to help:
* - get committee quickly for attestation verification
* - if a shuffling is not available (which does not happen with default chain option of maxSkipSlots = 32), track a promise to make sure we don't compute the same shuffling twice
* - skip computing shuffling when loading state bytes from disk
*/
export class ShufflingCache {
constructor(metrics = null, logger = null, opts = {}, precalculatedShufflings) {
this.metrics = metrics;
this.logger = logger;
/** LRU cache implemented as a map, pruned every time we add an item */
this.itemsByDecisionRootByEpoch = new MapDef(() => new Map());
if (metrics) {
metrics.shufflingCache.size.addCollect(() => metrics.shufflingCache.size.set(Array.from(this.itemsByDecisionRootByEpoch.values()).reduce((total, innerMap) => total + innerMap.size, 0)));
}
this.maxEpochs = opts.maxShufflingCacheEpochs ?? MAX_EPOCHS;
precalculatedShufflings?.map(({ shuffling, decisionRoot }) => {
if (shuffling !== null) {
this.set(shuffling, decisionRoot);
}
});
}
/**
* Insert a promise to make sure we don't regen state for the same shuffling.
* Bound by MAX_SHUFFLING_PROMISE to make sure our node does not blow up.
*/
insertPromise(epoch, decisionRoot) {
const promiseCount = Array.from(this.itemsByDecisionRootByEpoch.values())
.flatMap((innerMap) => Array.from(innerMap.values()))
.filter((item) => isPromiseCacheItem(item)).length;
if (promiseCount >= MAX_PROMISES) {
throw new Error(`Too many shuffling promises: ${promiseCount}, shufflingEpoch: ${epoch}, decisionRootHex: ${decisionRoot}`);
}
let resolveFn = null;
const promise = new Promise((resolve) => {
resolveFn = resolve;
});
if (resolveFn === null) {
throw new Error("Promise Constructor was not executed immediately");
}
const cacheItem = {
type: CacheItemType.promise,
timeInsertedMs: Date.now(),
promise,
resolveFn,
};
this.itemsByDecisionRootByEpoch.getOrDefault(epoch).set(decisionRoot, cacheItem);
this.metrics?.shufflingCache.insertPromiseCount.inc();
}
/**
* Most of the time, this should return a shuffling immediately.
* If there's a promise, it means we are computing the same shuffling, so we wait for the promise to resolve.
* Return null if we don't have a shuffling for this epoch and dependentRootHex.
*/
async get(epoch, decisionRoot) {
const cacheItem = this.itemsByDecisionRootByEpoch.getOrDefault(epoch).get(decisionRoot);
if (cacheItem === undefined) {
this.metrics?.shufflingCache.miss.inc();
return null;
}
if (isShufflingCacheItem(cacheItem)) {
this.metrics?.shufflingCache.hit.inc();
return cacheItem.shuffling;
}
this.metrics?.shufflingCache.shufflingPromiseNotResolved.inc();
return cacheItem.promise;
}
/**
* Gets a cached shuffling via the epoch and decision root. If the shuffling is not
* available it will build it synchronously and return the shuffling.
*
* NOTE: If a shuffling is already queued and not calculated it will build and resolve
* the promise but the already queued build will happen at some later time
*/
getSync(epoch, decisionRoot, buildProps) {
const cacheItem = this.itemsByDecisionRootByEpoch.getOrDefault(epoch).get(decisionRoot);
if (!cacheItem) {
this.metrics?.shufflingCache.miss.inc();
}
else if (isShufflingCacheItem(cacheItem)) {
this.metrics?.shufflingCache.hit.inc();
return cacheItem.shuffling;
}
else if (buildProps) {
// TODO: (@matthewkeil) This should possible log a warning??
this.metrics?.shufflingCache.shufflingPromiseNotResolvedAndThrownAway.inc();
}
else {
this.metrics?.shufflingCache.shufflingPromiseNotResolved.inc();
}
let shuffling = null;
if (buildProps) {
const timer = this.metrics?.shufflingCache.shufflingCalculationTime.startTimer({ source: "getSync" });
shuffling = computeEpochShuffling(buildProps.state, buildProps.activeIndices, epoch);
timer?.();
this.set(shuffling, decisionRoot);
}
return shuffling;
}
/**
* Queue asynchronous build for an EpochShuffling, triggered from state-transition
*/
build(epoch, decisionRoot, state, activeIndices) {
this.insertPromise(epoch, decisionRoot);
/**
* TODO: (@matthewkeil) This will get replaced by a proper build queue and a worker to do calculations
* on a NICE thread
*/
const timer = this.metrics?.shufflingCache.shufflingCalculationTime.startTimer({ source: "build" });
computeEpochShufflingAsync(state, activeIndices, epoch)
.then((shuffling) => {
this.set(shuffling, decisionRoot);
})
.catch((err) => this.logger?.error(`error building shuffling for epoch ${epoch} at decisionRoot ${decisionRoot}`, {}, err))
.finally(() => {
timer?.();
});
}
/**
* Add an EpochShuffling to the ShufflingCache. If a promise for the shuffling is present it will
* resolve the promise with the built shuffling
*/
set(shuffling, decisionRoot) {
const shufflingAtEpoch = this.itemsByDecisionRootByEpoch.getOrDefault(shuffling.epoch);
// if a pending shuffling promise exists, resolve it
const cacheItem = shufflingAtEpoch.get(decisionRoot);
if (cacheItem) {
if (isPromiseCacheItem(cacheItem)) {
cacheItem.resolveFn(shuffling);
this.metrics?.shufflingCache.shufflingPromiseResolutionTime.observe((Date.now() - cacheItem.timeInsertedMs) / 1000);
}
else {
this.metrics?.shufflingCache.shufflingBuiltMultipleTimes.inc();
}
}
// set the shuffling
shufflingAtEpoch.set(decisionRoot, { type: CacheItemType.shuffling, shuffling });
// prune the cache
pruneSetToMax(this.itemsByDecisionRootByEpoch, this.maxEpochs);
}
}
function isShufflingCacheItem(item) {
return item.type === CacheItemType.shuffling;
}
function isPromiseCacheItem(item) {
return item.type === CacheItemType.promise;
}
export var ShufflingCacheErrorCode;
(function (ShufflingCacheErrorCode) {
ShufflingCacheErrorCode["NO_SHUFFLING_FOUND"] = "SHUFFLING_CACHE_ERROR_NO_SHUFFLING_FOUND";
})(ShufflingCacheErrorCode || (ShufflingCacheErrorCode = {}));
export class ShufflingCacheError extends LodestarError {
}
//# sourceMappingURL=shufflingCache.js.map