@lodestar/light-client 1.35.0-dev.f80d2d52da → 1.35.0-dev.fcf8d024ea

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/lib/events.d.ts +1 -5
  2. package/lib/events.d.ts.map +1 -0
  3. package/lib/events.js.map +1 -1
  4. package/lib/index.d.ts +1 -1
  5. package/lib/index.d.ts.map +1 -0
  6. package/lib/index.js +10 -5
  7. package/lib/index.js.map +1 -1
  8. package/lib/spec/index.d.ts +1 -1
  9. package/lib/spec/index.d.ts.map +1 -0
  10. package/lib/spec/index.js +3 -0
  11. package/lib/spec/index.js.map +1 -1
  12. package/lib/spec/isBetterUpdate.d.ts.map +1 -0
  13. package/lib/spec/processLightClientUpdate.d.ts.map +1 -0
  14. package/lib/spec/store.d.ts.map +1 -0
  15. package/lib/spec/store.js +7 -3
  16. package/lib/spec/store.js.map +1 -1
  17. package/lib/spec/utils.d.ts.map +1 -0
  18. package/lib/spec/utils.js.map +1 -1
  19. package/lib/spec/validateLightClientBootstrap.d.ts.map +1 -0
  20. package/lib/spec/validateLightClientUpdate.d.ts.map +1 -0
  21. package/lib/transport/index.d.ts.map +1 -0
  22. package/lib/transport/interface.d.ts.map +1 -0
  23. package/lib/transport/rest.d.ts +1 -1
  24. package/lib/transport/rest.d.ts.map +1 -0
  25. package/lib/transport/rest.js +5 -4
  26. package/lib/transport/rest.js.map +1 -1
  27. package/lib/transport.d.ts.map +1 -0
  28. package/lib/types.d.ts.map +1 -0
  29. package/lib/utils/api.d.ts.map +1 -0
  30. package/lib/utils/chunkify.d.ts.map +1 -0
  31. package/lib/utils/clock.d.ts.map +1 -0
  32. package/lib/utils/domain.d.ts.map +1 -0
  33. package/lib/utils/index.d.ts.map +1 -0
  34. package/lib/utils/logger.d.ts.map +1 -0
  35. package/lib/utils/map.d.ts.map +1 -0
  36. package/lib/utils/normalizeMerkleBranch.d.ts.map +1 -0
  37. package/lib/utils/update.d.ts.map +1 -0
  38. package/lib/utils/utils.d.ts.map +1 -0
  39. package/lib/utils/verifyMerkleBranch.d.ts.map +1 -0
  40. package/lib/utils.d.ts.map +1 -0
  41. package/lib/validation.d.ts.map +1 -0
  42. package/package.json +16 -18
  43. package/src/events.ts +17 -0
  44. package/src/index.ts +340 -0
  45. package/src/spec/index.ts +71 -0
  46. package/src/spec/isBetterUpdate.ts +94 -0
  47. package/src/spec/processLightClientUpdate.ts +119 -0
  48. package/src/spec/store.ts +105 -0
  49. package/src/spec/utils.ts +266 -0
  50. package/src/spec/validateLightClientBootstrap.ts +41 -0
  51. package/src/spec/validateLightClientUpdate.ts +154 -0
  52. package/src/transport/index.ts +2 -0
  53. package/src/transport/interface.ts +37 -0
  54. package/src/transport/rest.ts +89 -0
  55. package/src/transport.ts +2 -0
  56. package/src/types.ts +20 -0
  57. package/src/utils/api.ts +19 -0
  58. package/src/utils/chunkify.ts +26 -0
  59. package/src/utils/clock.ts +45 -0
  60. package/src/utils/domain.ts +44 -0
  61. package/src/utils/index.ts +8 -0
  62. package/src/utils/logger.ts +29 -0
  63. package/src/utils/map.ts +20 -0
  64. package/src/utils/normalizeMerkleBranch.ts +15 -0
  65. package/src/utils/update.ts +30 -0
  66. package/src/utils/utils.ts +95 -0
  67. package/src/utils/verifyMerkleBranch.ts +29 -0
  68. package/src/utils.ts +2 -0
  69. package/src/validation.ts +201 -0
