Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add electra support for light-client #7063

Merged
merged 10 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading