diff --git a/packages/beacon-node/src/chain/lightClient/index.ts b/packages/beacon-node/src/chain/lightClient/index.ts index 0a41ea059e7..bb7de1f0478 100644 --- a/packages/beacon-node/src/chain/lightClient/index.ts +++ b/packages/beacon-node/src/chain/lightClient/index.ts @@ -388,12 +388,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) { diff --git a/packages/beacon-node/src/chain/lightClient/proofs.ts b/packages/beacon-node/src/chain/lightClient/proofs.ts index 8d273e30ae5..5c01ae3059f 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.right.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/test/unit/chain/lightclient/proof.test.ts b/packages/beacon-node/test/unit/chain/lightclient/proof.test.ts index b30e0f9a9dd..38f4ab6aea3 100644 --- a/packages/beacon-node/test/unit/chain/lightclient/proof.test.ts +++ b/packages/beacon-node/test/unit/chain/lightclient/proof.test.ts @@ -1,6 +1,6 @@ import {describe, it, expect, beforeAll} from "vitest"; import {BeaconStateAltair} from "@lodestar/state-transition"; -import {SYNC_COMMITTEE_SIZE} from "@lodestar/params"; +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"; @@ -25,7 +25,7 @@ describe("chain / lightclient / proof", () => { }); it("SyncCommittees proof", () => { - const syncCommitteesWitness = getSyncCommitteesWitness(state); + const syncCommitteesWitness = getSyncCommitteesWitness(ForkName.altair, state); const syncCommitteesLeaf = hash( syncCommitteesWitness.currentSyncCommitteeRoot, syncCommitteesWitness.nextSyncCommitteeRoot @@ -42,7 +42,7 @@ describe("chain / lightclient / proof", () => { }); it("currentSyncCommittee proof", () => { - const syncCommitteesWitness = getSyncCommitteesWitness(state); + const syncCommitteesWitness = getSyncCommitteesWitness(ForkName.altair, state); const currentSyncCommitteeBranch = [syncCommitteesWitness.nextSyncCommitteeRoot, ...syncCommitteesWitness.witness]; expect( @@ -56,7 +56,7 @@ describe("chain / lightclient / proof", () => { }); it("nextSyncCommittee proof", () => { - const syncCommitteesWitness = getSyncCommitteesWitness(state); + const syncCommitteesWitness = getSyncCommitteesWitness(ForkName.altair, state); const nextSyncCommitteeBranch = getNextSyncCommitteeBranch(syncCommitteesWitness); expect( diff --git a/packages/light-client/src/spec/index.ts b/packages/light-client/src/spec/index.ts index 0934e15b1c1..4fd45f0f2e7 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"; @@ -39,7 +39,7 @@ export class LightclientSpec { nextSyncCommittee: ZERO_SYNC_COMMITTEE, nextSyncCommitteeBranch: getZeroSyncCommitteeBranch(this.config.getForkName(finalityUpdate.signatureSlot)), finalizedHeader: finalityUpdate.finalizedHeader, - finalityBranch: finalityUpdate.finalityBranch, + finalityBranch: getZeroFinalityBranch(this.config.getForkName(finalityUpdate.signatureSlot)), syncAggregate: finalityUpdate.syncAggregate, signatureSlot: finalityUpdate.signatureSlot, }); @@ -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 + ); +}