@lodestar/state-transition 1.45.0-rc.0 → 1.46.0-dev.1cd9a671ea

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 (91) 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/util/execution.d.ts.map +1 -1
  55. package/lib/util/execution.js +7 -2
  56. package/lib/util/execution.js.map +1 -1
  57. package/lib/util/gloas.d.ts +3 -1
  58. package/lib/util/gloas.d.ts.map +1 -1
  59. package/lib/util/gloas.js +8 -11
  60. package/lib/util/gloas.js.map +1 -1
  61. package/lib/util/index.d.ts +1 -0
  62. package/lib/util/index.d.ts.map +1 -1
  63. package/lib/util/index.js +1 -0
  64. package/lib/util/index.js.map +1 -1
  65. package/lib/util/ssz.d.ts +9 -0
  66. package/lib/util/ssz.d.ts.map +1 -0
  67. package/lib/util/ssz.js +39 -0
  68. package/lib/util/ssz.js.map +1 -0
  69. package/package.json +8 -8
  70. package/src/block/processAttestationsAltair.ts +9 -6
  71. package/src/block/processBuilderDepositRequest.ts +21 -8
  72. package/src/block/processOperations.ts +28 -1
  73. package/src/block/processParentExecutionPayload.ts +26 -1
  74. package/src/block/processWithdrawals.ts +1 -1
  75. package/src/cache/stateCache.ts +2 -4
  76. package/src/epoch/processParticipationFlagUpdates.ts +20 -2
  77. package/src/epoch/processRewardsAndPenalties.ts +1 -2
  78. package/src/lightClient/proofs.ts +36 -1
  79. package/src/lightClient/spec/index.ts +8 -4
  80. package/src/lightClient/spec/processLightClientUpdate.ts +13 -1
  81. package/src/lightClient/spec/utils.ts +200 -30
  82. package/src/lightClient/spec/validateLightClientBootstrap.ts +3 -10
  83. package/src/lightClient/spec/validateLightClientUpdate.ts +11 -21
  84. package/src/lightClient/types.ts +3 -1
  85. package/src/metrics.ts +2 -4
  86. package/src/slot/upgradeStateToAltair.ts +1 -1
  87. package/src/slot/upgradeStateToGloas.ts +84 -9
  88. package/src/util/execution.ts +9 -4
  89. package/src/util/gloas.ts +9 -14
  90. package/src/util/index.ts +1 -0
  91. package/src/util/ssz.ts +42 -0
