UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

225 lines • 10.7 kB
import { isForkPostDeneb } from "@lodestar/params"; import { computeStartSlotAtEpoch } from "@lodestar/state-transition"; import { LodestarError, toRootHex } from "@lodestar/utils"; import { BlockInputBlobs, BlockInputPreData, DAType, isBlockInputBlobs, isDaOutOfRange, } from "../blocks/blockInput/index.js"; import { ChainEvent } from "../emitter.js"; const MAX_BLOCK_INPUT_CACHE_SIZE = 5; /** * Consumers that create BlockInputs or change types of old BlockInputs * * - gossipHandlers (block and blob) * - beaconBlocksMaybeBlobsByRange * - unavailableBeaconBlobsByRoot (beaconBlocksMaybeBlobsByRoot) * - publishBlock in the beacon/blocks/index.ts API * https://github.com/ChainSafe/lodestar/blob/unstable/packages/beacon-node/src/api/impl/beacon/blocks/index.ts#L62 * - maybeValidateBlobs in verifyBlocksDataAvailability (is_data_available spec function) * https://github.com/ChainSafe/lodestar/blob/unstable/packages/beacon-node/src/chain/blocks/verifyBlocksDataAvailability.ts#L111 * * * Pruning management for SeenBlockInputCache * ------------------------------------------ * There are four cases for how pruning needs to be handled * - Normal operation following head via gossip (and/or reqresp). For this situation the consumer (process pipeline or * caller of processBlock) will call the `prune` method to remove any processed BlockInputs from the cache. This will * also remove any ancestors of the processed BlockInput as that will also need to have been successfully processed * for import to work correctly * - onFinalized event handler will help to prune any non-canonical forks once the chain finalizes. Any block-slots that * are before the finalized checkpoint will be pruned. * - Range-sync periods. The range process uses this cache to store and sync blocks with DA data as the chain is pulled * from peers. We pull batches, by epoch, so 32 slots are pulled at a time and several batches are pulled concurrently. * It is important to set the MAX_BLOCK_INPUT_CACHE_SIZE high enough to support range sync activities. Currently the * value is set for 5 batches of 32 slots. As process block is called (similar to following head) the BlockInput and * its ancestors will be pruned. * - Non-Finality times. This is a bit more tricky. There can be long periods of non-finality and storing everything * will cause OOM. The pruneToMax will help ensure a hard limit on the number of stored blocks (with DA) that are held * in memory at any one time. The value for MAX_BLOCK_INPUT_CACHE_SIZE is set to accommodate range-sync but in * practice this value may need to be massaged in the future if we find issues when debugging non-finality */ export class SeenBlockInputCache { constructor({ config, clock, chainEvents, signal, metrics, logger }) { this.blockInputs = new Map(); this.onFinalized = (checkpoint) => { const cutoffSlot = computeStartSlotAtEpoch(checkpoint.epoch); for (const [rootHex, blockInput] of this.blockInputs) { if (blockInput.slot < cutoffSlot) { this.blockInputs.delete(rootHex); } } this.pruneToMaxSize(); }; this.config = config; this.clock = clock; this.chainEvents = chainEvents; this.signal = signal; this.metrics = metrics; this.logger = logger; if (metrics) { metrics.seenCache.blockInput.blockInputCount.addCollect(() => metrics.seenCache.blockInput.blockInputCount.set(this.blockInputs.size)); } this.chainEvents.on(ChainEvent.forkChoiceFinalized, this.onFinalized); this.signal.addEventListener("abort", () => { this.chainEvents.off(ChainEvent.forkChoiceFinalized, this.onFinalized); }); } has(rootHex) { return this.blockInputs.has(rootHex); } get(rootHex) { return this.blockInputs.get(rootHex); } /** * Removes the single BlockInput from the cache */ remove(rootHex) { this.blockInputs.delete(rootHex); } /** * Removes a processed BlockInput from the cache and also removes any ancestors of processed blocks */ prune(rootHex) { let blockInput = this.blockInputs.get(rootHex); let parentRootHex = blockInput?.parentRootHex; while (blockInput) { this.blockInputs.delete(blockInput.blockRootHex); blockInput = this.blockInputs.get(parentRootHex ?? ""); parentRootHex = blockInput?.parentRootHex; } this.pruneToMaxSize(); } getByBlock({ block, source, seenTimestampSec, peerIdStr }) { const blockRoot = this.config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message); const blockRootHex = toRootHex(blockRoot); // TODO(peerDAS): Why is it necessary to static cast this here. All conditional paths result in a valid value so should be defined correctly below let blockInput = this.blockInputs.get(blockRootHex); if (!blockInput) { const { forkName, daOutOfRange } = this.buildCommonProps(block.message.slot); if (!isForkPostDeneb(forkName)) { blockInput = BlockInputPreData.createFromBlock({ block, blockRootHex, daOutOfRange, forkName, source: { source, seenTimestampSec, peerIdStr, }, }); } // else if (isForkPostFulu(forkName)) { // blockInput = new BlockInputColumns.createFromBlock({ // block, // blockRootHex, // daOutOfRange, // forkName, // custodyColumns: this.custodyConfig.custodyColumns, // sampledColumns: this.custodyConfig.sampledColumns, // source: { // source, // seenTimestampSec, // peerIdStr // } // }) // } else { blockInput = BlockInputBlobs.createFromBlock({ block: block, blockRootHex, daOutOfRange, forkName, source: { source, seenTimestampSec, peerIdStr, }, }); } this.blockInputs.set(blockInput.blockRootHex, blockInput); } if (!blockInput.hasBlock()) { blockInput.addBlock({ block, blockRootHex, source: { source, seenTimestampSec, peerIdStr } }); } else { this.logger?.debug("Attempt to cache block but is already cached on BlockInput", blockInput.getLogMeta()); this.metrics?.seenCache.blockInput.duplicateBlockCount.inc({ source }); } return blockInput; } getByBlob({ blobSidecar, source, seenTimestampSec, peerIdStr }, opts = {}) { const blockRoot = this.config .getForkTypes(blobSidecar.signedBlockHeader.message.slot) .BeaconBlockHeader.hashTreeRoot(blobSidecar.signedBlockHeader.message); const blockRootHex = toRootHex(blockRoot); // TODO(peerDAS): Why is it necessary to static cast this here. All conditional paths result in a valid value so should be defined correctly below let blockInput = this.blockInputs.get(blockRootHex); let created = false; if (!blockInput) { created = true; const { forkName, daOutOfRange } = this.buildCommonProps(blobSidecar.signedBlockHeader.message.slot); blockInput = BlockInputBlobs.createFromBlob({ blobSidecar, blockRootHex, daOutOfRange, forkName, source, seenTimestampSec, peerIdStr, }); this.metrics?.seenCache.blockInput.createdByBlob.inc(); this.blockInputs.set(blockRootHex, blockInput); } if (!isBlockInputBlobs(blockInput)) { throw new SeenBlockInputCacheError({ code: SeenBlockInputCacheErrorCode.WRONG_BLOCK_INPUT_TYPE, cachedType: blockInput.type, requestedType: DAType.Blobs, ...blockInput.getLogMeta(), }, `BlockInputType mismatch adding blobIndex=${blobSidecar.index}`); } if (!blockInput.hasBlob(blobSidecar.index)) { blockInput.addBlob({ blobSidecar, blockRootHex, source, seenTimestampSec, peerIdStr }); } else if (!created) { this.logger?.debug(`Attempt to cache blob index #${blobSidecar.index} but is already cached on BlockInput`, blockInput.getLogMeta()); this.metrics?.seenCache.blockInput.duplicateBlobCount.inc({ source }); if (opts.throwErrorIfAlreadyKnown) { throw new SeenBlockInputCacheError({ code: SeenBlockInputCacheErrorCode.GOSSIP_BLOB_ALREADY_KNOWN, ...blockInput.getLogMeta(), }); } } return blockInput; } buildCommonProps(slot) { const forkName = this.config.getForkName(slot); return { forkName, daOutOfRange: isDaOutOfRange(this.config, forkName, slot, this.clock.currentEpoch), }; } /** * Use custom implementation of pruneSetToMax to allow for sorting by slot * and deleting via key/rootHex */ pruneToMaxSize() { let itemsToDelete = this.blockInputs.size - MAX_BLOCK_INPUT_CACHE_SIZE; if (itemsToDelete > 0) { const sorted = [...this.blockInputs.entries()].sort((a, b) => b[1].slot - a[1].slot); for (const [rootHex] of sorted) { this.blockInputs.delete(rootHex); itemsToDelete--; if (itemsToDelete <= 0) return; } } } } var SeenBlockInputCacheErrorCode; (function (SeenBlockInputCacheErrorCode) { SeenBlockInputCacheErrorCode["WRONG_BLOCK_INPUT_TYPE"] = "BLOCK_INPUT_CACHE_ERROR_WRONG_BLOCK_INPUT_TYPE"; SeenBlockInputCacheErrorCode["GOSSIP_BLOB_ALREADY_KNOWN"] = "BLOCK_INPUT_CACHE_ERROR_GOSSIP_BLOB_ALREADY_KNOWN"; })(SeenBlockInputCacheErrorCode || (SeenBlockInputCacheErrorCode = {})); class SeenBlockInputCacheError extends LodestarError { } //# sourceMappingURL=seenBlockInput.js.map