@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
256 lines • 11.8 kB
JavaScript
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