@lodestar/state-transition 1.45.0-dev.fadf0fbb1f → 1.46.0-dev.8fc4e297d2

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 (105) hide show
  1. package/lib/block/processAttestationsAltair.d.ts +4 -1
  2. package/lib/block/processAttestationsAltair.d.ts.map +1 -1
  3. package/lib/block/processAttestationsAltair.js +7 -5
  4. package/lib/block/processAttestationsAltair.js.map +1 -1
  5. package/lib/block/processBuilderDepositRequest.d.ts.map +1 -1
  6. package/lib/block/processBuilderDepositRequest.js +13 -7
  7. package/lib/block/processBuilderDepositRequest.js.map +1 -1
  8. package/lib/block/processOperations.d.ts.map +1 -1
  9. package/lib/block/processOperations.js +17 -1
  10. package/lib/block/processOperations.js.map +1 -1
  11. package/lib/block/processParentExecutionPayload.d.ts.map +1 -1
  12. package/lib/block/processParentExecutionPayload.js +15 -1
  13. package/lib/block/processParentExecutionPayload.js.map +1 -1
  14. package/lib/block/processWithdrawals.js +1 -1
  15. package/lib/block/processWithdrawals.js.map +1 -1
  16. package/lib/cache/stateCache.d.ts.map +1 -1
  17. package/lib/cache/stateCache.js +2 -4
  18. package/lib/cache/stateCache.js.map +1 -1
  19. package/lib/epoch/processParticipationFlagUpdates.d.ts +2 -2
  20. package/lib/epoch/processParticipationFlagUpdates.d.ts.map +1 -1
  21. package/lib/epoch/processParticipationFlagUpdates.js +13 -0
  22. package/lib/epoch/processParticipationFlagUpdates.js.map +1 -1
  23. package/lib/epoch/processRewardsAndPenalties.d.ts.map +1 -1
  24. package/lib/epoch/processRewardsAndPenalties.js +1 -2
  25. package/lib/epoch/processRewardsAndPenalties.js.map +1 -1
  26. package/lib/lightClient/proofs.d.ts.map +1 -1
  27. package/lib/lightClient/proofs.js +28 -2
  28. package/lib/lightClient/proofs.js.map +1 -1
  29. package/lib/lightClient/spec/index.d.ts.map +1 -1
  30. package/lib/lightClient/spec/index.js +7 -4
  31. package/lib/lightClient/spec/index.js.map +1 -1
  32. package/lib/lightClient/spec/processLightClientUpdate.d.ts.map +1 -1
  33. package/lib/lightClient/spec/processLightClientUpdate.js +8 -2
  34. package/lib/lightClient/spec/processLightClientUpdate.js.map +1 -1
  35. package/lib/lightClient/spec/utils.d.ts +7 -2
  36. package/lib/lightClient/spec/utils.d.ts.map +1 -1
  37. package/lib/lightClient/spec/utils.js +130 -19
  38. package/lib/lightClient/spec/utils.js.map +1 -1
  39. package/lib/lightClient/spec/validateLightClientBootstrap.d.ts.map +1 -1
  40. package/lib/lightClient/spec/validateLightClientBootstrap.js +2 -7
  41. package/lib/lightClient/spec/validateLightClientBootstrap.js.map +1 -1
  42. package/lib/lightClient/spec/validateLightClientUpdate.d.ts.map +1 -1
  43. package/lib/lightClient/spec/validateLightClientUpdate.js +6 -5
  44. package/lib/lightClient/spec/validateLightClientUpdate.js.map +1 -1
  45. package/lib/lightClient/types.d.ts +3 -1
  46. package/lib/lightClient/types.d.ts.map +1 -1
  47. package/lib/metrics.js +2 -4
  48. package/lib/metrics.js.map +1 -1
  49. package/lib/slot/upgradeStateToAltair.js +1 -1
  50. package/lib/slot/upgradeStateToAltair.js.map +1 -1
  51. package/lib/slot/upgradeStateToGloas.d.ts.map +1 -1
  52. package/lib/slot/upgradeStateToGloas.js +48 -9
  53. package/lib/slot/upgradeStateToGloas.js.map +1 -1
  54. package/lib/stateView/beaconStateView.d.ts +0 -1
  55. package/lib/stateView/beaconStateView.d.ts.map +1 -1
  56. package/lib/stateView/beaconStateView.js +0 -8
  57. package/lib/stateView/beaconStateView.js.map +1 -1
  58. package/lib/stateView/interface.d.ts +0 -1
  59. package/lib/stateView/interface.d.ts.map +1 -1
  60. package/lib/stateView/interface.js.map +1 -1
  61. package/lib/stateView/nativeBeaconStateView.d.ts +0 -2
  62. package/lib/stateView/nativeBeaconStateView.d.ts.map +1 -1
  63. package/lib/stateView/nativeBeaconStateView.js +0 -9
  64. package/lib/stateView/nativeBeaconStateView.js.map +1 -1
  65. package/lib/util/execution.d.ts.map +1 -1
  66. package/lib/util/execution.js +7 -2
  67. package/lib/util/execution.js.map +1 -1
  68. package/lib/util/gloas.d.ts +3 -1
  69. package/lib/util/gloas.d.ts.map +1 -1
  70. package/lib/util/gloas.js +8 -11
  71. package/lib/util/gloas.js.map +1 -1
  72. package/lib/util/index.d.ts +1 -0
  73. package/lib/util/index.d.ts.map +1 -1
  74. package/lib/util/index.js +1 -0
  75. package/lib/util/index.js.map +1 -1
  76. package/lib/util/ssz.d.ts +9 -0
  77. package/lib/util/ssz.d.ts.map +1 -0
  78. package/lib/util/ssz.js +39 -0
  79. package/lib/util/ssz.js.map +1 -0
  80. package/package.json +11 -11
  81. package/src/block/processAttestationsAltair.ts +9 -6
  82. package/src/block/processBuilderDepositRequest.ts +21 -8
  83. package/src/block/processOperations.ts +28 -1
  84. package/src/block/processParentExecutionPayload.ts +26 -1
  85. package/src/block/processWithdrawals.ts +1 -1
  86. package/src/cache/stateCache.ts +2 -4
  87. package/src/epoch/processParticipationFlagUpdates.ts +20 -2
  88. package/src/epoch/processRewardsAndPenalties.ts +1 -2
  89. package/src/lightClient/proofs.ts +36 -1
  90. package/src/lightClient/spec/index.ts +8 -4
  91. package/src/lightClient/spec/processLightClientUpdate.ts +13 -1
  92. package/src/lightClient/spec/utils.ts +200 -30
  93. package/src/lightClient/spec/validateLightClientBootstrap.ts +3 -10
  94. package/src/lightClient/spec/validateLightClientUpdate.ts +11 -21
  95. package/src/lightClient/types.ts +3 -1
  96. package/src/metrics.ts +2 -4
  97. package/src/slot/upgradeStateToAltair.ts +1 -1
  98. package/src/slot/upgradeStateToGloas.ts +84 -9
  99. package/src/stateView/beaconStateView.ts +0 -8
  100. package/src/stateView/interface.ts +0 -1
  101. package/src/stateView/nativeBeaconStateView.ts +0 -10
  102. package/src/util/execution.ts +9 -4
  103. package/src/util/gloas.ts +9 -14
  104. package/src/util/index.ts +1 -0
  105. package/src/util/ssz.ts +42 -0