@@ -0,0 +1,105 @@
1
+ import type {PublicKey} from "@chainsafe/bls/types";
2
+ import {BeaconConfig} from "@lodestar/config";
3
+ import {LightClientBootstrap, LightClientHeader, LightClientUpdate, SyncPeriod} from "@lodestar/types";
4
+ import {computeSyncPeriodAtSlot, deserializeSyncCommittee} from "../utils/index.js";
5
+ import {LightClientUpdateSummary} from "./isBetterUpdate.js";
6
+
7
+ export const MAX_SYNC_PERIODS_CACHE = 2;
8
+
9
+ export interface ILightClientStore {
10
+ readonly config: BeaconConfig;
11
+
12
+ /** Map of trusted SyncCommittee to be used for sig validation */
13
+ readonly syncCommittees: Map<SyncPeriod, SyncCommitteeFast>;
14
+ /** Map of best valid updates */
15
+ readonly bestValidUpdates: Map<SyncPeriod, LightClientUpdateWithSummary>;
16
+
17
+ getMaxActiveParticipants(period: SyncPeriod): number;
18
+ setActiveParticipants(period: SyncPeriod, activeParticipants: number): void;
19
+
20
+ // Header that is finalized
21
+ finalizedHeader: LightClientHeader;
22
+
23
+ // Most recent available reasonably-safe header
24
+ optimisticHeader: LightClientHeader;
25
+ }
26
+
27
+ export interface LightClientStoreEvents {
28
+ onSetFinalizedHeader?: (header: LightClientHeader) => void;
29
+ onSetOptimisticHeader?: (header: LightClientHeader) => void;
30
+ }
31
+
32
+ export class LightClientStore implements ILightClientStore {
33
+ readonly syncCommittees = new Map<SyncPeriod, SyncCommitteeFast>();
34
+ readonly bestValidUpdates = new Map<SyncPeriod, LightClientUpdateWithSummary>();
35
+
36
+ private _finalizedHeader: LightClientHeader;
37
+ private _optimisticHeader: LightClientHeader;
38
+
39
+ private readonly maxActiveParticipants = new Map<SyncPeriod, number>();
40
+
41
+ constructor(
42
+ readonly config: BeaconConfig,
43
+ bootstrap: LightClientBootstrap,
44
+ private readonly events: LightClientStoreEvents
45
+ ) {
46
+ const bootstrapPeriod = computeSyncPeriodAtSlot(bootstrap.header.beacon.slot);
47
+ this.syncCommittees.set(bootstrapPeriod, deserializeSyncCommittee(bootstrap.currentSyncCommittee));
48
+ this._finalizedHeader = bootstrap.header;
49
+ this._optimisticHeader = bootstrap.header;
50
+ }
51
+
52
+ get finalizedHeader(): LightClientHeader {
53
+ return this._finalizedHeader;
54
+ }
55
+
56
+ set finalizedHeader(value: LightClientHeader) {
57
+ this._finalizedHeader = value;
58
+ this.events.onSetFinalizedHeader?.(value);
59
+ }
60
+
61
+ get optimisticHeader(): LightClientHeader {
62
+ return this._optimisticHeader;
63
+ }
64
+
65
+ set optimisticHeader(value: LightClientHeader) {
66
+ this._optimisticHeader = value;
67
+ this.events.onSetOptimisticHeader?.(value);
68
+ }
69
+
70
+ getMaxActiveParticipants(period: SyncPeriod): number {
71
+ const currMaxParticipants = this.maxActiveParticipants.get(period) ?? 0;
72
+ const prevMaxParticipants = this.maxActiveParticipants.get(period - 1) ?? 0;
73
+
74
+ return Math.max(currMaxParticipants, prevMaxParticipants);
75
+ }
76
+
77
+ setActiveParticipants(period: SyncPeriod, activeParticipants: number): void {
78
+ const maxActiveParticipants = this.maxActiveParticipants.get(period) ?? 0;
79
+ if (activeParticipants > maxActiveParticipants) {
80
+ this.maxActiveParticipants.set(period, activeParticipants);
81
+ }
82
+
83
+ // Prune old entries
84
+ for (const key of this.maxActiveParticipants.keys()) {
85
+ if (key < period - MAX_SYNC_PERIODS_CACHE) {
86
+ this.maxActiveParticipants.delete(key);
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ export type SyncCommitteeFast = {
93
+ pubkeys: PublicKey[];
94
+ aggregatePubkey: PublicKey;
95
+ };
96
+
97
+ export type LightClientUpdateWithSummary = {
98
+ update: LightClientUpdate;
99
+ summary: LightClientUpdateSummary;
100
+ };
101
+
102
+ // === storePeriod ? store.currentSyncCommittee : store.nextSyncCommittee;
103
+ // if (!syncCommittee) {
104
+ // throw Error(`syncCommittee not available for signature period ${updateSignaturePeriod}`);
105
+ // }
@@ -0,0 +1,266 @@
1
+ import {BitArray, byteArrayEquals} from "@chainsafe/ssz";
2
+ import {ChainForkConfig} from "@lodestar/config";
3
+ import {
4
+ BLOCK_BODY_EXECUTION_PAYLOAD_DEPTH as EXECUTION_PAYLOAD_DEPTH,
5
+ BLOCK_BODY_EXECUTION_PAYLOAD_INDEX as EXECUTION_PAYLOAD_INDEX,
6
+ FINALIZED_ROOT_DEPTH,
7
+ FINALIZED_ROOT_DEPTH_ELECTRA,
8
+ ForkName,
9
+ ForkSeq,
10
+ NEXT_SYNC_COMMITTEE_DEPTH,
11
+ NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA,
12
+ isForkPostElectra,
13
+ } from "@lodestar/params";
14
+ import {
15
+ BeaconBlockHeader,
16
+ LightClientFinalityUpdate,
17
+ LightClientHeader,
18
+ LightClientOptimisticUpdate,
19
+ LightClientUpdate,
20
+ Slot,
21
+ SyncCommittee,
22
+ isElectraLightClientUpdate,
23
+ ssz,
24
+ } from "@lodestar/types";
25
+ import {computeEpochAtSlot, computeSyncPeriodAtSlot, isValidMerkleBranch} from "../utils/index.js";
26
+ import {normalizeMerkleBranch} from "../utils/normalizeMerkleBranch.js";
27
+ import {LightClientStore} from "./store.js";
28
+
29
+ export const GENESIS_SLOT = 0;
30
+ export const ZERO_HASH = new Uint8Array(32);
31
+ export const ZERO_PUBKEY = new Uint8Array(48);
32
+ export const ZERO_SYNC_COMMITTEE = ssz.altair.SyncCommittee.defaultValue();
33
+ export const ZERO_HEADER = ssz.phase0.BeaconBlockHeader.defaultValue();
34
+ /** From https://notes.ethereum.org/@vbuterin/extended_light_client_protocol#Optimistic-head-determining-function */
35
+ const SAFETY_THRESHOLD_FACTOR = 2;
36
+
37
+ export function sumBits(bits: BitArray): number {
38
+ return bits.getTrueBitIndexes().length;
39
+ }
40
+
41
+ export function getSafetyThreshold(maxActiveParticipants: number): number {
42
+ return Math.floor(maxActiveParticipants / SAFETY_THRESHOLD_FACTOR);
43
+ }
44
+
45
+ export function getZeroSyncCommitteeBranch(fork: ForkName): Uint8Array[] {
46
+ const nextSyncCommitteeDepth = isForkPostElectra(fork)
47
+ ? NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA
48
+ : NEXT_SYNC_COMMITTEE_DEPTH;
49
+
50
+ return Array.from({length: nextSyncCommitteeDepth}, () => ZERO_HASH);
51
+ }
52
+
53
+ export function getZeroFinalityBranch(fork: ForkName): Uint8Array[] {
54
+ const finalizedRootDepth = isForkPostElectra(fork) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH;
55
+
56
+ return Array.from({length: finalizedRootDepth}, () => ZERO_HASH);
57
+ }
58
+
59
+ export function isSyncCommitteeUpdate(update: LightClientUpdate): boolean {
60
+ return (
61
+ // Fast return for when constructing full LightClientUpdate from partial updates
62
+ update.nextSyncCommitteeBranch !==
63
+ getZeroSyncCommitteeBranch(isElectraLightClientUpdate(update) ? ForkName.electra : ForkName.altair) &&
64
+ update.nextSyncCommitteeBranch.some((branch) => !byteArrayEquals(branch, ZERO_HASH))
65
+ );
66
+ }
67
+
68
+ export function isFinalityUpdate(update: LightClientUpdate): boolean {
69
+ return (
70
+ // Fast return for when constructing full LightClientUpdate from partial updates
71
+ update.finalityBranch !==
72
+ getZeroFinalityBranch(isElectraLightClientUpdate(update) ? ForkName.electra : ForkName.altair) &&
73
+ update.finalityBranch.some((branch) => !byteArrayEquals(branch, ZERO_HASH))
74
+ );
75
+ }
76
+
77
+ export function isZeroedHeader(header: BeaconBlockHeader): boolean {
78
+ // Fast return for when constructing full LightClientUpdate from partial updates
79
+ return header === ZERO_HEADER || byteArrayEquals(header.bodyRoot, ZERO_HASH);
80
+ }
81
+
82
+ export function isZeroedSyncCommittee(syncCommittee: SyncCommittee): boolean {
83
+ // Fast return for when constructing full LightClientUpdate from partial updates
84
+ return syncCommittee === ZERO_SYNC_COMMITTEE || byteArrayEquals(syncCommittee.pubkeys[0], ZERO_PUBKEY);
85
+ }
86
+
87
+ export function upgradeLightClientHeader(
88
+ config: ChainForkConfig,
89
+ targetFork: ForkName,
90
+ header: LightClientHeader
91
+ ): LightClientHeader {
92
+ const headerFork = config.getForkName(header.beacon.slot);
93
+ if (ForkSeq[headerFork] >= ForkSeq[targetFork]) {
94
+ throw Error(`Invalid upgrade request from headerFork=${headerFork} to targetFork=${targetFork}`);
95
+ }
96
+
97
+ // We are modifying the same header object, may be we could create a copy, but its
98
+ // not required as of now
99
+ const upgradedHeader = header;
100
+ const startUpgradeFromFork = Object.values(ForkName)[ForkSeq[headerFork] + 1];
101
+
102
+ switch (startUpgradeFromFork) {
103
+ // biome-ignore lint/suspicious/useDefaultSwitchClauseLast: We want default to evaluate at first to throw error early
104
+ default:
105
+ throw Error(
106
+ `Invalid startUpgradeFromFork=${startUpgradeFromFork} for headerFork=${headerFork} in upgradeLightClientHeader to targetFork=${targetFork}`
107
+ );
108
+
109
+ case ForkName.altair:
110
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here
111
+ case ForkName.bellatrix:
112
+ // Break if no further upgradation is required else fall through
113
+ if (ForkSeq[targetFork] <= ForkSeq.bellatrix) break;
114
+
115
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here
116
+ case ForkName.capella:
117
+ (upgradedHeader as LightClientHeader<ForkName.capella>).execution =
118
+ ssz.capella.LightClientHeader.fields.execution.defaultValue();
119
+ (upgradedHeader as LightClientHeader<ForkName.capella>).executionBranch =
120
+ ssz.capella.LightClientHeader.fields.executionBranch.defaultValue();
121
+
122
+ // Break if no further upgradation is required else fall through
123
+ if (ForkSeq[targetFork] <= ForkSeq.capella) break;
124
+
125
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here
126
+ case ForkName.deneb:
127
+ (upgradedHeader as LightClientHeader<ForkName.deneb>).execution.blobGasUsed =
128
+ ssz.deneb.LightClientHeader.fields.execution.fields.blobGasUsed.defaultValue();
129
+ (upgradedHeader as LightClientHeader<ForkName.deneb>).execution.excessBlobGas =
130
+ ssz.deneb.LightClientHeader.fields.execution.fields.excessBlobGas.defaultValue();
131
+
132
+ // Break if no further upgradation is required else fall through
133
+ if (ForkSeq[targetFork] <= ForkSeq.deneb) break;
134
+
135
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here
136
+ case ForkName.electra:
137
+ // No changes to LightClientHeader in Electra
138
+
139
+ // Break if no further upgrades is required else fall through
140
+ if (ForkSeq[targetFork] <= ForkSeq.electra) break;
141
+
142
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here
143
+ case ForkName.fulu:
144
+ // No changes to LightClientHeader in Fulu
145
+
146
+ // Break if no further upgrades is required else fall through
147
+ if (ForkSeq[targetFork] <= ForkSeq.fulu) break;
148
+
149
+ case ForkName.gloas:
150
+ // No changes to LightClientHeader in Gloas
151
+
152
+ // Break if no further upgrades is required else fall through
153
+ if (ForkSeq[targetFork] <= ForkSeq.gloas) break;
154
+ }
155
+ return upgradedHeader;
156
+ }
157
+
158
+ export function isValidLightClientHeader(config: ChainForkConfig, header: LightClientHeader): boolean {
159
+ const epoch = computeEpochAtSlot(header.beacon.slot);
160
+
161
+ if (epoch < config.CAPELLA_FORK_EPOCH) {
162
+ return (
163
+ ((header as LightClientHeader<ForkName.capella>).execution === undefined ||
164
+ ssz.capella.ExecutionPayloadHeader.equals(
165
+ (header as LightClientHeader<ForkName.capella>).execution,
166
+ ssz.capella.LightClientHeader.fields.execution.defaultValue()
167
+ )) &&
168
+ ((header as LightClientHeader<ForkName.capella>).executionBranch === undefined ||
169
+ ssz.capella.LightClientHeader.fields.executionBranch.equals(
170
+ ssz.capella.LightClientHeader.fields.executionBranch.defaultValue(),
171
+ (header as LightClientHeader<ForkName.capella>).executionBranch
172
+ ))
173
+ );
174
+ }
175
+
176
+ if (
177
+ epoch < config.DENEB_FORK_EPOCH &&
178
+ (((header as LightClientHeader<ForkName.deneb>).execution.blobGasUsed &&
179
+ (header as LightClientHeader<ForkName.deneb>).execution.blobGasUsed !== BigInt(0)) ||
180
+ ((header as LightClientHeader<ForkName.deneb>).execution.excessBlobGas &&
181
+ (header as LightClientHeader<ForkName.deneb>).execution.excessBlobGas !== BigInt(0)))
182
+ ) {
183
+ return false;
184
+ }
185
+
186
+ return isValidMerkleBranch(
187
+ config
188
+ .getPostBellatrixForkTypes(header.beacon.slot)
189
+ .ExecutionPayloadHeader.hashTreeRoot((header as LightClientHeader<ForkName.capella>).execution),
190
+ (header as LightClientHeader<ForkName.capella>).executionBranch,
191
+ EXECUTION_PAYLOAD_DEPTH,
192
+ EXECUTION_PAYLOAD_INDEX,
193
+ header.beacon.bodyRoot
194
+ );
195
+ }
196
+
197
+ export function upgradeLightClientUpdate(
198
+ config: ChainForkConfig,
199
+ targetFork: ForkName,
200
+ update: LightClientUpdate
201
+ ): LightClientUpdate {
202
+ update.attestedHeader = upgradeLightClientHeader(config, targetFork, update.attestedHeader);
203
+ update.finalizedHeader = upgradeLightClientHeader(config, targetFork, update.finalizedHeader);
204
+ update.nextSyncCommitteeBranch = normalizeMerkleBranch(
205
+ update.nextSyncCommitteeBranch,
206
+ isForkPostElectra(targetFork) ? NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA : NEXT_SYNC_COMMITTEE_DEPTH
207
+ );
208
+ update.finalityBranch = normalizeMerkleBranch(
209
+ update.finalityBranch,
210
+ isForkPostElectra(targetFork) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH
211
+ );
212
+
213
+ return update;
214
+ }
215
+
216
+ export function upgradeLightClientFinalityUpdate(
217
+ config: ChainForkConfig,
218
+ targetFork: ForkName,
219
+ finalityUpdate: LightClientFinalityUpdate
220
+ ): LightClientFinalityUpdate {
221
+ finalityUpdate.attestedHeader = upgradeLightClientHeader(config, targetFork, finalityUpdate.attestedHeader);
222
+ finalityUpdate.finalizedHeader = upgradeLightClientHeader(config, targetFork, finalityUpdate.finalizedHeader);
223
+ finalityUpdate.finalityBranch = normalizeMerkleBranch(
224
+ finalityUpdate.finalityBranch,
225
+ isForkPostElectra(targetFork) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH
226
+ );
227
+
228
+ return finalityUpdate;
229
+ }
230
+
231
+ export function upgradeLightClientOptimisticUpdate(
232
+ config: ChainForkConfig,
233
+ targetFork: ForkName,
234
+ optimisticUpdate: LightClientOptimisticUpdate
235
+ ): LightClientOptimisticUpdate {
236
+ optimisticUpdate.attestedHeader = upgradeLightClientHeader(config, targetFork, optimisticUpdate.attestedHeader);
237
+
238
+ return optimisticUpdate;
239
+ }
240
+
241
+ /**
242
+ * Currently this upgradation is not required because all processing is done based on the
243
+ * summary that the store generates and maintains. In case store needs to be saved to disk,
244
+ * this could be required depending on the format the store is saved to the disk
245
+ */
246
+ export function upgradeLightClientStore(
247
+ config: ChainForkConfig,
248
+ targetFork: ForkName,
249
+ store: LightClientStore,
250
+ signatureSlot: Slot
251
+ ): LightClientStore {
252
+ const updateSignaturePeriod = computeSyncPeriodAtSlot(signatureSlot);
253
+ const bestValidUpdate = store.bestValidUpdates.get(updateSignaturePeriod);
254
+
255
+ if (bestValidUpdate) {
256
+ store.bestValidUpdates.set(updateSignaturePeriod, {
257
+ update: upgradeLightClientUpdate(config, targetFork, bestValidUpdate.update),
258
+ summary: bestValidUpdate.summary,
259
+ });
260
+ }
261
+
262
+ store.finalizedHeader = upgradeLightClientHeader(config, targetFork, store.finalizedHeader);
263
+ store.optimisticHeader = upgradeLightClientHeader(config, targetFork, store.optimisticHeader);
264
+
265
+ return store;
266
+ }
@@ -0,0 +1,41 @@
1
+ import {byteArrayEquals} from "@chainsafe/ssz";
2
+ import {ChainForkConfig} from "@lodestar/config";
3
+ import {isForkPostElectra} from "@lodestar/params";
4
+ import {LightClientBootstrap, Root, ssz} from "@lodestar/types";
5
+ import {toHex} from "@lodestar/utils";
6
+ import {isValidMerkleBranch} from "../utils/verifyMerkleBranch.js";
7
+ import {isValidLightClientHeader} from "./utils.js";
8
+
9
+ const CURRENT_SYNC_COMMITTEE_INDEX = 22;
10
+ const CURRENT_SYNC_COMMITTEE_DEPTH = 5;
11
+ const CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA = 22;
12
+ const CURRENT_SYNC_COMMITTEE_DEPTH_ELECTRA = 6;
13
+
14
+ export function validateLightClientBootstrap(
15
+ config: ChainForkConfig,
16
+ trustedBlockRoot: Root,
17
+ bootstrap: LightClientBootstrap
18
+ ): void {
19
+ const headerRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(bootstrap.header.beacon);
20
+ const fork = config.getForkName(bootstrap.header.beacon.slot);
21
+
22
+ if (!isValidLightClientHeader(config, bootstrap.header)) {
23
+ throw Error("Bootstrap Header is not Valid Light Client Header");
24
+ }
25
+
26
+ if (!byteArrayEquals(headerRoot, trustedBlockRoot)) {
27
+ throw Error(`bootstrap header root ${toHex(headerRoot)} != trusted root ${toHex(trustedBlockRoot)}`);
28
+ }
29
+
30
+ if (
31
+ !isValidMerkleBranch(
32
+ ssz.altair.SyncCommittee.hashTreeRoot(bootstrap.currentSyncCommittee),
33
+ bootstrap.currentSyncCommitteeBranch,
34
+ isForkPostElectra(fork) ? CURRENT_SYNC_COMMITTEE_DEPTH_ELECTRA : CURRENT_SYNC_COMMITTEE_DEPTH,
35
+ isForkPostElectra(fork) ? CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA : CURRENT_SYNC_COMMITTEE_INDEX,
36
+ bootstrap.header.beacon.stateRoot
37
+ )
38
+ ) {
39
+ throw Error("Invalid currentSyncCommittee merkle branch");
40
+ }
41
+ }
@@ -0,0 +1,154 @@
1
+ import bls from "@chainsafe/bls";
2
+ import type {PublicKey, Signature} from "@chainsafe/bls/types";
3
+ import {ChainForkConfig} from "@lodestar/config";
4
+ import {
5
+ DOMAIN_SYNC_COMMITTEE,
6
+ FINALIZED_ROOT_DEPTH,
7
+ FINALIZED_ROOT_DEPTH_ELECTRA,
8
+ FINALIZED_ROOT_INDEX,
9
+ FINALIZED_ROOT_INDEX_ELECTRA,
10
+ GENESIS_SLOT,
11
+ MIN_SYNC_COMMITTEE_PARTICIPANTS,
12
+ NEXT_SYNC_COMMITTEE_DEPTH,
13
+ NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA,
14
+ NEXT_SYNC_COMMITTEE_INDEX,
15
+ NEXT_SYNC_COMMITTEE_INDEX_ELECTRA,
16
+ } from "@lodestar/params";
17
+ import {LightClientUpdate, Root, isElectraLightClientUpdate, ssz} from "@lodestar/types";
18
+ import {SyncCommitteeFast} from "../types.js";
19
+ import {isValidMerkleBranch} from "../utils/index.js";
20
+ import {getParticipantPubkeys, sumBits} from "../utils/utils.js";
21
+ import {ILightClientStore} from "./store.js";
22
+ import {
23
+ ZERO_HASH,
24
+ isFinalityUpdate,
25
+ isSyncCommitteeUpdate,
26
+ isValidLightClientHeader,
27
+ isZeroedHeader,
28
+ isZeroedSyncCommittee,
29
+ } from "./utils.js";
30
+
31
+ export function validateLightClientUpdate(
32
+ config: ChainForkConfig,
33
+ store: ILightClientStore,
34
+ update: LightClientUpdate,
35
+ syncCommittee: SyncCommitteeFast
36
+ ): void {
37
+ // Verify sync committee has sufficient participants
38
+ if (sumBits(update.syncAggregate.syncCommitteeBits) < MIN_SYNC_COMMITTEE_PARTICIPANTS) {
39
+ throw Error("Sync committee has not sufficient participants");
40
+ }
41
+
42
+ if (!isValidLightClientHeader(config, update.attestedHeader)) {
43
+ throw Error("Attested Header is not Valid Light Client Header");
44
+ }
45
+
46
+ // Sanity check that slots are in correct order
47
+ if (update.signatureSlot <= update.attestedHeader.beacon.slot) {
48
+ throw Error(
49
+ `signature slot ${update.signatureSlot} must be after attested header slot ${update.attestedHeader.beacon.slot}`
50
+ );
51
+ }
52
+ if (update.attestedHeader.beacon.slot < update.finalizedHeader.beacon.slot) {
53
+ throw Error(
54
+ `attested header slot ${update.signatureSlot} must be after finalized header slot ${update.finalizedHeader.beacon.slot}`
55
+ );
56
+ }
57
+
58
+ // Verify that the `finality_branch`, if present, confirms `finalized_header`
59
+ // to match the finalized checkpoint root saved in the state of `attested_header`.
60
+ // Note that the genesis finalized checkpoint root is represented as a zero hash.
61
+ if (!isFinalityUpdate(update)) {
62
+ if (!isZeroedHeader(update.finalizedHeader.beacon)) {
63
+ throw Error("finalizedHeader must be zero for non-finality update");
64
+ }
65
+ } else {
66
+ let finalizedRoot: Root;
67
+
68
+ if (update.finalizedHeader.beacon.slot === GENESIS_SLOT) {
69
+ if (!isZeroedHeader(update.finalizedHeader.beacon)) {
70
+ throw Error("finalizedHeader must be zero for not finality update");
71
+ }
72
+ finalizedRoot = ZERO_HASH;
73
+ } else {
74
+ if (!isValidLightClientHeader(config, update.finalizedHeader)) {
75
+ throw Error("Finalized Header is not valid Light Client Header");
76
+ }
77
+
78
+ finalizedRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(update.finalizedHeader.beacon);
79
+ }
80
+
81
+ if (
82
+ !isValidMerkleBranch(
83
+ finalizedRoot,
84
+ update.finalityBranch,
85
+ isElectraLightClientUpdate(update) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH,
86
+ isElectraLightClientUpdate(update) ? FINALIZED_ROOT_INDEX_ELECTRA : FINALIZED_ROOT_INDEX,
87
+ update.attestedHeader.beacon.stateRoot
88
+ )
89
+ ) {
90
+ throw Error("Invalid finality header merkle branch");
91
+ }
92
+ }
93
+
94
+ // Verify that the `next_sync_committee`, if present, actually is the next sync committee saved in the
95
+ // state of the `attested_header`
96
+ if (!isSyncCommitteeUpdate(update)) {
97
+ if (!isZeroedSyncCommittee(update.nextSyncCommittee)) {
98
+ throw Error("nextSyncCommittee must be zero for non sync committee update");
99
+ }
100
+ } else {
101
+ if (
102
+ !isValidMerkleBranch(
103
+ ssz.altair.SyncCommittee.hashTreeRoot(update.nextSyncCommittee),
104
+ update.nextSyncCommitteeBranch,
105
+ isElectraLightClientUpdate(update) ? NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA : NEXT_SYNC_COMMITTEE_DEPTH,
106
+ isElectraLightClientUpdate(update) ? NEXT_SYNC_COMMITTEE_INDEX_ELECTRA : NEXT_SYNC_COMMITTEE_INDEX,
107
+ update.attestedHeader.beacon.stateRoot
108
+ )
109
+ ) {
110
+ throw Error("Invalid next sync committee merkle branch");
111
+ }
112
+ }
113
+
114
+ // Verify sync committee aggregate signature
115
+
116
+ const participantPubkeys = getParticipantPubkeys(syncCommittee.pubkeys, update.syncAggregate.syncCommitteeBits);
117
+
118
+ const signingRoot = ssz.phase0.SigningData.hashTreeRoot({
119
+ objectRoot: ssz.phase0.BeaconBlockHeader.hashTreeRoot(update.attestedHeader.beacon),
120
+ domain: store.config.getDomain(update.signatureSlot - 1, DOMAIN_SYNC_COMMITTEE),
121
+ });
122
+
123
+ if (!isValidBlsAggregate(participantPubkeys, signingRoot, update.syncAggregate.syncCommitteeSignature)) {
124
+ throw Error("Invalid aggregate signature");
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Same as BLS.verifyAggregate but with detailed error messages
130
+ */
131
+ function isValidBlsAggregate(publicKeys: PublicKey[], message: Uint8Array, signature: Uint8Array): boolean {
132
+ let aggPubkey: PublicKey;
133
+ try {
134
+ aggPubkey = bls.PublicKey.aggregate(publicKeys);
135
+ } catch (e) {
136
+ (e as Error).message = `Error aggregating pubkeys: ${(e as Error).message}`;
137
+ throw e;
138
+ }
139
+
140
+ let sig: Signature;
141
+ try {
142
+ sig = bls.Signature.fromBytes(signature, undefined, true);
143
+ } catch (e) {
144
+ (e as Error).message = `Error deserializing signature: ${(e as Error).message}`;
145
+ throw e;
146
+ }
147
+
148
+ try {
149
+ return sig.verify(aggPubkey, message);
150
+ } catch (e) {
151
+ (e as Error).message = `Error verifying signature: ${(e as Error).message}`;
152
+ throw e;
153
+ }
154
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./interface.js";
2
+ export * from "./rest.js";
@@ -0,0 +1,37 @@
1
+ import {ForkName} from "@lodestar/params";
2
+ import {
3
+ LightClientBootstrap,
4
+ LightClientFinalityUpdate,
5
+ LightClientOptimisticUpdate,
6
+ LightClientUpdate,
7
+ SyncPeriod,
8
+ } from "@lodestar/types";
9
+
10
+ export interface LightClientTransport {
11
+ getUpdates(
12
+ startPeriod: SyncPeriod,
13
+ count: number
14
+ ): Promise<
15
+ {
16
+ version: ForkName;
17
+ data: LightClientUpdate;
18
+ }[]
19
+ >;
20
+ /**
21
+ * Returns the latest optimistic head update available. Clients should use the SSE type `light_client_optimistic_update`
22
+ * unless to get the very first head update after syncing, or if SSE are not supported by the server.
23
+ */
24
+ getOptimisticUpdate(): Promise<{version: ForkName; data: LightClientOptimisticUpdate}>;
25
+ getFinalityUpdate(): Promise<{version: ForkName; data: LightClientFinalityUpdate}>;
26
+ /**
27
+ * Fetch a bootstrapping state with a proof to a trusted block root.
28
+ * The trusted block root should be fetched with similar means to a weak subjectivity checkpoint.
29
+ * Only block roots for checkpoints are guaranteed to be available.
30
+ */
31
+ getBootstrap(blockRoot: string): Promise<{version: ForkName; data: LightClientBootstrap}>;
32
+
33
+ // registers handler for LightClientOptimisticUpdate. This can come either via sse or p2p
34
+ onOptimisticUpdate(handler: (optimisticUpdate: LightClientOptimisticUpdate) => void): void;
35
+ // registers handler for LightClientFinalityUpdate. This can come either via sse or p2p
36
+ onFinalityUpdate(handler: (finalityUpdate: LightClientFinalityUpdate) => void): void;
37
+ }