import {Root, phase0} from "@lodestar/types"; import {binarySearchLte} from "../../util/binarySearch.js"; import {Eth1Error, Eth1ErrorCode} from "../errors.js"; import {DepositTree} from "../../db/repositories/depositDataRoot.js"; import {Eth1Block} from "../interface.js"; type BlockNumber = number; /** * Appends partial eth1 data (depositRoot, depositCount) in a sequence of blocks * eth1 data deposit is inferred from sparse eth1 data obtained from the deposit logs */ export async function getEth1DataForBlocks( blocks: Eth1Block[], depositDescendingStream: AsyncIterable, depositRootTree: DepositTree, lastProcessedDepositBlockNumber: BlockNumber | null ): Promise<(phase0.Eth1Data & Eth1Block)[]> { // Exclude blocks for which there is no valid eth1 data deposit if (lastProcessedDepositBlockNumber !== null) { blocks = blocks.filter((block) => block.blockNumber <= lastProcessedDepositBlockNumber); } // A valid block can be constructed using previous `state.eth1Data`, don't throw if (blocks.length === 0) { return []; } // Collect the latest deposit of each blockNumber in a block number range const fromBlock = blocks[0].blockNumber; const toBlock = blocks[blocks.length - 1].blockNumber; const depositsByBlockNumber = await getDepositsByBlockNumber(fromBlock, toBlock, depositDescendingStream); if (depositsByBlockNumber.length === 0) { throw new Eth1Error({code: Eth1ErrorCode.NO_DEPOSITS_FOR_BLOCK_RANGE, fromBlock, toBlock}); } // Precompute a map of depositCount => depositRoot (from depositRootTree) const depositCounts = depositsByBlockNumber.map((event) => event.index + 1); const depositRootByDepositCount = getDepositRootByDepositCount(depositCounts, depositRootTree); const eth1Datas: (phase0.Eth1Data & Eth1Block)[] = []; for (const block of blocks) { const deposit = binarySearchLte(depositsByBlockNumber, block.blockNumber, (event) => event.blockNumber); const depositCount = deposit.index + 1; const depositRoot = depositRootByDepositCount.get(depositCount); if (depositRoot === undefined) { throw new Eth1Error({code: Eth1ErrorCode.NO_DEPOSIT_ROOT, depositCount}); } eth1Datas.push({...block, depositCount, depositRoot}); } return eth1Datas; } /** * Collect depositCount by blockNumber from a stream matching a block number range * For a given blockNumber it's depositCount is equal to the index + 1 of the * closest deposit event whose deposit.blockNumber <= blockNumber * @returns array ascending by blockNumber */ export async function getDepositsByBlockNumber( fromBlock: BlockNumber, toBlock: BlockNumber, depositEventDescendingStream: AsyncIterable ): Promise { const depositCountMap = new Map(); // Take blocks until the block under the range lower bound (included) for await (const deposit of depositEventDescendingStream) { if (deposit.blockNumber <= toBlock && !depositCountMap.has(deposit.blockNumber)) { depositCountMap.set(deposit.blockNumber, deposit); } if (deposit.blockNumber < fromBlock) { break; } } return Array.from(depositCountMap.values()).sort((a, b) => a.blockNumber - b.blockNumber); } /** * Precompute a map of depositCount => depositRoot from a depositRootTree filled beforehand */ export function getDepositRootByDepositCount(depositCounts: number[], depositRootTree: DepositTree): Map { // Unique + sort numerically in descending order depositCounts = [...new Set(depositCounts)].sort((a, b) => b - a); if (depositCounts.length > 0) { const maxIndex = depositCounts[0] - 1; const treeLength = depositRootTree.length - 1; if (maxIndex > treeLength) { throw new Eth1Error({code: Eth1ErrorCode.NOT_ENOUGH_DEPOSIT_ROOTS, index: maxIndex, treeLength}); } } const depositRootByDepositCount = new Map(); for (const depositCount of depositCounts) { depositRootTree = depositRootTree.sliceTo(depositCount - 1); depositRootByDepositCount.set(depositCount, depositRootTree.hashTreeRoot()); } return depositRootByDepositCount; }