@lodestar/state-transition 1.43.0-dev.bc569affb9 → 1.43.0-dev.c98da75ec7

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 (115) 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/processDepositRequest.d.ts +3 -11
  5. package/lib/block/processDepositRequest.d.ts.map +1 -1
  6. package/lib/block/processDepositRequest.js +27 -35
  7. package/lib/block/processDepositRequest.js.map +1 -1
  8. package/lib/block/processParentExecutionPayload.d.ts +2 -2
  9. package/lib/block/processParentExecutionPayload.d.ts.map +1 -1
  10. package/lib/block/processParentExecutionPayload.js +7 -6
  11. package/lib/block/processParentExecutionPayload.js.map +1 -1
  12. package/lib/block/processWithdrawals.d.ts.map +1 -1
  13. package/lib/block/processWithdrawals.js +4 -6
  14. package/lib/block/processWithdrawals.js.map +1 -1
  15. package/lib/cache/epochCache.js +3 -3
  16. package/lib/cache/epochCache.js.map +1 -1
  17. package/lib/epoch/processPendingDeposits.d.ts.map +1 -1
  18. package/lib/epoch/processPendingDeposits.js +4 -2
  19. package/lib/epoch/processPendingDeposits.js.map +1 -1
  20. package/lib/lightClient/spec/index.d.ts +22 -0
  21. package/lib/lightClient/spec/index.d.ts.map +1 -0
  22. package/lib/lightClient/spec/index.js +58 -0
  23. package/lib/lightClient/spec/index.js.map +1 -0
  24. package/lib/lightClient/spec/isBetterUpdate.d.ts +23 -0
  25. package/lib/lightClient/spec/isBetterUpdate.d.ts.map +1 -0
  26. package/lib/lightClient/spec/isBetterUpdate.js +66 -0
  27. package/lib/lightClient/spec/isBetterUpdate.js.map +1 -0
  28. package/lib/lightClient/spec/processLightClientUpdate.d.ts +12 -0
  29. package/lib/lightClient/spec/processLightClientUpdate.d.ts.map +1 -0
  30. package/lib/lightClient/spec/processLightClientUpdate.js +80 -0
  31. package/lib/lightClient/spec/processLightClientUpdate.js.map +1 -0
  32. package/lib/lightClient/spec/store.d.ts +45 -0
  33. package/lib/lightClient/spec/store.d.ts.map +1 -0
  34. package/lib/lightClient/spec/store.js +56 -0
  35. package/lib/lightClient/spec/store.js.map +1 -0
  36. package/lib/lightClient/spec/utils.d.ts +47 -0
  37. package/lib/lightClient/spec/utils.d.ts.map +1 -0
  38. package/lib/lightClient/spec/utils.js +197 -0
  39. package/lib/lightClient/spec/utils.js.map +1 -0
  40. package/lib/lightClient/spec/validateLightClientBootstrap.d.ts +4 -0
  41. package/lib/lightClient/spec/validateLightClientBootstrap.d.ts.map +1 -0
  42. package/lib/lightClient/spec/validateLightClientBootstrap.js +22 -0
  43. package/lib/lightClient/spec/validateLightClientBootstrap.js.map +1 -0
  44. package/lib/lightClient/spec/validateLightClientUpdate.d.ts +5 -0
  45. package/lib/lightClient/spec/validateLightClientUpdate.d.ts.map +1 -0
  46. package/lib/lightClient/spec/validateLightClientUpdate.js +88 -0
  47. package/lib/lightClient/spec/validateLightClientUpdate.js.map +1 -0
  48. package/lib/signatureSets/index.d.ts +1 -0
  49. package/lib/signatureSets/index.d.ts.map +1 -1
  50. package/lib/signatureSets/index.js +1 -0
  51. package/lib/signatureSets/index.js.map +1 -1
  52. package/lib/signatureSets/proposerPreferences.d.ts +4 -0
  53. package/lib/signatureSets/proposerPreferences.d.ts.map +1 -0
  54. package/lib/signatureSets/proposerPreferences.js +8 -0
  55. package/lib/signatureSets/proposerPreferences.js.map +1 -0
  56. package/lib/slot/upgradeStateToElectra.d.ts.map +1 -1
  57. package/lib/slot/upgradeStateToElectra.js +2 -2
  58. package/lib/slot/upgradeStateToElectra.js.map +1 -1
  59. package/lib/slot/upgradeStateToGloas.d.ts.map +1 -1
  60. package/lib/slot/upgradeStateToGloas.js +33 -28
  61. package/lib/slot/upgradeStateToGloas.js.map +1 -1
  62. package/lib/stateView/beaconStateView.d.ts +14 -5
  63. package/lib/stateView/beaconStateView.d.ts.map +1 -1
  64. package/lib/stateView/beaconStateView.js +40 -11
  65. package/lib/stateView/beaconStateView.js.map +1 -1
  66. package/lib/stateView/interface.d.ts +7 -5
  67. package/lib/stateView/interface.d.ts.map +1 -1
  68. package/lib/stateView/interface.js.map +1 -1
  69. package/lib/util/epoch.d.ts.map +1 -1
  70. package/lib/util/epoch.js +6 -4
  71. package/lib/util/epoch.js.map +1 -1
  72. package/lib/util/gloas.d.ts +0 -1
  73. package/lib/util/gloas.d.ts.map +1 -1
  74. package/lib/util/gloas.js +0 -4
  75. package/lib/util/gloas.js.map +1 -1
  76. package/lib/util/index.d.ts +1 -0
  77. package/lib/util/index.d.ts.map +1 -1
  78. package/lib/util/index.js +1 -0
  79. package/lib/util/index.js.map +1 -1
  80. package/lib/util/loadState/loadState.js +4 -4
  81. package/lib/util/loadState/loadState.js.map +1 -1
  82. package/lib/util/pendingDepositsLookup.d.ts +40 -0
  83. package/lib/util/pendingDepositsLookup.d.ts.map +1 -0
  84. package/lib/util/pendingDepositsLookup.js +84 -0
  85. package/lib/util/pendingDepositsLookup.js.map +1 -0
  86. package/lib/util/validator.d.ts +14 -2
  87. package/lib/util/validator.d.ts.map +1 -1
  88. package/lib/util/validator.js +24 -2
  89. package/lib/util/validator.js.map +1 -1
  90. package/package.json +13 -8
  91. package/src/block/processConsolidationRequest.ts +2 -1
  92. package/src/block/processDepositRequest.ts +29 -47
  93. package/src/block/processParentExecutionPayload.ts +7 -6
  94. package/src/block/processWithdrawals.ts +6 -6
  95. package/src/cache/epochCache.ts +3 -3
  96. package/src/epoch/processPendingDeposits.ts +5 -2
  97. package/src/lightClient/spec/index.ts +101 -0
  98. package/src/lightClient/spec/isBetterUpdate.ts +94 -0
  99. package/src/lightClient/spec/processLightClientUpdate.ts +119 -0
  100. package/src/lightClient/spec/store.ts +106 -0
  101. package/src/lightClient/spec/utils.ts +317 -0
  102. package/src/lightClient/spec/validateLightClientBootstrap.ts +39 -0
  103. package/src/lightClient/spec/validateLightClientUpdate.ts +145 -0
  104. package/src/signatureSets/index.ts +1 -0
  105. package/src/signatureSets/proposerPreferences.ts +12 -0
  106. package/src/slot/upgradeStateToElectra.ts +4 -2
  107. package/src/slot/upgradeStateToGloas.ts +41 -44
  108. package/src/stateView/beaconStateView.ts +43 -12
  109. package/src/stateView/interface.ts +7 -5
  110. package/src/util/epoch.ts +13 -4
  111. package/src/util/gloas.ts +0 -5
  112. package/src/util/index.ts +1 -0
  113. package/src/util/loadState/loadState.ts +4 -4
  114. package/src/util/pendingDepositsLookup.ts +105 -0
  115. package/src/util/validator.ts +42 -2
