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

fix: light client generating LightClientUpdate with wrong length of branches #7187

Open
wants to merge 5 commits into
base: unstable
Choose a base branch
from
Open
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
27 changes: 22 additions & 5 deletions packages/beacon-node/src/chain/lightClient/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
altair,
BeaconBlock,
BeaconBlockBody,
electra,
LightClientBootstrap,
LightClientFinalityUpdate,
LightClientHeader,
Expand Down Expand Up @@ -42,6 +43,7 @@ import {
ForkLightClient,
highestFork,
forkLightClient,
isForkPostElectra,
} from "@lodestar/params";

import {IBeaconDb} from "../../db/index.js";
Expand All @@ -57,6 +59,7 @@ import {
getCurrentSyncCommitteeBranch,
getBlockBodyExecutionHeaderProof,
} from "./proofs.js";
import {NUM_WITNESS, NUM_WITNESS_ELECTRA} from "../../db/repositories/lightclientSyncCommitteeWitness.js";

export type LightClientServerOpts = {
disableLightClientServerOnImportBlockHead?: boolean;
Expand Down Expand Up @@ -208,7 +211,9 @@ export class LightClientServer {
private checkpointHeaders = new Map<BlockRooHex, LightClientHeader>();
private latestHeadUpdate: LightClientOptimisticUpdate | null = null;

private readonly zero: Pick<altair.LightClientUpdate, "finalityBranch" | "finalizedHeader">;
private readonly zero:
| Pick<altair.LightClientUpdate, "finalityBranch" | "finalizedHeader">
| Pick<electra.LightClientUpdate, "finalityBranch" | "finalizedHeader">;
private finalized: LightClientFinalityUpdate | null = null;

constructor(
Expand All @@ -225,7 +230,9 @@ export class LightClientServer {
this.zero = {
// Assign the hightest fork's default value because it can always be typecasted down to correct fork
finalizedHeader: sszTypesFor(highestFork(forkLightClient)).LightClientHeader.defaultValue(),
finalityBranch: ssz.altair.LightClientUpdate.fields.finalityBranch.defaultValue(),
// Electra finalityBranch has fixed length of 5 whereas altair has 4. The fifth element will be ignored
// when serializing as altair LightClientUpdate
finalityBranch: ssz.electra.LightClientUpdate.fields.finalityBranch.defaultValue(),
};

if (metrics) {
Expand Down Expand Up @@ -388,12 +395,13 @@ export class LightClientServer {
parentBlockSlot: Slot
): Promise<void> {
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) {
Expand Down Expand Up @@ -621,6 +629,16 @@ export class LightClientServer {
if (!syncCommitteeWitness) {
throw Error(`syncCommitteeWitness not available at ${toRootHex(attestedData.blockRoot)}`);
}

const attestedFork = this.config.getForkName(attestedHeader.beacon.slot);
const numWitness = syncCommitteeWitness.witness.length;
if (isForkPostElectra(attestedFork) && numWitness !== NUM_WITNESS_ELECTRA) {
throw Error(`Expected ${NUM_WITNESS_ELECTRA} witnesses in post-Electra numWiteness=${numWitness}`);
}
if (!isForkPostElectra(attestedFork) && numWitness !== NUM_WITNESS) {
throw Error(`Expected ${NUM_WITNESS} witnesses in pre-Electra numWiteness=${numWitness}`);
}

const nextSyncCommittee = await this.db.syncCommittee.get(syncCommitteeWitness.nextSyncCommitteeRoot);
if (!nextSyncCommittee) {
throw Error("nextSyncCommittee not available");
Expand All @@ -641,7 +659,6 @@ export class LightClientServer {
finalityBranch = attestedData.finalityBranch;
finalizedHeader = finalizedHeaderAttested;
// Fork of LightClientUpdate is based off on attested header's fork
const attestedFork = this.config.getForkName(attestedHeader.beacon.slot);
if (this.config.getForkName(finalizedHeader.beacon.slot) !== attestedFork) {
finalizedHeader = upgradeLightClientHeader(this.config, attestedFork, finalizedHeader);
}
Expand Down
54 changes: 40 additions & 14 deletions packages/beacon-node/src/chain/lightClient/proofs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.left.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,
Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/src/chain/lightClient/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
* ```
*/
export type SyncCommitteeWitness = {
/** Vector[Bytes32, 4] */
/** Vector[Bytes32, 4] or Vector[Bytes32, 5] depends on the fork */
witness: Uint8Array[];
currentSyncCommitteeRoot: Uint8Array;
nextSyncCommitteeRoot: Uint8Array;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ import {ssz} from "@lodestar/types";
import {SyncCommitteeWitness} from "../../chain/lightClient/types.js";
import {Bucket, getBucketNameByValue} from "../buckets.js";

// We add a 1-byte prefix where 0 means pre-electra and 1 means post-electra
enum PrefixByte {
PRE_ELECTRA = 0,
POST_ELECTRA = 1,
}

export const NUM_WITNESS = 4;
export const NUM_WITNESS_ELECTRA = 5;

/**
* Historical sync committees witness by block root
*
Expand All @@ -13,12 +22,56 @@ import {Bucket, getBucketNameByValue} from "../buckets.js";
export class SyncCommitteeWitnessRepository extends Repository<Uint8Array, SyncCommitteeWitness> {
constructor(config: ChainForkConfig, db: DatabaseController<Uint8Array, Uint8Array>) {
const bucket = Bucket.lightClient_syncCommitteeWitness;
// Pick some type but won't be used. Witness can be 4 or 5 so need to handle dynamically
const type = new ContainerType({
witness: new VectorCompositeType(ssz.Root, 4),
witness: new VectorCompositeType(ssz.Root, NUM_WITNESS),
currentSyncCommitteeRoot: ssz.Root,
nextSyncCommitteeRoot: ssz.Root,
});

super(config, db, bucket, type, getBucketNameByValue(bucket));
}

// Overrides for multi-fork
encodeValue(value: SyncCommitteeWitness): Uint8Array {
const numWitness = value.witness.length;

if (numWitness !== NUM_WITNESS && numWitness !== NUM_WITNESS_ELECTRA) {
throw Error(`Number of witness can only be 4 pre-electra or 5 post-electra numWitness=${numWitness}`);
}

const type = new ContainerType({
witness: new VectorCompositeType(ssz.Root, numWitness),
currentSyncCommitteeRoot: ssz.Root,
nextSyncCommitteeRoot: ssz.Root,
});

const valueBytes = type.serialize(value);

// We need to differentiate between post-electra and pre-electra witness
// such that we can deserialize correctly
const isPostElectra = numWitness === NUM_WITNESS_ELECTRA;
const prefixByte = new Uint8Array(1);
prefixByte[0] = isPostElectra ? PrefixByte.POST_ELECTRA : PrefixByte.PRE_ELECTRA;

const prefixedData = new Uint8Array(1 + valueBytes.length);
prefixedData.set(prefixByte, 0);
prefixedData.set(valueBytes, 1);

return prefixedData;
}

decodeValue(data: Uint8Array): SyncCommitteeWitness {
// First byte is written
const prefix = data.subarray(0, 1);
const isPostElectra = prefix[0] === PrefixByte.POST_ELECTRA;

const type = new ContainerType({
witness: new VectorCompositeType(ssz.Root, isPostElectra ? NUM_WITNESS_ELECTRA : NUM_WITNESS),
currentSyncCommitteeRoot: ssz.Root,
nextSyncCommitteeRoot: ssz.Root,
});

return type.deserialize(data.subarray(1));
}
}
Loading
Loading