@@ -1,4 +1,12 @@
1
- import {ForkPostGloas, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
1
+ import {
2
+ ForkPostGloas,
3
+ MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD,
4
+ MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD,
5
+ MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD,
6
+ MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD,
7
+ SLOTS_PER_EPOCH,
8
+ SLOTS_PER_HISTORICAL_ROOT,
9
+ } from "@lodestar/params";
2
10
  import {BeaconBlock, gloas, ssz} from "@lodestar/types";
3
11
  import {byteArrayEquals, toRootHex} from "@lodestar/utils";
4
12
  import {CachedBeaconStateGloas} from "../types.js";
@@ -47,6 +55,8 @@ export function processParentExecutionPayload(state: CachedBeaconStateGloas, blo
47
55
  * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/beacon-chain.md#new-apply_parent_execution_payload
48
56
  */
49
57
  export function applyParentExecutionPayload(state: CachedBeaconStateGloas, requests: gloas.ExecutionRequests): void {
58
+ assertExecutionRequestsWithinLimits(requests);
59
+
50
60
  const fork = state.config.getForkSeq(state.slot);
51
61
  const parentBid = state.latestExecutionPayloadBid;
52
62
  const parentSlot = parentBid.slot;
@@ -116,6 +126,21 @@ function settleBuilderPayment(state: CachedBeaconStateGloas, paymentIndex: numbe
116
126
  state.builderPendingPayments.set(paymentIndex, ssz.gloas.BuilderPendingPayment.defaultViewDU());
117
127
  }
118
128
 
129
+ function assertExecutionRequestsWithinLimits(requests: gloas.ExecutionRequests): void {
130
+ assertMaxLength("withdrawals", requests.withdrawals.length, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD);
131
+ assertMaxLength("consolidations", requests.consolidations.length, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD);
132
+ // New in GLOAS:EIP8282
133
+ assertMaxLength("builderDeposits", requests.builderDeposits.length, MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD);
134
+ // New in GLOAS:EIP8282
135
+ assertMaxLength("builderExits", requests.builderExits.length, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD);
136
+ }
137
+
138
+ function assertMaxLength(name: string, length: number, limit: number): void {
139
+ if (length > limit) {
140
+ throw new Error(`Too many parent execution request ${name} count=${length} limit=${limit}`);
141
+ }
142
+ }
143
+
119
144
  function assertEmptyExecutionRequests(requests: gloas.ExecutionRequests): void {
120
145
  if (
121
146
  requests.deposits.length !== 0 ||
@@ -99,7 +99,7 @@ export function processWithdrawals(
99
99
  const stateGloas = state as CachedBeaconStateGloas;
100
100
 
101
101
  // Store expected withdrawals for verification
102
- stateGloas.payloadExpectedWithdrawals = ssz.capella.Withdrawals.toViewDU(expectedWithdrawals);
102
+ stateGloas.payloadExpectedWithdrawals = ssz.gloas.Withdrawals.toViewDU(expectedWithdrawals);
103
103
 
104
104
  // Update builder pending withdrawals queue
105
105
  stateGloas.builderPendingWithdrawals = stateGloas.builderPendingWithdrawals.sliceFrom(
@@ -254,11 +254,9 @@ export function isCachedBeaconState<T extends BeaconStateAllForks>(
254
254
  // This cache is populated during epoch transition, and should be preserved for performance.
255
255
  // If the cache is missing too often, means that our clone strategy is not working well.
256
256
  export function isStateValidatorsNodesPopulated(state: CachedBeaconStateAllForks): boolean {
257
- // biome-ignore lint/complexity/useLiteralKeys: It is a private attribute
258
- return state.validators["nodesPopulated"] === true;
257
+ return (state.validators as unknown as {nodesPopulated?: boolean}).nodesPopulated === true;
259
258
  }
260
259
 
261
260
  export function isStateBalancesNodesPopulated(state: CachedBeaconStateAllForks): boolean {
262
- // biome-ignore lint/complexity/useLiteralKeys: It is a private attribute
263
- return state.balances["nodesPopulated"] === true;
261
+ return (state.balances as unknown as {nodesPopulated?: boolean}).nodesPopulated === true;
264
262
  }
@@ -1,6 +1,8 @@
1
1
  import {zeroNode} from "@chainsafe/persistent-merkle-tree";
2
2
  import {ssz} from "@lodestar/types";
3
- import {CachedBeaconStateAltair} from "../types.js";
3
+ import type {BeaconStateAltair, BeaconStateGloas} from "../types.js";
4
+ import {isGloasStateType} from "../util/execution.js";
5
+ import {zeroProgressiveListBasicRootNode} from "../util/ssz.js";
4
6
 
5
7
  /**
6
8
  * Updates `state.previousEpochParticipation` with precalculated epoch participation. Creates a new empty tree for
@@ -9,7 +11,12 @@ import {CachedBeaconStateAltair} from "../types.js";
9
11
  * PERF: Cost = 'proportional' $VALIDATOR_COUNT. Since it updates all of them at once, it will always recreate both
10
12
  * trees completely.
11
13
  */
12
- export function processParticipationFlagUpdates(state: CachedBeaconStateAltair): void {
14
+ export function processParticipationFlagUpdates(state: BeaconStateAltair | BeaconStateGloas): void {
15
+ if (isGloasStateType(state)) {
16
+ processParticipationFlagUpdatesGloas(state);
17
+ return;
18
+ }
19
+
13
20
  // Set view and tree from currentEpochParticipation to previousEpochParticipation
14
21
  state.previousEpochParticipation = state.currentEpochParticipation;
15
22
 
@@ -25,3 +32,14 @@ export function processParticipationFlagUpdates(state: CachedBeaconStateAltair):
25
32
 
26
33
  state.currentEpochParticipation = ssz.altair.EpochParticipation.getViewDU(currentEpochParticipationNode);
27
34
  }
35
+
36
+ function processParticipationFlagUpdatesGloas(state: BeaconStateGloas): void {
37
+ state.previousEpochParticipation = state.currentEpochParticipation;
38
+
39
+ // Same trick as the altair path above, adapted to the progressive-list tree shape: all chunks
40
+ // are zero so the chunks tree is a chain of pre-computed zeroNodes, built in O(log n) instead
41
+ // of re-merkleizing a validator-count-sized array every epoch.
42
+ state.currentEpochParticipation = ssz.gloas.EpochParticipation.getViewDU(
43
+ zeroProgressiveListBasicRootNode(ssz.gloas.EpochParticipation.itemsPerChunk, state.currentEpochParticipation.length)
44
+ );
45
+ }
@@ -1,5 +1,4 @@
1
1
  import {ForkSeq, GENESIS_EPOCH} from "@lodestar/params";
2
- import {ssz} from "@lodestar/types";
3
2
  import {
4
3
  CachedBeaconStateAllForks,
5
4
  CachedBeaconStateAltair,
@@ -39,7 +38,7 @@ export function processRewardsAndPenalties(
39
38
 
40
39
  // important: do not change state one balance at a time. Set them all at once, constructing the tree in one go
41
40
  // cache the balances array, too
42
- state.balances = ssz.phase0.Balances.toViewDU(balances);
41
+ state.balances = state.type.fields.balances.toViewDU(balances);
43
42
 
44
43
  // For processEffectiveBalanceUpdates() to prevent having to re-compute the balances array.
45
44
  // For validator metrics
@@ -1,11 +1,15 @@
1
1
  import {Tree} from "@chainsafe/persistent-merkle-tree";
2
2
  import {
3
3
  BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX,
4
+ CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS,
4
5
  FINALIZED_ROOT_GINDEX,
5
6
  FINALIZED_ROOT_GINDEX_ELECTRA,
7
+ FINALIZED_ROOT_GINDEX_GLOAS,
6
8
  ForkName,
7
9
  ForkPostBellatrix,
10
+ NEXT_SYNC_COMMITTEE_GINDEX_GLOAS,
8
11
  isForkPostElectra,
12
+ isForkPostGloas,
9
13
  } from "@lodestar/params";
10
14
  import {BeaconBlockBody, SSZTypesFor, ssz} from "@lodestar/types";
11
15
  import {BeaconStateAllForks, CachedBeaconStateAllForks} from "../types.js";
@@ -17,6 +21,24 @@ export function getSyncCommitteesWitness(fork: ForkName, state: BeaconStateAllFo
17
21
  let currentSyncCommitteeRoot: Uint8Array;
18
22
  let nextSyncCommitteeRoot: Uint8Array;
19
23
 
24
+ if (isForkPostGloas(fork)) {
25
+ const tree = new Tree(state.node);
26
+ const currentSyncCommitteeGindex = BigInt(CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS);
27
+ const nextSyncCommitteeGindex = BigInt(NEXT_SYNC_COMMITTEE_GINDEX_GLOAS);
28
+
29
+ currentSyncCommitteeRoot = tree.getRoot(currentSyncCommitteeGindex);
30
+ nextSyncCommitteeRoot = tree.getRoot(nextSyncCommitteeGindex);
31
+ witness = [];
32
+
33
+ return {
34
+ witness,
35
+ currentSyncCommitteeRoot,
36
+ nextSyncCommitteeRoot,
37
+ currentSyncCommitteeBranch: tree.getSingleProof(currentSyncCommitteeGindex),
38
+ nextSyncCommitteeBranch: tree.getSingleProof(nextSyncCommitteeGindex),
39
+ };
40
+ }
41
+
20
42
  if (isForkPostElectra(fork)) {
21
43
  const n2 = n1.left;
22
44
  const n5 = n2.right;
@@ -60,17 +82,30 @@ export function getSyncCommitteesWitness(fork: ForkName, state: BeaconStateAllFo
60
82
  }
61
83
 
62
84
  export function getNextSyncCommitteeBranch(syncCommitteesWitness: SyncCommitteeWitness): Uint8Array[] {
85
+ if (syncCommitteesWitness.nextSyncCommitteeBranch) {
86
+ return syncCommitteesWitness.nextSyncCommitteeBranch;
87
+ }
88
+
63
89
  // Witness branch is sorted by descending gindex
64
90
  return [syncCommitteesWitness.currentSyncCommitteeRoot, ...syncCommitteesWitness.witness];
65
91
  }
66
92
 
67
93
  export function getCurrentSyncCommitteeBranch(syncCommitteesWitness: SyncCommitteeWitness): Uint8Array[] {
94
+ if (syncCommitteesWitness.currentSyncCommitteeBranch) {
95
+ return syncCommitteesWitness.currentSyncCommitteeBranch;
96
+ }
97
+
68
98
  // Witness branch is sorted by descending gindex
69
99
  return [syncCommitteesWitness.nextSyncCommitteeRoot, ...syncCommitteesWitness.witness];
70
100
  }
71
101
 
72
102
  export function getFinalizedRootProof(state: CachedBeaconStateAllForks): Uint8Array[] {
73
- const finalizedRootGindex = state.epochCtx.isPostElectra() ? FINALIZED_ROOT_GINDEX_ELECTRA : FINALIZED_ROOT_GINDEX;
103
+ const fork = state.config.getForkName(state.slot);
104
+ const finalizedRootGindex = isForkPostGloas(fork)
105
+ ? FINALIZED_ROOT_GINDEX_GLOAS
106
+ : state.epochCtx.isPostElectra()
107
+ ? FINALIZED_ROOT_GINDEX_ELECTRA
108
+ : FINALIZED_ROOT_GINDEX;
74
109
  return new Tree(state.node).getSingleProof(BigInt(finalizedRootGindex));
75
110
  }
76
111
 
@@ -14,7 +14,7 @@ import {
14
14
  processLightClientUpdate,
15
15
  } from "./processLightClientUpdate.js";
16
16
  import {type ILightClientStore, LightClientStore, type LightClientStoreEvents} from "./store.js";
17
- import {ZERO_HEADER, ZERO_SYNC_COMMITTEE, getZeroFinalityBranch, getZeroSyncCommitteeBranch} from "./utils.js";
17
+ import {ZERO_SYNC_COMMITTEE, getZeroFinalityBranch, getZeroSyncCommitteeBranch} from "./utils.js";
18
18
 
19
19
  export type {LightClientUpdateSummary} from "./isBetterUpdate.js";
20
20
  export {isBetterUpdate, toLightClientUpdateSummary} from "./isBetterUpdate.js";
@@ -76,12 +76,16 @@ export class LightclientSpec {
76
76
  }
77
77
 
78
78
  onOptimisticUpdate(currentSlot: Slot, optimisticUpdate: LightClientOptimisticUpdate): void {
79
+ const fork = this.config.getForkName(optimisticUpdate.signatureSlot);
80
+
79
81
  this.onUpdate(currentSlot, {
80
82
  attestedHeader: optimisticUpdate.attestedHeader,
81
83
  nextSyncCommittee: ZERO_SYNC_COMMITTEE,
82
- nextSyncCommitteeBranch: getZeroSyncCommitteeBranch(this.config.getForkName(optimisticUpdate.signatureSlot)),
83
- finalizedHeader: {beacon: ZERO_HEADER},
84
- finalityBranch: getZeroFinalityBranch(this.config.getForkName(optimisticUpdate.signatureSlot)),
84
+ nextSyncCommitteeBranch: getZeroSyncCommitteeBranch(fork),
85
+ finalizedHeader: this.config
86
+ .getPostAltairForkTypes(optimisticUpdate.signatureSlot)
87
+ .LightClientHeader.defaultValue(),
88
+ finalityBranch: getZeroFinalityBranch(fork),
85
89
  syncAggregate: optimisticUpdate.syncAggregate,
86
90
  signatureSlot: optimisticUpdate.signatureSlot,
87
91
  });
@@ -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
  ) {