Skip to content

Commit 7c3f403

Browse files
ensi321nflaig
andcommitted
fix: light client generating LightClientUpdate with wrong length of branches (#7187)
* initial commit * Rewrite SyncCommitteeWitnessRepository * Fix finality branch * Update unit test * fix e2e * Review PR --------- Co-authored-by: Nico Flaig <nflaig@protonmail.com>
1 parent 54d5886 commit 7c3f403

File tree

9 files changed

+304
-108
lines changed

9 files changed

+304
-108
lines changed

packages/beacon-node/src/chain/lightClient/index.ts

+57-39
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,64 @@
11
import {BitArray, CompositeViewDU} from "@chainsafe/ssz";
2+
import {routes} from "@lodestar/api";
3+
import {ChainForkConfig} from "@lodestar/config";
4+
import {
5+
LightClientUpdateSummary,
6+
isBetterUpdate,
7+
toLightClientUpdateSummary,
8+
upgradeLightClientHeader,
9+
} from "@lodestar/light-client/spec";
10+
import {
11+
ForkExecution,
12+
ForkLightClient,
13+
ForkName,
14+
ForkSeq,
15+
MIN_SYNC_COMMITTEE_PARTICIPANTS,
16+
SYNC_COMMITTEE_SIZE,
17+
forkLightClient,
18+
highestFork,
19+
isForkPostElectra,
20+
} from "@lodestar/params";
21+
import {
22+
CachedBeaconStateAltair,
23+
computeStartSlotAtEpoch,
24+
computeSyncPeriodAtEpoch,
25+
computeSyncPeriodAtSlot,
26+
executionPayloadToPayloadHeader,
27+
} from "@lodestar/state-transition";
228
import {
3-
altair,
429
BeaconBlock,
530
BeaconBlockBody,
631
LightClientBootstrap,
732
LightClientFinalityUpdate,
833
LightClientHeader,
934
LightClientOptimisticUpdate,
1035
LightClientUpdate,
11-
phase0,
1236
Root,
1337
RootHex,
38+
SSZTypesFor,
1439
Slot,
40+
SyncPeriod,
41+
altair,
42+
electra,
43+
phase0,
1544
ssz,
1645
sszTypesFor,
17-
SSZTypesFor,
18-
SyncPeriod,
1946
} from "@lodestar/types";
20-
import {ChainForkConfig} from "@lodestar/config";
21-
import {
22-
CachedBeaconStateAltair,
23-
computeStartSlotAtEpoch,
24-
computeSyncPeriodAtEpoch,
25-
computeSyncPeriodAtSlot,
26-
executionPayloadToPayloadHeader,
27-
} from "@lodestar/state-transition";
28-
import {
29-
isBetterUpdate,
30-
toLightClientUpdateSummary,
31-
LightClientUpdateSummary,
32-
upgradeLightClientHeader,
33-
} from "@lodestar/light-client/spec";
3447
import {Logger, MapDef, pruneSetToMax, toRootHex} from "@lodestar/utils";
35-
import {routes} from "@lodestar/api";
36-
import {
37-
MIN_SYNC_COMMITTEE_PARTICIPANTS,
38-
SYNC_COMMITTEE_SIZE,
39-
ForkName,
40-
ForkSeq,
41-
ForkExecution,
42-
ForkLightClient,
43-
highestFork,
44-
forkLightClient,
45-
} from "@lodestar/params";
4648

49+
import {ZERO_HASH} from "../../constants/index.js";
4750
import {IBeaconDb} from "../../db/index.js";
51+
import {NUM_WITNESS, NUM_WITNESS_ELECTRA} from "../../db/repositories/lightclientSyncCommitteeWitness.js";
4852
import {Metrics} from "../../metrics/index.js";
49-
import {ChainEventEmitter} from "../emitter.js";
5053
import {byteArrayEquals} from "../../util/bytes.js";
51-
import {ZERO_HASH} from "../../constants/index.js";
54+
import {ChainEventEmitter} from "../emitter.js";
5255
import {LightClientServerError, LightClientServerErrorCode} from "../errors/lightClientError.js";
5356
import {
57+
getBlockBodyExecutionHeaderProof,
58+
getCurrentSyncCommitteeBranch,
59+
getFinalizedRootProof,
5460
getNextSyncCommitteeBranch,
5561
getSyncCommitteesWitness,
56-
getFinalizedRootProof,
57-
getCurrentSyncCommitteeBranch,
58-
getBlockBodyExecutionHeaderProof,
5962
} from "./proofs.js";
6063

6164
export type LightClientServerOpts = {
@@ -208,7 +211,10 @@ export class LightClientServer {
208211
private checkpointHeaders = new Map<BlockRooHex, LightClientHeader>();
209212
private latestHeadUpdate: LightClientOptimisticUpdate | null = null;
210213

211-
private readonly zero: Pick<altair.LightClientUpdate, "finalityBranch" | "finalizedHeader">;
214+
private readonly zero: Pick<
215+
altair.LightClientUpdate | electra.LightClientUpdate,
216+
"finalityBranch" | "finalizedHeader"
217+
>;
212218
private finalized: LightClientFinalityUpdate | null = null;
213219

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

231239
if (metrics) {
@@ -388,12 +396,13 @@ export class LightClientServer {
388396
parentBlockSlot: Slot
389397
): Promise<void> {
390398
const blockSlot = block.slot;
391-
const header = blockToLightClientHeader(this.config.getForkName(blockSlot), block);
399+
const fork = this.config.getForkName(blockSlot);
400+
const header = blockToLightClientHeader(fork, block);
392401

393402
const blockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(header.beacon);
394403
const blockRootHex = toRootHex(blockRoot);
395404

396-
const syncCommitteeWitness = getSyncCommitteesWitness(postState);
405+
const syncCommitteeWitness = getSyncCommitteesWitness(fork, postState);
397406

398407
// Only store current sync committee once per run
399408
if (!this.storedCurrentSyncCommittee) {
@@ -621,6 +630,16 @@ export class LightClientServer {
621630
if (!syncCommitteeWitness) {
622631
throw Error(`syncCommitteeWitness not available at ${toRootHex(attestedData.blockRoot)}`);
623632
}
633+
634+
const attestedFork = this.config.getForkName(attestedHeader.beacon.slot);
635+
const numWitness = syncCommitteeWitness.witness.length;
636+
if (isForkPostElectra(attestedFork) && numWitness !== NUM_WITNESS_ELECTRA) {
637+
throw Error(`Expected ${NUM_WITNESS_ELECTRA} witnesses in post-Electra numWitness=${numWitness}`);
638+
}
639+
if (!isForkPostElectra(attestedFork) && numWitness !== NUM_WITNESS) {
640+
throw Error(`Expected ${NUM_WITNESS} witnesses in pre-Electra numWitness=${numWitness}`);
641+
}
642+
624643
const nextSyncCommittee = await this.db.syncCommittee.get(syncCommitteeWitness.nextSyncCommitteeRoot);
625644
if (!nextSyncCommittee) {
626645
throw Error("nextSyncCommittee not available");
@@ -641,7 +660,6 @@ export class LightClientServer {
641660
finalityBranch = attestedData.finalityBranch;
642661
finalizedHeader = finalizedHeaderAttested;
643662
// Fork of LightClientUpdate is based off on attested header's fork
644-
const attestedFork = this.config.getForkName(attestedHeader.beacon.slot);
645663
if (this.config.getForkName(finalizedHeader.beacon.slot) !== attestedFork) {
646664
finalizedHeader = upgradeLightClientHeader(this.config, attestedFork, finalizedHeader);
647665
}

packages/beacon-node/src/chain/lightClient/proofs.ts

+43-17
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,58 @@
11
import {Tree} from "@chainsafe/persistent-merkle-tree";
2-
import {BeaconStateAllForks, CachedBeaconStateAllForks} from "@lodestar/state-transition";
32
import {
4-
FINALIZED_ROOT_GINDEX,
53
BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX,
6-
ForkExecution,
4+
FINALIZED_ROOT_GINDEX,
75
FINALIZED_ROOT_GINDEX_ELECTRA,
6+
ForkExecution,
7+
ForkName,
8+
isForkPostElectra,
89
} from "@lodestar/params";
10+
import {BeaconStateAllForks, CachedBeaconStateAllForks} from "@lodestar/state-transition";
911
import {BeaconBlockBody, SSZTypesFor, ssz} from "@lodestar/types";
1012

1113
import {SyncCommitteeWitness} from "./types.js";
1214

13-
export function getSyncCommitteesWitness(state: BeaconStateAllForks): SyncCommitteeWitness {
15+
export function getSyncCommitteesWitness(fork: ForkName, state: BeaconStateAllForks): SyncCommitteeWitness {
1416
state.commit();
1517
const n1 = state.node;
16-
const n3 = n1.right; // [1]0110
17-
const n6 = n3.left; // 1[0]110
18-
const n13 = n6.right; // 10[1]10
19-
const n27 = n13.right; // 101[1]0
20-
const currentSyncCommitteeRoot = n27.left.root; // n54 1011[0]
21-
const nextSyncCommitteeRoot = n27.right.root; // n55 1011[1]
18+
let witness: Uint8Array[];
19+
let currentSyncCommitteeRoot: Uint8Array;
20+
let nextSyncCommitteeRoot: Uint8Array;
2221

23-
// Witness branch is sorted by descending gindex
24-
const witness = [
25-
n13.left.root, // 26
26-
n6.left.root, // 12
27-
n3.right.root, // 7
28-
n1.left.root, // 2
29-
];
22+
if (isForkPostElectra(fork)) {
23+
const n2 = n1.left;
24+
const n5 = n2.right;
25+
const n10 = n5.left;
26+
const n21 = n10.right;
27+
const n43 = n21.right;
28+
29+
currentSyncCommitteeRoot = n43.left.root; // n86
30+
nextSyncCommitteeRoot = n43.right.root; // n87
31+
32+
// Witness branch is sorted by descending gindex
33+
witness = [
34+
n21.left.root, // 42
35+
n10.left.root, // 20
36+
n5.right.root, // 11
37+
n2.left.root, // 4
38+
n1.right.root, // 3
39+
];
40+
} else {
41+
const n3 = n1.right; // [1]0110
42+
const n6 = n3.left; // 1[0]110
43+
const n13 = n6.right; // 10[1]10
44+
const n27 = n13.right; // 101[1]0
45+
currentSyncCommitteeRoot = n27.left.root; // n54 1011[0]
46+
nextSyncCommitteeRoot = n27.right.root; // n55 1011[1]
47+
48+
// Witness branch is sorted by descending gindex
49+
witness = [
50+
n13.left.root, // 26
51+
n6.left.root, // 12
52+
n3.right.root, // 7
53+
n1.left.root, // 2
54+
];
55+
}
3056

3157
return {
3258
witness,

packages/beacon-node/src/chain/lightClient/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
* ```
2727
*/
2828
export type SyncCommitteeWitness = {
29-
/** Vector[Bytes32, 4] */
29+
/** Vector[Bytes32, 4] or Vector[Bytes32, 5] depending on the fork */
3030
witness: Uint8Array[];
3131
currentSyncCommitteeRoot: Uint8Array;
3232
nextSyncCommitteeRoot: Uint8Array;

packages/beacon-node/src/db/repositories/lightclientSyncCommitteeWitness.ts

+54-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ import {ssz} from "@lodestar/types";
55
import {SyncCommitteeWitness} from "../../chain/lightClient/types.js";
66
import {Bucket, getBucketNameByValue} from "../buckets.js";
77

8+
// We add a 1-byte prefix where 0 means pre-electra and 1 means post-electra
9+
enum PrefixByte {
10+
PRE_ELECTRA = 0,
11+
POST_ELECTRA = 1,
12+
}
13+
14+
export const NUM_WITNESS = 4;
15+
export const NUM_WITNESS_ELECTRA = 5;
16+
817
/**
918
* Historical sync committees witness by block root
1019
*
@@ -13,12 +22,56 @@ import {Bucket, getBucketNameByValue} from "../buckets.js";
1322
export class SyncCommitteeWitnessRepository extends Repository<Uint8Array, SyncCommitteeWitness> {
1423
constructor(config: ChainForkConfig, db: DatabaseController<Uint8Array, Uint8Array>) {
1524
const bucket = Bucket.lightClient_syncCommitteeWitness;
25+
// Pick some type but won't be used. Witness can be 4 or 5 so need to handle dynamically
1626
const type = new ContainerType({
17-
witness: new VectorCompositeType(ssz.Root, 4),
27+
witness: new VectorCompositeType(ssz.Root, NUM_WITNESS),
1828
currentSyncCommitteeRoot: ssz.Root,
1929
nextSyncCommitteeRoot: ssz.Root,
2030
});
2131

2232
super(config, db, bucket, type, getBucketNameByValue(bucket));
2333
}
34+
35+
// Overrides for multi-fork
36+
encodeValue(value: SyncCommitteeWitness): Uint8Array {
37+
const numWitness = value.witness.length;
38+
39+
if (numWitness !== NUM_WITNESS && numWitness !== NUM_WITNESS_ELECTRA) {
40+
throw Error(`Number of witness can only be 4 pre-electra or 5 post-electra numWitness=${numWitness}`);
41+
}
42+
43+
const type = new ContainerType({
44+
witness: new VectorCompositeType(ssz.Root, numWitness),
45+
currentSyncCommitteeRoot: ssz.Root,
46+
nextSyncCommitteeRoot: ssz.Root,
47+
});
48+
49+
const valueBytes = type.serialize(value);
50+
51+
// We need to differentiate between post-electra and pre-electra witness
52+
// such that we can deserialize correctly
53+
const isPostElectra = numWitness === NUM_WITNESS_ELECTRA;
54+
const prefixByte = new Uint8Array(1);
55+
prefixByte[0] = isPostElectra ? PrefixByte.POST_ELECTRA : PrefixByte.PRE_ELECTRA;
56+
57+
const prefixedData = new Uint8Array(1 + valueBytes.length);
58+
prefixedData.set(prefixByte, 0);
59+
prefixedData.set(valueBytes, 1);
60+
61+
return prefixedData;
62+
}
63+
64+
decodeValue(data: Uint8Array): SyncCommitteeWitness {
65+
// First byte is written
66+
const prefix = data.subarray(0, 1);
67+
const isPostElectra = prefix[0] === PrefixByte.POST_ELECTRA;
68+
69+
const type = new ContainerType({
70+
witness: new VectorCompositeType(ssz.Root, isPostElectra ? NUM_WITNESS_ELECTRA : NUM_WITNESS),
71+
currentSyncCommitteeRoot: ssz.Root,
72+
nextSyncCommitteeRoot: ssz.Root,
73+
});
74+
75+
return type.deserialize(data.subarray(1));
76+
}
2477
}

0 commit comments

Comments
 (0)