@lodestar/prover
Version:
A Typescript implementation of the Ethereum Consensus light client
224 lines (184 loc) • 7.82 kB
text/typescript
import {ApiClient} from "@lodestar/api";
import {ForkName} from "@lodestar/params";
import {ExecutionPayload, LightClientHeader} from "@lodestar/types";
import {Logger} from "@lodestar/utils";
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";
type BlockELRoot = string;
type BlockELRootAndSlot = {
blockELRoot: BlockELRoot;
slot: number;
};
type BlockCLRoot = string;
/**
* The in-memory store for the execution payloads to be used to verify the proofs
*/
export class PayloadStore {
// We store the block root from execution for finalized blocks
// As these blocks are finalized, so not to be worried about conflicting roots
private finalizedRoots = new OrderedMap<BlockELRootAndSlot>();
// 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
private unfinalizedRoots = new Map<BlockCLRoot, BlockELRoot>();
// Payloads store with BlockELRoot as key
private payloads = new Map<BlockELRoot, ExecutionPayload>();
private latestBlockRoot: BlockELRoot | null = null;
constructor(private opts: {api: ApiClient; logger: Logger}) {}
get finalized(): ExecutionPayload | undefined {
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(): ExecutionPayload | undefined {
if (this.latestBlockRoot) {
return this.payloads.get(this.latestBlockRoot);
}
return undefined;
}
async get(blockId: number | string): Promise<ExecutionPayload | undefined> {
// 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;
}
protected async getOrFetchFinalizedPayload(blockNumber: number): Promise<ExecutionPayload | undefined> {
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: ExecutionPayload, slot: number, finalized: boolean): void {
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: LightClientHeader<ForkName.capella>, finalized = false): Promise<void> {
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(): void {
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);
}
}
}
}