diff --git a/packages/beacon-node/src/chain/lightClient/index.ts b/packages/beacon-node/src/chain/lightClient/index.ts index 0a41ea059e7..9eb1ea3161e 100644 --- a/packages/beacon-node/src/chain/lightClient/index.ts +++ b/packages/beacon-node/src/chain/lightClient/index.ts @@ -3,6 +3,7 @@ import { altair, BeaconBlock, BeaconBlockBody, + electra, LightClientBootstrap, LightClientFinalityUpdate, LightClientHeader, @@ -42,6 +43,7 @@ import { ForkLightClient, highestFork, forkLightClient, + isForkPostElectra, } from "@lodestar/params"; import {IBeaconDb} from "../../db/index.js"; @@ -57,6 +59,7 @@ import { getCurrentSyncCommitteeBranch, getBlockBodyExecutionHeaderProof, } from "./proofs.js"; +import {NUM_WITNESS, NUM_WITNESS_ELECTRA} from "../../db/repositories/lightclientSyncCommitteeWitness.js"; export type LightClientServerOpts = { disableLightClientServerOnImportBlockHead?: boolean; @@ -208,7 +211,9 @@ export class LightClientServer { private checkpointHeaders = new Map(); private latestHeadUpdate: LightClientOptimisticUpdate | null = null; - private readonly zero: Pick; + private readonly zero: + | Pick + | Pick; private finalized: LightClientFinalityUpdate | null = null; constructor( @@ -225,7 +230,9 @@ export class LightClientServer { this.zero = { // Assign the hightest fork's default value because it can always be typecasted down to correct fork finalizedHeader: sszTypesFor(highestFork(forkLightClient)).LightClientHeader.defaultValue(), - finalityBranch: ssz.altair.LightClientUpdate.fields.finalityBranch.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) { @@ -388,12 +395,13 @@ export class LightClientServer { parentBlockSlot: Slot ): Promise { const blockSlot = block.slot; - const header = blockToLightClientHeader(this.config.getForkName(blockSlot), block); + 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(postState); + const syncCommitteeWitness = getSyncCommitteesWitness(fork, postState); // Only store current sync committee once per run if (!this.storedCurrentSyncCommittee) { @@ -621,6 +629,16 @@ export class LightClientServer { 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 numWiteness=${numWitness}`); + } + if (!isForkPostElectra(attestedFork) && numWitness !== NUM_WITNESS) { + throw Error(`Expected ${NUM_WITNESS} witnesses in pre-Electra numWiteness=${numWitness}`); + } + const nextSyncCommittee = await this.db.syncCommittee.get(syncCommitteeWitness.nextSyncCommitteeRoot); if (!nextSyncCommittee) { throw Error("nextSyncCommittee not available"); @@ -641,7 +659,6 @@ export class LightClientServer { finalityBranch = attestedData.finalityBranch; finalizedHeader = finalizedHeaderAttested; // Fork of LightClientUpdate is based off on attested header's fork - const attestedFork = this.config.getForkName(attestedHeader.beacon.slot); if (this.config.getForkName(finalizedHeader.beacon.slot) !== attestedFork) { finalizedHeader = upgradeLightClientHeader(this.config, attestedFork, finalizedHeader); } diff --git a/packages/beacon-node/src/chain/lightClient/proofs.ts b/packages/beacon-node/src/chain/lightClient/proofs.ts index 8d273e30ae5..cc4cb247b9d 100644 --- a/packages/beacon-node/src/chain/lightClient/proofs.ts +++ b/packages/beacon-node/src/chain/lightClient/proofs.ts @@ -5,28 +5,54 @@ import { BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX, ForkExecution, FINALIZED_ROOT_GINDEX_ELECTRA, + ForkName, + isForkPostElectra, } from "@lodestar/params"; import {BeaconBlockBody, SSZTypesFor, ssz} from "@lodestar/types"; import {SyncCommitteeWitness} from "./types.js"; -export function getSyncCommitteesWitness(state: BeaconStateAllForks): SyncCommitteeWitness { +export function getSyncCommitteesWitness(fork: ForkName, state: BeaconStateAllForks): SyncCommitteeWitness { state.commit(); const n1 = state.node; - const n3 = n1.right; // [1]0110 - const n6 = n3.left; // 1[0]110 - const n13 = n6.right; // 10[1]10 - const n27 = n13.right; // 101[1]0 - const currentSyncCommitteeRoot = n27.left.root; // n54 1011[0] - const nextSyncCommitteeRoot = n27.right.root; // n55 1011[1] + let witness: Uint8Array[]; + let currentSyncCommitteeRoot: Uint8Array; + let nextSyncCommitteeRoot: Uint8Array; - // Witness branch is sorted by descending gindex - const witness = [ - n13.left.root, // 26 - n6.left.root, // 12 - n3.right.root, // 7 - n1.left.root, // 2 - ]; + if (isForkPostElectra(fork)) { + const n2 = n1.left; + const n5 = n2.right; + const n10 = n5.left; + const n21 = n10.right; + const n43 = n21.right; + + currentSyncCommitteeRoot = n43.left.root; // n86 + nextSyncCommitteeRoot = n43.right.root; // n87 + + // Witness branch is sorted by descending gindex + witness = [ + n21.left.root, // 42 + n10.left.root, // 20 + n5.right.root, // 11 + n2.left.root, // 4 + n1.right.root, // 3 + ]; + } else { + const n3 = n1.right; // [1]0110 + const n6 = n3.left; // 1[0]110 + const n13 = n6.right; // 10[1]10 + const n27 = n13.right; // 101[1]0 + currentSyncCommitteeRoot = n27.left.root; // n54 1011[0] + nextSyncCommitteeRoot = n27.right.root; // n55 1011[1] + + // Witness branch is sorted by descending gindex + witness = [ + n13.left.root, // 26 + n6.left.root, // 12 + n3.right.root, // 7 + n1.left.root, // 2 + ]; + } return { witness, diff --git a/packages/beacon-node/src/chain/lightClient/types.ts b/packages/beacon-node/src/chain/lightClient/types.ts index b253c05d45f..00a819f30c1 100644 --- a/packages/beacon-node/src/chain/lightClient/types.ts +++ b/packages/beacon-node/src/chain/lightClient/types.ts @@ -26,7 +26,7 @@ * ``` */ export type SyncCommitteeWitness = { - /** Vector[Bytes32, 4] */ + /** Vector[Bytes32, 4] or Vector[Bytes32, 5] depends on the fork */ witness: Uint8Array[]; currentSyncCommitteeRoot: Uint8Array; nextSyncCommitteeRoot: Uint8Array; diff --git a/packages/beacon-node/src/db/repositories/lightclientSyncCommitteeWitness.ts b/packages/beacon-node/src/db/repositories/lightclientSyncCommitteeWitness.ts index 45f91f15999..e323c3e55f6 100644 --- a/packages/beacon-node/src/db/repositories/lightclientSyncCommitteeWitness.ts +++ b/packages/beacon-node/src/db/repositories/lightclientSyncCommitteeWitness.ts @@ -5,6 +5,15 @@ import {ssz} from "@lodestar/types"; import {SyncCommitteeWitness} from "../../chain/lightClient/types.js"; import {Bucket, getBucketNameByValue} from "../buckets.js"; +// We add a 1-byte prefix where 0 means pre-electra and 1 means post-electra +enum PrefixByte { + PRE_ELECTRA = 0, + POST_ELECTRA = 1, +} + +export const NUM_WITNESS = 4; +export const NUM_WITNESS_ELECTRA = 5; + /** * Historical sync committees witness by block root * @@ -13,12 +22,56 @@ import {Bucket, getBucketNameByValue} from "../buckets.js"; export class SyncCommitteeWitnessRepository extends Repository { constructor(config: ChainForkConfig, db: DatabaseController) { const bucket = Bucket.lightClient_syncCommitteeWitness; + // Pick some type but won't be used. Witness can be 4 or 5 so need to handle dynamically const type = new ContainerType({ - witness: new VectorCompositeType(ssz.Root, 4), + witness: new VectorCompositeType(ssz.Root, NUM_WITNESS), currentSyncCommitteeRoot: ssz.Root, nextSyncCommitteeRoot: ssz.Root, }); super(config, db, bucket, type, getBucketNameByValue(bucket)); } + + // Overrides for multi-fork + encodeValue(value: SyncCommitteeWitness): Uint8Array { + const numWitness = value.witness.length; + + if (numWitness !== NUM_WITNESS && numWitness !== NUM_WITNESS_ELECTRA) { + throw Error(`Number of witness can only be 4 pre-electra or 5 post-electra numWitness=${numWitness}`); + } + + const type = new ContainerType({ + witness: new VectorCompositeType(ssz.Root, numWitness), + currentSyncCommitteeRoot: ssz.Root, + nextSyncCommitteeRoot: ssz.Root, + }); + + const valueBytes = type.serialize(value); + + // We need to differentiate between post-electra and pre-electra witness + // such that we can deserialize correctly + const isPostElectra = numWitness === NUM_WITNESS_ELECTRA; + const prefixByte = new Uint8Array(1); + prefixByte[0] = isPostElectra ? PrefixByte.POST_ELECTRA : PrefixByte.PRE_ELECTRA; + + const prefixedData = new Uint8Array(1 + valueBytes.length); + prefixedData.set(prefixByte, 0); + prefixedData.set(valueBytes, 1); + + return prefixedData; + } + + decodeValue(data: Uint8Array): SyncCommitteeWitness { + // First byte is written + const prefix = data.subarray(0, 1); + const isPostElectra = prefix[0] === PrefixByte.POST_ELECTRA; + + const type = new ContainerType({ + witness: new VectorCompositeType(ssz.Root, isPostElectra ? NUM_WITNESS_ELECTRA : NUM_WITNESS), + currentSyncCommitteeRoot: ssz.Root, + nextSyncCommitteeRoot: ssz.Root, + }); + + return type.deserialize(data.subarray(1)); + } } diff --git a/packages/beacon-node/test/unit/chain/lightclient/proof.test.ts b/packages/beacon-node/test/unit/chain/lightclient/proof.test.ts index b30e0f9a9dd..ea8438ea84b 100644 --- a/packages/beacon-node/test/unit/chain/lightclient/proof.test.ts +++ b/packages/beacon-node/test/unit/chain/lightclient/proof.test.ts @@ -1,73 +1,146 @@ import {describe, it, expect, beforeAll} from "vitest"; -import {BeaconStateAltair} from "@lodestar/state-transition"; -import {SYNC_COMMITTEE_SIZE} from "@lodestar/params"; +import {BeaconStateAltair, BeaconStateElectra} from "@lodestar/state-transition"; +import {ForkName, SYNC_COMMITTEE_SIZE} from "@lodestar/params"; import {altair, ssz} from "@lodestar/types"; import {verifyMerkleBranch, hash} from "@lodestar/utils"; import {getNextSyncCommitteeBranch, getSyncCommitteesWitness} from "../../../../src/chain/lightClient/proofs.js"; +import {NUM_WITNESS, NUM_WITNESS_ELECTRA} from "../../../../src/db/repositories/lightclientSyncCommitteeWitness.js"; const currentSyncCommitteeGindex = 54; const nextSyncCommitteeGindex = 55; const syncCommitteesGindex = 27; +const currentSyncCommitteeGindexElectra = 86; +const nextSyncCommitteeGindexElectra = 87; +const syncCommitteesGindexElectra = 43; describe("chain / lightclient / proof", () => { - let state: BeaconStateAltair; - let stateRoot: Uint8Array; + let stateAltair: BeaconStateAltair; + let stateElectra: BeaconStateElectra; + let stateRootAltair: Uint8Array; + let stateRootElectra: Uint8Array; const currentSyncCommittee = fillSyncCommittee(Buffer.alloc(48, 0xbb)); const nextSyncCommittee = fillSyncCommittee(Buffer.alloc(48, 0xcc)); beforeAll(() => { - state = ssz.altair.BeaconState.defaultViewDU(); - state.currentSyncCommittee = ssz.altair.SyncCommittee.toViewDU(currentSyncCommittee); - state.nextSyncCommittee = ssz.altair.SyncCommittee.toViewDU(nextSyncCommittee); + stateAltair = ssz.altair.BeaconState.defaultViewDU(); + stateAltair.currentSyncCommittee = ssz.altair.SyncCommittee.toViewDU(currentSyncCommittee); + stateAltair.nextSyncCommittee = ssz.altair.SyncCommittee.toViewDU(nextSyncCommittee); // Note: .hashTreeRoot() automatically commits() - stateRoot = state.hashTreeRoot(); + stateRootAltair = stateAltair.hashTreeRoot(); + + stateElectra = ssz.electra.BeaconState.defaultViewDU(); + stateElectra.currentSyncCommittee = ssz.altair.SyncCommittee.toViewDU(currentSyncCommittee); + stateElectra.nextSyncCommittee = ssz.altair.SyncCommittee.toViewDU(nextSyncCommittee); + stateRootElectra = stateElectra.hashTreeRoot(); }); - it("SyncCommittees proof", () => { - const syncCommitteesWitness = getSyncCommitteesWitness(state); + it("SyncCommittees proof altair", () => { + const syncCommitteesWitness = getSyncCommitteesWitness(ForkName.altair, stateAltair); const syncCommitteesLeaf = hash( syncCommitteesWitness.currentSyncCommitteeRoot, syncCommitteesWitness.nextSyncCommitteeRoot ); + expect(syncCommitteesWitness.witness.length).toBe(NUM_WITNESS); expect( verifyMerkleBranch( syncCommitteesLeaf, syncCommitteesWitness.witness, ...fromGindex(syncCommitteesGindex), - stateRoot + stateRootAltair ) ).toBe(true); }); - it("currentSyncCommittee proof", () => { - const syncCommitteesWitness = getSyncCommitteesWitness(state); + it("currentSyncCommittee proof altair", () => { + const syncCommitteesWitness = getSyncCommitteesWitness(ForkName.altair, stateAltair); const currentSyncCommitteeBranch = [syncCommitteesWitness.nextSyncCommitteeRoot, ...syncCommitteesWitness.witness]; + expect(syncCommitteesWitness.witness.length).toBe(NUM_WITNESS); expect( verifyMerkleBranch( ssz.altair.SyncCommittee.hashTreeRoot(currentSyncCommittee), currentSyncCommitteeBranch, ...fromGindex(currentSyncCommitteeGindex), - stateRoot + stateRootAltair ) ).toBe(true); }); - it("nextSyncCommittee proof", () => { - const syncCommitteesWitness = getSyncCommitteesWitness(state); + it("nextSyncCommittee proof altair", () => { + const syncCommitteesWitness = getSyncCommitteesWitness(ForkName.altair, stateAltair); const nextSyncCommitteeBranch = getNextSyncCommitteeBranch(syncCommitteesWitness); + expect(syncCommitteesWitness.witness.length).toBe(NUM_WITNESS); expect( verifyMerkleBranch( ssz.altair.SyncCommittee.hashTreeRoot(nextSyncCommittee), nextSyncCommitteeBranch, ...fromGindex(nextSyncCommitteeGindex), - stateRoot + stateRootAltair + ) + ).toBe(true); + }); + + it("SyncCommittees proof electra", () => { + const syncCommitteesWitness = getSyncCommitteesWitness(ForkName.electra, stateElectra); + const syncCommitteesLeaf = hash( + syncCommitteesWitness.currentSyncCommitteeRoot, + syncCommitteesWitness.nextSyncCommitteeRoot + ); + + expect(syncCommitteesWitness.witness.length).toBe(NUM_WITNESS_ELECTRA); + expect( + verifyMerkleBranch( + syncCommitteesLeaf, + syncCommitteesWitness.witness, + ...fromGindex(syncCommitteesGindexElectra), + stateRootElectra + ) + ).toBe(true); + }); + + it("currentSyncCommittee proof electra", () => { + const syncCommitteesWitness = getSyncCommitteesWitness(ForkName.electra, stateElectra); + const currentSyncCommitteeBranch = [syncCommitteesWitness.nextSyncCommitteeRoot, ...syncCommitteesWitness.witness]; + + expect(syncCommitteesWitness.witness.length).toBe(NUM_WITNESS_ELECTRA); + expect( + verifyMerkleBranch( + ssz.altair.SyncCommittee.hashTreeRoot(currentSyncCommittee), + currentSyncCommitteeBranch, + ...fromGindex(currentSyncCommitteeGindexElectra), + stateRootElectra + ) + ).toBe(true); + }); + + it("nextSyncCommittee proof electra", () => { + const syncCommitteesWitness = getSyncCommitteesWitness(ForkName.electra, stateElectra); + const nextSyncCommitteeBranch = getNextSyncCommitteeBranch(syncCommitteesWitness); + + expect( + verifyMerkleBranch( + ssz.altair.SyncCommittee.hashTreeRoot(nextSyncCommittee), + nextSyncCommitteeBranch, + ...fromGindex(nextSyncCommitteeGindexElectra), + stateRootElectra ) ).toBe(true); }); + + it("getSyncCommitteesWitness returns correct number of witness altair", () => { + const syncCommitteesWitness = getSyncCommitteesWitness(ForkName.altair, stateAltair); + + expect(syncCommitteesWitness.witness.length).toBe(NUM_WITNESS); + }); + + it("getSyncCommitteesWitness returns correct number of witness electra", () => { + const syncCommitteesWitness = getSyncCommitteesWitness(ForkName.electra, stateElectra); + + expect(syncCommitteesWitness.witness.length).toBe(NUM_WITNESS_ELECTRA); + }); }); function fillSyncCommittee(pubkey: Uint8Array): altair.SyncCommittee { diff --git a/packages/light-client/src/spec/index.ts b/packages/light-client/src/spec/index.ts index 0934e15b1c1..11ce1f8b3f2 100644 --- a/packages/light-client/src/spec/index.ts +++ b/packages/light-client/src/spec/index.ts @@ -10,7 +10,7 @@ import { import {computeSyncPeriodAtSlot} from "../utils/index.js"; import {getSyncCommitteeAtPeriod, processLightClientUpdate, ProcessUpdateOpts} from "./processLightClientUpdate.js"; import {ILightClientStore, LightClientStore, LightClientStoreEvents} from "./store.js"; -import {ZERO_FINALITY_BRANCH, ZERO_HEADER, ZERO_SYNC_COMMITTEE, getZeroSyncCommitteeBranch} from "./utils.js"; +import {ZERO_HEADER, ZERO_SYNC_COMMITTEE, getZeroFinalityBranch, getZeroSyncCommitteeBranch} from "./utils.js"; export {isBetterUpdate, toLightClientUpdateSummary} from "./isBetterUpdate.js"; export type {LightClientUpdateSummary} from "./isBetterUpdate.js"; @@ -51,7 +51,7 @@ export class LightclientSpec { nextSyncCommittee: ZERO_SYNC_COMMITTEE, nextSyncCommitteeBranch: getZeroSyncCommitteeBranch(this.config.getForkName(optimisticUpdate.signatureSlot)), finalizedHeader: {beacon: ZERO_HEADER}, - finalityBranch: ZERO_FINALITY_BRANCH, + finalityBranch: getZeroFinalityBranch(this.config.getForkName(optimisticUpdate.signatureSlot)), syncAggregate: optimisticUpdate.syncAggregate, signatureSlot: optimisticUpdate.signatureSlot, }); diff --git a/packages/light-client/src/spec/utils.ts b/packages/light-client/src/spec/utils.ts index 36bc7098fcc..602e1264e15 100644 --- a/packages/light-client/src/spec/utils.ts +++ b/packages/light-client/src/spec/utils.ts @@ -33,7 +33,6 @@ 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(); -export const ZERO_FINALITY_BRANCH = Array.from({length: FINALIZED_ROOT_DEPTH}, () => ZERO_HASH); /** From https://notes.ethereum.org/@vbuterin/extended_light_client_protocol#Optimistic-head-determining-function */ const SAFETY_THRESHOLD_FACTOR = 2; @@ -53,6 +52,12 @@ export function getZeroSyncCommitteeBranch(fork: ForkName): Uint8Array[] { 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 @@ -65,7 +70,8 @@ export function isSyncCommitteeUpdate(update: LightClientUpdate): boolean { export function isFinalityUpdate(update: LightClientUpdate): boolean { return ( // Fast return for when constructing full LightClientUpdate from partial updates - update.finalityBranch !== ZERO_FINALITY_BRANCH && + update.finalityBranch !== + getZeroFinalityBranch(isElectraLightClientUpdate(update) ? ForkName.electra : ForkName.altair) && update.finalityBranch.some((branch) => !byteArrayEquals(branch, ZERO_HASH)) ); } diff --git a/packages/light-client/src/validation.ts b/packages/light-client/src/validation.ts index c756d612f3e..bb9e13eec89 100644 --- a/packages/light-client/src/validation.ts +++ b/packages/light-client/src/validation.ts @@ -2,6 +2,7 @@ import bls from "@chainsafe/bls"; import type {PublicKey, Signature} from "@chainsafe/bls/types"; import { altair, + isELectraLightClientFinalityUpdate, isElectraLightClientUpdate, LightClientFinalityUpdate, LightClientUpdate, @@ -19,6 +20,7 @@ import { NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA, FINALIZED_ROOT_DEPTH_ELECTRA, NEXT_SYNC_COMMITTEE_INDEX_ELECTRA, + FINALIZED_ROOT_INDEX_ELECTRA, } from "@lodestar/params"; import {BeaconConfig} from "@lodestar/config"; import {isValidMerkleBranch} from "./utils/verifyMerkleBranch.js"; @@ -80,12 +82,19 @@ export function assertValidLightClientUpdate( * Where `hashTreeRoot(state) == update.finalityHeader.stateRoot` */ export function assertValidFinalityProof(update: LightClientFinalityUpdate): void { + const finalizedRootDepth = isELectraLightClientFinalityUpdate(update) + ? FINALIZED_ROOT_DEPTH_ELECTRA + : FINALIZED_ROOT_DEPTH; + const finalizedRootIndex = isELectraLightClientFinalityUpdate(update) + ? FINALIZED_ROOT_INDEX_ELECTRA + : FINALIZED_ROOT_INDEX; + if ( !isValidMerkleBranch( ssz.phase0.BeaconBlockHeader.hashTreeRoot(update.finalizedHeader.beacon), update.finalityBranch, - FINALIZED_ROOT_DEPTH, - FINALIZED_ROOT_INDEX, + finalizedRootDepth, + finalizedRootIndex, update.attestedHeader.beacon.stateRoot ) ) { diff --git a/packages/types/src/utils/typeguards.ts b/packages/types/src/utils/typeguards.ts index a892c3a0c9c..6afde52da47 100644 --- a/packages/types/src/utils/typeguards.ts +++ b/packages/types/src/utils/typeguards.ts @@ -15,6 +15,7 @@ import { BeaconBlock, Attestation, LightClientUpdate, + LightClientFinalityUpdate, } from "../types.js"; export function isExecutionPayload( @@ -80,3 +81,13 @@ export function isElectraLightClientUpdate(update: LightClientUpdate): update is updatePostElectra.finalityBranch.length === FINALIZED_ROOT_DEPTH_ELECTRA ); } + +export function isELectraLightClientFinalityUpdate( + update: LightClientFinalityUpdate +): update is LightClientFinalityUpdate { + const updatePostElectra = update as LightClientUpdate; + return ( + updatePostElectra.finalityBranch !== undefined && + updatePostElectra.finalityBranch.length === FINALIZED_ROOT_DEPTH_ELECTRA + ); +}