@@ -5,7 +5,13 @@ import {pruneSetToMax} from "@lodestar/utils";
5
5
  import {computeSyncPeriodAtSlot} from "../../util/epoch.js";
6
6
  import {LightClientUpdateSummary, isBetterUpdate, toLightClientUpdateSummary} from "./isBetterUpdate.js";
7
7
  import {type ILightClientStore, MAX_SYNC_PERIODS_CACHE, type SyncCommitteeFast} from "./store.js";
8
- import {deserializeSyncCommittee, getSafetyThreshold, isSyncCommitteeUpdate, sumBits} from "./utils.js";
8
+ import {
9
+ deserializeSyncCommittee,
10
+ getSafetyThreshold,
11
+ isFinalityUpdate,
12
+ isSyncCommitteeUpdate,
13
+ sumBits,
14
+ } from "./utils.js";
9
15
  import {validateLightClientUpdate} from "./validateLightClientUpdate.js";
10
16
 
11
17
  export interface ProcessUpdateOpts {
@@ -44,7 +50,13 @@ export function processLightClientUpdate(
44
50
  }
45
51
 
46
52
  // Update finalized header
53
+ // Defense-in-depth: require isFinalityUpdate(update) so a non-finality update can never
54
+ // overwrite store.finalizedHeader, regardless of any future loosening of the zeroed-header
55
+ // checks in validateLightClientUpdate. Spec-faithful behavior also holds without this guard
56
+ // once update.finalizedHeader is guaranteed fully-zero in the non-finality case, but the
57
+ // guard makes the invariant local to this write site.
47
58
  if (
59
+ isFinalityUpdate(update) &&
48
60
  syncCommitteeTrueBits * 3 >= SYNC_COMMITTEE_SIZE * 2 &&
49
61
  update.finalizedHeader.beacon.slot > store.finalizedHeader.beacon.slot
50
62
  ) {
@@ -1,16 +1,25 @@
1
1
  import {PublicKey} from "@chainsafe/blst";
2
+ import {Tree} from "@chainsafe/persistent-merkle-tree";
2
3
  import {BitArray} from "@chainsafe/ssz";
3
4
  import {ChainForkConfig} from "@lodestar/config";
4
5
  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,
6
+ BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX,
7
+ CURRENT_SYNC_COMMITTEE_GINDEX,
8
+ CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA,
9
+ CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS,
10
+ EXECUTION_BLOCK_HASH_GINDEX,
11
+ EXECUTION_BLOCK_HASH_GINDEX_DENEB,
12
+ EXECUTION_BLOCK_HASH_GINDEX_GLOAS,
13
+ FINALIZED_ROOT_GINDEX,
14
+ FINALIZED_ROOT_GINDEX_ELECTRA,
15
+ FINALIZED_ROOT_GINDEX_GLOAS,
9
16
  ForkName,
10
17
  ForkSeq,
11
- NEXT_SYNC_COMMITTEE_DEPTH,
12
- NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA,
18
+ NEXT_SYNC_COMMITTEE_GINDEX,
19
+ NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA,
20
+ NEXT_SYNC_COMMITTEE_GINDEX_GLOAS,
13
21
  isForkPostElectra,
22
+ isForkPostGloas,
14
23
  } from "@lodestar/params";
15
24
  import {
16
25
  BeaconBlockHeader,
@@ -29,7 +38,6 @@ import type {LightClientStore, SyncCommitteeFast} from "./store.js";
29
38
 
30
39
  export const GENESIS_SLOT = 0;
31
40
  export const ZERO_HASH = new Uint8Array(32);
32
- export const ZERO_PUBKEY = new Uint8Array(48);
33
41
  export const ZERO_SYNC_COMMITTEE = ssz.altair.SyncCommittee.defaultValue();
34
42
  export const ZERO_HEADER = ssz.phase0.BeaconBlockHeader.defaultValue();
35
43
  /** From https://notes.ethereum.org/@vbuterin/extended_light_client_protocol#Optimistic-head-determining-function */
@@ -74,17 +82,11 @@ export function getSafetyThreshold(maxActiveParticipants: number): number {
74
82
  }
75
83
 
76
84
  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);
85
+ return Array.from({length: getGindexDepth(nextSyncCommitteeGindexAtFork(fork))}, () => ZERO_HASH);
82
86
  }
