UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

556 lines • 28.3 kB
import { routes } from "@lodestar/api"; import { isBetterUpdate, toLightClientUpdateSummary, upgradeLightClientHeader, } from "@lodestar/light-client/spec"; import { ForkSeq, MIN_SYNC_COMMITTEE_PARTICIPANTS, SYNC_COMMITTEE_SIZE, forkPostAltair, highestFork, isForkPostElectra, } from "@lodestar/params"; import { computeStartSlotAtEpoch, computeSyncPeriodAtEpoch, computeSyncPeriodAtSlot, executionPayloadToPayloadHeader, } from "@lodestar/state-transition"; import { ssz, sszTypesFor, } from "@lodestar/types"; import { MapDef, pruneSetToMax, toRootHex } from "@lodestar/utils"; import { ZERO_HASH } from "../../constants/index.js"; import { NUM_WITNESS, NUM_WITNESS_ELECTRA } from "../../db/repositories/lightclientSyncCommitteeWitness.js"; import { byteArrayEquals } from "../../util/bytes.js"; import { LightClientServerError, LightClientServerErrorCode } from "../errors/lightClientError.js"; import { getBlockBodyExecutionHeaderProof, getCurrentSyncCommitteeBranch, getFinalizedRootProof, getNextSyncCommitteeBranch, getSyncCommitteesWitness, } from "./proofs.js"; const MAX_CACHED_FINALIZED_HEADERS = 3; const MAX_PREV_HEAD_DATA = 32; /** * Compute and cache "init" proofs as the chain advances. * Will compute proofs for: * - All finalized blocks * - All non-finalized checkpoint blocks * * Params: * - How many epochs ago do you consider a re-org can happen? 10 * - How many consecutive slots in a epoch you consider can be skipped? 32 * * ### What data to store? * * An altair beacon state has 24 fields, with a depth of 5. * | field | gindex | index | * | --------------------- | ------ | ----- | * | finalizedCheckpoint | 52 | 20 | * | currentSyncCommittee | 54 | 22 | * | nextSyncCommittee | 55 | 23 | * * Fields `currentSyncCommittee` and `nextSyncCommittee` are contiguous fields. Since they change its * more optimal to only store the witnesses different blocks of interest. * * ```ts * SyncCommitteeWitness = Container({ * witness: Vector[Bytes32, 4], * currentSyncCommitteeRoot: Bytes32, * nextSyncCommitteeRoot: Bytes32, * }) * ``` * * To produce finalized light-client updates, need the FinalizedCheckpointWitness + the finalized header the checkpoint * points too. It's cheaper to send a full BeaconBlockHeader `3*32 + 2*8` than a proof to `state_root` `(3+1)*32`. * * ```ts * FinalizedCheckpointWitness = Container({ * witness: Vector[Bytes32, 5], * root: Bytes32, * epoch: Epoch, * }) * ``` * * ### When to store data? * * Lightclient servers don't really need to support serving data for light-client at all possible roots to have a * functional use-case. * - For init proofs light-clients will probably use a finalized weak-subjectivity checkpoint * - For sync updates, light-clients need any update within a given period * * Fully tree-backed states are not guaranteed to be available at any time but just after processing a block. Then, * the server must pre-compute all data for all blocks until there's certainity of what block becomes a checkpoint * and which blocks doesn't. * * - SyncAggregate -> ParentBlock -> FinalizedCheckpoint -> nextSyncCommittee * * After importing a new block + postState: * - Persist SyncCommitteeWitness, indexed by block root of state's witness, always * - Persist currentSyncCommittee, indexed by hashTreeRoot, once (not necessary after the first run) * - Persist nextSyncCommittee, indexed by hashTreeRoot, for each period + dependentRoot * - Persist FinalizedCheckpointWitness only if checkpoint period = syncAggregate period * * TODO: Prune strategy: * - [Low value] On finalized or in finalized lookup, prune SyncCommittee that's not finalized * - [High value] After some time prune un-used FinalizedCheckpointWitness + finalized headers * - [High value] After some time prune to-be-checkpoint items that will never become checkpoints * - After sync period is over all pending headers are useless * * !!! BEST = finalized + highest bit count + oldest (less chance of re-org, less writes) * * Then when light-client requests the best finalized update at period N: * - Fetch best finalized SyncAggregateHeader in period N * - Fetch FinalizedCheckpointWitness at that header's block root * - Fetch SyncCommitteeWitness at that FinalizedCheckpointWitness.header.root * - Fetch SyncCommittee at that SyncCommitteeWitness.nextSyncCommitteeRoot * * When light-client request best non-finalized update at period N: * - Fetch best non-finalized SyncAggregateHeader in period N * - Fetch SyncCommitteeWitness at that SyncAggregateHeader.header.root * - Fetch SyncCommittee at that SyncCommitteeWitness.nextSyncCommitteeRoot * * ``` * Finalized Block Sync * Checkpoint Header Aggreate * ----------------------|-----------------------|-------|---------> time * <--------------------- <---- * finalizes signs * ``` * * ### What's the cost of this data? * * To estimate the data costs, let's analyze monthly. Yearly may not make sense due to weak subjectivity: * - 219145 slots / month * - 6848 epochs / month * - 27 sync periods / month * * The byte size of a SyncCommittee (mainnet preset) is fixed to `48 * (512 + 1) = 24624`. So with SyncCommittee only * the data cost to store them is `24624 * 27 = 664848` ~ 0.6 MB/m. * * Storing 4 witness per block costs `219145 * 4 * 32 = 28050560 ~ 28 MB/m`. * Storing 4 witness per epoch costs `6848 * 4 * 32 = 876544 ~ 0.9 MB/m`. */ export class LightClientServer { constructor(opts, modules) { this.opts = opts; this.knownSyncCommittee = new MapDef(() => new Set()); this.storedCurrentSyncCommittee = false; /** * Keep in memory since this data is very transient, not useful after a few slots */ this.prevHeadData = new Map(); this.checkpointHeaders = new Map(); this.latestHeadUpdate = null; this.finalized = null; const { config, db, metrics, emitter, logger } = modules; this.config = config; this.db = db; this.metrics = metrics; this.emitter = emitter; this.logger = logger; this.zero = { // Assign the hightest fork's default value because it can always be typecasted down to correct fork finalizedHeader: sszTypesFor(highestFork(forkPostAltair)).LightClientHeader.defaultValue(), // Electra finalityBranch has fixed length of 5 whereas altair has 4. The fifth element will be ignored // when serializing as altair LightClientUpdate finalityBranch: ssz.electra.LightClientUpdate.fields.finalityBranch.defaultValue(), }; if (metrics) { metrics.lightclientServer.highestSlot.addCollect(() => { if (this.latestHeadUpdate) { metrics.lightclientServer.highestSlot.set({ item: "latest_head_update" }, this.latestHeadUpdate.attestedHeader.beacon.slot); } if (this.finalized) { metrics.lightclientServer.highestSlot.set({ item: "latest_finalized_update" }, this.finalized.attestedHeader.beacon.slot); } }); } } /** * Call after importing a block head, having the postState available in memory for proof generation. * - Persist state witness * - Use block's syncAggregate */ onImportBlockHead(block, postState, parentBlockSlot) { // TEMP: To disable this functionality for fork_choice spec tests. // Since the tests have deep-reorgs attested data is not available often printing lots of error logs. // While this function is only called for head blocks, best to disable. if (this.opts.disableLightClientServerOnImportBlockHead) { return; } // What is the syncAggregate signing? // From the state-transition // ``` // const previousSlot = Math.max(block.slot, 1) - 1; // const rootSigned = getBlockRootAtSlot(state, previousSlot); // ``` // In skipped slots the next value of blockRoots is set to the last block root. // So rootSigned will always equal to the parentBlock. const signedBlockRoot = block.parentRoot; const syncPeriod = computeSyncPeriodAtSlot(block.slot); this.onSyncAggregate(syncPeriod, block.body.syncAggregate, block.slot, signedBlockRoot).catch((e) => { this.logger.error("Error onSyncAggregate", {}, e); this.metrics?.lightclientServer.onSyncAggregate.inc({ event: "error" }); }); this.persistPostBlockImportData(block, postState, parentBlockSlot).catch((e) => { this.logger.error("Error persistPostBlockImportData", {}, e); }); } /** * API ROUTE to get `currentSyncCommittee` and `nextSyncCommittee` from a trusted state root */ async getBootstrap(blockRoot) { const syncCommitteeWitness = await this.db.syncCommitteeWitness.get(blockRoot); if (!syncCommitteeWitness) { throw new LightClientServerError({ code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE }, `syncCommitteeWitness not available ${toRootHex(blockRoot)}`); } const [currentSyncCommittee, nextSyncCommittee] = await Promise.all([ this.db.syncCommittee.get(syncCommitteeWitness.currentSyncCommitteeRoot), this.db.syncCommittee.get(syncCommitteeWitness.nextSyncCommitteeRoot), ]); if (!currentSyncCommittee) { throw new LightClientServerError({ code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE }, "currentSyncCommittee not available"); } if (!nextSyncCommittee) { throw new LightClientServerError({ code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE }, "nextSyncCommittee not available"); } const header = await this.db.checkpointHeader.get(blockRoot); if (!header) { throw new LightClientServerError({ code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE }, "header not available"); } return { header, currentSyncCommittee, currentSyncCommitteeBranch: getCurrentSyncCommitteeBranch(syncCommitteeWitness), }; } /** * API ROUTE to get the best available update for `period` to transition to the next sync committee. * Criteria for best in priority order: * - Is finalized * - Has the most bits * - Signed header at the oldest slot */ async getUpdate(period) { // Signature data const update = await this.db.bestLightClientUpdate.get(period); if (!update) { throw Error(`No partialUpdate available for period ${period}`); } return update; } /** * API ROUTE to get the sync committee hash from the best available update for `period`. */ async getCommitteeRoot(period) { const { attestedHeader } = await this.getUpdate(period); const blockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(attestedHeader.beacon); const syncCommitteeWitness = await this.db.syncCommitteeWitness.get(blockRoot); if (!syncCommitteeWitness) { throw new LightClientServerError({ code: LightClientServerErrorCode.RESOURCE_UNAVAILABLE }, `syncCommitteeWitness not available ${toRootHex(blockRoot)} period ${period}`); } return syncCommitteeWitness.currentSyncCommitteeRoot; } /** * API ROUTE to poll LightclientHeaderUpdate. * Clients should use the SSE type `light_client_optimistic_update` if available */ getOptimisticUpdate() { return this.latestHeadUpdate; } getFinalityUpdate() { return this.finalized; } /** * With forkchoice data compute which block roots will never become checkpoints and prune them. */ async pruneNonCheckpointData(nonCheckpointBlockRoots) { // TODO: Batch delete with native leveldb batching not just Promise.all() await Promise.all([ this.db.syncCommitteeWitness.batchDelete(nonCheckpointBlockRoots), this.db.checkpointHeader.batchDelete(nonCheckpointBlockRoots), ]); } async persistPostBlockImportData(block, postState, parentBlockSlot) { const blockSlot = block.slot; const fork = this.config.getForkName(blockSlot); const header = blockToLightClientHeader(fork, block); const blockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(header.beacon); const blockRootHex = toRootHex(blockRoot); const syncCommitteeWitness = getSyncCommitteesWitness(fork, postState); // Only store current sync committee once per run if (!this.storedCurrentSyncCommittee) { await Promise.all([ this.storeSyncCommittee(postState.currentSyncCommittee, syncCommitteeWitness.currentSyncCommitteeRoot), this.storeSyncCommittee(postState.nextSyncCommittee, syncCommitteeWitness.nextSyncCommitteeRoot), ]); this.storedCurrentSyncCommittee = true; this.logger.debug("Stored currentSyncCommittee", { slot: blockSlot }); } // Only store next sync committee once per dependent root const parentBlockPeriod = computeSyncPeriodAtSlot(parentBlockSlot); const period = computeSyncPeriodAtSlot(blockSlot); if (parentBlockPeriod < period) { // If the parentBlock is in a previous epoch it must be the dependentRoot of this epoch transition const dependentRoot = toRootHex(block.parentRoot); const periodDependentRoots = this.knownSyncCommittee.getOrDefault(period); if (!periodDependentRoots.has(dependentRoot)) { periodDependentRoots.add(dependentRoot); await this.storeSyncCommittee(postState.nextSyncCommittee, syncCommitteeWitness.nextSyncCommitteeRoot); this.logger.debug("Stored nextSyncCommittee", { period, slot: blockSlot, dependentRoot }); } } // Ensure referenced syncCommittee are persisted before persiting this one await this.db.syncCommitteeWitness.put(blockRoot, syncCommitteeWitness); // Store header in case it is referenced latter by a future finalized checkpoint await this.db.checkpointHeader.put(blockRoot, header); // Store finalized checkpoint data const finalizedCheckpoint = postState.finalizedCheckpoint; const finalizedCheckpointPeriod = computeSyncPeriodAtEpoch(finalizedCheckpoint.epoch); const isFinalized = finalizedCheckpointPeriod === period && // Consider the edge case of genesis: Genesis state's finalizedCheckpoint is zero'ed. // If finalizedCheckpoint is zeroed, consider not finalized (ignore) since there won't exist a // finalized header for that root finalizedCheckpoint.epoch !== 0 && !byteArrayEquals(finalizedCheckpoint.root, ZERO_HASH); this.prevHeadData.set(blockRootHex, isFinalized ? { isFinalized: true, attestedHeader: header, blockRoot, finalityBranch: getFinalizedRootProof(postState), finalizedCheckpoint, } : { isFinalized: false, attestedHeader: header, blockRoot, }); pruneSetToMax(this.prevHeadData, MAX_PREV_HEAD_DATA); } /** * 1. Subscribe to gossip topics `sync_committee_{subnet_id}` and collect `sync_committee_message` * ``` * slot: Slot * beacon_block_root: Root * validator_index: ValidatorIndex * signature: BLSSignature * ``` * * 2. Subscribe to `sync_committee_contribution_and_proof` and collect `signed_contribution_and_proof` * ``` * slot: Slot * beacon_block_root: Root * subcommittee_index: uint64 * aggregation_bits: Bitvector[SYNC_COMMITTEE_SIZE // SYNC_COMMITTEE_SUBNET_COUNT] * signature: BLSSignature * ``` * * 3. On new blocks use `block.body.sync_aggregate`, `block.parent_root` and `block.slot - 1` * * @param syncPeriod The sync period of the sync aggregate and signed block root */ async onSyncAggregate(syncPeriod, syncAggregate, signatureSlot, signedBlockRoot) { this.metrics?.lightclientServer.onSyncAggregate.inc({ event: "processed" }); const signedBlockRootHex = toRootHex(signedBlockRoot); const attestedData = this.prevHeadData.get(signedBlockRootHex); if (!attestedData) { // Log cacheSize since at start this.prevHeadData will be empty this.logger.debug("attestedData not available", { root: signedBlockRootHex, cacheSize: this.prevHeadData.size }); this.metrics?.lightclientServer.onSyncAggregate.inc({ event: "ignore_no_attested_data" }); return; } const { attestedHeader, isFinalized } = attestedData; const attestedPeriod = computeSyncPeriodAtSlot(attestedHeader.beacon.slot); if (syncPeriod !== attestedPeriod) { this.logger.debug("attested data period different than signature period", { syncPeriod, attestedPeriod }); this.metrics?.lightclientServer.onSyncAggregate.inc({ event: "ignore_attested_period_diff" }); return; } const headerUpdate = { attestedHeader, syncAggregate, signatureSlot, }; const syncAggregateParticipation = sumBits(syncAggregate.syncCommitteeBits); if (syncAggregateParticipation < MIN_SYNC_COMMITTEE_PARTICIPANTS) { this.logger.debug("sync committee below required MIN_SYNC_COMMITTEE_PARTICIPANTS", { syncPeriod, attestedPeriod, syncAggregateParticipation, }); this.metrics?.lightclientServer.onSyncAggregate.inc({ event: "ignore_sync_committee_low" }); return; } // Fork of LightClientOptimisticUpdate and LightClientFinalityUpdate is based off on attested header's fork const attestedFork = this.config.getForkName(attestedHeader.beacon.slot); // Emit update // Note: Always emit optimistic update even if we have emitted one with higher or equal attested_header.slot this.emitter.emit(routes.events.EventType.lightClientOptimisticUpdate, { version: attestedFork, data: headerUpdate, }); // Persist latest best update for getLatestHeadUpdate() // TODO: Once SyncAggregate are constructed from P2P too, count bits to decide "best" if (!this.latestHeadUpdate || attestedHeader.beacon.slot > this.latestHeadUpdate.attestedHeader.beacon.slot) { this.latestHeadUpdate = headerUpdate; this.metrics?.lightclientServer.onSyncAggregate.inc({ event: "update_latest_head_update" }); } if (isFinalized) { const finalizedCheckpointRoot = attestedData.finalizedCheckpoint.root; let finalizedHeader = await this.getFinalizedHeader(finalizedCheckpointRoot); if (finalizedHeader && (!this.finalized || finalizedHeader.beacon.slot > this.finalized.finalizedHeader.beacon.slot || syncAggregateParticipation > sumBits(this.finalized.syncAggregate.syncCommitteeBits))) { if (this.config.getForkName(finalizedHeader.beacon.slot) !== attestedFork) { finalizedHeader = upgradeLightClientHeader(this.config, attestedFork, finalizedHeader); } this.finalized = { attestedHeader, finalizedHeader, syncAggregate, finalityBranch: attestedData.finalityBranch, signatureSlot, }; this.metrics?.lightclientServer.onSyncAggregate.inc({ event: "update_latest_finalized_update" }); // Note: Ignores gossip rule to always emit finality_update with higher finalized_header.slot, for simplicity this.emitter.emit(routes.events.EventType.lightClientFinalityUpdate, { version: attestedFork, data: this.finalized, }); } } // Check if this update is better, otherwise ignore try { await this.maybeStoreNewBestUpdate(syncPeriod, syncAggregate, signatureSlot, attestedData); } catch (e) { this.logger.error("Error updating best LightClientUpdate", { syncPeriod, slot: attestedHeader.beacon.slot, blockRoot: toRootHex(attestedData.blockRoot) }, e); } } /** * Given a new `syncAggregate` maybe persist a new best partial update if its better than the current stored for * that sync period. */ async maybeStoreNewBestUpdate(syncPeriod, syncAggregate, signatureSlot, attestedData) { const prevBestUpdate = await this.db.bestLightClientUpdate.get(syncPeriod); const { attestedHeader } = attestedData; if (prevBestUpdate) { const prevBestUpdateSummary = toLightClientUpdateSummary(prevBestUpdate); const nextBestUpdate = { activeParticipants: sumBits(syncAggregate.syncCommitteeBits), attestedHeaderSlot: attestedHeader.beacon.slot, signatureSlot, // The actual finalizedHeader is fetched below. To prevent a DB read we approximate the actual slot. // If update is not finalized finalizedHeaderSlot does not matter (see is_better_update), so setting // to zero to set it some number. finalizedHeaderSlot: attestedData.isFinalized ? computeStartSlotAtEpoch(attestedData.finalizedCheckpoint.epoch) : 0, // All updates include a valid `nextSyncCommitteeBranch`, see below code isSyncCommitteeUpdate: true, isFinalityUpdate: attestedData.isFinalized, }; if (!isBetterUpdate(nextBestUpdate, prevBestUpdateSummary)) { this.metrics?.lightclientServer.updateNotBetter.inc(); return; } } const syncCommitteeWitness = await this.db.syncCommitteeWitness.get(attestedData.blockRoot); if (!syncCommitteeWitness) { throw Error(`syncCommitteeWitness not available at ${toRootHex(attestedData.blockRoot)}`); } const attestedFork = this.config.getForkName(attestedHeader.beacon.slot); const numWitness = syncCommitteeWitness.witness.length; if (isForkPostElectra(attestedFork) && numWitness !== NUM_WITNESS_ELECTRA) { throw Error(`Expected ${NUM_WITNESS_ELECTRA} witnesses in post-Electra numWitness=${numWitness}`); } if (!isForkPostElectra(attestedFork) && numWitness !== NUM_WITNESS) { throw Error(`Expected ${NUM_WITNESS} witnesses in pre-Electra numWitness=${numWitness}`); } const nextSyncCommittee = await this.db.syncCommittee.get(syncCommitteeWitness.nextSyncCommitteeRoot); if (!nextSyncCommittee) { throw Error("nextSyncCommittee not available"); } const nextSyncCommitteeBranch = getNextSyncCommitteeBranch(syncCommitteeWitness); const finalizedHeaderAttested = attestedData.isFinalized ? await this.getFinalizedHeader(attestedData.finalizedCheckpoint.root) : null; let isFinalized, finalityBranch, finalizedHeader; if (attestedData.isFinalized && finalizedHeaderAttested && computeSyncPeriodAtSlot(finalizedHeaderAttested.beacon.slot) === syncPeriod) { isFinalized = true; finalityBranch = attestedData.finalityBranch; finalizedHeader = finalizedHeaderAttested; // Fork of LightClientUpdate is based off on attested header's fork if (this.config.getForkName(finalizedHeader.beacon.slot) !== attestedFork) { finalizedHeader = upgradeLightClientHeader(this.config, attestedFork, finalizedHeader); } } else { isFinalized = false; finalityBranch = this.zero.finalityBranch; // No need to upgrade finalizedHeader because its anyway set to zero of highest fork finalizedHeader = this.zero.finalizedHeader; } const newUpdate = { attestedHeader, nextSyncCommittee: nextSyncCommittee, nextSyncCommitteeBranch, finalizedHeader, finalityBranch, syncAggregate, signatureSlot, }; // attestedData and the block of syncAggregate may not be in same sync period // should not use attested data slot as sync period // see https://github.com/ChainSafe/lodestar/issues/3933 await this.db.bestLightClientUpdate.put(syncPeriod, newUpdate); this.logger.debug("Stored new PartialLightClientUpdate", { syncPeriod, isFinalized, participation: sumBits(syncAggregate.syncCommitteeBits) / SYNC_COMMITTEE_SIZE, }); // Count total persisted updates per type. DB metrics don't diff between each type. // The frequency of finalized vs non-finalized is critical to debug if finalizedHeader is not available this.metrics?.lightclientServer.onSyncAggregate.inc({ event: isFinalized ? "store_finalized_update" : "store_nonfinalized_update", }); this.metrics?.lightclientServer.highestSlot.set({ item: isFinalized ? "best_finalized_update" : "best_nonfinalized_update" }, newUpdate.attestedHeader.beacon.slot); } async storeSyncCommittee(syncCommittee, syncCommitteeRoot) { const isKnown = await this.db.syncCommittee.has(syncCommitteeRoot); if (!isKnown) { await this.db.syncCommittee.putBinary(syncCommitteeRoot, syncCommittee.serialize()); } } /** * Get finalized header from db. Keeps a small in-memory cache to speed up most of the lookups */ async getFinalizedHeader(finalizedBlockRoot) { const finalizedBlockRootHex = toRootHex(finalizedBlockRoot); const cachedFinalizedHeader = this.checkpointHeaders.get(finalizedBlockRootHex); if (cachedFinalizedHeader) { return cachedFinalizedHeader; } const finalizedHeader = await this.db.checkpointHeader.get(finalizedBlockRoot); if (!finalizedHeader) { // finalityHeader is not available during sync, since started after the finalized checkpoint. // See https://github.com/ChainSafe/lodestar/issues/3495 // To prevent excesive logging this condition is not considered an error, but the lightclient updater // will just create a non-finalized update. this.logger.debug("finalizedHeader not available", { root: finalizedBlockRootHex }); return null; } this.checkpointHeaders.set(finalizedBlockRootHex, finalizedHeader); pruneSetToMax(this.checkpointHeaders, MAX_CACHED_FINALIZED_HEADERS); return finalizedHeader; } } export function sumBits(bits) { return bits.getTrueBitIndexes().length; } export function blockToLightClientHeader(fork, block) { const blockSlot = block.slot; const beacon = { slot: blockSlot, proposerIndex: block.proposerIndex, parentRoot: block.parentRoot, stateRoot: block.stateRoot, bodyRoot: ssz[fork].BeaconBlockBody.hashTreeRoot(block.body), }; if (ForkSeq[fork] >= ForkSeq.capella) { const blockBody = block.body; const execution = executionPayloadToPayloadHeader(ForkSeq[fork], blockBody.executionPayload); return { beacon, execution, executionBranch: getBlockBodyExecutionHeaderProof(fork, blockBody), }; } return { beacon }; } //# sourceMappingURL=index.js.map