Skip to content

Commit

Permalink
feat: add electra support for light-client (#7063)
Browse files Browse the repository at this point in the history
* Add constant

* Use constants

* Remove ZERO_NEXT_SYNC_COMMITTEE_BRANCH

* Add normalizeMerkleBranch

* add getId to CheckpointHeaderRepository

* fix: light-client unit tests

* chore: lint

* Fix normalizeMerkleBranch

* Enable LC spec test

* fix spec test

---------

Co-authored-by: NC <[email protected]>
  • Loading branch information
2 people authored and philknows committed Sep 27, 2024
1 parent 258ca67 commit 08a5055
Show file tree
Hide file tree
Showing 14 changed files with 155 additions and 42 deletions.
14 changes: 10 additions & 4 deletions packages/beacon-node/src/chain/lightClient/proofs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import {Tree} from "@chainsafe/persistent-merkle-tree";
import {BeaconStateAllForks} from "@lodestar/state-transition";
import {FINALIZED_ROOT_GINDEX, BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX, ForkExecution} from "@lodestar/params";
import {BeaconStateAllForks, CachedBeaconStateAllForks} from "@lodestar/state-transition";
import {
FINALIZED_ROOT_GINDEX,
BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX,
ForkExecution,
FINALIZED_ROOT_GINDEX_ELECTRA,
} from "@lodestar/params";
import {BeaconBlockBody, SSZTypesFor, ssz} from "@lodestar/types";

import {SyncCommitteeWitness} from "./types.js";
Expand Down Expand Up @@ -40,9 +45,10 @@ export function getCurrentSyncCommitteeBranch(syncCommitteesWitness: SyncCommitt
return [syncCommitteesWitness.nextSyncCommitteeRoot, ...syncCommitteesWitness.witness];
}

export function getFinalizedRootProof(state: BeaconStateAllForks): Uint8Array[] {
export function getFinalizedRootProof(state: CachedBeaconStateAllForks): Uint8Array[] {
state.commit();
return new Tree(state.node).getSingleProof(BigInt(FINALIZED_ROOT_GINDEX));
const finalizedRootGindex = state.epochCtx.isPostElectra() ? FINALIZED_ROOT_GINDEX_ELECTRA : FINALIZED_ROOT_GINDEX;
return new Tree(state.node).getSingleProof(BigInt(finalizedRootGindex));
}

export function getBlockBodyExecutionHeaderProof(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ export class CheckpointHeaderRepository extends Repository<Uint8Array, LightClie
decodeValue(data: Uint8Array): LightClientHeader {
return getLightClientHeaderTypeFromBytes(this.config, data).deserialize(data);
}

getId(value: LightClientHeader): Uint8Array {
return this.config.getLightClientForkTypes(value.beacon.slot).LightClientHeader.hashTreeRoot(value);
}
}
23 changes: 18 additions & 5 deletions packages/beacon-node/src/network/reqresp/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import {Type} from "@chainsafe/ssz";
import {ForkLightClient, ForkName, isForkLightClient} from "@lodestar/params";
import {Protocol, ProtocolHandler, ReqRespRequest} from "@lodestar/reqresp";
import {Metadata, Root, SignedBeaconBlock, altair, deneb, phase0, ssz, sszTypesFor} from "@lodestar/types";
import {
LightClientBootstrap,
LightClientFinalityUpdate,
LightClientOptimisticUpdate,
LightClientUpdate,
Metadata,
Root,
SignedBeaconBlock,
altair,
deneb,
phase0,
ssz,
sszTypesFor,
} from "@lodestar/types";

export type ProtocolNoHandler = Omit<Protocol, "handler">;

Expand Down Expand Up @@ -48,10 +61,10 @@ type ResponseBodyByMethod = {
[ReqRespMethod.BeaconBlocksByRoot]: SignedBeaconBlock;
[ReqRespMethod.BlobSidecarsByRange]: deneb.BlobSidecar;
[ReqRespMethod.BlobSidecarsByRoot]: deneb.BlobSidecar;
[ReqRespMethod.LightClientBootstrap]: altair.LightClientBootstrap;
[ReqRespMethod.LightClientUpdatesByRange]: altair.LightClientUpdate;
[ReqRespMethod.LightClientFinalityUpdate]: altair.LightClientFinalityUpdate;
[ReqRespMethod.LightClientOptimisticUpdate]: altair.LightClientOptimisticUpdate;
[ReqRespMethod.LightClientBootstrap]: LightClientBootstrap;
[ReqRespMethod.LightClientUpdatesByRange]: LightClientUpdate;
[ReqRespMethod.LightClientFinalityUpdate]: LightClientFinalityUpdate;
[ReqRespMethod.LightClientOptimisticUpdate]: LightClientOptimisticUpdate;
};

/** Request SSZ type for each method and ForkName */
Expand Down
5 changes: 2 additions & 3 deletions packages/beacon-node/test/spec/utils/specTestIterator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,9 @@ export const defaultSkipOpts: SkipOpts = {
skippedTestSuites: [
/^capella\/light_client\/single_merkle_proof\/BeaconBlockBody.*/,
/^deneb\/light_client\/single_merkle_proof\/BeaconBlockBody.*/,
/^electra\/light_client\/.*/,
/^electra\/light_client\/single_merkle_proof\/BeaconBlockBody.*/,
],
// TODO Electra: Review this test in the next spec test release
skippedTests: [/^deneb\/light_client\/sync\/.*electra_fork.*/],
skippedTests: [],
skippedRunners: ["merkle_proof", "networking"],
};

Expand Down
6 changes: 3 additions & 3 deletions packages/light-client/src/spec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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_NEXT_SYNC_COMMITTEE_BRANCH, ZERO_SYNC_COMMITTEE} from "./utils.js";
import {ZERO_FINALITY_BRANCH, ZERO_HEADER, ZERO_SYNC_COMMITTEE, getZeroSyncCommitteeBranch} from "./utils.js";

export {isBetterUpdate, toLightClientUpdateSummary} from "./isBetterUpdate.js";
export type {LightClientUpdateSummary} from "./isBetterUpdate.js";
Expand All @@ -37,7 +37,7 @@ export class LightclientSpec {
this.onUpdate(currentSlot, {
attestedHeader: finalityUpdate.attestedHeader,
nextSyncCommittee: ZERO_SYNC_COMMITTEE,
nextSyncCommitteeBranch: ZERO_NEXT_SYNC_COMMITTEE_BRANCH,
nextSyncCommitteeBranch: getZeroSyncCommitteeBranch(this.config.getForkName(finalityUpdate.signatureSlot)),
finalizedHeader: finalityUpdate.finalizedHeader,
finalityBranch: finalityUpdate.finalityBranch,
syncAggregate: finalityUpdate.syncAggregate,
Expand All @@ -49,7 +49,7 @@ export class LightclientSpec {
this.onUpdate(currentSlot, {
attestedHeader: optimisticUpdate.attestedHeader,
nextSyncCommittee: ZERO_SYNC_COMMITTEE,
nextSyncCommitteeBranch: ZERO_NEXT_SYNC_COMMITTEE_BRANCH,
nextSyncCommitteeBranch: getZeroSyncCommitteeBranch(this.config.getForkName(optimisticUpdate.signatureSlot)),
finalizedHeader: {beacon: ZERO_HEADER},
finalityBranch: ZERO_FINALITY_BRANCH,
syncAggregate: optimisticUpdate.syncAggregate,
Expand Down
32 changes: 29 additions & 3 deletions packages/light-client/src/spec/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
ForkName,
BLOCK_BODY_EXECUTION_PAYLOAD_DEPTH as EXECUTION_PAYLOAD_DEPTH,
BLOCK_BODY_EXECUTION_PAYLOAD_INDEX as EXECUTION_PAYLOAD_INDEX,
NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA,
isForkPostElectra,
FINALIZED_ROOT_DEPTH_ELECTRA,
} from "@lodestar/params";
import {
ssz,
Expand All @@ -17,17 +20,18 @@ import {
LightClientUpdate,
BeaconBlockHeader,
SyncCommittee,
isElectraLightClientUpdate,
} from "@lodestar/types";
import {ChainForkConfig} from "@lodestar/config";

import {isValidMerkleBranch, computeEpochAtSlot, computeSyncPeriodAtSlot} 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_NEXT_SYNC_COMMITTEE_BRANCH = Array.from({length: NEXT_SYNC_COMMITTEE_DEPTH}, () => ZERO_HASH);
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 */
Expand All @@ -41,10 +45,19 @@ 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 isSyncCommitteeUpdate(update: LightClientUpdate): boolean {
return (
// Fast return for when constructing full LightClientUpdate from partial updates
update.nextSyncCommitteeBranch !== ZERO_NEXT_SYNC_COMMITTEE_BRANCH &&
update.nextSyncCommitteeBranch !==
getZeroSyncCommitteeBranch(isElectraLightClientUpdate(update) ? ForkName.electra : ForkName.altair) &&
update.nextSyncCommitteeBranch.some((branch) => !byteArrayEquals(branch, ZERO_HASH))
);
}
Expand Down Expand Up @@ -160,7 +173,8 @@ export function isValidLightClientHeader(config: ChainForkConfig, header: LightC
if (epoch < config.ELECTRA_FORK_EPOCH) {
if (
(header as LightClientHeader<ForkName.electra>).execution.depositRequestsRoot !== undefined ||
(header as LightClientHeader<ForkName.electra>).execution.withdrawalRequestsRoot !== undefined
(header as LightClientHeader<ForkName.electra>).execution.withdrawalRequestsRoot !== undefined ||
(header as LightClientHeader<ForkName.electra>).execution.consolidationRequestsRoot !== undefined
) {
return false;
}
Expand All @@ -184,6 +198,14 @@ export function upgradeLightClientUpdate(
): 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;
}
Expand All @@ -195,6 +217,10 @@ export function upgradeLightClientFinalityUpdate(
): 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ import {byteArrayEquals} from "@chainsafe/ssz";
import {LightClientBootstrap, Root, ssz} from "@lodestar/types";
import {ChainForkConfig} from "@lodestar/config";
import {toHex} from "@lodestar/utils";
import {isForkPostElectra} from "@lodestar/params";
import {isValidMerkleBranch} from "../utils/verifyMerkleBranch.js";
import {isValidLightClientHeader} from "./utils.js";

const CURRENT_SYNC_COMMITTEE_INDEX = 22;
const CURRENT_SYNC_COMMITTEE_DEPTH = 5;
const CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA = 22;
const CURRENT_SYNC_COMMITTEE_DEPTH_ELECTRA = 6;

export function validateLightClientBootstrap(
config: ChainForkConfig,
trustedBlockRoot: Root,
bootstrap: LightClientBootstrap
): void {
const headerRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(bootstrap.header.beacon);
const fork = config.getForkName(bootstrap.header.beacon.slot);

if (!isValidLightClientHeader(config, bootstrap.header)) {
throw Error("Bootstrap Header is not Valid Light Client Header");
Expand All @@ -27,8 +31,8 @@ export function validateLightClientBootstrap(
!isValidMerkleBranch(
ssz.altair.SyncCommittee.hashTreeRoot(bootstrap.currentSyncCommittee),
bootstrap.currentSyncCommitteeBranch,
CURRENT_SYNC_COMMITTEE_DEPTH,
CURRENT_SYNC_COMMITTEE_INDEX,
isForkPostElectra(fork) ? CURRENT_SYNC_COMMITTEE_DEPTH_ELECTRA : CURRENT_SYNC_COMMITTEE_DEPTH,
isForkPostElectra(fork) ? CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA : CURRENT_SYNC_COMMITTEE_INDEX,
bootstrap.header.beacon.stateRoot
)
) {
Expand Down
16 changes: 10 additions & 6 deletions packages/light-client/src/spec/validateLightClientUpdate.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import bls from "@chainsafe/bls";
import type {PublicKey, Signature} from "@chainsafe/bls/types";
import {LightClientUpdate, Root, ssz} from "@lodestar/types";
import {LightClientUpdate, Root, isElectraLightClientUpdate, ssz} from "@lodestar/types";
import {ChainForkConfig} from "@lodestar/config";
import {
FINALIZED_ROOT_INDEX,
FINALIZED_ROOT_DEPTH,
NEXT_SYNC_COMMITTEE_INDEX,
NEXT_SYNC_COMMITTEE_DEPTH,
MIN_SYNC_COMMITTEE_PARTICIPANTS,
DOMAIN_SYNC_COMMITTEE,
GENESIS_SLOT,
NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA,
NEXT_SYNC_COMMITTEE_INDEX_ELECTRA,
NEXT_SYNC_COMMITTEE_INDEX,
FINALIZED_ROOT_DEPTH_ELECTRA,
FINALIZED_ROOT_INDEX_ELECTRA,
} from "@lodestar/params";
import {getParticipantPubkeys, sumBits} from "../utils/utils.js";
import {isValidMerkleBranch} from "../utils/index.js";
Expand Down Expand Up @@ -78,8 +82,8 @@ export function validateLightClientUpdate(
!isValidMerkleBranch(
finalizedRoot,
update.finalityBranch,
FINALIZED_ROOT_DEPTH,
FINALIZED_ROOT_INDEX,
isElectraLightClientUpdate(update) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH,
isElectraLightClientUpdate(update) ? FINALIZED_ROOT_INDEX_ELECTRA : FINALIZED_ROOT_INDEX,
update.attestedHeader.beacon.stateRoot
)
) {
Expand All @@ -98,8 +102,8 @@ export function validateLightClientUpdate(
!isValidMerkleBranch(
ssz.altair.SyncCommittee.hashTreeRoot(update.nextSyncCommittee),
update.nextSyncCommitteeBranch,
NEXT_SYNC_COMMITTEE_DEPTH,
NEXT_SYNC_COMMITTEE_INDEX,
isElectraLightClientUpdate(update) ? NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA : NEXT_SYNC_COMMITTEE_DEPTH,
isElectraLightClientUpdate(update) ? NEXT_SYNC_COMMITTEE_INDEX_ELECTRA : NEXT_SYNC_COMMITTEE_INDEX,
update.attestedHeader.beacon.stateRoot
)
) {
Expand Down
15 changes: 15 additions & 0 deletions packages/light-client/src/utils/normalizeMerkleBranch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {ZERO_HASH} from "../spec/utils.js";

export const SYNC_COMMITTEES_DEPTH = 4;
export const SYNC_COMMITTEES_INDEX = 11;

/**
* Given merkle branch ``branch``, extend its depth according to ``depth``
* If given ``depth`` is less than the depth of ``branch``, it will return
* unmodified ``branch``
*/
export function normalizeMerkleBranch(branch: Uint8Array[], depth: number): Uint8Array[] {
const numExtraDepth = depth - branch.length;

return [...Array.from({length: numExtraDepth}, () => ZERO_HASH), ...branch];
}
23 changes: 19 additions & 4 deletions packages/light-client/src/validation.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import bls from "@chainsafe/bls";
import type {PublicKey, Signature} from "@chainsafe/bls/types";
import {altair, LightClientFinalityUpdate, LightClientUpdate, Root, Slot, ssz} from "@lodestar/types";
import {
altair,
isElectraLightClientUpdate,
LightClientFinalityUpdate,
LightClientUpdate,
Root,
Slot,
ssz,
} from "@lodestar/types";
import {
FINALIZED_ROOT_INDEX,
FINALIZED_ROOT_DEPTH,
NEXT_SYNC_COMMITTEE_INDEX,
NEXT_SYNC_COMMITTEE_DEPTH,
MIN_SYNC_COMMITTEE_PARTICIPANTS,
DOMAIN_SYNC_COMMITTEE,
NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA,
FINALIZED_ROOT_DEPTH_ELECTRA,
NEXT_SYNC_COMMITTEE_INDEX_ELECTRA,
} from "@lodestar/params";
import {BeaconConfig} from "@lodestar/config";
import {isValidMerkleBranch} from "./utils/verifyMerkleBranch.js";
Expand Down Expand Up @@ -39,7 +50,11 @@ export function assertValidLightClientUpdate(
if (isFinalized) {
assertValidFinalityProof(update);
} else {
assertZeroHashes(update.finalityBranch, FINALIZED_ROOT_DEPTH, "finalityBranches");
assertZeroHashes(
update.finalityBranch,
isElectraLightClientUpdate(update) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH,
"finalityBranches"
);
}

// DIFF FROM SPEC:
Expand Down Expand Up @@ -99,8 +114,8 @@ export function assertValidSyncCommitteeProof(update: LightClientUpdate): void {
!isValidMerkleBranch(
ssz.altair.SyncCommittee.hashTreeRoot(update.nextSyncCommittee),
update.nextSyncCommitteeBranch,
NEXT_SYNC_COMMITTEE_DEPTH,
NEXT_SYNC_COMMITTEE_INDEX,
isElectraLightClientUpdate(update) ? NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA : NEXT_SYNC_COMMITTEE_DEPTH,
isElectraLightClientUpdate(update) ? NEXT_SYNC_COMMITTEE_INDEX_ELECTRA : NEXT_SYNC_COMMITTEE_INDEX,
update.attestedHeader.beacon.stateRoot
)
) {
Expand Down
16 changes: 16 additions & 0 deletions packages/light-client/test/unit/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {describe, it, expect} from "vitest";
import {isValidMerkleBranch} from "../../src/utils/verifyMerkleBranch.js";
import {computeMerkleBranch} from "../utils/utils.js";
import {normalizeMerkleBranch} from "../../src/utils/normalizeMerkleBranch.js";
import {ZERO_HASH} from "../../src/spec/utils.js";

describe("utils", () => {
it("constructMerkleBranch", () => {
Expand All @@ -11,4 +13,18 @@ describe("utils", () => {

expect(isValidMerkleBranch(leaf, proof, depth, index, root)).toBe(true);
});
it("normalizeMerkleBranch", () => {
const branch: Uint8Array[] = [];
const branchDepth = 5;
const newDepth = 7;

for (let i = 0; i < branchDepth; i++) {
branch.push(new Uint8Array(Array.from({length: 32}, () => i)));
}

const normalizedBranch = normalizeMerkleBranch(branch, newDepth);
const expectedNormalizedBranch = [ZERO_HASH, ZERO_HASH, ...branch];

expect(normalizedBranch).toEqual(expectedNormalizedBranch);
});
});
6 changes: 4 additions & 2 deletions packages/params/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,9 @@ export const BLOBSIDECAR_FIXED_SIZE = ACTIVE_PRESET === PresetName.minimal ? 131
// Electra Misc
export const UNSET_DEPOSIT_REQUESTS_START_INDEX = 2n ** 64n - 1n;
export const FULL_EXIT_REQUEST_AMOUNT = 0;
export const FINALIZED_ROOT_GINDEX_ELECTRA = 169;
export const FINALIZED_ROOT_DEPTH_ELECTRA = 7;
export const FINALIZED_ROOT_INDEX_ELECTRA = 41;
export const NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA = 87;
export const NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA = 6;
export const FINALIZED_ROOT_DEPTH_ELECTRA = 7;
export const FINALIZED_ROOT_INDEX_ELECTRA = 169;
export const NEXT_SYNC_COMMITTEE_INDEX_ELECTRA = 23;
Loading

0 comments on commit 08a5055

Please sign in to comment.