import { ApiClient, routes } from "@lodestar/api"; import { ChainForkConfig } from "@lodestar/config"; import { BLSPubkey, Epoch, RootHex, Slot } from "@lodestar/types"; import { Metrics } from "../metrics.js"; import { PubkeyHex } from "../types.js"; import { IClock, LoggerVc } from "../util/index.js"; import { ChainHeaderTracker } from "./chainHeaderTracker.js"; import { ValidatorStore } from "./validatorStore.js"; export declare const GENESIS_SLOT = 0; export type BlockDutyAtEpoch = { dependentRoot: RootHex; data: routes.validator.ProposerDuty[]; }; type NotifyBlockProductionFn = (slot: Slot, proposers: BLSPubkey[]) => void; export declare class BlockDutiesService { private readonly config; private readonly logger; private readonly api; private readonly clock; private readonly validatorStore; private readonly metrics; /** Notify the block service if it should produce a block. */ private notifyBlockProductionFn; /** Maps an epoch to all *local* proposers in this epoch. Notably, this does not contain proposals for any validators which are not registered locally. */ private readonly proposers; /** * Tracks which proposer pubkeys we have already notified for the active slot so that * a late-arriving cache update (SSE-driven refetch, slow initial poll) only fires * `notifyBlockProductionFn` for *newly discovered* proposers, never duplicates. */ private notifiedSlot; private readonly notifiedProposers; /** * True once `notifyProposersForSlot` has been invoked for `notifiedSlot`, regardless of * whether anything was notified. Any subsequent invocation that finds *new* proposers is * therefore a late detection — the signal tracked by `newProposalDutiesDetected`. */ private notifiedSlotInitialPass; constructor(config: ChainForkConfig, logger: LoggerVc, api: ApiClient, clock: IClock, validatorStore: ValidatorStore, chainHeaderTracker: ChainHeaderTracker, metrics: Metrics | null); /** * Late-bind the production callback. Allows the duties service to be constructed * before the consumer that handles proposal production. */ setNotifyBlockProductionFn(notifyBlockProductionFn: NotifyBlockProductionFn): void; /** * Returns the pubkeys of the validators which are assigned to propose in the given slot. * * It is possible that multiple validators have an identical proposal slot, however that is * likely the result of heavy forking (lol) or inconsistent beacon node connections. */ getblockProposersAtSlot(slot: Slot): BLSPubkey[]; /** * Returns the cached `{dependentRoot, data}` entry for `epoch`, or `undefined` if duties * for that epoch are not yet known. Consumers can detect a proposer-shuffling change * (e.g. after a reorg) by observing a different `dependentRoot` than the one they last * read for the same epoch. */ getProposersAtEpoch(epoch: Epoch): BlockDutyAtEpoch | undefined; removeDutiesForKey(pubkey: PubkeyHex): void; /** * Baseline per-epoch fetch. Fires at epoch boundaries (and once at startup). Post-Gloas the * deterministic 1-epoch lookahead (consumed via v2) lets us also pre-fetch `epoch + 1`; * pre-Gloas the next epoch's dep_root only stabilizes at the boundary and is handled by * `runEverySlotTask`. * * Mid-epoch refreshes (e.g. reorgs) are driven by `onNewHead` instead of polling every slot. */ private runEveryEpochTask; /** * Slot-tick handler. Notifies block production for cached proposers in this slot, and on * the last slot of a pre-Gloas epoch schedules the boundary fetch for `nextEpoch` duties. * Reorg detection is handled by `onNewHead`, so this task does not re-poll on every slot. */ private runEverySlotTask; /** * SSE head-event handler. The beacon-API `head` event carries attester-duty dep_roots, * which coincide with the proposer dep_roots the VC caches. The offset depends on the * proposer-duties endpoint the VC uses, which is gated on Gloas (v1 before, v2 after): * * Pre-Gloas (v1 proposer dep_root(E) = block@startSlot(E) - 1): * currentDutyDependentRoot ≡ proposer_dep_root(currentEpoch) * (next-epoch proposer dep_root is not exposed; pre-Gloas falls back to the * `runEverySlotTask` boundary poll.) * * Post-Gloas (v2 proposer dep_root(E) = block@startSlot(E - 1) - 1, EIP-7917): * previousDutyDependentRoot ≡ proposer_dep_root(currentEpoch) * currentDutyDependentRoot ≡ proposer_dep_root(nextEpoch) * * On a dep_root mismatch (reorg, or initial sync delivering a fresher head) we refetch * just the affected epoch, mirroring `AttestationDutiesService.onNewHead`. */ private onNewHead; private refetchIfDepRootChanged; private pollBeaconProposersBeforeBoundary; /** * Notify block production for *newly discovered* proposers in this slot. Notifications are * deduplicated per-slot so that a late SSE refetch can extend the proposer set without * triggering a duplicate `createAndPublishBlock` for already-notified validators. * * ## Multi-notification safety * * Within a single slot the cache can be updated from several sources (cold-cache backfill at * startup, SSE-driven reorg refetch). Each update may fire this function again. The contract * we keep is: each pubkey is notified *at most once per slot*. The additional notifications * only carry proposers that were not part of an earlier notification. * * Is this safe? Firstly, the dedup above guarantees we never ask the same validator to * propose twice for the same slot. Secondly, slashing protection in `ValidatorStore` acts as * a second line of defense should the dedup ever fail. Together they provide an acceptable * level of safety for the "notify-from-cache, refine-after-refetch" pattern. */ private notifyProposersForSlot; private pollBeaconProposers; /** Run once per epoch to prune `this.proposers` map */ private pruneOldDuties; } export {}; //# sourceMappingURL=blockDuties.d.ts.map