@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
146 lines • 7.41 kB
JavaScript
import { SLOTS_PER_EPOCH } from "@lodestar/params";
import { computeEpochAtSlot, computeStartSlotAtEpoch } from "@lodestar/state-transition";
import { AllocSource } from "../../../util/bufferPool.js";
import { getStateSlotFromBytes } from "../../../util/multifork.js";
import { serializeState } from "../../serializeState.js";
/**
* Minimum number of epochs between single temp archived states
* These states will be pruned once a new state is persisted
*/
export const PERSIST_TEMP_STATE_EVERY_EPOCHS = 32;
export { FrequencyStateArchiveStep };
var FrequencyStateArchiveStep;
(function (FrequencyStateArchiveStep) {
FrequencyStateArchiveStep["LoadLastStoredSlot"] = "load_last_stored_slot";
FrequencyStateArchiveStep["GetFinalizedState"] = "get_finalized_state";
// SerializeState is tracked via stateSerializeDuration metric
FrequencyStateArchiveStep["PersistState"] = "persist_state";
FrequencyStateArchiveStep["LoadStoredSlotsToDelete"] = "load_stored_slots_to_delete";
FrequencyStateArchiveStep["DeleteOldStates"] = "delete_old_states";
})(FrequencyStateArchiveStep || (FrequencyStateArchiveStep = {}));
/**
* Archives finalized states from active bucket to archive bucket.
*
* Only the new finalized state is stored to disk
*/
export class FrequencyStateArchiveStrategy {
regen;
db;
logger;
opts;
bufferPool;
constructor(regen, db, logger, opts, bufferPool) {
this.regen = regen;
this.db = db;
this.logger = logger;
this.opts = opts;
this.bufferPool = bufferPool;
}
async onFinalizedCheckpoint(_finalized, _metrics) { }
async onCheckpoint(_stateRoot, _metrics) { }
/**
* Persist states every some epochs to
* - Minimize disk space, storing the least states possible
* - Minimize the sync progress lost on unexpected crash, storing temp state every few epochs
*
* At epoch `e` there will be states peristed at intervals of `PERSIST_STATE_EVERY_EPOCHS` = 32
* and one at `PERSIST_TEMP_STATE_EVERY_EPOCHS` = 1024
* ```
* | | | .
* epoch - 1024*2 epoch - 1024 epoch - 32 epoch
* ```
*/
async maybeArchiveState(finalized, metrics) {
let timer = metrics?.processFinalizedCheckpoint.frequencyStateArchive.startTimer();
const lastStoredSlot = await this.db.stateArchive.lastKey();
timer?.({ step: FrequencyStateArchiveStep.LoadLastStoredSlot });
const lastStoredEpoch = computeEpochAtSlot(lastStoredSlot ?? 0);
const { archiveStateEpochFrequency } = this.opts;
const logCtx = { finalizedEpoch: finalized.epoch, lastStoredEpoch, archiveStateEpochFrequency };
if (finalized.epoch - lastStoredEpoch >= Math.min(PERSIST_TEMP_STATE_EVERY_EPOCHS, archiveStateEpochFrequency)) {
this.logger.verbose("Start archiving state", logCtx);
await this.archiveState(finalized, metrics);
// Only check the current and previous intervals
const minEpoch = Math.max(0, (Math.floor(finalized.epoch / archiveStateEpochFrequency) - 1) * archiveStateEpochFrequency);
timer = metrics?.processFinalizedCheckpoint.frequencyStateArchive.startTimer();
const storedStateSlots = await this.db.stateArchive.keys({
lt: computeStartSlotAtEpoch(finalized.epoch),
gte: computeStartSlotAtEpoch(minEpoch),
});
timer?.({ step: FrequencyStateArchiveStep.LoadStoredSlotsToDelete });
const statesSlotsToDelete = computeStateSlotsToDelete(storedStateSlots, archiveStateEpochFrequency);
timer = metrics?.processFinalizedCheckpoint.frequencyStateArchive.startTimer();
if (statesSlotsToDelete.length > 0) {
await this.db.stateArchive.batchDelete(statesSlotsToDelete);
}
timer?.({ step: FrequencyStateArchiveStep.DeleteOldStates });
// More logs to investigate the rss spike issue https://github.com/ChainSafe/lodestar/issues/5591
this.logger.verbose("Archived state completed", {
...logCtx,
minEpoch,
storedStateSlots: storedStateSlots.join(","),
statesSlotsToDelete: statesSlotsToDelete.join(","),
});
}
else {
this.logger.verbose("Skip archiving state", logCtx);
}
}
/**
* Archives finalized states from active bucket to archive bucket.
* Only the new finalized state is stored to disk
*/
async archiveState(finalized, metrics) {
// starting from Mar 2024, the finalized state could be from disk or in memory
let timer = metrics?.processFinalizedCheckpoint.frequencyStateArchive.startTimer();
const finalizedHex = { epoch: finalized.epoch, rootHex: finalized.rootHex };
const finalizedStateOrBytes = await this.regen.getCheckpointStateOrBytes(finalizedHex);
timer?.({ step: FrequencyStateArchiveStep.GetFinalizedState });
const { rootHex } = finalized;
if (!finalizedStateOrBytes) {
throw Error(`No state in cache for finalized checkpoint state epoch #${finalized.epoch} root ${rootHex}`);
}
if (finalizedStateOrBytes instanceof Uint8Array) {
const slot = getStateSlotFromBytes(finalizedStateOrBytes);
timer = metrics?.processFinalizedCheckpoint.frequencyStateArchive.startTimer();
await this.db.stateArchive.putBinary(slot, finalizedStateOrBytes);
timer?.({ step: FrequencyStateArchiveStep.PersistState });
this.logger.verbose("Archived finalized state bytes", { epoch: finalized.epoch, slot, root: rootHex });
}
else {
// serialize state using BufferPool if provided
const sszTimer = metrics?.stateSerializeDuration.startTimer({ source: AllocSource.ARCHIVE_STATE });
await serializeState(finalizedStateOrBytes, AllocSource.ARCHIVE_STATE, async (stateBytes) => {
sszTimer?.();
timer = metrics?.processFinalizedCheckpoint.frequencyStateArchive.startTimer();
await this.db.stateArchive.putBinary(finalizedStateOrBytes.slot, stateBytes);
timer?.({ step: FrequencyStateArchiveStep.PersistState });
}, this.bufferPool);
// don't delete states before the finalized state, auto-prune will take care of it
this.logger.verbose("Archived finalized state", {
epoch: finalized.epoch,
slot: finalizedStateOrBytes.slot,
root: rootHex,
});
}
}
}
/**
* Keeps first epoch per interval of persistEveryEpochs, deletes the rest
*/
export function computeStateSlotsToDelete(storedStateSlots, persistEveryEpochs) {
const persistEverySlots = persistEveryEpochs * SLOTS_PER_EPOCH;
const intervalsWithStates = new Set();
const stateSlotsToDelete = new Set();
for (const slot of storedStateSlots) {
const interval = Math.floor(slot / persistEverySlots);
if (intervalsWithStates.has(interval)) {
stateSlotsToDelete.add(slot);
}
else {
intervalsWithStates.add(interval);
}
}
return Array.from(stateSlotsToDelete.values());
}
//# sourceMappingURL=frequencyStateArchiveStrategy.js.map