UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

130 lines 6.01 kB
import { MapDef } from "@lodestar/utils"; /** * To prevent our node from having to reprocess while struggling to sync, * we only want to reprocess attestations if block reaches our node before this time. */ export const REPROCESS_MIN_TIME_TO_NEXT_SLOT_SEC = 2; /** * Reprocess status for metrics */ export var ReprocessStatus; (function (ReprocessStatus) { /** * There are too many attestations that have unknown block root. */ ReprocessStatus["reached_limit"] = "reached_limit"; /** * The awaiting attestation is pruned per clock slot. */ ReprocessStatus["expired"] = "expired"; })(ReprocessStatus || (ReprocessStatus = {})); // How many attestations (aggregate + unaggregate) we keep before new ones get dropped. const MAXIMUM_QUEUED_ATTESTATIONS = 16_384; /** * Some attestations may reach our node before the voted block, so we manage a cache to reprocess them * when the block come. * (n) (n + 1) * |----------------|----------------|----------|------| * | | | * att agg att | * block * Since the gossip handler has to return validation result to js-libp2p-gossipsub, this class should not * reprocess attestations, it should control when the attestations are ready to reprocess instead. */ export class ReprocessController { constructor(metrics) { this.metrics = metrics; this.awaitingPromisesCount = 0; this.awaitingPromisesByRootBySlot = new MapDef(() => new Map()); } /** * Returns Promise that resolves either on block found or once 1 slot passes. * Used to handle unknown block root for both unaggregated and aggregated attestations. * @returns true if blockFound */ waitForBlockOfAttestation(slot, root) { this.metrics?.reprocessApiAttestations.total.inc(); if (this.awaitingPromisesCount >= MAXIMUM_QUEUED_ATTESTATIONS) { this.metrics?.reprocessApiAttestations.reject.inc({ reason: ReprocessStatus.reached_limit }); return Promise.resolve(false); } this.awaitingPromisesCount++; const awaitingPromisesByRoot = this.awaitingPromisesByRootBySlot.getOrDefault(slot); const promiseCached = awaitingPromisesByRoot.get(root); if (promiseCached) { promiseCached.awaitingAttestationsCount++; return promiseCached.promise; } // Capture both the promise and its callbacks. // It is not spec'ed but in tests in Firefox and NodeJS the promise constructor is run immediately let resolve = null; const promise = new Promise((resolveCB) => { resolve = resolveCB; }); if (resolve === null) { throw Error("Promise Constructor was not executed immediately"); } awaitingPromisesByRoot.set(root, { promise, awaitingAttestationsCount: 1, resolve, addedTimeMs: Date.now(), }); return promise; } /** * It's important to make sure our node is synced before we reprocess, * it means the processed slot is same to clock slot * Note that we want to use clock advanced by REPROCESS_MIN_TIME_TO_NEXT_SLOT instead of * clockSlot because we want to make sure our node is healthy while reprocessing attestations. * If a block reach our node 1s before the next slot, for example, then probably node * is struggling and we don't want to reprocess anything at that time. */ onBlockImported({ slot: blockSlot, root }, advancedSlot) { // we are probably resyncing, don't want to reprocess attestations here if (blockSlot < advancedSlot) return; // resolve all related promises const awaitingPromisesBySlot = this.awaitingPromisesByRootBySlot.getOrDefault(blockSlot); const awaitingPromise = awaitingPromisesBySlot.get(root); if (awaitingPromise) { const { resolve, addedTimeMs, awaitingAttestationsCount } = awaitingPromise; resolve(true); this.awaitingPromisesCount -= awaitingAttestationsCount; this.metrics?.reprocessApiAttestations.resolve.inc(awaitingAttestationsCount); this.metrics?.reprocessApiAttestations.waitSecBeforeResolve.set((Date.now() - addedTimeMs) / 1000); } // prune awaitingPromisesBySlot.delete(root); } /** * It's important to make sure our node is synced before reprocessing attestations, * it means clockSlot is the same to last processed block's slot, and we don't reprocess * attestations of old slots. * So we reject and prune all old awaiting promises per clock slot. * @param clockSlot */ onSlot(clockSlot) { const now = Date.now(); for (const [key, awaitingPromisesByRoot] of this.awaitingPromisesByRootBySlot.entries()) { if (key < clockSlot) { // reject all related promises for (const awaitingPromise of awaitingPromisesByRoot.values()) { const { resolve, addedTimeMs } = awaitingPromise; resolve(false); this.metrics?.reprocessApiAttestations.waitSecBeforeReject.set({ reason: ReprocessStatus.expired }, (now - addedTimeMs) / 1000); this.metrics?.reprocessApiAttestations.reject.inc({ reason: ReprocessStatus.expired }); } // prune this.awaitingPromisesByRootBySlot.delete(key); } else { break; } } // in theory there are maybe some awaiting promises waiting for a slot > clockSlot // in reality this never happens so reseting awaitingPromisesCount to 0 to make it simple this.awaitingPromisesCount = 0; } } //# sourceMappingURL=reprocess.js.map