import {PublicKey, Signature, fastAggregateVerify} from "@chainsafe/lodestar-z/blst"; import {ChainForkConfig} from "@lodestar/config"; import {DOMAIN_SYNC_COMMITTEE, GENESIS_SLOT, MIN_SYNC_COMMITTEE_PARTICIPANTS} from "@lodestar/params"; import {LightClientUpdate, Root, ssz} from "@lodestar/types"; import type {ILightClientStore, SyncCommitteeFast} from "./store.js"; import { ZERO_HASH, finalizedRootGindexAtFork, getParticipantPubkeys, isFinalityUpdate, isSyncCommitteeUpdate, isValidLightClientHeader, isValidNormalizedMerkleBranch, isZeroedHeader, isZeroedSyncCommittee, nextSyncCommitteeGindexAtFork, sumBits, } from "./utils.js"; export function validateLightClientUpdate( config: ChainForkConfig, store: ILightClientStore, update: LightClientUpdate, syncCommittee: SyncCommitteeFast ): void { const attestedFork = config.getForkName(update.attestedHeader.beacon.slot); // Verify sync committee has sufficient participants if (sumBits(update.syncAggregate.syncCommitteeBits) < MIN_SYNC_COMMITTEE_PARTICIPANTS) { 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( `signature slot ${update.signatureSlot} must be after attested header slot ${update.attestedHeader.beacon.slot}` ); } if (update.attestedHeader.beacon.slot < update.finalizedHeader.beacon.slot) { throw Error( `attested header slot ${update.signatureSlot} must be after finalized header slot ${update.finalizedHeader.beacon.slot}` ); } // Verify that the `finality_branch`, if present, confirms `finalized_header` // to match the finalized checkpoint root saved in the state of `attested_header`. // Note that the genesis finalized checkpoint root is represented as a zero hash. if (!isFinalityUpdate(update)) { if (!isZeroedHeader(update.finalizedHeader.beacon)) { throw Error("finalizedHeader must be zero for non-finality update"); } } else { let finalizedRoot: Root; 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); } if ( !isValidNormalizedMerkleBranch( finalizedRoot, update.finalityBranch, finalizedRootGindexAtFork(attestedFork), update.attestedHeader.beacon.stateRoot ) ) { throw Error("Invalid finality header merkle branch"); } } // Verify that the `next_sync_committee`, if present, actually is the next sync committee saved in the // state of the `attested_header` if (!isSyncCommitteeUpdate(update)) { if (!isZeroedSyncCommittee(update.nextSyncCommittee)) { throw Error("nextSyncCommittee must be zero for non sync committee update"); } } else { if ( !isValidNormalizedMerkleBranch( ssz.altair.SyncCommittee.hashTreeRoot(update.nextSyncCommittee), update.nextSyncCommitteeBranch, nextSyncCommitteeGindexAtFork(attestedFork), update.attestedHeader.beacon.stateRoot ) ) { throw Error("Invalid next sync committee merkle branch"); } } // Verify sync committee aggregate signature const participantPubkeys = getParticipantPubkeys(syncCommittee.pubkeys, update.syncAggregate.syncCommitteeBits); const signingRoot = ssz.phase0.SigningData.hashTreeRoot({ objectRoot: ssz.phase0.BeaconBlockHeader.hashTreeRoot(update.attestedHeader.beacon), domain: store.config.getDomain(update.signatureSlot - 1, DOMAIN_SYNC_COMMITTEE), }); if (!isValidBlsAggregate(participantPubkeys, signingRoot, update.syncAggregate.syncCommitteeSignature)) { throw Error("Invalid aggregate signature"); } } /** * Same as BLS.verifyAggregate but with detailed error messages */ function isValidBlsAggregate(publicKeys: PublicKey[], message: Uint8Array, signature: Uint8Array): boolean { let sig: Signature; try { sig = Signature.fromBytes(signature, true); } catch (e) { (e as Error).message = `Error deserializing signature: ${(e as Error).message}`; throw e; } try { return fastAggregateVerify(message, publicKeys, sig); } catch (e) { (e as Error).message = `Error verifying signature: ${(e as Error).message}`; throw e; } }