diff --git a/packages/light-client/src/index.ts b/packages/light-client/src/index.ts index 6dcdf15fcd27..aaf3c3916267 100644 --- a/packages/light-client/src/index.ts +++ b/packages/light-client/src/index.ts @@ -154,7 +154,7 @@ export class Lightclient { // Fetch bootstrap state with proof at the trusted block root const {data: bootstrap} = await transport.getBootstrap(toHexString(checkpointRoot)); - validateLightClientBootstrap(checkpointRoot, bootstrap); + validateLightClientBootstrap(args.config, checkpointRoot, bootstrap); return new Lightclient({...args, bootstrap}); } diff --git a/packages/light-client/src/spec/index.ts b/packages/light-client/src/spec/index.ts index a29a36c5b679..2cd58c42883e 100644 --- a/packages/light-client/src/spec/index.ts +++ b/packages/light-client/src/spec/index.ts @@ -11,6 +11,7 @@ export {upgradeLightClientHeader} from "./utils.js"; export class LightclientSpec { readonly store: ILightClientStore; + readonly config: BeaconConfig; constructor( config: BeaconConfig, @@ -18,10 +19,11 @@ export class LightclientSpec { bootstrap: allForks.LightClientBootstrap ) { this.store = new LightClientStore(config, bootstrap, opts); + this.config = config; } onUpdate(currentSlot: Slot, update: allForks.LightClientUpdate): void { - processLightClientUpdate(this.store, currentSlot, this.opts, update); + processLightClientUpdate(this.config, this.store, currentSlot, this.opts, update); } onFinalityUpdate(currentSlot: Slot, finalityUpdate: allForks.LightClientFinalityUpdate): void { diff --git a/packages/light-client/src/spec/processLightClientUpdate.ts b/packages/light-client/src/spec/processLightClientUpdate.ts index 9ac0beb023ec..acd6a46d0175 100644 --- a/packages/light-client/src/spec/processLightClientUpdate.ts +++ b/packages/light-client/src/spec/processLightClientUpdate.ts @@ -1,5 +1,6 @@ import {SYNC_COMMITTEE_SIZE} from "@lodestar/params"; import {Slot, SyncPeriod, allForks} from "@lodestar/types"; +import {ChainForkConfig} from "@lodestar/config"; import {pruneSetToMax} from "@lodestar/utils"; import {computeSyncPeriodAtSlot, deserializeSyncCommittee, sumBits} from "../utils/index.js"; import {isBetterUpdate, LightClientUpdateSummary, toLightClientUpdateSummary} from "./isBetterUpdate.js"; @@ -13,6 +14,7 @@ export interface ProcessUpdateOpts { } export function processLightClientUpdate( + config: ChainForkConfig, store: ILightClientStore, currentSlot: Slot, opts: ProcessUpdateOpts, @@ -27,7 +29,7 @@ export function processLightClientUpdate( // Note: store.getSyncCommitteeAtPeriod() may advance store const syncCommittee = getSyncCommitteeAtPeriod(store, updateSignaturePeriod, opts); - validateLightClientUpdate(store, update, syncCommittee); + validateLightClientUpdate(config, store, update, syncCommittee); // Track the maximum number of active participants in the committee signatures const syncCommitteeTrueBits = sumBits(update.syncAggregate.syncCommitteeBits); diff --git a/packages/light-client/src/spec/utils.ts b/packages/light-client/src/spec/utils.ts index 2f58e55ddd95..d986e22606a8 100644 --- a/packages/light-client/src/spec/utils.ts +++ b/packages/light-client/src/spec/utils.ts @@ -1,7 +1,18 @@ import {BitArray, byteArrayEquals} from "@chainsafe/ssz"; -import {FINALIZED_ROOT_DEPTH, NEXT_SYNC_COMMITTEE_DEPTH, ForkSeq, ForkName} from "@lodestar/params"; + +import { + FINALIZED_ROOT_DEPTH, + NEXT_SYNC_COMMITTEE_DEPTH, + ForkSeq, + ForkName, + BLOCK_BODY_EXECUTION_PAYLOAD_DEPTH as EXECUTION_PAYLOAD_DEPTH, + BLOCK_BODY_EXECUTION_PAYLOAD_INDEX as EXECUTION_PAYLOAD_INDEX, +} from "@lodestar/params"; import {altair, phase0, ssz, allForks, capella, deneb} from "@lodestar/types"; import {ChainForkConfig} from "@lodestar/config"; +import {computeEpochAtSlot} from "@lodestar/state-transition"; + +import {isValidMerkleBranch} from "../utils/verifyMerkleBranch.js"; export const GENESIS_SLOT = 0; export const ZERO_HASH = new Uint8Array(32); @@ -91,3 +102,41 @@ export function upgradeLightClientHeader( } return upgradedHeader; } + +export function isValidLightClientHeader(config: ChainForkConfig, header: allForks.LightClientHeader): boolean { + const epoch = computeEpochAtSlot(header.beacon.slot); + + if (epoch < config.CAPELLA_FORK_EPOCH) { + return ( + ((header as capella.LightClientHeader).execution === undefined || + ssz.capella.ExecutionPayloadHeader.equals( + (header as capella.LightClientHeader).execution, + ssz.capella.LightClientHeader.fields.execution.defaultValue() + )) && + ((header as capella.LightClientHeader).executionBranch === undefined || + ssz.capella.LightClientHeader.fields.executionBranch.equals( + ssz.capella.LightClientHeader.fields.executionBranch.defaultValue(), + (header as capella.LightClientHeader).executionBranch + )) + ); + } + + if (epoch < config.EIP4844_FORK_EPOCH) { + if ( + (header as deneb.LightClientHeader).execution.excessDataGas && + (header as deneb.LightClientHeader).execution.excessDataGas !== BigInt(0) + ) { + return false; + } + } + + return isValidMerkleBranch( + config + .getExecutionForkTypes(header.beacon.slot) + .ExecutionPayloadHeader.hashTreeRoot((header as capella.LightClientHeader).execution), + (header as capella.LightClientHeader).executionBranch, + EXECUTION_PAYLOAD_DEPTH, + EXECUTION_PAYLOAD_INDEX, + header.beacon.bodyRoot + ); +} diff --git a/packages/light-client/src/spec/validateLightClientBootstrap.ts b/packages/light-client/src/spec/validateLightClientBootstrap.ts index e1334d82800c..ab3660ee87ec 100644 --- a/packages/light-client/src/spec/validateLightClientBootstrap.ts +++ b/packages/light-client/src/spec/validateLightClientBootstrap.ts @@ -1,13 +1,24 @@ import {byteArrayEquals} from "@chainsafe/ssz"; import {Root, ssz, allForks} from "@lodestar/types"; +import {ChainForkConfig} from "@lodestar/config"; import {toHex} from "@lodestar/utils"; import {isValidMerkleBranch} from "../utils/verifyMerkleBranch.js"; +import {isValidLightClientHeader} from "./utils.js"; const CURRENT_SYNC_COMMITTEE_INDEX = 22; const CURRENT_SYNC_COMMITTEE_DEPTH = 5; -export function validateLightClientBootstrap(trustedBlockRoot: Root, bootstrap: allForks.LightClientBootstrap): void { +export function validateLightClientBootstrap( + config: ChainForkConfig, + trustedBlockRoot: Root, + bootstrap: allForks.LightClientBootstrap +): void { const headerRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(bootstrap.header.beacon); + + if (!isValidLightClientHeader(config, bootstrap.header)) { + throw Error("Bootstrap Header is not Valid Light Client Header"); + } + if (!byteArrayEquals(headerRoot, trustedBlockRoot)) { throw Error(`bootstrap header root ${toHex(headerRoot)} != trusted root ${toHex(trustedBlockRoot)}`); } diff --git a/packages/light-client/src/spec/validateLightClientUpdate.ts b/packages/light-client/src/spec/validateLightClientUpdate.ts index 1599d988fb38..3504773b76ab 100644 --- a/packages/light-client/src/spec/validateLightClientUpdate.ts +++ b/packages/light-client/src/spec/validateLightClientUpdate.ts @@ -1,4 +1,5 @@ import {Root, ssz, allForks} from "@lodestar/types"; +import {ChainForkConfig} from "@lodestar/config"; import bls from "@chainsafe/bls/switchable"; import type {PublicKey, Signature} from "@chainsafe/bls/types"; import { @@ -13,10 +14,18 @@ import { import {getParticipantPubkeys, sumBits} from "../utils/utils.js"; import {isValidMerkleBranch} from "../utils/index.js"; import {SyncCommitteeFast} from "../types.js"; -import {isFinalityUpdate, isSyncCommitteeUpdate, isZeroedHeader, isZeroedSyncCommittee, ZERO_HASH} from "./utils.js"; +import { + isFinalityUpdate, + isSyncCommitteeUpdate, + isZeroedHeader, + isZeroedSyncCommittee, + ZERO_HASH, + isValidLightClientHeader, +} from "./utils.js"; import {ILightClientStore} from "./store.js"; export function validateLightClientUpdate( + config: ChainForkConfig, store: ILightClientStore, update: allForks.LightClientUpdate, syncCommittee: SyncCommitteeFast @@ -26,6 +35,10 @@ export function validateLightClientUpdate( throw Error("Sync committee has not sufficient participants"); } + if (!isValidLightClientHeader(config, update.attestedHeader)) { + throw Error("Attested Header is not Valid Light Client Header"); + } + // Sanity check that slots are in correct order if (update.signatureSlot <= update.attestedHeader.beacon.slot) { throw Error( @@ -48,12 +61,16 @@ export function validateLightClientUpdate( } else { let finalizedRoot: Root; - if (update.finalizedHeader.beacon.slot == GENESIS_SLOT) { + if (update.finalizedHeader.beacon.slot === GENESIS_SLOT) { if (!isZeroedHeader(update.finalizedHeader.beacon)) { throw Error("finalizedHeader must be zero for not finality update"); } finalizedRoot = ZERO_HASH; } else { + if (!isValidLightClientHeader(config, update.finalizedHeader)) { + throw Error("Finalized Header is not valid Light Client Header"); + } + finalizedRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(update.finalizedHeader.beacon); } diff --git a/packages/light-client/test/unit/isValidLightClientHeader.test.ts b/packages/light-client/test/unit/isValidLightClientHeader.test.ts new file mode 100644 index 000000000000..033a2c806098 --- /dev/null +++ b/packages/light-client/test/unit/isValidLightClientHeader.test.ts @@ -0,0 +1,97 @@ +import {expect} from "chai"; +import {ssz, allForks} from "@lodestar/types"; +import {createBeaconConfig, createChainForkConfig, defaultChainConfig} from "@lodestar/config"; +import {fromHexString} from "@chainsafe/ssz"; +import {isValidLightClientHeader} from "../../src/spec/utils.js"; + +describe("isValidLightClientHeader", function () { + /* eslint-disable @typescript-eslint/naming-convention */ + const chainConfig = createChainForkConfig({ + ...defaultChainConfig, + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 0, + CAPELLA_FORK_EPOCH: 1, + EIP4844_FORK_EPOCH: Infinity, + }); + + const genesisValidatorsRoot = Buffer.alloc(32, 0xaa); + const config = createBeaconConfig(chainConfig, genesisValidatorsRoot); + + const altairLCHeader = { + beacon: { + slot: 5, + proposerIndex: 29852, + parentRoot: fromHexString("0x2490c4e438b6c1476c4a666011955f6a239b82e7c31af452ee72e263e8a82cef"), + stateRoot: fromHexString("0x95f9a788a5ebed7275dc809978064ddf62a57864a0c48b10aa87e9ebec87b6c5"), + bodyRoot: fromHexString("0x48a2ebb21de0cf70f599d4c0bcdb2e4ca791d5e9b396cbebfe50ce3295396041"), + }, + }; + + const altairUpgradedCapellaLCHeader = { + beacon: altairLCHeader.beacon, + execution: ssz.capella.LightClientHeader.fields.execution.defaultValue(), + executionBranch: ssz.capella.LightClientHeader.fields.executionBranch.defaultValue(), + }; + + const altairUpgradedDenebLCHeader = { + beacon: altairLCHeader.beacon, + execution: ssz.deneb.LightClientHeader.fields.execution.defaultValue(), + executionBranch: ssz.deneb.LightClientHeader.fields.executionBranch.defaultValue(), + }; + + const capellaLCHeader = { + beacon: { + slot: 100936, + proposerIndex: 29852, + parentRoot: fromHexString("0x2490c4e438b6c1476c4a666011955f6a239b82e7c31af452ee72e263e8a82cef"), + stateRoot: fromHexString("0x95f9a788a5ebed7275dc809978064ddf62a57864a0c48b10aa87e9ebec87b6c5"), + bodyRoot: fromHexString("0x48a2ebb21de0cf70f599d4c0bcdb2e4ca791d5e9b396cbebfe50ce3295396041"), + }, + execution: { + parentHash: fromHexString("0x8dbfa7d03da88416dabda95cf83e3d2c7bbc820bfbe2a685a2a629eda54b2320"), + feeRecipient: fromHexString("0xf97e180c050e5ab072211ad2c213eb5aee4df134"), + stateRoot: fromHexString("0x51a4df7228204e2d2ba15c8664c16b7abd41480e8087727a96d3b84e04f661d8"), + receiptsRoot: fromHexString("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), + logsBloom: fromHexString( + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ), + prevRandao: fromHexString("0x7408cf3778b7d879b3317160858c3ef2818d2cad53dbf3a638af598c351be46b"), + blockNumber: 96392, + gasLimit: 30000000, + gasUsed: 0, + timestamp: 1676474832, + extraData: fromHexString("0x"), + baseFeePerGas: BigInt(7), + blockHash: fromHexString("0x5570e6072f4e469f256fd2c2d0ee7ddce2da8cce8b0ac1b36495e5aa25987e7b"), + transactionsRoot: fromHexString("0x7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1"), + withdrawalsRoot: fromHexString("0x21bb571478b90df5866e87aa358f5a5e93682db3ba242baf2bdf127f2a9a54ce"), + }, + executionBranch: [ + fromHexString("0xbc9397945c24273581f86275332d37761dff3b9fdaaddee03749bcee644213a8"), + fromHexString("0x336488033fe5f3ef4ccc12af07b9370b92e553e35ecb4a337a1b1c0e4afe1e0e"), + fromHexString("0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71"), + fromHexString("0x8bbebf0f8b663cd4a56f3739019e52507ba6c6b99ad530ab36925c12d84b7b5e"), + ], + }; + + const capellaUpgradedDenebHeader = { + beacon: capellaLCHeader.beacon, + execution: {...capellaLCHeader.execution, excessDataGas: 0}, + executionBranch: capellaLCHeader.executionBranch, + }; + + const testCases: [string, allForks.LightClientHeader][] = [ + ["altair LC header", altairLCHeader], + ["altair upgraded to capella", altairUpgradedCapellaLCHeader], + ["altair upgraded to deneb", altairUpgradedDenebLCHeader], + ["capella LC header", capellaLCHeader], + ["capella upgraded to deneb LC header", capellaUpgradedDenebHeader], + ]; + + testCases.forEach(([name, header]: [string, allForks.LightClientHeader]) => { + it(name, function () { + const isValid = isValidLightClientHeader(config, header); + expect(isValid).to.be.true; + }); + }); +});