UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

256 lines • 11.8 kB
import { pruneSetToMax, toRootHex } from "@lodestar/utils"; import { ZERO_HASH_HEX } from "../constants/index.js"; import { enumToIndexMap } from "../util/enum.js"; import { dataToRootHex, quantityToBigint, quantityToNum } from "./provider/utils.js"; export var StatusCode; (function (StatusCode) { StatusCode["STOPPED"] = "STOPPED"; StatusCode["SEARCHING"] = "SEARCHING"; StatusCode["FOUND"] = "FOUND"; })(StatusCode || (StatusCode = {})); /** For metrics, index order = declaration order of StatusCode */ const statusCodeIdx = enumToIndexMap(StatusCode); /** * Bounds `blocksByHashCache` cache, imposing a max distance between highest and lowest block numbers. * In case of extreme forking the cache might grow unbounded. */ const MAX_CACHE_POW_BLOCKS = 1024; const MAX_TD_RENDER_VALUE = Number.MAX_SAFE_INTEGER; // get_pow_block_at_total_difficulty /** * Follows the eth1 chain to find a (or multiple?) merge blocks that cross the threshold of total terminal difficulty * * Finding the mergeBlock could be done in demand when proposing pre-merge blocks. However, that would slow block * production during the weeks between BELLATRIX_EPOCH and TTD. */ export class Eth1MergeBlockTracker { constructor({ config, logger, signal, metrics }, eth1Provider) { this.eth1Provider = eth1Provider; this.blocksByHashCache = new Map(); this.intervals = []; this.latestEth1Block = null; this.getTerminalPowBlockFromEth1Promise = null; this.config = config; this.logger = logger; this.metrics = metrics; this.status = { code: StatusCode.STOPPED }; signal.addEventListener("abort", () => this.close(), { once: true }); this.safeTDFactor = getSafeTDFactor(this.config.TERMINAL_TOTAL_DIFFICULTY); const scaledTTD = this.config.TERMINAL_TOTAL_DIFFICULTY / this.safeTDFactor; // Only run metrics if necessary if (metrics) { // TTD can't be dynamically changed during execution, register metric once metrics.eth1.eth1MergeTTD.set(Number(scaledTTD)); metrics.eth1.eth1MergeTDFactor.set(Number(this.safeTDFactor)); metrics.eth1.eth1MergeStatus.addCollect(() => { // Set merge ttd, merge status and merge block status metrics.eth1.eth1MergeStatus.set(statusCodeIdx[this.status.code]); if (this.latestEth1Block !== null) { // Set latestBlock stats metrics.eth1.eth1LatestBlockNumber.set(this.latestEth1Block.number); metrics.eth1.eth1LatestBlockTD.set(Number(this.latestEth1Block.totalDifficulty / this.safeTDFactor)); metrics.eth1.eth1LatestBlockTimestamp.set(this.latestEth1Block.timestamp); } }); } } /** * Returns the most recent POW block that satisfies the merge block condition */ async getTerminalPowBlock() { switch (this.status.code) { case StatusCode.STOPPED: // If not module is not polling fetch the mergeBlock explicitly return this.getTerminalPowBlockFromEth1(); case StatusCode.SEARCHING: // Assume that polling would have found the block return null; case StatusCode.FOUND: return this.status.mergeBlock; } } getTDProgress() { if (this.latestEth1Block === null) { return this.latestEth1Block; } const tdDiff = this.config.TERMINAL_TOTAL_DIFFICULTY - this.latestEth1Block.totalDifficulty; if (tdDiff > BigInt(0)) { return { ttdHit: false, tdFactor: this.safeTDFactor, tdDiffScaled: Number((tdDiff / this.safeTDFactor)), ttd: this.config.TERMINAL_TOTAL_DIFFICULTY, td: this.latestEth1Block.totalDifficulty, timestamp: this.latestEth1Block.timestamp, }; } return { ttdHit: true, }; } /** * Get a POW block by hash checking the local cache first */ async getPowBlock(powBlockHash) { // Check cache first const cachedBlock = this.blocksByHashCache.get(powBlockHash); if (cachedBlock) { return cachedBlock; } // Fetch from node const blockRaw = await this.eth1Provider.getBlockByHash(powBlockHash); if (blockRaw) { const block = toPowBlock(blockRaw); this.cacheBlock(block); return block; } return null; } /** * Should only start polling for mergeBlock if: * - after BELLATRIX_FORK_EPOCH * - Beacon node synced * - head state not isMergeTransitionComplete */ startPollingMergeBlock() { if (this.status.code !== StatusCode.STOPPED) { return; } this.status = { code: StatusCode.SEARCHING }; this.logger.info("Starting search for terminal POW block", { TERMINAL_TOTAL_DIFFICULTY: this.config.TERMINAL_TOTAL_DIFFICULTY, }); const interval = setInterval(() => { // Preemptively try to find merge block and cache it if found. // Future callers of getTerminalPowBlock() will re-use the cached found mergeBlock. this.getTerminalPowBlockFromEth1().catch((e) => { this.logger.error("Error on findMergeBlock", {}, e); this.metrics?.eth1.eth1PollMergeBlockErrors.inc(); }); }, this.config.SECONDS_PER_ETH1_BLOCK * 1000); this.intervals.push(interval); } close() { this.intervals.forEach(clearInterval); } async getTerminalPowBlockFromEth1() { if (!this.getTerminalPowBlockFromEth1Promise) { this.getTerminalPowBlockFromEth1Promise = this.internalGetTerminalPowBlockFromEth1() .then((mergeBlock) => { // Persist found merge block here to affect both caller paths: // - internal searcher // - external caller if STOPPED if (mergeBlock && this.status.code !== StatusCode.FOUND) { if (this.status.code === StatusCode.SEARCHING) { this.close(); } this.logger.info("Terminal POW block found!", { hash: mergeBlock.blockHash, number: mergeBlock.number, totalDifficulty: mergeBlock.totalDifficulty, }); this.status = { code: StatusCode.FOUND, mergeBlock }; this.metrics?.eth1.eth1MergeBlockDetails.set({ terminalBlockHash: mergeBlock.blockHash, // Convert all number/bigints to string labels terminalBlockNumber: mergeBlock.number.toString(10), terminalBlockTD: mergeBlock.totalDifficulty.toString(10), }, 1); } return mergeBlock; }) .finally(() => { this.getTerminalPowBlockFromEth1Promise = null; }); } else { // This should no happen, since getTerminalPowBlockFromEth1() should resolve faster than SECONDS_PER_ETH1_BLOCK. // else something is wrong: the el-cl comms are two slow, or the backsearch got stuck in a deep search. this.metrics?.eth1.getTerminalPowBlockPromiseCacheHit.inc(); } return this.getTerminalPowBlockFromEth1Promise; } /** * **internal** + **unsafe** since it can create multiple backward searches that overload the eth1 client. * Must be called in a wrapper to ensure that there's only once concurrent call to this fn. */ async internalGetTerminalPowBlockFromEth1() { // Search merge block by hash // Terminal block hash override takes precedence over terminal total difficulty const terminalBlockHash = toRootHex(this.config.TERMINAL_BLOCK_HASH); if (terminalBlockHash !== ZERO_HASH_HEX) { const block = await this.getPowBlock(terminalBlockHash); if (block) { return block; } // if a TERMINAL_BLOCK_HASH other than ZERO_HASH is configured and we can't find it, return NONE return null; } // Search merge block by TTD const latestBlockRaw = await this.eth1Provider.getBlockByNumber("latest"); if (!latestBlockRaw) { throw Error("getBlockByNumber('latest') returned null"); } let block = toPowBlock(latestBlockRaw); this.latestEth1Block = { ...block, timestamp: quantityToNum(latestBlockRaw.timestamp) }; this.cacheBlock(block); // This code path to look backwards for the merge block is only necessary if: // - The network has not yet found the merge block // - There are descendants of the merge block in the eth1 chain // For the search below to require more than a few hops, multiple block proposers in a row must fail to detect // an existing merge block. Such situation is extremely unlikely, so this search is left un-optimized. Since // this class can start eagerly looking for the merge block when not necessary, startPollingMergeBlock() should // only be called when there is certainty that a mergeBlock search is necessary. while (true) { if (block.totalDifficulty < this.config.TERMINAL_TOTAL_DIFFICULTY) { // TTD not reached yet return null; } // else block.totalDifficulty >= this.config.TERMINAL_TOTAL_DIFFICULTY // Potential mergeBlock! Must find the first block that passes TTD // Allow genesis block to reach TTD https://github.com/ethereum/consensus-specs/pull/2719 if (block.parentHash === ZERO_HASH_HEX) { return block; } const parent = await this.getPowBlock(block.parentHash); if (!parent) { throw Error(`Unknown parent of block with TD>TTD ${block.parentHash}`); } this.metrics?.eth1.eth1ParentBlocksFetched.inc(); // block.td > TTD && parent.td < TTD => block is mergeBlock if (parent.totalDifficulty < this.config.TERMINAL_TOTAL_DIFFICULTY) { // Is terminal total difficulty block AND has verified block -> parent relationship return block; } block = parent; } } cacheBlock(block) { this.blocksByHashCache.set(block.blockHash, block); pruneSetToMax(this.blocksByHashCache, MAX_CACHE_POW_BLOCKS); } } export function toPowBlock(block) { // Validate untrusted data from API return { number: quantityToNum(block.number), blockHash: dataToRootHex(block.hash), parentHash: dataToRootHex(block.parentHash), totalDifficulty: quantityToBigint(block.totalDifficulty), }; } /** * TTD values can be very large, for xDAI > 1e45. So scale down. * To be good, TTD should be rendered as a number < Number.MAX_TD_RENDER_VALUE ~= 9e15 */ export function getSafeTDFactor(ttd) { const safeIntegerMult = ttd / BigInt(MAX_TD_RENDER_VALUE); // TTD < MAX_TD_RENDER_VALUE, no need to scale down if (safeIntegerMult === BigInt(0)) { return BigInt(1); } // Return closest power of 10 to ensure TD < max const safeIntegerMultDigits = safeIntegerMult.toString(10).length; return BigInt(10) ** BigInt(safeIntegerMultDigits); } //# sourceMappingURL=eth1MergeBlockTracker.js.map