import {BitArray} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; import { BLOCK_BODY_EXECUTION_PAYLOAD_DEPTH as EXECUTION_PAYLOAD_DEPTH, BLOCK_BODY_EXECUTION_PAYLOAD_INDEX as EXECUTION_PAYLOAD_INDEX, FINALIZED_ROOT_DEPTH, FINALIZED_ROOT_DEPTH_ELECTRA, ForkName, ForkSeq, NEXT_SYNC_COMMITTEE_DEPTH, NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA, isForkPostElectra, } from "@lodestar/params"; import { BeaconBlockHeader, LightClientFinalityUpdate, LightClientHeader, LightClientOptimisticUpdate, LightClientUpdate, Slot, SyncCommittee, isElectraLightClientUpdate, ssz, } from "@lodestar/types"; import {byteArrayEquals} from "@lodestar/utils"; import {computeEpochAtSlot, computeSyncPeriodAtSlot, isValidMerkleBranch} from "../utils/index.js"; import {normalizeMerkleBranch} from "../utils/normalizeMerkleBranch.js"; import {LightClientStore} from "./store.js"; export const GENESIS_SLOT = 0; export const ZERO_HASH = new Uint8Array(32); export const ZERO_PUBKEY = new Uint8Array(48); export const ZERO_SYNC_COMMITTEE = ssz.altair.SyncCommittee.defaultValue(); export const ZERO_HEADER = ssz.phase0.BeaconBlockHeader.defaultValue(); /** From https://notes.ethereum.org/@vbuterin/extended_light_client_protocol#Optimistic-head-determining-function */ const SAFETY_THRESHOLD_FACTOR = 2; export function sumBits(bits: BitArray): number { return bits.getTrueBitIndexes().length; } export function getSafetyThreshold(maxActiveParticipants: number): number { return Math.floor(maxActiveParticipants / SAFETY_THRESHOLD_FACTOR); } export function getZeroSyncCommitteeBranch(fork: ForkName): Uint8Array[] { const nextSyncCommitteeDepth = isForkPostElectra(fork) ? NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA : NEXT_SYNC_COMMITTEE_DEPTH; return Array.from({length: nextSyncCommitteeDepth}, () => ZERO_HASH); } export function getZeroFinalityBranch(fork: ForkName): Uint8Array[] { const finalizedRootDepth = isForkPostElectra(fork) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH; return Array.from({length: finalizedRootDepth}, () => ZERO_HASH); } export function isSyncCommitteeUpdate(update: LightClientUpdate): boolean { return ( // Fast return for when constructing full LightClientUpdate from partial updates update.nextSyncCommitteeBranch !== getZeroSyncCommitteeBranch(isElectraLightClientUpdate(update) ? ForkName.electra : ForkName.altair) && update.nextSyncCommitteeBranch.some((branch) => !byteArrayEquals(branch, ZERO_HASH)) ); } export function isFinalityUpdate(update: LightClientUpdate): boolean { return ( // Fast return for when constructing full LightClientUpdate from partial updates update.finalityBranch !== getZeroFinalityBranch(isElectraLightClientUpdate(update) ? ForkName.electra : ForkName.altair) && update.finalityBranch.some((branch) => !byteArrayEquals(branch, ZERO_HASH)) ); } export function isZeroedHeader(header: BeaconBlockHeader): boolean { // Fast return for when constructing full LightClientUpdate from partial updates return header === ZERO_HEADER || byteArrayEquals(header.bodyRoot, ZERO_HASH); } export function isZeroedSyncCommittee(syncCommittee: SyncCommittee): boolean { // Fast return for when constructing full LightClientUpdate from partial updates return syncCommittee === ZERO_SYNC_COMMITTEE || byteArrayEquals(syncCommittee.pubkeys[0], ZERO_PUBKEY); } export function upgradeLightClientHeader( config: ChainForkConfig, targetFork: ForkName, header: LightClientHeader ): LightClientHeader { const headerFork = config.getForkName(header.beacon.slot); if (ForkSeq[headerFork] >= ForkSeq[targetFork]) { throw Error(`Invalid upgrade request from headerFork=${headerFork} to targetFork=${targetFork}`); } // We are modifying the same header object, may be we could create a copy, but its // not required as of now const upgradedHeader = header; const startUpgradeFromFork = Object.values(ForkName)[ForkSeq[headerFork] + 1]; switch (startUpgradeFromFork) { // biome-ignore lint/suspicious/useDefaultSwitchClauseLast: We want default to evaluate at first to throw error early default: throw Error( `Invalid startUpgradeFromFork=${startUpgradeFromFork} for headerFork=${headerFork} in upgradeLightClientHeader to targetFork=${targetFork}` ); case ForkName.altair: // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here case ForkName.bellatrix: // Break if no further upgradation is required else fall through if (ForkSeq[targetFork] <= ForkSeq.bellatrix) break; // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here case ForkName.capella: (upgradedHeader as LightClientHeader).execution = ssz.capella.LightClientHeader.fields.execution.defaultValue(); (upgradedHeader as LightClientHeader).executionBranch = ssz.capella.LightClientHeader.fields.executionBranch.defaultValue(); // Break if no further upgradation is required else fall through if (ForkSeq[targetFork] <= ForkSeq.capella) break; // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here case ForkName.deneb: (upgradedHeader as LightClientHeader).execution.blobGasUsed = ssz.deneb.LightClientHeader.fields.execution.fields.blobGasUsed.defaultValue(); (upgradedHeader as LightClientHeader).execution.excessBlobGas = ssz.deneb.LightClientHeader.fields.execution.fields.excessBlobGas.defaultValue(); // Break if no further upgradation is required else fall through if (ForkSeq[targetFork] <= ForkSeq.deneb) break; // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here case ForkName.electra: // No changes to LightClientHeader in Electra // Break if no further upgrades is required else fall through if (ForkSeq[targetFork] <= ForkSeq.electra) break; // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here case ForkName.fulu: // No changes to LightClientHeader in Fulu // Break if no further upgrades is required else fall through if (ForkSeq[targetFork] <= ForkSeq.fulu) break; case ForkName.gloas: // No changes to LightClientHeader in Gloas // Break if no further upgrades is required else fall through if (ForkSeq[targetFork] <= ForkSeq.gloas) break; } return upgradedHeader; } export function isValidLightClientHeader(config: ChainForkConfig, header: LightClientHeader): boolean { const epoch = computeEpochAtSlot(header.beacon.slot); if (epoch < config.CAPELLA_FORK_EPOCH) { return ( ((header as LightClientHeader).execution === undefined || ssz.capella.ExecutionPayloadHeader.equals( (header as LightClientHeader).execution, ssz.capella.LightClientHeader.fields.execution.defaultValue() )) && ((header as LightClientHeader).executionBranch === undefined || ssz.capella.LightClientHeader.fields.executionBranch.equals( ssz.capella.LightClientHeader.fields.executionBranch.defaultValue(), (header as LightClientHeader).executionBranch )) ); } if ( epoch < config.DENEB_FORK_EPOCH && (((header as LightClientHeader).execution.blobGasUsed && (header as LightClientHeader).execution.blobGasUsed !== BigInt(0)) || ((header as LightClientHeader).execution.excessBlobGas && (header as LightClientHeader).execution.excessBlobGas !== BigInt(0))) ) { return false; } return isValidMerkleBranch( config .getPostBellatrixForkTypes(header.beacon.slot) .ExecutionPayloadHeader.hashTreeRoot((header as LightClientHeader).execution), (header as LightClientHeader).executionBranch, EXECUTION_PAYLOAD_DEPTH, EXECUTION_PAYLOAD_INDEX, header.beacon.bodyRoot ); } export function upgradeLightClientUpdate( config: ChainForkConfig, targetFork: ForkName, update: LightClientUpdate ): LightClientUpdate { update.attestedHeader = upgradeLightClientHeader(config, targetFork, update.attestedHeader); update.finalizedHeader = upgradeLightClientHeader(config, targetFork, update.finalizedHeader); update.nextSyncCommitteeBranch = normalizeMerkleBranch( update.nextSyncCommitteeBranch, isForkPostElectra(targetFork) ? NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA : NEXT_SYNC_COMMITTEE_DEPTH ); update.finalityBranch = normalizeMerkleBranch( update.finalityBranch, isForkPostElectra(targetFork) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH ); return update; } export function upgradeLightClientFinalityUpdate( config: ChainForkConfig, targetFork: ForkName, finalityUpdate: LightClientFinalityUpdate ): LightClientFinalityUpdate { finalityUpdate.attestedHeader = upgradeLightClientHeader(config, targetFork, finalityUpdate.attestedHeader); finalityUpdate.finalizedHeader = upgradeLightClientHeader(config, targetFork, finalityUpdate.finalizedHeader); finalityUpdate.finalityBranch = normalizeMerkleBranch( finalityUpdate.finalityBranch, isForkPostElectra(targetFork) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH ); return finalityUpdate; } export function upgradeLightClientOptimisticUpdate( config: ChainForkConfig, targetFork: ForkName, optimisticUpdate: LightClientOptimisticUpdate ): LightClientOptimisticUpdate { optimisticUpdate.attestedHeader = upgradeLightClientHeader(config, targetFork, optimisticUpdate.attestedHeader); return optimisticUpdate; } /** * Currently this upgradation is not required because all processing is done based on the * summary that the store generates and maintains. In case store needs to be saved to disk, * this could be required depending on the format the store is saved to the disk */ export function upgradeLightClientStore( config: ChainForkConfig, targetFork: ForkName, store: LightClientStore, signatureSlot: Slot ): LightClientStore { const updateSignaturePeriod = computeSyncPeriodAtSlot(signatureSlot); const bestValidUpdate = store.bestValidUpdates.get(updateSignaturePeriod); if (bestValidUpdate) { store.bestValidUpdates.set(updateSignaturePeriod, { update: upgradeLightClientUpdate(config, targetFork, bestValidUpdate.update), summary: bestValidUpdate.summary, }); } store.finalizedHeader = upgradeLightClientHeader(config, targetFork, store.finalizedHeader); store.optimisticHeader = upgradeLightClientHeader(config, targetFork, store.optimisticHeader); return store; }