@@ -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
+ }
@@ -20,6 +20,7 @@ export * from "./executionPayloadEnvelope.js";
20
20
  export * from "./indexedAttestation.js";
21
21
  export * from "./indexedPayloadAttestation.js";
22
22
  export * from "./proposer.js";
23
+ export * from "./proposerPreferences.js";
23
24
  export * from "./proposerSlashings.js";
24
25
  export * from "./randao.js";
25
26
  export * from "./voluntaryExits.js";
@@ -0,0 +1,12 @@
1
+ import {BeaconConfig} from "@lodestar/config";
2
+ import {DOMAIN_PROPOSER_PREFERENCES} from "@lodestar/params";
3
+ import {gloas, ssz} from "@lodestar/types";
4
+ import {computeSigningRoot} from "../util/index.js";
5
+
6
+ export function getProposerPreferencesSigningRoot(
7
+ config: BeaconConfig,
8
+ preferences: gloas.ProposerPreferences
9
+ ): Uint8Array {
10
+ const domain = config.getDomain(preferences.proposalSlot, DOMAIN_PROPOSER_PREFERENCES);
11
+ return computeSigningRoot(ssz.gloas.ProposerPreferences, preferences, domain);
12
+ }
@@ -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;
@@ -1,12 +1,12 @@
1
1
  import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