83
87
 
84
88
  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);
89
+ return Array.from({length: getGindexDepth(finalizedRootGindexAtFork(fork))}, () => ZERO_HASH);
88
90
  }
89
91
 
90
92
  export function isSyncCommitteeUpdate(update: LightClientUpdate): boolean {
@@ -106,13 +108,16 @@ export function isFinalityUpdate(update: LightClientUpdate): boolean {
106
108
  }
107
109
 
108
110
  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
+ // Spec requires the whole header to equal LightClientHeader() in the non-finality case
112
+ // (see altair/light-client/sync-protocol.md `process_light_client_update`). Checking only
113
+ // bodyRoot would let an attacker smuggle arbitrary slot/proposerIndex/parentRoot/stateRoot
114
+ // through the non-finality branch and overwrite store.finalizedHeader.
115
+ return header === ZERO_HEADER || ssz.phase0.BeaconBlockHeader.equals(header, ZERO_HEADER);
111
116
  }
112
117
 
113
118
  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);
119
+ // Spec requires the whole SyncCommittee to equal SyncCommittee() in the non-sync-committee-update case.
120
+ return syncCommittee === ZERO_SYNC_COMMITTEE || ssz.altair.SyncCommittee.equals(syncCommittee, ZERO_SYNC_COMMITTEE);
116
121
  }
117
122
 
