@lodestar/state-transition 1.43.0-dev.4358217e12 → 1.43.0-dev.4451fec75a

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 (79) hide show
  1. package/lib/block/processConsolidationRequest.d.ts.map +1 -1
  2. package/lib/block/processConsolidationRequest.js +2 -1
  3. package/lib/block/processConsolidationRequest.js.map +1 -1
  4. package/lib/block/processParentExecutionPayload.d.ts +2 -2
  5. package/lib/block/processParentExecutionPayload.js +3 -3
  6. package/lib/block/processWithdrawals.d.ts.map +1 -1
  7. package/lib/block/processWithdrawals.js +2 -4
  8. package/lib/block/processWithdrawals.js.map +1 -1
  9. package/lib/cache/epochCache.js +3 -3
  10. package/lib/cache/epochCache.js.map +1 -1
  11. package/lib/epoch/processPendingDeposits.d.ts.map +1 -1
  12. package/lib/epoch/processPendingDeposits.js +4 -2
  13. package/lib/epoch/processPendingDeposits.js.map +1 -1
  14. package/lib/lightClient/spec/index.d.ts +22 -0
  15. package/lib/lightClient/spec/index.d.ts.map +1 -0
  16. package/lib/lightClient/spec/index.js +58 -0
  17. package/lib/lightClient/spec/index.js.map +1 -0
  18. package/lib/lightClient/spec/isBetterUpdate.d.ts +23 -0
  19. package/lib/lightClient/spec/isBetterUpdate.d.ts.map +1 -0
  20. package/lib/lightClient/spec/isBetterUpdate.js +66 -0
  21. package/lib/lightClient/spec/isBetterUpdate.js.map +1 -0
  22. package/lib/lightClient/spec/processLightClientUpdate.d.ts +12 -0
  23. package/lib/lightClient/spec/processLightClientUpdate.d.ts.map +1 -0
  24. package/lib/lightClient/spec/processLightClientUpdate.js +80 -0
  25. package/lib/lightClient/spec/processLightClientUpdate.js.map +1 -0
  26. package/lib/lightClient/spec/store.d.ts +45 -0
  27. package/lib/lightClient/spec/store.d.ts.map +1 -0
  28. package/lib/lightClient/spec/store.js +56 -0
  29. package/lib/lightClient/spec/store.js.map +1 -0
  30. package/lib/lightClient/spec/utils.d.ts +47 -0
  31. package/lib/lightClient/spec/utils.d.ts.map +1 -0
  32. package/lib/lightClient/spec/utils.js +197 -0
  33. package/lib/lightClient/spec/utils.js.map +1 -0
  34. package/lib/lightClient/spec/validateLightClientBootstrap.d.ts +4 -0
  35. package/lib/lightClient/spec/validateLightClientBootstrap.d.ts.map +1 -0
  36. package/lib/lightClient/spec/validateLightClientBootstrap.js +22 -0
  37. package/lib/lightClient/spec/validateLightClientBootstrap.js.map +1 -0
  38. package/lib/lightClient/spec/validateLightClientUpdate.d.ts +5 -0
  39. package/lib/lightClient/spec/validateLightClientUpdate.d.ts.map +1 -0
  40. package/lib/lightClient/spec/validateLightClientUpdate.js +88 -0
  41. package/lib/lightClient/spec/validateLightClientUpdate.js.map +1 -0
  42. package/lib/slot/upgradeStateToElectra.d.ts.map +1 -1
  43. package/lib/slot/upgradeStateToElectra.js +2 -2
  44. package/lib/slot/upgradeStateToElectra.js.map +1 -1
  45. package/lib/stateView/beaconStateView.d.ts +14 -5
  46. package/lib/stateView/beaconStateView.d.ts.map +1 -1
  47. package/lib/stateView/beaconStateView.js +40 -11
  48. package/lib/stateView/beaconStateView.js.map +1 -1
  49. package/lib/stateView/interface.d.ts +7 -5
  50. package/lib/stateView/interface.d.ts.map +1 -1
  51. package/lib/stateView/interface.js.map +1 -1
  52. package/lib/util/epoch.d.ts.map +1 -1
  53. package/lib/util/epoch.js +6 -4
  54. package/lib/util/epoch.js.map +1 -1
  55. package/lib/util/loadState/loadState.js +4 -4
  56. package/lib/util/loadState/loadState.js.map +1 -1
  57. package/lib/util/validator.d.ts +14 -2
  58. package/lib/util/validator.d.ts.map +1 -1
  59. package/lib/util/validator.js +24 -2
  60. package/lib/util/validator.js.map +1 -1
  61. package/package.json +13 -8
  62. package/src/block/processConsolidationRequest.ts +2 -1
  63. package/src/block/processParentExecutionPayload.ts +3 -3
  64. package/src/block/processWithdrawals.ts +2 -4
  65. package/src/cache/epochCache.ts +3 -3
  66. package/src/epoch/processPendingDeposits.ts +5 -2
  67. package/src/lightClient/spec/index.ts +101 -0
  68. package/src/lightClient/spec/isBetterUpdate.ts +94 -0
  69. package/src/lightClient/spec/processLightClientUpdate.ts +119 -0
  70. package/src/lightClient/spec/store.ts +106 -0
  71. package/src/lightClient/spec/utils.ts +317 -0
  72. package/src/lightClient/spec/validateLightClientBootstrap.ts +39 -0
  73. package/src/lightClient/spec/validateLightClientUpdate.ts +145 -0
  74. package/src/slot/upgradeStateToElectra.ts +4 -2
  75. package/src/stateView/beaconStateView.ts +43 -12
  76. package/src/stateView/interface.ts +7 -5
  77. package/src/util/epoch.ts +13 -4
  78. package/src/util/loadState/loadState.ts +4 -4
  79. package/src/util/validator.ts +42 -2