2
2
  import {ssz} from "@lodestar/types";
3
- import {toHex} from "@lodestar/utils";
4
- import {isValidDepositSignature} from "../block/processDeposit.js";
3
+ import {toPubkeyHex} from "@lodestar/utils";
5
4
  import {applyDepositForBuilder} from "../block/processDepositRequest.js";
6
5
  import {getCachedBeaconState} from "../cache/stateCache.js";
7
6
  import {CachedBeaconStateFulu, CachedBeaconStateGloas} from "../types.js";
8
7
  import {initializePtcWindow, isBuilderWithdrawalCredential} from "../util/gloas.js";
9
8
  import {isValidatorKnown} from "../util/index.js";
9
+ import {PendingDepositsLookup} from "../util/pendingDepositsLookup.js";
10
10
 
11
11
  /**
12
12
  * Upgrade a state from Fulu to Gloas.
@@ -89,63 +89,60 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea
89
89
  * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.2/specs/gloas/fork.md#new-onboard_builders_from_pending_deposits
90
90
  */
91
91
  function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void {
92
- // Track pubkeys of new validators to keep their deposits pending
93
- const validatorPubkeys = new Set<string>();
94
-
95
92
  // Track pubkeys of new builders added when applying deposits
96
93
  const builderPubkeys = new Set<string>();
97
94
 
98
- const remainingPendingDeposits = ssz.electra.PendingDeposits.defaultViewDU();
95
+ const pendingDeposits = ssz.electra.PendingDeposits.defaultViewDU();
96
+ const pendingDepositsLookup = PendingDepositsLookup.buildEmpty();
97
+
99
98
  for (let i = 0; i < state.pendingDeposits.length; i++) {
100
99
  const deposit = state.pendingDeposits.getReadonly(i);
101
100
 
102
101
  const validatorIndex = state.epochCtx.getValidatorIndex(deposit.pubkey);
103
- const pubkeyHex = toHex(deposit.pubkey);
102
+ const pubkeyHex = toPubkeyHex(deposit.pubkey);
104
103
 
105
- // Deposits for existing validators stay in pending queue
106
- if (isValidatorKnown(state, validatorIndex) || validatorPubkeys.has(pubkeyHex)) {
107
- remainingPendingDeposits.push(deposit);
104
+ // Deposits for existing validators stay in the pending queue
105
+ if (isValidatorKnown(state, validatorIndex)) {
106
+ pendingDeposits.push(deposit);
107
+ pendingDepositsLookup.add(deposit, pubkeyHex);
108
108
  continue;
109
109
  }
110
110
 
111
- // If the pubkey is associated with a builder that was created in a previous iteration
112
- // or it is a builder deposit, try to apply the deposit to the new/existing builder
113
- const isExistingBuilder = builderPubkeys.has(pubkeyHex);
114
- const hasBuilderCredentials = isBuilderWithdrawalCredential(deposit.withdrawalCredentials);
115
- if (isExistingBuilder || hasBuilderCredentials) {
116
- const buildersLenBefore = state.builders.length;
117
- applyDepositForBuilder(
118
- state,
119
- deposit.pubkey,
120
- deposit.withdrawalCredentials,
121
- deposit.amount,
122
- deposit.signature,
123
- deposit.slot
124
- );
125
- // Track newly added builders for subsequent iterations
126
- if (!isExistingBuilder && state.builders.length > buildersLenBefore) {
127
- builderPubkeys.add(pubkeyHex);
111
+ // `applyDepositForBuilder` can mutate the state and add a builder to the registry, so
112
+ // the set of builder pubkeys must be recomputed each iteration. `builderPubkeys` stands
113
+ // in for the spec's `[b.pubkey for b in state.builders]`: `state.builders` starts empty
114
+ // at the fork, so every builder is one added in a previous iteration of this loop.
115
+ if (!builderPubkeys.has(pubkeyHex)) {
116
+ // Deposits for non-builders stay in the pending queue. If there is a valid pending
117
+ // deposit for a new validator with this pubkey, keep this deposit in the pending
118
+ // queue to be applied to that validator later.
119
+ if (!isBuilderWithdrawalCredential(deposit.withdrawalCredentials)) {
120
+ pendingDeposits.push(deposit);
121
+ pendingDepositsLookup.add(deposit, pubkeyHex);
122
+ continue;
123
+ }
124
+ if (pendingDepositsLookup.hasPendingValidator(state.config, pubkeyHex)) {
125
+ pendingDeposits.push(deposit);
126
+ pendingDepositsLookup.add(deposit, pubkeyHex);
127
+ continue;
128
128
  }
129
- continue;
130
129
  }
131
130
 
132
- // If there is a pending deposit for a new validator that has a valid signature, track the
133
- // pubkey so that subsequent builder deposits for the same pubkey stay in pending (applied to
134
- // the validator later) rather than creating a builder. Deposits with invalid signatures are
135
- // dropped here since they would fail in apply_pending_deposit anyway.
136
- if (
137
- isValidDepositSignature(
138
- state.config,
139
- deposit.pubkey,
140
- deposit.withdrawalCredentials,
141
- deposit.amount,
142
- deposit.signature
143
- )
144
- ) {
145
- validatorPubkeys.add(pubkeyHex);
146
- remainingPendingDeposits.push(deposit);
131
+ const buildersLenBefore = state.builders.length;
132
+ // TODO GLOAS: handle 20k 1ETH deposits on time
133
+ // there is a note in the spec https://github.com/ethereum/consensus-specs/pull/5227
134
+ applyDepositForBuilder(
135
+ state,
136
+ deposit.pubkey,
137
+ deposit.withdrawalCredentials,
138
+ deposit.amount,
139
+ deposit.signature,
140
+ deposit.slot
141
+ );
142
+ if (state.builders.length > buildersLenBefore) {
143
+ builderPubkeys.add(pubkeyHex);
147
144
  }
148
145
  }
149
146
 
150
- state.pendingDeposits = remainingPendingDeposits;
147
+ state.pendingDeposits = pendingDeposits;
151
148
  }
@@ -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 =
package/src/util/gloas.ts CHANGED
@@ -172,11 +172,6 @@ export function isAttestationSameSlotRootCache(rootCache: RootCache, data: Attes
172
172
  return isMatchingBlockRoot && isCurrentBlockRoot;
173
173
  }
174
174
 
175
- // TODO GLOAS: This function no longer exists in v1.7.0-alpha.5 specs. Remove it when appropriate to do so
176
- export function isParentBlockFull(state: CachedBeaconStateGloas): boolean {
177
- return byteArrayEquals(state.latestExecutionPayloadBid.blockHash, state.latestBlockHash);
178
- }
179
-
180
175
  export function initializePtcWindow(state: CachedBeaconStateFulu): Uint32Array[] {
181
176
  const ptcWindow: Uint32Array[] = Array.from({length: SLOTS_PER_EPOCH}, () => new Uint32Array(PTC_SIZE));
182
177
  const currentEpoch = state.epochCtx.epoch;
package/src/util/index.ts CHANGED
@@ -18,6 +18,7 @@ export * from "./genesis.js";
18
18
  export * from "./gloas.js";
19
19
  export * from "./interop.js";
20
20
  export * from "./loadState/index.js";
21
+ export * from "./pendingDepositsLookup.js";
21
22
  export * from "./rootCache.js";
22
23
  export * from "./seed.js";
23
24
  export * from "./shuffling.js";
@@ -110,8 +110,8 @@ function loadInactivityScores(
110
110
  seedState: BeaconStateAltair,
111
111
  inactivityScoresBytes: Uint8Array
112
112
  ): void {
113
- // migratedState starts with the same inactivityScores to seed state
114
- migratedState.inactivityScores = seedState.inactivityScores.clone();
113
+ // true = do not transfer cache
114
+ migratedState.inactivityScores = seedState.inactivityScores.clone(true);
115
115
  const oldValidator = migratedState.inactivityScores.length;
116
116
  // UintNum64 = 8 bytes
117
117
  const newValidator = inactivityScoresBytes.length / 8;
@@ -187,8 +187,8 @@ function loadValidators(
187
187
  const newValidatorCount = Math.floor(newValidatorsBytes.length / VALIDATOR_BYTES_SIZE);
188
188
  const isMoreValidator = newValidatorCount >= seedValidatorCount;
189
189
  const minValidatorCount = Math.min(seedValidatorCount, newValidatorCount);
190
- // migrated state starts with the same validators to seed state
191
- migratedState.validators = seedState.validators.clone();
190
+ // true = do not transfer cache
191
+ migratedState.validators = seedState.validators.clone(true);
192
192
  // 80% of validators serialization time comes from memory allocation
193
193
  // seedStateValidatorsBytes is an optimization at beacon-node side to avoid memory allocation here
194
194
  const seedValidatorsBytes = seedStateValidatorsBytes ?? seedState.validators.serialize();
@@ -0,0 +1,105 @@
1
+ import {BeaconConfig} from "@lodestar/config";
2
+ import {PubkeyHex, electra} from "@lodestar/types";
3
+ import {toPubkeyHex} from "@lodestar/utils";
4
+ import {isValidDepositSignature} from "../block/processDeposit.js";
5
+ import {CachedBeaconStateGloas} from "../types.js";
6
+
7
+ type PendingDepositsValidation = {
8
+ hasValidSignature: boolean;
9
+ validatedCount: number;
10
+ };
11
+
12
+ /**
13
+ * Mutable lookup for the pending-deposit sequence used by builder-routing logic.
14
+ *
15
+ * This is to implement the spec's `is_pending_validator(pending_deposits, pubkey)` lazily:
16
+ * deposits are grouped by pubkey without verifying signatures, and BLS verification is
17
+ * deferred until a builder deposit needs to know whether the same pubkey already has a
18
+ * valid pending validator deposit.
19
+ *
20
+ * Call `add()` whenever a deposit is appended to the represented sequence. A cached `true`
21
+ * result short-circuits all subsequent checks for that pubkey; a cached `false` records
22
+ * how many deposits were already verified, so appending a new deposit only verifies the
23
+ * newly-appended tail rather than re-running BLS on previously-invalid entries.
24
+ */
25
+ export class PendingDepositsLookup {
26
+ private constructor(
27
+ private readonly depositsByPubkey: Map<PubkeyHex, electra.PendingDeposit[]>,
28
+ private readonly validationCache: Map<PubkeyHex, PendingDepositsValidation>
29
+ ) {}
30
+
31
+ /** Build an empty lookup for a sequence that will be populated incrementally. */
32
+ static buildEmpty(): PendingDepositsLookup {
33
+ return new PendingDepositsLookup(new Map(), new Map());
34
+ }
35
+
36
+ /**
37
+ * Build a pubkey -> pending-deposits lookup from `state.pendingDeposits`.
38
+ * No BLS work is done here; signature verification happens lazily in `hasPendingValidator`.
39
+ */
40
+ static build(state: CachedBeaconStateGloas): PendingDepositsLookup {
41
+ const lookup = PendingDepositsLookup.buildEmpty();
42
+ for (const pendingDeposit of state.pendingDeposits.getAllReadonly()) {
43
+ lookup.add(pendingDeposit);
44
+ }
45
+ return lookup;
46
+ }
47
+
48
+ /**
49
+ * Append a pending deposit to the represented sequence.
50
+ * Pass `pubkeyHex` if the caller has already computed it.
51
+ */
52
+ add(pendingDeposit: electra.PendingDeposit, pubkeyHex?: PubkeyHex): void {
53
+ const key = pubkeyHex ?? toPubkeyHex(pendingDeposit.pubkey);
54
+ const existing = this.depositsByPubkey.get(key);
55
+ if (existing) {
56
+ existing.push(pendingDeposit);
57
+ } else {
58
+ this.depositsByPubkey.set(key, [pendingDeposit]);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Returns true if any pending deposit for `pubkeyHex` has a valid BLS deposit signature.
64
+ * Memoizes the result in `validationCache` so repeated checks for the same pubkey
65
+ * within a block only verify deposits that have not already been checked.
66
+ */
67
+ hasPendingValidator(config: BeaconConfig, pubkeyHex: PubkeyHex): boolean {
68
+ const validation = this.validationCache.get(pubkeyHex);
69
+ if (validation?.hasValidSignature === true) {
70
+ return true;
71
+ }
72
+
73
+ const deposits = this.depositsByPubkey.get(pubkeyHex);
74
+ if (deposits === undefined) {
75
+ return false;
76
+ }
77
+
78
+ // hasValidSignature is false or undefined; resume from the last validatedCount so
79
+ // previously-checked invalid deposits are not re-verified.
80
+ const startIndex = validation?.validatedCount ?? 0;
81
+ if (startIndex === deposits.length) {
82
+ // Nothing new to check; the cached false result still holds.
83
+ return false;
84
+ }
85
+
86
+ for (let i = startIndex; i < deposits.length; i++) {
87
+ const deposit = deposits[i];
88
+ if (
89
+ isValidDepositSignature(
90
+ config,
91
+ deposit.pubkey,
92
+ deposit.withdrawalCredentials,
93
+ deposit.amount,
94
+ deposit.signature
95
+ )
96
+ ) {
97
+ this.validationCache.set(pubkeyHex, {hasValidSignature: true, validatedCount: i + 1});
98
+ return true;
99
+ }
100
+ }
101
+
102
+ this.validationCache.set(pubkeyHex, {hasValidSignature: false, validatedCount: deposits.length});
103
+ return false;
104
+ }
105
+ }
@@ -44,7 +44,12 @@ export function getActiveValidatorIndices(state: BeaconStateAllForks, epoch: Epo
44
44
  return new Uint32Array(indices);
45
45
  }
46
46
 
47
- export function getActivationChurnLimit(config: ChainForkConfig, fork: ForkSeq, activeValidatorCount: number): number {
47
+ // Deneb fork upgrade only
48
+ export function getValidatorActivationChurnLimit(
49
+ config: ChainForkConfig,
50
+ fork: ForkSeq,
51
+ activeValidatorCount: number
52
+ ): number {
48
53
  if (fork >= ForkSeq.deneb) {
49
54
  return Math.min(config.MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT, getChurnLimit(config, activeValidatorCount));
50
55
  }
@@ -84,7 +89,42 @@ export function getActivationExitChurnLimit(epochCtx: EpochCache): number {
84
89
  return Math.min(epochCtx.config.MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT, getBalanceChurnLimitFromCache(epochCtx));
85
90
  }
86
91
 
87
- export function getConsolidationChurnLimit(epochCtx: EpochCache): number {
92
+ /**
93
+ * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/beacon-chain.md#new-get_activation_churn_limit
94
+ */
95
+ export function getActivationChurnLimit(epochCtx: EpochCache): number {
96
+ const churn = getBalanceChurnLimit(
97
+ epochCtx.totalActiveBalanceIncrements,
98
+ epochCtx.config.CHURN_LIMIT_QUOTIENT_GLOAS,
99
+ epochCtx.config.MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA
100
+ );
101
+ return Math.min(epochCtx.config.MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS, churn);
102
+ }
103
+
104
+ /**
105
+ * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/beacon-chain.md#new-get_exit_churn_limit
106
+ */
107
+ export function getExitChurnLimit(epochCtx: EpochCache): number {
108
+ return getBalanceChurnLimit(
109
+ epochCtx.totalActiveBalanceIncrements,
110
+ epochCtx.config.CHURN_LIMIT_QUOTIENT_GLOAS,
111
+ epochCtx.config.MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA
112
+ );
113
+ }
114
+
115
+ /**
116
+ * Spec (electra): get_consolidation_churn_limit (uses combined balance churn minus activation+exit churn)
117
+ * Spec (gloas): get_consolidation_churn_limit (independent quotient, no MIN floor)
118
+ */
119
+ export function getConsolidationChurnLimit(fork: ForkSeq, epochCtx: EpochCache): number {
120
+ if (fork >= ForkSeq.gloas) {
121
+ // No MIN floor — pass 0 so getBalanceChurnLimit's max(churn, min) is a no-op.
122
+ return getBalanceChurnLimit(
123
+ epochCtx.totalActiveBalanceIncrements,
124
+ epochCtx.config.CONSOLIDATION_CHURN_LIMIT_QUOTIENT,
125
+ 0
126
+ );
127
+ }
88
128
  return getBalanceChurnLimitFromCache(epochCtx) - getActivationExitChurnLimit(epochCtx);
89
129
  }
90
130