UNPKG

@lodestar/prover

Version:

A Typescript implementation of the Ethereum Consensus light client

176 lines 7.92 kB
import { MAX_PAYLOAD_HISTORY } from "../constants.js"; import { fetchBlock, getExecutionPayloadForBlockNumber } from "../utils/consensus.js"; import { bufferToHex, hexToNumber } from "../utils/conversion.js"; import { OrderedMap } from "./ordered_map.js"; /** * The in-memory store for the execution payloads to be used to verify the proofs */ export class PayloadStore { constructor(opts) { this.opts = opts; // We store the block root from execution for finalized blocks // As these blocks are finalized, so not to be worried about conflicting roots this.finalizedRoots = new OrderedMap(); // Unfinalized blocks may change over time and may have conflicting roots // We can receive multiple light-client headers for the same block of execution // So we why store unfinalized payloads by their CL root, which is only used // in processing the light-client headers this.unfinalizedRoots = new Map(); // Payloads store with BlockELRoot as key this.payloads = new Map(); this.latestBlockRoot = null; } get finalized() { const maxBlockNumberForFinalized = this.finalizedRoots.max; if (maxBlockNumberForFinalized === undefined) { return undefined; } const finalizedMaxRoot = this.finalizedRoots.get(maxBlockNumberForFinalized); if (finalizedMaxRoot) { return this.payloads.get(finalizedMaxRoot.blockELRoot); } return undefined; } get latest() { if (this.latestBlockRoot) { return this.payloads.get(this.latestBlockRoot); } return undefined; } async get(blockId) { // Given block id is a block hash in hex (32 bytes root takes 64 hex chars + 2 for 0x prefix) if (typeof blockId === "string" && blockId.startsWith("0x") && blockId.length === 64 + 2) { return this.payloads.get(blockId); } // Given block id is a block number in hex if (typeof blockId === "string" && blockId.startsWith("0x")) { return this.getOrFetchFinalizedPayload(hexToNumber(blockId)); } // Given block id is a block number in decimal string if (typeof blockId === "string" && !blockId.startsWith("0x")) { return this.getOrFetchFinalizedPayload(parseInt(blockId, 10)); } // Given block id is a block number in decimal if (typeof blockId === "number") { return this.getOrFetchFinalizedPayload(blockId); } return undefined; } async getOrFetchFinalizedPayload(blockNumber) { const maxBlockNumberForFinalized = this.finalizedRoots.max; const minBlockNumberForFinalized = this.finalizedRoots.min; if (maxBlockNumberForFinalized === undefined || minBlockNumberForFinalized === undefined) { return; } if (blockNumber > maxBlockNumberForFinalized) { throw new Error(`Block number ${blockNumber} is higher than the latest finalized block number. We recommend to use block hash for unfinalized blocks.`); } let blockELRoot = this.finalizedRoots.get(blockNumber); // check if we have payload cached locally else fetch from api if (!blockELRoot) { const finalizedMaxRoot = this.finalizedRoots.get(maxBlockNumberForFinalized); const slot = finalizedMaxRoot?.slot; if (slot !== undefined) { const payloads = await getExecutionPayloadForBlockNumber(this.opts.api, slot, blockNumber); for (const [slot, payload] of payloads.entries()) { this.set(payload, slot, true); } } } blockELRoot = this.finalizedRoots.get(blockNumber); if (blockELRoot) { return this.payloads.get(blockELRoot.blockELRoot); } return undefined; } set(payload, slot, finalized) { const blockELRoot = bufferToHex(payload.blockHash); this.payloads.set(blockELRoot, payload); if (this.latestBlockRoot) { const latestPayload = this.payloads.get(this.latestBlockRoot); if (latestPayload && latestPayload.blockNumber < payload.blockNumber) { this.latestBlockRoot = blockELRoot; } } else { this.latestBlockRoot = blockELRoot; } if (finalized) { this.finalizedRoots.set(payload.blockNumber, { blockELRoot, slot }); } } async processLCHeader(header, finalized = false) { const blockSlot = header.beacon.slot; const blockNumber = header.execution.blockNumber; const blockELRoot = bufferToHex(header.execution.blockHash); const blockCLRoot = bufferToHex(header.beacon.stateRoot); const existingELRoot = this.unfinalizedRoots.get(blockCLRoot); // ==== Finalized blocks ==== // if the block is finalized, we need to update the finalizedRoots map if (finalized) { this.finalizedRoots.set(blockNumber, { blockELRoot, slot: blockSlot }); // If the block is finalized and we already have the payload // We can remove it from the unfinalizedRoots map and do nothing else if (existingELRoot) { this.unfinalizedRoots.delete(blockCLRoot); } // If the block is finalized and we do not have the payload // We need to fetch and set the payload else { const block = await fetchBlock(this.opts.api, blockSlot); if (block) { this.payloads.set(blockELRoot, block.message.body.executionPayload); } else { this.opts.logger.error("Failed to fetch block", blockSlot); } } return; } // ==== Unfinalized blocks ==== // We already have the payload for this block if (existingELRoot && existingELRoot === blockELRoot) { return; } // Re-org happened, we need to update the payload if (existingELRoot && existingELRoot !== blockELRoot) { this.payloads.delete(existingELRoot); } // This is unfinalized header we need to store it's root related to cl root this.unfinalizedRoots.set(blockCLRoot, blockELRoot); // We do not have the payload for this block, we need to fetch it const block = await fetchBlock(this.opts.api, blockSlot); if (block) { this.set(block.message.body.executionPayload, blockSlot, false); } else { this.opts.logger.error("Failed to fetch finalized block", blockSlot); } this.prune(); } prune() { if (this.finalizedRoots.size <= MAX_PAYLOAD_HISTORY) return; // Store doe not have any finalized blocks means it's recently initialized if (this.finalizedRoots.max === undefined || this.finalizedRoots.min === undefined) return; for (let blockNumber = this.finalizedRoots.max - MAX_PAYLOAD_HISTORY; blockNumber >= this.finalizedRoots.min; blockNumber--) { const blockELRoot = this.finalizedRoots.get(blockNumber); if (blockELRoot) { this.payloads.delete(blockELRoot.blockELRoot); this.finalizedRoots.delete(blockNumber); } } for (const [clRoot, elRoot] of this.unfinalizedRoots) { const payload = this.payloads.get(elRoot); if (!payload) { this.unfinalizedRoots.delete(clRoot); continue; } if (payload.blockNumber < this.finalizedRoots.min) { this.unfinalizedRoots.delete(clRoot); } } } } //# sourceMappingURL=payload_store.js.map