@@ -0,0 +1,317 @@
1
+ import {PublicKey} from "@chainsafe/blst";
2
+ import {BitArray} from "@chainsafe/ssz";
3
+ import {ChainForkConfig} from "@lodestar/config";
4
+ import {
5
+ BLOCK_BODY_EXECUTION_PAYLOAD_DEPTH as EXECUTION_PAYLOAD_DEPTH,
6
+ BLOCK_BODY_EXECUTION_PAYLOAD_INDEX as EXECUTION_PAYLOAD_INDEX,
7
+ FINALIZED_ROOT_DEPTH,
8
+ FINALIZED_ROOT_DEPTH_ELECTRA,
9
+ ForkName,
10
+ ForkSeq,
11
+ NEXT_SYNC_COMMITTEE_DEPTH,
12
+ NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA,
13
+ isForkPostElectra,
14
+ } from "@lodestar/params";
15
+ import {
16
+ BeaconBlockHeader,
17
+ LightClientFinalityUpdate,
18
+ LightClientHeader,
19
+ LightClientOptimisticUpdate,
20
+ LightClientUpdate,
21
+ Slot,
22
+ SyncCommittee,
23
+ isElectraLightClientUpdate,
24
+ ssz,
25
+ } from "@lodestar/types";
26
+ import {byteArrayEquals, verifyMerkleBranch} from "@lodestar/utils";
27
+ import {computeEpochAtSlot, computeSyncPeriodAtSlot} from "../../util/epoch.js";
28
+ import type {LightClientStore, SyncCommitteeFast} from "./store.js";
29
+
30
+ export const GENESIS_SLOT = 0;
31
+ export const ZERO_HASH = new Uint8Array(32);
32
+ export const ZERO_PUBKEY = new Uint8Array(48);
33
+ export const ZERO_SYNC_COMMITTEE = ssz.altair.SyncCommittee.defaultValue();
34
+ export const ZERO_HEADER = ssz.phase0.BeaconBlockHeader.defaultValue();
35
+ /** From https://notes.ethereum.org/@vbuterin/extended_light_client_protocol#Optimistic-head-determining-function */
36
+ const SAFETY_THRESHOLD_FACTOR = 2;
37
+
38
+ export function sumBits(bits: BitArray): number {
39
+ return bits.getTrueBitIndexes().length;
40
+ }
41
+
42
+ /**
43
+ * Util to guarantee that all bits have a corresponding pubkey.
44
+ */
45
+ export function getParticipantPubkeys<T>(pubkeys: T[], bits: BitArray): T[] {
46
+ // BitArray.intersectValues() checks the length is correct.
47
+ return bits.intersectValues(pubkeys);
48
+ }
49
+
50
+ function deserializePubkeys(pubkeys: SyncCommittee["pubkeys"]): PublicKey[] {
51
+ return pubkeys.map((pk) => PublicKey.fromBytes(pk, true));
52
+ }
53
+
54
+ function serializePubkeys(pubkeys: PublicKey[]): SyncCommittee["pubkeys"] {
55
+ return pubkeys.map((pk) => pk.toBytes());
56
+ }
57
+
58
+ export function deserializeSyncCommittee(syncCommittee: SyncCommittee): SyncCommitteeFast {
59
+ return {
60
+ pubkeys: deserializePubkeys(syncCommittee.pubkeys),
61
+ aggregatePubkey: PublicKey.fromBytes(syncCommittee.aggregatePubkey, true),
62
+ };
63
+ }
64
+
65
+ export function serializeSyncCommittee(syncCommittee: SyncCommitteeFast): SyncCommittee {
66
+ return {
67
+ pubkeys: serializePubkeys(syncCommittee.pubkeys),
68
+ aggregatePubkey: syncCommittee.aggregatePubkey.toBytes(),
69
+ };
70
+ }
71
+
72
+ export function getSafetyThreshold(maxActiveParticipants: number): number {
73
+ return Math.floor(maxActiveParticipants / SAFETY_THRESHOLD_FACTOR);
74
+ }
75
+
76
+ export function getZeroSyncCommitteeBranch(fork: ForkName): Uint8Array[] {
77
+ const nextSyncCommitteeDepth = isForkPostElectra(fork)
78
+ ? NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA
79
+ : NEXT_SYNC_COMMITTEE_DEPTH;
80
+
81
+ return Array.from({length: nextSyncCommitteeDepth}, () => ZERO_HASH);
82
+ }
83
+
84
+ export function getZeroFinalityBranch(fork: ForkName): Uint8Array[] {
85
+ const finalizedRootDepth = isForkPostElectra(fork) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH;
86
+
87
+ return Array.from({length: finalizedRootDepth}, () => ZERO_HASH);
88
+ }
89
+
90
+ export function isSyncCommitteeUpdate(update: LightClientUpdate): boolean {
91
+ return (
92
+ // Fast return for when constructing full LightClientUpdate from partial updates
93
+ update.nextSyncCommitteeBranch !==
94
+ getZeroSyncCommitteeBranch(isElectraLightClientUpdate(update) ? ForkName.electra : ForkName.altair) &&
95
+ update.nextSyncCommitteeBranch.some((branch) => !byteArrayEquals(branch, ZERO_HASH))
96
+ );
97
+ }
98
+
99
+ export function isFinalityUpdate(update: LightClientUpdate): boolean {
100
+ return (
101
+ // Fast return for when constructing full LightClientUpdate from partial updates
102
+ update.finalityBranch !==
103
+ getZeroFinalityBranch(isElectraLightClientUpdate(update) ? ForkName.electra : ForkName.altair) &&
104
+ update.finalityBranch.some((branch) => !byteArrayEquals(branch, ZERO_HASH))
105
+ );
106
+ }
107
+
108
+ export function isZeroedHeader(header: BeaconBlockHeader): boolean {
109
+ // Fast return for when constructing full LightClientUpdate from partial updates
110
+ return header === ZERO_HEADER || byteArrayEquals(header.bodyRoot, ZERO_HASH);
111
+ }
112
+
113
+ export function isZeroedSyncCommittee(syncCommittee: SyncCommittee): boolean {
114
+ // Fast return for when constructing full LightClientUpdate from partial updates
115
+ return syncCommittee === ZERO_SYNC_COMMITTEE || byteArrayEquals(syncCommittee.pubkeys[0], ZERO_PUBKEY);
116
+ }
117
+
118
+ export function isValidMerkleBranch(
119
+ leaf: Uint8Array,
120
+ branch: Uint8Array[],
121
+ depth: number,
122
+ index: number,
123
+ root: Uint8Array
124
+ ): boolean {
125
+ if (branch.length !== depth) {
126
+ return false;
127
+ }
128
+
129
+ return verifyMerkleBranch(leaf, branch, depth, index, root);
130
+ }
131
+
132
+ export function normalizeMerkleBranch(branch: Uint8Array[], depth: number): Uint8Array[] {
133
+ const numExtraDepth = depth - branch.length;
134
+
135
+ return [...Array.from({length: numExtraDepth}, () => ZERO_HASH), ...branch];
136
+ }
137
+
138
+ export function upgradeLightClientHeader(
139
+ config: ChainForkConfig,
140
+ targetFork: ForkName,
141
+ header: LightClientHeader
142
+ ): LightClientHeader {
143
+ const headerFork = config.getForkName(header.beacon.slot);
144
+ if (ForkSeq[headerFork] >= ForkSeq[targetFork]) {
145
+ throw Error(`Invalid upgrade request from headerFork=${headerFork} to targetFork=${targetFork}`);
146
+ }
147
+
148
+ // We are modifying the same header object, may be we could create a copy, but its
149
+ // not required as of now
150
+ const upgradedHeader = header;
151
+ const startUpgradeFromFork = Object.values(ForkName)[ForkSeq[headerFork] + 1];
152
+
153
+ switch (startUpgradeFromFork) {
154
+ // biome-ignore lint/suspicious/useDefaultSwitchClauseLast: We want default to evaluate at first to throw error early
155
+ default:
156
+ throw Error(
157
+ `Invalid startUpgradeFromFork=${startUpgradeFromFork} for headerFork=${headerFork} in upgradeLightClientHeader to targetFork=${targetFork}`
158
+ );
159
+
160
+ case ForkName.altair:
161
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here
162
+ case ForkName.bellatrix:
163
+ // Break if no further upgradation is required else fall through
164
+ if (ForkSeq[targetFork] <= ForkSeq.bellatrix) break;
165
+
166
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here
167
+ case ForkName.capella:
168
+ (upgradedHeader as LightClientHeader<ForkName.capella>).execution =
169
+ ssz.capella.LightClientHeader.fields.execution.defaultValue();
170
+ (upgradedHeader as LightClientHeader<ForkName.capella>).executionBranch =
171
+ ssz.capella.LightClientHeader.fields.executionBranch.defaultValue();
172
+
173
+ // Break if no further upgradation is required else fall through
174
+ if (ForkSeq[targetFork] <= ForkSeq.capella) break;
175
+
176
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here
177
+ case ForkName.deneb:
178
+ (upgradedHeader as LightClientHeader<ForkName.deneb>).execution.blobGasUsed =
179
+ ssz.deneb.LightClientHeader.fields.execution.fields.blobGasUsed.defaultValue();
180
+ (upgradedHeader as LightClientHeader<ForkName.deneb>).execution.excessBlobGas =
181
+ ssz.deneb.LightClientHeader.fields.execution.fields.excessBlobGas.defaultValue();
182
+
183
+ // Break if no further upgradation is required else fall through
184
+ if (ForkSeq[targetFork] <= ForkSeq.deneb) break;
185
+
186
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here
187
+ case ForkName.electra:
188
+ // No changes to LightClientHeader in Electra
189
+
190
+ // Break if no further upgrades is required else fall through
191
+ if (ForkSeq[targetFork] <= ForkSeq.electra) break;
192
+
193
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here
194
+ case ForkName.fulu:
195
+ // No changes to LightClientHeader in Fulu
196
+
197
+ // Break if no further upgrades is required else fall through
198
+ if (ForkSeq[targetFork] <= ForkSeq.fulu) break;
199
+
200
+ case ForkName.gloas:
201
+ // No changes to LightClientHeader in Gloas
202
+
203
+ // Break if no further upgrades is required else fall through
204
+ if (ForkSeq[targetFork] <= ForkSeq.gloas) break;
205
+ }
206
+ return upgradedHeader;
207
+ }
208
+
209
+ export function isValidLightClientHeader(config: ChainForkConfig, header: LightClientHeader): boolean {
210
+ const epoch = computeEpochAtSlot(header.beacon.slot);
211
+
212
+ if (epoch < config.CAPELLA_FORK_EPOCH) {
213
+ return (
214
+ ((header as LightClientHeader<ForkName.capella>).execution === undefined ||
215
+ ssz.capella.ExecutionPayloadHeader.equals(
216
+ (header as LightClientHeader<ForkName.capella>).execution,
217
+ ssz.capella.LightClientHeader.fields.execution.defaultValue()
218
+ )) &&
219
+ ((header as LightClientHeader<ForkName.capella>).executionBranch === undefined ||
220
+ ssz.capella.LightClientHeader.fields.executionBranch.equals(
221
+ ssz.capella.LightClientHeader.fields.executionBranch.defaultValue(),
222
+ (header as LightClientHeader<ForkName.capella>).executionBranch
223
+ ))
224
+ );
225
+ }
226
+
227
+ if (
228
+ epoch < config.DENEB_FORK_EPOCH &&
229
+ (((header as LightClientHeader<ForkName.deneb>).execution.blobGasUsed &&
230
+ (header as LightClientHeader<ForkName.deneb>).execution.blobGasUsed !== BigInt(0)) ||
231
+ ((header as LightClientHeader<ForkName.deneb>).execution.excessBlobGas &&
232
+ (header as LightClientHeader<ForkName.deneb>).execution.excessBlobGas !== BigInt(0)))
233
+ ) {
234
+ return false;
235
+ }
236
+
237
+ return isValidMerkleBranch(
238
+ config
239
+ .getPostBellatrixForkTypes(header.beacon.slot)
240
+ .ExecutionPayloadHeader.hashTreeRoot((header as LightClientHeader<ForkName.capella>).execution),
241
+ (header as LightClientHeader<ForkName.capella>).executionBranch,
242
+ EXECUTION_PAYLOAD_DEPTH,
243
+ EXECUTION_PAYLOAD_INDEX,
244
+ header.beacon.bodyRoot
245
+ );
246
+ }
247
+
248
+ export function upgradeLightClientUpdate(
249
+ config: ChainForkConfig,
250
+ targetFork: ForkName,
251
+ update: LightClientUpdate
252
+ ): LightClientUpdate {
253
+ update.attestedHeader = upgradeLightClientHeader(config, targetFork, update.attestedHeader);
254
+ update.finalizedHeader = upgradeLightClientHeader(config, targetFork, update.finalizedHeader);
255
+ update.nextSyncCommitteeBranch = normalizeMerkleBranch(
256
+ update.nextSyncCommitteeBranch,
257
+ isForkPostElectra(targetFork) ? NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA : NEXT_SYNC_COMMITTEE_DEPTH
258
+ );
259
+ update.finalityBranch = normalizeMerkleBranch(
260
+ update.finalityBranch,
261
+ isForkPostElectra(targetFork) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH
262
+ );
263
+
264
+ return update;
265
+ }
266
+
267
+ export function upgradeLightClientFinalityUpdate(
268
+ config: ChainForkConfig,
269
+ targetFork: ForkName,
270
+ finalityUpdate: LightClientFinalityUpdate
271
+ ): LightClientFinalityUpdate {
272
+ finalityUpdate.attestedHeader = upgradeLightClientHeader(config, targetFork, finalityUpdate.attestedHeader);
273
+ finalityUpdate.finalizedHeader = upgradeLightClientHeader(config, targetFork, finalityUpdate.finalizedHeader);
274
+ finalityUpdate.finalityBranch = normalizeMerkleBranch(
275
+ finalityUpdate.finalityBranch,
276
+ isForkPostElectra(targetFork) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH
277
+ );
278
+
279
+ return finalityUpdate;
280
+ }
281
+
282
+ export function upgradeLightClientOptimisticUpdate(
283
+ config: ChainForkConfig,
284
+ targetFork: ForkName,
285
+ optimisticUpdate: LightClientOptimisticUpdate
286
+ ): LightClientOptimisticUpdate {
287
+ optimisticUpdate.attestedHeader = upgradeLightClientHeader(config, targetFork, optimisticUpdate.attestedHeader);
288
+
289
+ return optimisticUpdate;
290
+ }
291
+
292
+ /**
293
+ * Currently this upgradation is not required because all processing is done based on the
294
+ * summary that the store generates and maintains. In case store needs to be saved to disk,
295
+ * this could be required depending on the format the store is saved to the disk
296
+ */
297
+ export function upgradeLightClientStore(
298
+ config: ChainForkConfig,
299
+ targetFork: ForkName,
300
+ store: LightClientStore,
301
+ signatureSlot: Slot
302
+ ): LightClientStore {
303
+ const updateSignaturePeriod = computeSyncPeriodAtSlot(signatureSlot);
304
+ const bestValidUpdate = store.bestValidUpdates.get(updateSignaturePeriod);
305
+
306
+ if (bestValidUpdate) {
307
+ store.bestValidUpdates.set(updateSignaturePeriod, {
308
+ update: upgradeLightClientUpdate(config, targetFork, bestValidUpdate.update),
309
+ summary: bestValidUpdate.summary,
310
+ });
311
+ }
312
+
313
+ store.finalizedHeader = upgradeLightClientHeader(config, targetFork, store.finalizedHeader);
314
+ store.optimisticHeader = upgradeLightClientHeader(config, targetFork, store.optimisticHeader);
315
+
316
+ return store;
317
+ }
@@ -0,0 +1,39 @@
1
+ import {ChainForkConfig} from "@lodestar/config";
2
+ import {isForkPostElectra} from "@lodestar/params";
3
+ import {LightClientBootstrap, Root, ssz} from "@lodestar/types";
4
+ import {byteArrayEquals, toHex} from "@lodestar/utils";
5
+ import {isValidLightClientHeader, isValidMerkleBranch} from "./utils.js";
6
+
7
+ const CURRENT_SYNC_COMMITTEE_INDEX = 22;
8
+ const CURRENT_SYNC_COMMITTEE_DEPTH = 5;
9
+ const CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA = 22;
10
+ const CURRENT_SYNC_COMMITTEE_DEPTH_ELECTRA = 6;
11
+
12
+ export function validateLightClientBootstrap(
13
+ config: ChainForkConfig,
14
+ trustedBlockRoot: Root,
15
+ bootstrap: LightClientBootstrap
16
+ ): void {
17
+ const headerRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(bootstrap.header.beacon);
18
+ const fork = config.getForkName(bootstrap.header.beacon.slot);
19
+
20
+ if (!isValidLightClientHeader(config, bootstrap.header)) {
21
+ throw Error("Bootstrap Header is not Valid Light Client Header");
22
+ }
23
+
24
+ if (!byteArrayEquals(headerRoot, trustedBlockRoot)) {
25
+ throw Error(`bootstrap header root ${toHex(headerRoot)} != trusted root ${toHex(trustedBlockRoot)}`);
26
+ }
27
+
28
+ if (
29
+ !isValidMerkleBranch(
30
+ ssz.altair.SyncCommittee.hashTreeRoot(bootstrap.currentSyncCommittee),
31
+ bootstrap.currentSyncCommitteeBranch,
32
+ isForkPostElectra(fork) ? CURRENT_SYNC_COMMITTEE_DEPTH_ELECTRA : CURRENT_SYNC_COMMITTEE_DEPTH,
33
+ isForkPostElectra(fork) ? CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA : CURRENT_SYNC_COMMITTEE_INDEX,
34
+ bootstrap.header.beacon.stateRoot
35
+ )
36
+ ) {
37
+ throw Error("Invalid currentSyncCommittee merkle branch");
38
+ }
39
+ }
@@ -0,0 +1,145 @@
1
+ import {PublicKey, Signature, fastAggregateVerify} from "@chainsafe/blst";
2
+ import {ChainForkConfig} from "@lodestar/config";
3
+ import {
4
+ DOMAIN_SYNC_COMMITTEE,
5
+ FINALIZED_ROOT_DEPTH,
6
+ FINALIZED_ROOT_DEPTH_ELECTRA,
7
+ FINALIZED_ROOT_INDEX,
8
+ FINALIZED_ROOT_INDEX_ELECTRA,
9
+ GENESIS_SLOT,
10
+ MIN_SYNC_COMMITTEE_PARTICIPANTS,
11
+ NEXT_SYNC_COMMITTEE_DEPTH,
12
+ NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA,
13
+ NEXT_SYNC_COMMITTEE_INDEX,
14
+ NEXT_SYNC_COMMITTEE_INDEX_ELECTRA,
15
+ } from "@lodestar/params";
16
+ import {LightClientUpdate, Root, isElectraLightClientUpdate, ssz} from "@lodestar/types";
17
+ import type {ILightClientStore, SyncCommitteeFast} from "./store.js";
18
+ import {
19
+ ZERO_HASH,
20
+ getParticipantPubkeys,
21
+ isFinalityUpdate,
22
+ isSyncCommitteeUpdate,
23
+ isValidLightClientHeader,
24
+ isValidMerkleBranch,
25
+ isZeroedHeader,
26
+ isZeroedSyncCommittee,
27
+ sumBits,
28
+ } from "./utils.js";
29
+
30
+ export function validateLightClientUpdate(
31
+ config: ChainForkConfig,
32
+ store: ILightClientStore,
33
+ update: LightClientUpdate,
34
+ syncCommittee: SyncCommitteeFast
35
+ ): void {
36
+ // Verify sync committee has sufficient participants
37
+ if (sumBits(update.syncAggregate.syncCommitteeBits) < MIN_SYNC_COMMITTEE_PARTICIPANTS) {
38
+ throw Error("Sync committee has not sufficient participants");
39
+ }
40
+
41
+ if (!isValidLightClientHeader(config, update.attestedHeader)) {
42
+ throw Error("Attested Header is not Valid Light Client Header");
43
+ }
44
+
45
+ // Sanity check that slots are in correct order
46
+ if (update.signatureSlot <= update.attestedHeader.beacon.slot) {
47
+ throw Error(
48
+ `signature slot ${update.signatureSlot} must be after attested header slot ${update.attestedHeader.beacon.slot}`
49
+ );
50
+ }
51
+ if (update.attestedHeader.beacon.slot < update.finalizedHeader.beacon.slot) {
52
+ throw Error(
53
+ `attested header slot ${update.signatureSlot} must be after finalized header slot ${update.finalizedHeader.beacon.slot}`
54
+ );
55
+ }
56
+
57
+ // Verify that the `finality_branch`, if present, confirms `finalized_header`
58
+ // to match the finalized checkpoint root saved in the state of `attested_header`.
59
+ // Note that the genesis finalized checkpoint root is represented as a zero hash.
60
+ if (!isFinalityUpdate(update)) {
61
+ if (!isZeroedHeader(update.finalizedHeader.beacon)) {
62
+ throw Error("finalizedHeader must be zero for non-finality update");
63
+ }
64
+ } else {
65
+ let finalizedRoot: Root;
66
+
67
+ if (update.finalizedHeader.beacon.slot === GENESIS_SLOT) {
68
+ if (!isZeroedHeader(update.finalizedHeader.beacon)) {
69
+ throw Error("finalizedHeader must be zero for not finality update");
70
+ }
71
+ finalizedRoot = ZERO_HASH;
72
+ } else {
73
+ if (!isValidLightClientHeader(config, update.finalizedHeader)) {
74
+ throw Error("Finalized Header is not valid Light Client Header");
75
+ }
76
+
77
+ finalizedRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(update.finalizedHeader.beacon);
78
+ }
79
+
80
+ if (
81
+ !isValidMerkleBranch(
82
+ finalizedRoot,
83
+ update.finalityBranch,
84
+ isElectraLightClientUpdate(update) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH,
85
+ isElectraLightClientUpdate(update) ? FINALIZED_ROOT_INDEX_ELECTRA : FINALIZED_ROOT_INDEX,
86
+ update.attestedHeader.beacon.stateRoot
87
+ )
88
+ ) {
89
+ throw Error("Invalid finality header merkle branch");
90
+ }
91
+ }
92
+
93
+ // Verify that the `next_sync_committee`, if present, actually is the next sync committee saved in the
94
+ // state of the `attested_header`
95
+ if (!isSyncCommitteeUpdate(update)) {
96
+ if (!isZeroedSyncCommittee(update.nextSyncCommittee)) {
97
+ throw Error("nextSyncCommittee must be zero for non sync committee update");
98
+ }
99
+ } else {
100
+ if (
101
+ !isValidMerkleBranch(
102
+ ssz.altair.SyncCommittee.hashTreeRoot(update.nextSyncCommittee),
103
+ update.nextSyncCommitteeBranch,
104
+ isElectraLightClientUpdate(update) ? NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA : NEXT_SYNC_COMMITTEE_DEPTH,
105
+ isElectraLightClientUpdate(update) ? NEXT_SYNC_COMMITTEE_INDEX_ELECTRA : NEXT_SYNC_COMMITTEE_INDEX,
106
+ update.attestedHeader.beacon.stateRoot
107
+ )
108
+ ) {
109
+ throw Error("Invalid next sync committee merkle branch");
110
+ }
111
+ }
112
+
113
+ // Verify sync committee aggregate signature
114
+
115
+ const participantPubkeys = getParticipantPubkeys(syncCommittee.pubkeys, update.syncAggregate.syncCommitteeBits);
116
+
117
+ const signingRoot = ssz.phase0.SigningData.hashTreeRoot({
118
+ objectRoot: ssz.phase0.BeaconBlockHeader.hashTreeRoot(update.attestedHeader.beacon),
119
+ domain: store.config.getDomain(update.signatureSlot - 1, DOMAIN_SYNC_COMMITTEE),
120
+ });
121
+
122
+ if (!isValidBlsAggregate(participantPubkeys, signingRoot, update.syncAggregate.syncCommitteeSignature)) {
123
+ throw Error("Invalid aggregate signature");
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Same as BLS.verifyAggregate but with detailed error messages
129
+ */
130
+ function isValidBlsAggregate(publicKeys: PublicKey[], message: Uint8Array, signature: Uint8Array): boolean {
131
+ let sig: Signature;
132
+ try {
133
+ sig = Signature.fromBytes(signature, true);
134
+ } catch (e) {
135
+ (e as Error).message = `Error deserializing signature: ${(e as Error).message}`;
136
+ throw e;
137
+ }
138
+
139
+ try {
140
+ return fastAggregateVerify(message, publicKeys, sig);
141
+ } catch (e) {
142
+ (e as Error).message = `Error verifying signature: ${(e as Error).message}`;
143
+ throw e;
144
+ }
145
+ }
@@ -1,4 +1,4 @@
1
- import {FAR_FUTURE_EPOCH, GENESIS_SLOT, UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params";
1
+ import {FAR_FUTURE_EPOCH, ForkSeq, GENESIS_SLOT, UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params";
2
2
  import {ValidatorIndex, ssz} from "@lodestar/types";
3
3
  import {CachedBeaconStateElectra, getCachedBeaconState} from "../cache/stateCache.js";
4
4
  import {G2_POINT_AT_INFINITY} from "../constants/constants.js";
@@ -78,7 +78,9 @@ export function upgradeStateToElectra(stateDeneb: CachedBeaconStateDeneb): Cache
78
78
  stateElectraView.commit();
79
79
  const tmpElectraState = getCachedBeaconState(stateElectraView, stateDeneb);
80
80
  stateElectraView.exitBalanceToConsume = BigInt(getActivationExitChurnLimit(tmpElectraState.epochCtx));
81
- stateElectraView.consolidationBalanceToConsume = BigInt(getConsolidationChurnLimit(tmpElectraState.epochCtx));
81
+ stateElectraView.consolidationBalanceToConsume = BigInt(
82
+ getConsolidationChurnLimit(ForkSeq.electra, tmpElectraState.epochCtx)
83
+ );
82
84
 
83
85
  preActivation.sort((i0, i1) => {
84
86
  const res = validatorsArr[i0].activationEligibilityEpoch - validatorsArr[i1].activationEligibilityEpoch;
@@ -68,7 +68,7 @@ import {canBuilderCoverBid} from "../util/gloas.js";
68
68
  import {loadState} from "../util/loadState/loadState.js";
69
69
  import {getRandaoMix} from "../util/seed.js";
70
70
  import {getLatestWeakSubjectivityCheckpointEpoch} from "../util/weakSubjectivity.js";
71
- import {IBeaconStateView, IBeaconStateViewLatestFork} from "./interface.js";
71
+ import {IBeaconStateView, IBeaconStateViewGloas, IBeaconStateViewLatestFork, isStatePostGloas} from "./interface.js";
72
72
 
73
73
  export class BeaconStateView implements IBeaconStateViewLatestFork {
74
74
  private readonly config: BeaconConfig;
@@ -406,16 +406,44 @@ export class BeaconStateView implements IBeaconStateViewLatestFork {
406
406
  }
407
407
 
408
408
  /**
409
- * Return the index of the validator in the PTC committee for the given slot.
410
- * return -1 if validator is not in the PTC committee for the given slot.
409
+ * Return the PTCs for an epoch
411
410
  */
412
- getIndexInPayloadTimelinessCommittee(validatorIndex: ValidatorIndex, slot: Slot): number {
411
+ getEpochPTCs(epoch: Epoch): Uint32Array[] {
412
+ if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.gloas) {
413
+ throw new Error("PTC committees are not supported before Gloas");
414
+ }
415
+
416
+ const epochCtx = (this.cachedState as CachedBeaconStateGloas).epochCtx;
417
+ if (epoch === epochCtx.epoch) {
418
+ return epochCtx.payloadTimelinessCommittees;
419
+ }
420
+ if (epoch === epochCtx.nextEpoch) {
421
+ return epochCtx.nextPayloadTimelinessCommittees;
422
+ }
423
+ throw new Error(`PTC committees are not available for epoch=${epoch}`);
424
+ }
425
+ /**
426
+ * Return all positions of the validator in the PTC committee for the given slot.
427
+ *
428
+ * `compute_ptc` samples by effective balance and may place the same validator at multiple
429
+ * positions, so a validator can have more than one index. Returns an empty array if the
430
+ * validator is not in the PTC for the given slot.
431
+ *
432
+ * Spec: gloas/fork-choice.md#new-on_payload_attestation_message
433
+ */
434
+ getIndicesInPayloadTimelinessCommittee(validatorIndex: ValidatorIndex, slot: Slot): number[] {
413
435
  if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.gloas) {
414
436
  throw new Error("PTC committees are not supported before Gloas");
415
437
  }
416
438
 
417
439
  const ptcCommittee = (this.cachedState as CachedBeaconStateGloas).epochCtx.getPayloadTimelinessCommittee(slot);
418
- return ptcCommittee.indexOf(validatorIndex);
440
+ const indices: number[] = [];
441
+ for (let i = 0; i < ptcCommittee.length; i++) {
442
+ if (ptcCommittee[i] === validatorIndex) {
443
+ indices.push(i);
444
+ }
445
+ }
446
+ return indices;
419
447
  }
420
448
 
421
449
  // Shuffling and committees
@@ -786,16 +814,19 @@ export class BeaconStateView implements IBeaconStateViewLatestFork {
786
814
  /**
787
815
  * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/validator.md#executionpayload
788
816
  */
789
- getExpectedWithdrawalsForFullParent(executionRequests: electra.ExecutionRequests): capella.Withdrawal[] {
790
- const fork = this.config.getForkSeq(this.cachedState.slot);
791
- if (fork < ForkSeq.gloas) {
792
- throw new Error("getExpectedWithdrawalsForFullParent is not available before Gloas");
817
+ withParentPayloadApplied(executionRequests: electra.ExecutionRequests): IBeaconStateViewGloas {
818
+ if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.gloas) {
819
+ throw new Error("withParentPayloadApplied is not available before Gloas");
793
820
  }
794
- // Make a copy of the state to avoid mutability issues
795
821
  const stateCopy = this.cachedState.clone(true) as CachedBeaconStateGloas;
796
- // Apply parent payload before computing withdrawals
822
+
797
823
  applyParentExecutionPayload(stateCopy, executionRequests);
798
824
 
799
- return getExpectedWithdrawals(fork, stateCopy).expectedWithdrawals;
825
+ const stateView = new BeaconStateView(stateCopy);
826
+ if (!isStatePostGloas(stateView)) {
827
+ throw new Error("Expected gloas state after clone");
828
+ }
829
+
830
+ return stateView;
800
831
  }
801
832
  }
@@ -252,13 +252,15 @@ export interface IBeaconStateViewGloas extends IBeaconStateViewFulu {
252
252
  payloadExpectedWithdrawals: capella.Withdrawal[];
253
253
  getBuilder(index: BuilderIndex): gloas.Builder;
254
254
  canBuilderCoverBid(builderIndex: BuilderIndex, bidAmount: number): boolean;
255
- getIndexInPayloadTimelinessCommittee(validatorIndex: ValidatorIndex, slot: Slot): number;
255
+ getEpochPTCs(epoch: Epoch): Uint32Array[];
256
+ getIndicesInPayloadTimelinessCommittee(validatorIndex: ValidatorIndex, slot: Slot): number[];
256
257
  /**
257
- * Compute expected withdrawals as if the parent was FULL.
258
- * Clones the state, applies parent payload effects, then computes withdrawals.
259
- * Used by prepare_execution_payload when building on FULL parent.
258
+ * Clone the state and apply parent execution payload effects.
259
+ * Used during block production and prepareNextSlot so that withdrawals and
260
+ * operation selection (e.g. voluntary exits) see the same post-apply state that the block
261
+ * processor will see at import.
260
262
  */
261
- getExpectedWithdrawalsForFullParent(executionRequests: electra.ExecutionRequests): capella.Withdrawal[];
263
+ withParentPayloadApplied(executionRequests: electra.ExecutionRequests): IBeaconStateViewGloas;
262
264
  }
263
265
 
264
266
  /**
package/src/util/epoch.ts CHANGED
@@ -1,7 +1,13 @@
1
- import {EPOCHS_PER_SYNC_COMMITTEE_PERIOD, GENESIS_EPOCH, MAX_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params";
1
+ import {
2
+ EPOCHS_PER_SYNC_COMMITTEE_PERIOD,
3
+ ForkSeq,
4
+ GENESIS_EPOCH,
5
+ MAX_SEED_LOOKAHEAD,
6
+ SLOTS_PER_EPOCH,
7
+ } from "@lodestar/params";
2
8
  import {BeaconState, Epoch, Gwei, Slot, SyncPeriod} from "@lodestar/types";
3
9
  import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
4
- import {getActivationExitChurnLimit, getConsolidationChurnLimit} from "./validator.js";
10
+ import {getActivationExitChurnLimit, getConsolidationChurnLimit, getExitChurnLimit} from "./validator.js";
5
11
 
6
12
  /**
7
13
  * Return the epoch number at the given slot.
@@ -45,8 +51,10 @@ export function computeExitEpochAndUpdateChurn(
45
51
  state: CachedBeaconStateElectra | CachedBeaconStateGloas,
46
52
  exitBalance: Gwei
47
53
  ): number {
54
+ const fork = state.config.getForkSeq(state.slot);
48
55
  let earliestExitEpoch = Math.max(state.earliestExitEpoch, computeActivationExitEpoch(state.epochCtx.epoch));
49
- const perEpochChurn = getActivationExitChurnLimit(state.epochCtx);
56
+ const perEpochChurn =
57
+ fork >= ForkSeq.gloas ? getExitChurnLimit(state.epochCtx) : getActivationExitChurnLimit(state.epochCtx);
50
58
 
51
59
  // New epoch for exits.
52
60
  let exitBalanceToConsume =
@@ -71,11 +79,12 @@ export function computeConsolidationEpochAndUpdateChurn(
71
79
  state: CachedBeaconStateElectra | CachedBeaconStateGloas,
72
80
  consolidationBalance: Gwei
73
81
  ): number {
82
+ const fork = state.config.getForkSeq(state.slot);
74
83
  let earliestConsolidationEpoch = Math.max(
75
84
  state.earliestConsolidationEpoch,
76
85
  computeActivationExitEpoch(state.epochCtx.epoch)
77
86
  );
78
- const perEpochConsolidationChurn = getConsolidationChurnLimit(state.epochCtx);
87
+ const perEpochConsolidationChurn = getConsolidationChurnLimit(fork, state.epochCtx);
79
88
 
80
89
  // New epoch for consolidations
81
90
  let consolidationBalanceToConsume =