118
123
  export function isValidMerkleBranch(
@@ -129,12 +134,73 @@ export function isValidMerkleBranch(
129
134
  return verifyMerkleBranch(leaf, branch, depth, index, root);
130
135
  }
131
136
 
132
- export function normalizeMerkleBranch(branch: Uint8Array[], depth: number): Uint8Array[] {
137
+ export function isValidNormalizedMerkleBranch(
138
+ leaf: Uint8Array,
139
+ branch: Uint8Array[],
140
+ gindex: number,
141
+ root: Uint8Array
142
+ ): boolean {
143
+ const depth = getGindexDepth(gindex);
144
+ const index = getGindexIndex(gindex);
145
+ const numExtraDepth = branch.length - depth;
146
+ if (numExtraDepth < 0) {
147
+ return false;
148
+ }
149
+
150
+ for (let i = 0; i < numExtraDepth; i++) {
151
+ if (!byteArrayEquals(branch[i], ZERO_HASH)) {
152
+ return false;
153
+ }
154
+ }
155
+
156
+ return isValidMerkleBranch(leaf, branch.slice(numExtraDepth), depth, index, root);
157
+ }
158
+
159
+ export function normalizeMerkleBranch(branch: Uint8Array[], gindex: number): Uint8Array[] {
160
+ const depth = getGindexDepth(gindex);
133
161
  const numExtraDepth = depth - branch.length;
134
162
 
135
163
  return [...Array.from({length: numExtraDepth}, () => ZERO_HASH), ...branch];
136
164
  }
137
165
 
166
+ export function currentSyncCommitteeGindexAtFork(fork: ForkName): number {
167
+ if (isForkPostGloas(fork)) {
168
+ return CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS;
169
+ }
170
+ if (isForkPostElectra(fork)) {
171
+ return CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA;
172
+ }
173
+ return CURRENT_SYNC_COMMITTEE_GINDEX;
174
+ }
175
+
176
+ export function finalizedRootGindexAtFork(fork: ForkName): number {
177
+ if (isForkPostGloas(fork)) {
178
+ return FINALIZED_ROOT_GINDEX_GLOAS;
179
+ }
180
+ if (isForkPostElectra(fork)) {
181
+ return FINALIZED_ROOT_GINDEX_ELECTRA;
182
+ }
183
+ return FINALIZED_ROOT_GINDEX;
184
+ }
185
+
186
+ export function nextSyncCommitteeGindexAtFork(fork: ForkName): number {
187
+ if (isForkPostGloas(fork)) {
188
+ return NEXT_SYNC_COMMITTEE_GINDEX_GLOAS;
189
+ }
190
+ if (isForkPostElectra(fork)) {
191
+ return NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA;
192
+ }
193
+ return NEXT_SYNC_COMMITTEE_GINDEX;
194
+ }
195
+
196
+ export function getGindexDepth(gindex: number): number {
197
+ return Math.floor(Math.log2(gindex));
198
+ }
199
+
200
+ export function getGindexIndex(gindex: number): number {
201
+ return gindex - 2 ** getGindexDepth(gindex);
202
+ }
203
+
138
204
  export function upgradeLightClientHeader(
139
205
  config: ChainForkConfig,
140
206
  targetFork: ForkName,
@@ -147,7 +213,7 @@ export function upgradeLightClientHeader(
147
213
 
148
214
  // We are modifying the same header object, may be we could create a copy, but its
149
215
  // not required as of now
150
- const upgradedHeader = header;
216
+ let upgradedHeader = header;
151
217
  const startUpgradeFromFork = Object.values(ForkName)[ForkSeq[headerFork] + 1];
152
218
 
153
219
  switch (startUpgradeFromFork) {
@@ -198,7 +264,11 @@ export function upgradeLightClientHeader(
198
264
  if (ForkSeq[targetFork] <= ForkSeq.fulu) break;
199
265
 
200
266
  case ForkName.gloas:
201
- // No changes to LightClientHeader in Gloas
267
+ if (isGloasLightClientHeader(upgradedHeader)) {
268
+ break;
269
+ }
270
+
271
+ upgradedHeader = upgradeLightClientHeaderToGloas(config, upgradedHeader as LightClientHeader<ForkName.electra>);
202
272
 
203
273
  // Break if no further upgrades is required else fall through
204
274
  if (ForkSeq[targetFork] <= ForkSeq.gloas) break;
@@ -209,6 +279,40 @@ export function upgradeLightClientHeader(
209
279
  export function isValidLightClientHeader(config: ChainForkConfig, header: LightClientHeader): boolean {
210
280
  const epoch = computeEpochAtSlot(header.beacon.slot);
211
281
 
282
+ if (isGloasLightClientHeader(header)) {
283
+ if (epoch >= config.GLOAS_FORK_EPOCH) {
284
+ return isValidNormalizedMerkleBranch(
285
+ header.executionBlockHash,
286
+ header.executionBranch,
287
+ EXECUTION_BLOCK_HASH_GINDEX_GLOAS,
288
+ header.beacon.bodyRoot
289
+ );
290
+ }
291
+
292
+ if (epoch >= config.DENEB_FORK_EPOCH) {
293
+ return isValidNormalizedMerkleBranch(
294
+ header.executionBlockHash,
295
+ header.executionBranch,
296
+ EXECUTION_BLOCK_HASH_GINDEX_DENEB,
297
+ header.beacon.bodyRoot
298
+ );
299
+ }
300
+
301
+ if (epoch >= config.CAPELLA_FORK_EPOCH) {
302
+ return isValidNormalizedMerkleBranch(
303
+ header.executionBlockHash,
304
+ header.executionBranch,
305
+ EXECUTION_BLOCK_HASH_GINDEX,
306
+ header.beacon.bodyRoot
307
+ );
308
+ }
309
+
310
+ return (
311
+ byteArrayEquals(header.executionBlockHash, ZERO_HASH) &&
312
+ header.executionBranch.every((node) => byteArrayEquals(node, ZERO_HASH))
313
+ );
314
+ }
315
+
212
316
  if (epoch < config.CAPELLA_FORK_EPOCH) {
213
317
  return (
214
318
  ((header as LightClientHeader<ForkName.capella>).execution === undefined ||
@@ -239,8 +343,8 @@ export function isValidLightClientHeader(config: ChainForkConfig, header: LightC
239
343
  .getPostBellatrixForkTypes(header.beacon.slot)
240
344
  .ExecutionPayloadHeader.hashTreeRoot((header as LightClientHeader<ForkName.capella>).execution),
241
345
  (header as LightClientHeader<ForkName.capella>).executionBranch,
242
- EXECUTION_PAYLOAD_DEPTH,
243
- EXECUTION_PAYLOAD_INDEX,
346
+ getGindexDepth(BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX),
347
+ getGindexIndex(BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX),
244
348
  header.beacon.bodyRoot
245
349
  );
246
350
  }
@@ -254,12 +358,9 @@ export function upgradeLightClientUpdate(
254
358
  update.finalizedHeader = upgradeLightClientHeader(config, targetFork, update.finalizedHeader);
255
359
  update.nextSyncCommitteeBranch = normalizeMerkleBranch(
256
360
  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
361
+ nextSyncCommitteeGindexAtFork(targetFork)
262
362
  );
363
+ update.finalityBranch = normalizeMerkleBranch(update.finalityBranch, finalizedRootGindexAtFork(targetFork));
263
364
 
264
365
  return update;
265
366
  }
@@ -273,7 +374,7 @@ export function upgradeLightClientFinalityUpdate(
273
374
  finalityUpdate.finalizedHeader = upgradeLightClientHeader(config, targetFork, finalityUpdate.finalizedHeader);
274
375
  finalityUpdate.finalityBranch = normalizeMerkleBranch(
275
376
  finalityUpdate.finalityBranch,
276
- isForkPostElectra(targetFork) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH
377
+ finalizedRootGindexAtFork(targetFork)
277
378
  );
278
379
 
279
380
  return finalityUpdate;
@@ -315,3 +416,72 @@ export function upgradeLightClientStore(
315
416
 
316
417
  return store;
317
418
  }
419
+
420
+ function isGloasLightClientHeader(header: LightClientHeader): header is LightClientHeader<ForkName.gloas> {
421
+ return (header as LightClientHeader<ForkName.gloas>).executionBlockHash !== undefined;
422
+ }
423
+
424
+ function upgradeLightClientHeaderToGloas(
425
+ config: ChainForkConfig,
426
+ pre: LightClientHeader<ForkName.electra>
427
+ ): LightClientHeader<ForkName.gloas> {
428
+ if (ssz.electra.LightClientHeader.equals(pre, ssz.electra.LightClientHeader.defaultValue())) {
429
+ return ssz.gloas.LightClientHeader.defaultValue();
430
+ }
431
+
432
+ const epoch = computeEpochAtSlot(pre.beacon.slot);
433
+
434
+ if (epoch >= config.DENEB_FORK_EPOCH) {
435
+ const blockHashGindex = ssz.deneb.ExecutionPayloadHeader.getPathInfo(["blockHash"]).gindex;
436
+ const executionBranch = new Tree(ssz.deneb.ExecutionPayloadHeader.toView(pre.execution).node).getSingleProof(
437
+ blockHashGindex
438
+ );
439
+
440
+ return {
441
+ beacon: pre.beacon,
442
+ executionBlockHash: pre.execution.blockHash,
443
+ executionBranch: normalizeMerkleBranch(
444
+ [...executionBranch, ...pre.executionBranch],
445
+ EXECUTION_BLOCK_HASH_GINDEX_GLOAS
446
+ ),
447
+ };
448
+ }
449
+
450
+ if (epoch >= config.CAPELLA_FORK_EPOCH) {
451
+ const executionHeader = {
452
+ parentHash: pre.execution.parentHash,
453
+ feeRecipient: pre.execution.feeRecipient,
454
+ stateRoot: pre.execution.stateRoot,
455
+ receiptsRoot: pre.execution.receiptsRoot,
456
+ logsBloom: pre.execution.logsBloom,
457
+ prevRandao: pre.execution.prevRandao,
458
+ blockNumber: pre.execution.blockNumber,
459
+ gasLimit: pre.execution.gasLimit,
460
+ gasUsed: pre.execution.gasUsed,
461
+ timestamp: pre.execution.timestamp,
462
+ extraData: pre.execution.extraData,
463
+ baseFeePerGas: pre.execution.baseFeePerGas,
464
+ blockHash: pre.execution.blockHash,
465
+ transactionsRoot: pre.execution.transactionsRoot,
466
+ withdrawalsRoot: pre.execution.withdrawalsRoot,
467
+ };
468
+ const blockHashGindex = ssz.capella.ExecutionPayloadHeader.getPathInfo(["blockHash"]).gindex;
469
+ const executionBranch = new Tree(ssz.capella.ExecutionPayloadHeader.toView(executionHeader).node).getSingleProof(
470
+ blockHashGindex
471
+ );
472
+
473
+ return {
474
+ beacon: pre.beacon,
475
+ executionBlockHash: executionHeader.blockHash,
476
+ executionBranch: normalizeMerkleBranch(
477
+ [...executionBranch, ...pre.executionBranch],
478
+ EXECUTION_BLOCK_HASH_GINDEX_GLOAS
479
+ ),
480
+ };
481
+ }
482
+
483
+ return {
484
+ ...ssz.gloas.LightClientHeader.defaultValue(),
485
+ beacon: pre.beacon,
486
+ };
487
+ }
@@ -1,13 +1,7 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
- import {isForkPostElectra} from "@lodestar/params";
3
2
  import {LightClientBootstrap, Root, ssz} from "@lodestar/types";
4
3
  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;
4
+ import {currentSyncCommitteeGindexAtFork, isValidLightClientHeader, isValidNormalizedMerkleBranch} from "./utils.js";
11
5
 
12
6
  export function validateLightClientBootstrap(
13
7
  config: ChainForkConfig,
@@ -26,11 +20,10 @@ export function validateLightClientBootstrap(
26
20
  }
27
21
 
28
22
  if (
29
- !isValidMerkleBranch(
23
+ !isValidNormalizedMerkleBranch(
30
24
  ssz.altair.SyncCommittee.hashTreeRoot(bootstrap.currentSyncCommittee),
31
25
  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,
26
+ currentSyncCommitteeGindexAtFork(fork),
34
27
  bootstrap.header.beacon.stateRoot
35
28
  )
36
29
  ) {
@@ -1,29 +1,19 @@
1
1
  import {PublicKey, Signature, fastAggregateVerify} from "@chainsafe/blst";
2
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";
3
+ import {DOMAIN_SYNC_COMMITTEE, GENESIS_SLOT, MIN_SYNC_COMMITTEE_PARTICIPANTS} from "@lodestar/params";
4
+ import {LightClientUpdate, Root, ssz} from "@lodestar/types";
17
5
  import type {ILightClientStore, SyncCommitteeFast} from "./store.js";
18
6
  import {
19
7
  ZERO_HASH,
8
+ finalizedRootGindexAtFork,
20
9
  getParticipantPubkeys,
21
10
  isFinalityUpdate,
22
11
  isSyncCommitteeUpdate,
23
12
  isValidLightClientHeader,
24
- isValidMerkleBranch,
13
+ isValidNormalizedMerkleBranch,
25
14
  isZeroedHeader,
26
15
  isZeroedSyncCommittee,
16
+ nextSyncCommitteeGindexAtFork,
27
17
  sumBits,
28
18
  } from "./utils.js";
29
19
 
@@ -33,6 +23,8 @@ export function validateLightClientUpdate(
33
23
  update: LightClientUpdate,
34
24
  syncCommittee: SyncCommitteeFast
35
25
  ): void {
26
+ const attestedFork = config.getForkName(update.attestedHeader.beacon.slot);
27
+
36
28
  // Verify sync committee has sufficient participants
37
29
  if (sumBits(update.syncAggregate.syncCommitteeBits) < MIN_SYNC_COMMITTEE_PARTICIPANTS) {
38
30
  throw Error("Sync committee has not sufficient participants");
@@ -78,11 +70,10 @@ export function validateLightClientUpdate(
78
70
  }
79
71
 
80
72
  if (
81
- !isValidMerkleBranch(
73
+ !isValidNormalizedMerkleBranch(
82
74
  finalizedRoot,
83
75
  update.finalityBranch,
84
- isElectraLightClientUpdate(update) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH,
85
- isElectraLightClientUpdate(update) ? FINALIZED_ROOT_INDEX_ELECTRA : FINALIZED_ROOT_INDEX,
76
+ finalizedRootGindexAtFork(attestedFork),
86
77
  update.attestedHeader.beacon.stateRoot
87
78
  )
88
79
  ) {
@@ -98,11 +89,10 @@ export function validateLightClientUpdate(
98
89
  }
99
90
  } else {
100
91
  if (
101
- !isValidMerkleBranch(
92
+ !isValidNormalizedMerkleBranch(
102
93
  ssz.altair.SyncCommittee.hashTreeRoot(update.nextSyncCommittee),
103
94
  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,
95
+ nextSyncCommitteeGindexAtFork(attestedFork),
106
96
  update.attestedHeader.beacon.stateRoot
107
97
  )
108
98
  ) {
@@ -26,8 +26,10 @@
26
26
  * ```
27
27
  */
28
28
  export type SyncCommitteeWitness = {
29
- /** Vector[Bytes32, 4] or Vector[Bytes32, 5] depending on the fork */
29
+ /** Shared witness for pre-Gloas forks where current and next sync committees are siblings. */
30
30
  witness: Uint8Array[];
31
31
  currentSyncCommitteeRoot: Uint8Array;
32
32
  nextSyncCommitteeRoot: Uint8Array;
33
+ currentSyncCommitteeBranch?: Uint8Array[];
34
+ nextSyncCommitteeBranch?: Uint8Array[];
33
35
  };
package/src/metrics.ts CHANGED
@@ -159,11 +159,9 @@ export function onPostStateMetrics(postState: CachedBeaconStateAllForks, metrics
159
159
  // This cache is populated during epoch transition, and should be preserved for performance.
160
160
  // If the cache is missing too often, means that our clone strategy is not working well.
161
161
  function isValidatorsNodesPopulated(state: CachedBeaconStateAllForks): boolean {
162
- // biome-ignore lint/complexity/useLiteralKeys: It is a private attribute
163
- return state.validators["nodesPopulated"] === true;
162
+ return (state.validators as unknown as {nodesPopulated?: boolean}).nodesPopulated === true;
164
163
  }
165
164
 
166
165
  function isBalancesNodesPopulated(state: CachedBeaconStateAllForks): boolean {
167
- // biome-ignore lint/complexity/useLiteralKeys: It is a private attribute
168
- return state.balances["nodesPopulated"] === true;
166
+ return (state.balances as unknown as {nodesPopulated?: boolean}).nodesPopulated === true;
169
167
  }
@@ -131,7 +131,7 @@ function translateParticipation(
131
131
 
132
132
  for (const attestation of pendingAttesations.getAllReadonly()) {
133
133
  const data = attestation.data;
134
- const attestationFlags = getAttestationParticipationStatus(
134
+ const {flags: attestationFlags} = getAttestationParticipationStatus(
135
135
  ForkSeq.altair,
136
136
  data,
137
137
  attestation.inclusionDelay,
@@ -1,3 +1,15 @@
1
+ import {getNodesAtDepth} from "@chainsafe/persistent-merkle-tree";
2
+ import {
3
+ BasicType,
4
+ CompositeType,
5
+ CompositeView,
6
+ CompositeViewDU,
7
+ ListBasicTreeViewDU,
8
+ ListCompositeTreeViewDU,
9
+ ProgressiveListBasicType,
10
+ ProgressiveListCompositeType,
11
+ ValueOf,
12
+ } from "@chainsafe/ssz";
1
13
  import {PAYLOAD_BUILDER_VERSION, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
2
14
  import {ssz} from "@lodestar/types";
3
15
  import {toPubkeyHex} from "@lodestar/utils";
@@ -7,6 +19,7 @@ import {CachedBeaconStateFulu, CachedBeaconStateGloas} from "../types.js";
7
19
  import {addBuilderToRegistry, initializePtcWindow, isBuilderWithdrawalCredential} from "../util/gloas.js";
8
20
  import {isValidatorKnown} from "../util/index.js";
9
21
  import {PendingDepositsLookup} from "../util/pendingDepositsLookup.js";
22
+ import {progressiveListRootNode} from "../util/ssz.js";
10
23
 
11
24
  /**
12
25
  * Upgrade a state from Fulu to Gloas.
@@ -34,17 +47,26 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea
34
47
  stateGloasView.eth1Data = stateGloasCloned.eth1Data;
35
48
  stateGloasView.eth1DataVotes = stateGloasCloned.eth1DataVotes;
36
49
  stateGloasView.eth1DepositIndex = stateGloasCloned.eth1DepositIndex;
37
- stateGloasView.validators = stateGloasCloned.validators;
38
- stateGloasView.balances = stateGloasCloned.balances;
50
+ stateGloasView.validators = migrateCompositeListToGloas(stateGloasCloned.validators, ssz.gloas.Validators);
51
+ stateGloasView.balances = migrateBasicListToGloas(stateGloasCloned.balances, ssz.gloas.Balances);
39
52
  stateGloasView.randaoMixes = stateGloasCloned.randaoMixes;
40
53
  stateGloasView.slashings = stateGloasCloned.slashings;
41
- stateGloasView.previousEpochParticipation = stateGloasCloned.previousEpochParticipation;
42
- stateGloasView.currentEpochParticipation = stateGloasCloned.currentEpochParticipation;
54
+ stateGloasView.previousEpochParticipation = migrateBasicListToGloas(
55
+ stateGloasCloned.previousEpochParticipation,
56
+ ssz.gloas.EpochParticipation
57
+ );
58
+ stateGloasView.currentEpochParticipation = migrateBasicListToGloas(
59
+ stateGloasCloned.currentEpochParticipation,
60
+ ssz.gloas.EpochParticipation
61
+ );
43
62
  stateGloasView.justificationBits = stateGloasCloned.justificationBits;
44
63
  stateGloasView.previousJustifiedCheckpoint = stateGloasCloned.previousJustifiedCheckpoint;
45
64
  stateGloasView.currentJustifiedCheckpoint = stateGloasCloned.currentJustifiedCheckpoint;
46
65
  stateGloasView.finalizedCheckpoint = stateGloasCloned.finalizedCheckpoint;
47
- stateGloasView.inactivityScores = stateGloasCloned.inactivityScores;
66
+ stateGloasView.inactivityScores = migrateBasicListToGloas(
67
+ stateGloasCloned.inactivityScores,
68
+ ssz.gloas.InactivityScores
69
+ );
48
70
  stateGloasView.currentSyncCommittee = stateGloasCloned.currentSyncCommittee;
49
71
  stateGloasView.nextSyncCommittee = stateGloasCloned.nextSyncCommittee;
50
72
  stateGloasView.latestExecutionPayloadBid.blockHash = stateFulu.latestExecutionPayloadHeader.blockHash;
@@ -61,9 +83,18 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea
61
83
  stateGloasView.earliestExitEpoch = stateGloasCloned.earliestExitEpoch;
62
84
  stateGloasView.consolidationBalanceToConsume = stateGloasCloned.consolidationBalanceToConsume;
63
85
  stateGloasView.earliestConsolidationEpoch = stateGloasCloned.earliestConsolidationEpoch;
64
- stateGloasView.pendingDeposits = stateGloasCloned.pendingDeposits;
65
- stateGloasView.pendingPartialWithdrawals = stateGloasCloned.pendingPartialWithdrawals;
66
- stateGloasView.pendingConsolidations = stateGloasCloned.pendingConsolidations;
86
+ stateGloasView.pendingDeposits = migrateCompositeListToGloas(
87
+ stateGloasCloned.pendingDeposits,
88
+ ssz.gloas.PendingDeposits
89
+ );
90
+ stateGloasView.pendingPartialWithdrawals = migrateCompositeListToGloas(
91
+ stateGloasCloned.pendingPartialWithdrawals,
92
+ ssz.gloas.PendingPartialWithdrawals
93
+ );
94
+ stateGloasView.pendingConsolidations = migrateCompositeListToGloas(
95
+ stateGloasCloned.pendingConsolidations,
96
+ ssz.gloas.PendingConsolidations
97
+ );
67
98
  stateGloasView.proposerLookahead = stateGloasCloned.proposerLookahead;
68
99
  stateGloasView.ptcWindow = ssz.gloas.PtcWindow.toViewDU(initializePtcWindow(stateFulu));
69
100
 
@@ -85,6 +116,50 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea
85
116
  return stateGloas;
86
117
  }
87
118
 
119
+ /**
120
+ * Migrate a composite list from fulu to its gloas progressive-list equivalent by reusing the fulu
121
+ * list's element nodes.
122
+ *
123
+ * Works whenever the element type is identical across the fork (e.g. validators use ValidatorNodeStruct,
124
+ * the pending* queues use the same electra element types). Each element's cached subtree root is then
125
+ * valid under gloas, so only the progressive list superstructure is rebuilt and a subsequent
126
+ * hashTreeRoot() skips re-hashing every element — the dominant cost for large lists like validators.
127
+ * Much cheaper than `gloasType.toViewDU(fuluList.getAllReadonlyValues())`, which decodes every element
128
+ * to a value and forces a full re-hash.
129
+ *
130
+ * The chunk nodes of a composite list ARE the element root nodes, so they are extracted directly
131
+ * with getNodesAtDepth instead of allocating a temporary ViewDU wrapper per element (getAllReadonly).
132
+ * Requires the fulu view to be committed (done at the top of upgradeStateToGloas).
133
+ */
134
+ function migrateCompositeListToGloas<
135
+ ElementType extends CompositeType<ValueOf<ElementType>, CompositeView<ElementType>, CompositeViewDU<ElementType>>,
136
+ >(fuluList: ListCompositeTreeViewDU<ElementType>, gloasType: ProgressiveListCompositeType<ElementType>) {
137
+ const {length, type} = fuluList;
138
+ const elementNodes = getNodesAtDepth(fuluList.node.left, type.chunkDepth, 0, length);
139
+ return gloasType.getViewDU(progressiveListRootNode(elementNodes, length));
140
+ }
141
+
142
+ /**
143
+ * Migrate a basic list from fulu to its gloas progressive-list equivalent by reusing the fulu
144
+ * list's packed chunk leaf nodes.
145
+ *
146
+ * Packed leaf chunks are bit-identical between List[T, N] and ProgressiveList[T] (same 32-byte
147
+ * LE packing, zero-padded final chunk); only the superstructure above the leaves differs. Reusing
148
+ * the leaves avoids materializing the value array (getAll), re-serializing it, and allocating
149
+ * fresh LeafNodes — the gloas tree shares the leaf nodes with the fulu tree.
150
+ * Requires the fulu view to be committed (done at the top of upgradeStateToGloas).
151
+ */
152
+ function migrateBasicListToGloas<ElementType extends BasicType<unknown>>(
153
+ fuluList: ListBasicTreeViewDU<ElementType>,
154
+ gloasType: ProgressiveListBasicType<ElementType>
155
+ ) {
156
+ const {length, type} = fuluList;
157
+ const chunkCount = Math.ceil(length / type.itemsPerChunk);
158
+ // List root = BranchNode(chunksNode, lengthNode) → chunks tree is the left child
159
+ const chunkLeafNodes = getNodesAtDepth(fuluList.node.left, type.chunkDepth, 0, chunkCount);
160
+ return gloasType.getViewDU(progressiveListRootNode(chunkLeafNodes, length));
161
+ }
162
+
88
163
  /**
89
164
  * Applies any pending deposits for builders to onboard builders during the fork transition
90
165
  * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.8/specs/gloas/fork.md#new-onboard_builders_from_pending_deposits
@@ -94,7 +169,7 @@ function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void
94
169
  // at the fork, so every builder pubkey here is one added in an earlier iteration.
95
170
  const builderPubkeys = new Set<string>();
96
171
 
97
- const pendingDeposits = ssz.electra.PendingDeposits.defaultViewDU();
172
+ const pendingDeposits = ssz.gloas.PendingDeposits.defaultViewDU();
98
173
  const pendingDepositsLookup = PendingDepositsLookup.buildEmpty();
99
174
 
100
175
  for (let i = 0; i < state.pendingDeposits.length; i++) {
@@ -515,14 +515,6 @@ export class BeaconStateView implements IBeaconStateViewLatestFork {
515
515
  return this.cachedState.epochCtx.getBeaconProposer(slot);
516
516
  }
517
517
 
518
- getBeaconProposerOrNull(slot: Slot): ValidatorIndex | null {
519
- try {
520
- return this.cachedState.epochCtx.getBeaconProposer(slot);
521
- } catch {
522
- return null;
523
- }
524
- }
525
-
526
518
  computeAnchorCheckpoint(): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} {
527
519
  return computeAnchorCheckpoint(this.config, this.cachedState);
528
520
  }
@@ -92,7 +92,6 @@ export interface IBeaconStateView {
92
92
  currentProposers: ValidatorIndex[];
93
93
  nextProposers: ValidatorIndex[];
94
94
  getBeaconProposer(slot: Slot): ValidatorIndex;
95
- getBeaconProposerOrNull(slot: Slot): ValidatorIndex | null;
96
95
 
97
96
  // Validators and balances
98
97
  effectiveBalanceIncrements: EffectiveBalanceIncrements;