@lodestar/state-transition 1.43.0-dev.4fb05c546d → 1.43.0-dev.4fe6b362c9

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 (138) hide show
  1. package/lib/block/index.d.ts +2 -2
  2. package/lib/block/index.d.ts.map +1 -1
  3. package/lib/block/index.js +11 -4
  4. package/lib/block/index.js.map +1 -1
  5. package/lib/block/processConsolidationRequest.d.ts.map +1 -1
  6. package/lib/block/processConsolidationRequest.js +2 -1
  7. package/lib/block/processConsolidationRequest.js.map +1 -1
  8. package/lib/block/processDepositRequest.d.ts +3 -11
  9. package/lib/block/processDepositRequest.d.ts.map +1 -1
  10. package/lib/block/processDepositRequest.js +27 -35
  11. package/lib/block/processDepositRequest.js.map +1 -1
  12. package/lib/block/processParentExecutionPayload.d.ts +20 -0
  13. package/lib/block/processParentExecutionPayload.d.ts.map +1 -0
  14. package/lib/block/processParentExecutionPayload.js +101 -0
  15. package/lib/block/processParentExecutionPayload.js.map +1 -0
  16. package/lib/block/processWithdrawals.d.ts.map +1 -1
  17. package/lib/block/processWithdrawals.js +10 -4
  18. package/lib/block/processWithdrawals.js.map +1 -1
  19. package/lib/cache/epochCache.js +3 -3
  20. package/lib/cache/epochCache.js.map +1 -1
  21. package/lib/epoch/processPendingDeposits.d.ts.map +1 -1
  22. package/lib/epoch/processPendingDeposits.js +4 -2
  23. package/lib/epoch/processPendingDeposits.js.map +1 -1
  24. package/lib/lightClient/spec/index.d.ts +22 -0
  25. package/lib/lightClient/spec/index.d.ts.map +1 -0
  26. package/lib/lightClient/spec/index.js +58 -0
  27. package/lib/lightClient/spec/index.js.map +1 -0
  28. package/lib/lightClient/spec/isBetterUpdate.d.ts +23 -0
  29. package/lib/lightClient/spec/isBetterUpdate.d.ts.map +1 -0
  30. package/lib/lightClient/spec/isBetterUpdate.js +66 -0
  31. package/lib/lightClient/spec/isBetterUpdate.js.map +1 -0
  32. package/lib/lightClient/spec/processLightClientUpdate.d.ts +12 -0
  33. package/lib/lightClient/spec/processLightClientUpdate.d.ts.map +1 -0
  34. package/lib/lightClient/spec/processLightClientUpdate.js +80 -0
  35. package/lib/lightClient/spec/processLightClientUpdate.js.map +1 -0
  36. package/lib/lightClient/spec/store.d.ts +45 -0
  37. package/lib/lightClient/spec/store.d.ts.map +1 -0
  38. package/lib/lightClient/spec/store.js +56 -0
  39. package/lib/lightClient/spec/store.js.map +1 -0
  40. package/lib/lightClient/spec/utils.d.ts +47 -0
  41. package/lib/lightClient/spec/utils.d.ts.map +1 -0
  42. package/lib/lightClient/spec/utils.js +197 -0
  43. package/lib/lightClient/spec/utils.js.map +1 -0
  44. package/lib/lightClient/spec/validateLightClientBootstrap.d.ts +4 -0
  45. package/lib/lightClient/spec/validateLightClientBootstrap.d.ts.map +1 -0
  46. package/lib/lightClient/spec/validateLightClientBootstrap.js +22 -0
  47. package/lib/lightClient/spec/validateLightClientBootstrap.js.map +1 -0
  48. package/lib/lightClient/spec/validateLightClientUpdate.d.ts +5 -0
  49. package/lib/lightClient/spec/validateLightClientUpdate.d.ts.map +1 -0
  50. package/lib/lightClient/spec/validateLightClientUpdate.js +88 -0
  51. package/lib/lightClient/spec/validateLightClientUpdate.js.map +1 -0
  52. package/lib/signatureSets/executionPayloadEnvelope.js +1 -1
  53. package/lib/signatureSets/executionPayloadEnvelope.js.map +1 -1
  54. package/lib/signatureSets/index.d.ts +1 -0
  55. package/lib/signatureSets/index.d.ts.map +1 -1
  56. package/lib/signatureSets/index.js +1 -0
  57. package/lib/signatureSets/index.js.map +1 -1
  58. package/lib/signatureSets/proposerPreferences.d.ts +4 -0
  59. package/lib/signatureSets/proposerPreferences.d.ts.map +1 -0
  60. package/lib/signatureSets/proposerPreferences.js +8 -0
  61. package/lib/signatureSets/proposerPreferences.js.map +1 -0
  62. package/lib/slot/upgradeStateToElectra.d.ts.map +1 -1
  63. package/lib/slot/upgradeStateToElectra.js +2 -2
  64. package/lib/slot/upgradeStateToElectra.js.map +1 -1
  65. package/lib/slot/upgradeStateToGloas.d.ts.map +1 -1
  66. package/lib/slot/upgradeStateToGloas.js +34 -28
  67. package/lib/slot/upgradeStateToGloas.js.map +1 -1
  68. package/lib/stateView/beaconStateView.d.ts +20 -7
  69. package/lib/stateView/beaconStateView.d.ts.map +1 -1
  70. package/lib/stateView/beaconStateView.js +52 -16
  71. package/lib/stateView/beaconStateView.js.map +1 -1
  72. package/lib/stateView/interface.d.ts +14 -4
  73. package/lib/stateView/interface.d.ts.map +1 -1
  74. package/lib/stateView/interface.js.map +1 -1
  75. package/lib/util/computeAnchorCheckpoint.d.ts +1 -1
  76. package/lib/util/computeAnchorCheckpoint.d.ts.map +1 -1
  77. package/lib/util/computeAnchorCheckpoint.js +6 -19
  78. package/lib/util/computeAnchorCheckpoint.js.map +1 -1
  79. package/lib/util/epoch.d.ts.map +1 -1
  80. package/lib/util/epoch.js +6 -4
  81. package/lib/util/epoch.js.map +1 -1
  82. package/lib/util/gloas.d.ts +0 -1
  83. package/lib/util/gloas.d.ts.map +1 -1
  84. package/lib/util/gloas.js +0 -3
  85. package/lib/util/gloas.js.map +1 -1
  86. package/lib/util/index.d.ts +1 -0
  87. package/lib/util/index.d.ts.map +1 -1
  88. package/lib/util/index.js +1 -0
  89. package/lib/util/index.js.map +1 -1
  90. package/lib/util/loadState/loadState.js +4 -4
  91. package/lib/util/loadState/loadState.js.map +1 -1
  92. package/lib/util/pendingDepositsLookup.d.ts +40 -0
  93. package/lib/util/pendingDepositsLookup.d.ts.map +1 -0
  94. package/lib/util/pendingDepositsLookup.js +84 -0
  95. package/lib/util/pendingDepositsLookup.js.map +1 -0
  96. package/lib/util/shuffling.d.ts +6 -5
  97. package/lib/util/shuffling.d.ts.map +1 -1
  98. package/lib/util/shuffling.js +13 -15
  99. package/lib/util/shuffling.js.map +1 -1
  100. package/lib/util/validator.d.ts +14 -2
  101. package/lib/util/validator.d.ts.map +1 -1
  102. package/lib/util/validator.js +24 -2
  103. package/lib/util/validator.js.map +1 -1
  104. package/package.json +13 -8
  105. package/src/block/index.ts +12 -4
  106. package/src/block/processConsolidationRequest.ts +2 -1
  107. package/src/block/processDepositRequest.ts +29 -47
  108. package/src/block/processParentExecutionPayload.ts +117 -0
  109. package/src/block/processWithdrawals.ts +12 -4
  110. package/src/cache/epochCache.ts +3 -3
  111. package/src/epoch/processPendingDeposits.ts +5 -2
  112. package/src/lightClient/spec/index.ts +101 -0
  113. package/src/lightClient/spec/isBetterUpdate.ts +94 -0
  114. package/src/lightClient/spec/processLightClientUpdate.ts +119 -0
  115. package/src/lightClient/spec/store.ts +106 -0
  116. package/src/lightClient/spec/utils.ts +317 -0
  117. package/src/lightClient/spec/validateLightClientBootstrap.ts +39 -0
  118. package/src/lightClient/spec/validateLightClientUpdate.ts +145 -0
  119. package/src/signatureSets/executionPayloadEnvelope.ts +1 -1
  120. package/src/signatureSets/index.ts +1 -0
  121. package/src/signatureSets/proposerPreferences.ts +12 -0
  122. package/src/slot/upgradeStateToElectra.ts +4 -2
  123. package/src/slot/upgradeStateToGloas.ts +44 -44
  124. package/src/stateView/beaconStateView.ts +60 -25
  125. package/src/stateView/interface.ts +16 -7
  126. package/src/util/computeAnchorCheckpoint.ts +6 -19
  127. package/src/util/epoch.ts +13 -4
  128. package/src/util/gloas.ts +0 -4
  129. package/src/util/index.ts +1 -0
  130. package/src/util/loadState/loadState.ts +4 -4
  131. package/src/util/pendingDepositsLookup.ts +105 -0
  132. package/src/util/shuffling.ts +17 -15
  133. package/src/util/validator.ts +42 -2
  134. package/lib/block/processExecutionPayloadEnvelope.d.ts +0 -9
  135. package/lib/block/processExecutionPayloadEnvelope.d.ts.map +0 -1
  136. package/lib/block/processExecutionPayloadEnvelope.js +0 -106
  137. package/lib/block/processExecutionPayloadEnvelope.js.map +0 -1
  138. package/src/block/processExecutionPayloadEnvelope.ts +0 -175
@@ -14,8 +14,8 @@ import {processBlockHeader} from "./processBlockHeader.js";
14
14
  import {processEth1Data} from "./processEth1Data.js";
15
15
  import {processExecutionPayload} from "./processExecutionPayload.js";
16
16
  import {processExecutionPayloadBid} from "./processExecutionPayloadBid.js";
17
- import {processExecutionPayloadEnvelope} from "./processExecutionPayloadEnvelope.js";
18
17
  import {processOperations} from "./processOperations.js";
18
+ import {processParentExecutionPayload} from "./processParentExecutionPayload.js";
19
19
  import {processPayloadAttestation} from "./processPayloadAttestation.js";
20
20
  import {processRandao} from "./processRandao.js";
21
21
  import {processSyncAggregate} from "./processSyncCommittee.js";
@@ -32,7 +32,7 @@ export {
32
32
  processWithdrawals,
33
33
  processExecutionPayloadBid,
34
34
  processPayloadAttestation,
35
- processExecutionPayloadEnvelope,
35
+ processParentExecutionPayload,
36
36
  };
37
37
 
38
38
  export * from "./externalData.js";
@@ -51,10 +51,16 @@ export function processBlock(
51
51
  ): void {
52
52
  const {verifySignatures = true} = opts ?? {};
53
53
 
54
+ // Apply the parent's deferred payload effects before everything else. Must run before
55
+ // processBlockHeader and processExecutionPayloadBid so subsequent steps see the updated state.
56
+ if (fork >= ForkSeq.gloas) {
57
+ processParentExecutionPayload(state as CachedBeaconStateGloas, block as BeaconBlock<ForkPostGloas>);
58
+ }
59
+
54
60
  processBlockHeader(state, block);
55
61
 
56
62
  if (fork >= ForkSeq.gloas) {
57
- // After gloas, processWithdrawals does not take a payload parameter
63
+ // Parent payload's execution requests were already applied by processParentExecutionPayload above
58
64
  processWithdrawals(fork, state as CachedBeaconStateGloas);
59
65
  } else if (fork >= ForkSeq.capella) {
60
66
  const fullOrBlindedPayload = getFullOrBlindedPayload(block);
@@ -67,7 +73,9 @@ export function processBlock(
67
73
 
68
74
  // The call to the process_execution_payload must happen before the call to the process_randao as the former depends
69
75
  // on the randao_mix computed with the reveal of the previous block.
70
- // TODO GLOAS: We call processExecutionPayload somewhere else post-gloas
76
+ // Post-gloas: process_execution_payload is not part of block processing. The parent's payload
77
+ // effects are applied earlier via processParentExecutionPayload, and each execution payload is
78
+ // verified out-of-band via verifyExecutionPayloadEnvelope when it arrives.
71
79
  if (
72
80
  fork < ForkSeq.gloas &&
73
81
  fork >= ForkSeq.bellatrix &&
@@ -46,7 +46,8 @@ export function processConsolidationRequest(
46
46
  }
47
47
 
48
48
  // If there is too little available consolidation churn limit, consolidation requests are ignored
49
- if (getConsolidationChurnLimit(state.epochCtx) <= MIN_ACTIVATION_BALANCE) {
49
+ const fork = state.config.getForkSeq(state.slot);
50
+ if (getConsolidationChurnLimit(fork, state.epochCtx) <= MIN_ACTIVATION_BALANCE) {
50
51
  return;
51
52
  }
52
53
 
@@ -1,10 +1,10 @@
1
- import {BeaconConfig} from "@lodestar/config";
2
1
  import {FAR_FUTURE_EPOCH, ForkSeq, UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params";
3
- import {BLSPubkey, Bytes32, PubkeyHex, UintNum64, electra, ssz} from "@lodestar/types";
2
+ import {BLSPubkey, Bytes32, UintNum64, electra, ssz} from "@lodestar/types";
4
3
  import {toPubkeyHex} from "@lodestar/utils";
5
4
  import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
6
5
  import {findBuilderIndexByPubkey, isBuilderWithdrawalCredential} from "../util/gloas.js";
7
6
  import {computeEpochAtSlot, isValidatorKnown} from "../util/index.js";
7
+ import {PendingDepositsLookup} from "../util/pendingDepositsLookup.js";
8
8
  import {isValidDepositSignature} from "./processDeposit.js";
9
9
 
10
10
  /**
@@ -76,53 +76,60 @@ function addBuilderToRegistry(
76
76
  }
77
77
  }
78
78
 
79
- // TODO GLOAS: pendingValidatorPubkeys cache is currently naive and has room for improvement.
80
- // Currently the cache lives in process_block, but we should put it in epochCache or elsewhere that has longer
81
- // lifetime to avoid duplicated deposit signature computation
79
+ // TODO GLOAS: the PendingDepositsLookup is currently scoped to a single envelope of
80
+ // deposit-requests. We can track it as ephemeral within EpochCache and transfer to the next block
81
+ // transition to reuse cached signature verifications.
82
82
  // See https://github.com/ChainSafe/lodestar/issues/9181
83
83
  export function processDepositRequest(
84
84
  fork: ForkSeq,
85
85
  state: CachedBeaconStateElectra | CachedBeaconStateGloas,
86
86
  depositRequest: electra.DepositRequest,
87
- pendingValidatorPubkeysCache?: Set<PubkeyHex>
87
+ pendingDepositsLookup?: PendingDepositsLookup
88
88
  ): void {
89
89
  const {pubkey, withdrawalCredentials, amount, signature} = depositRequest;
90
90
 
91
- // Check if this is a builder or validator deposit
92
91
  if (fork >= ForkSeq.gloas) {
93
92
  const stateGloas = state as CachedBeaconStateGloas;
94
- const pendingValidatorPubkeys =
95
- pendingValidatorPubkeysCache ?? getPendingValidatorPubkeys(state.config, stateGloas);
93
+ const lookup = pendingDepositsLookup ?? PendingDepositsLookup.build(stateGloas);
96
94
  const pubkeyHex = toPubkeyHex(pubkey);
97
95
  const builderIndex = findBuilderIndexByPubkey(stateGloas, pubkey);
98
96
  const validatorIndex = state.epochCtx.getValidatorIndex(pubkey);
99
97
 
100
- // Regardless of the withdrawal credentials prefix, if a builder/validator
101
- // already exists with this pubkey, apply the deposit to their balance
102
98
  const isBuilder = builderIndex !== null;
103
99
  const isValidator = isValidatorKnown(state, validatorIndex);
104
- const isPendingValidator = pendingValidatorPubkeys.has(pubkeyHex);
105
100
 
106
- if (isBuilder || (isBuilderWithdrawalCredential(withdrawalCredentials) && !isValidator && !isPendingValidator)) {
107
- // Apply builder deposits immediately
101
+ if (isBuilder) {
102
+ // Top up an existing builder regardless of withdrawal credential prefix
108
103
  applyDepositForBuilder(stateGloas, pubkey, withdrawalCredentials, amount, signature, state.slot);
109
104
  return;
110
105
  }
111
106
 
112
- // Keep the shared cache in sync: if this deposit has a valid signature, subsequent
113
- // deposit requests for the same pubkey in this envelope must see it as a pending validator
107
+ // Only check the (expensive) "pending validator" condition when needed
114
108
  if (
115
- pendingValidatorPubkeysCache &&
109
+ isBuilderWithdrawalCredential(withdrawalCredentials) &&
116
110
  !isValidator &&
117
- !isPendingValidator &&
118
- isValidDepositSignature(state.config, pubkey, withdrawalCredentials, amount, signature)
111
+ !lookup.hasPendingValidator(state.config, pubkeyHex)
119
112
  ) {
120
- pendingValidatorPubkeys.add(pubkeyHex);
113
+ applyDepositForBuilder(stateGloas, pubkey, withdrawalCredentials, amount, signature, state.slot);
114
+ return;
121
115
  }
116
+
117
+ const pendingDeposit = ssz.electra.PendingDeposit.toViewDU({
118
+ pubkey,
119
+ withdrawalCredentials,
120
+ amount,
121
+ signature,
122
+ slot: state.slot,
123
+ });
124
+ // Keep the lookup in sync with state.pendingDeposits so later deposit-requests
125
+ // in the same envelope see this deposit
126
+ lookup.add(pendingDeposit, pubkeyHex);
127
+ state.pendingDeposits.push(pendingDeposit);
128
+ return;
122
129
  }
123
130
 
124
- // Only set deposit_requests_start_index in Electra fork, not Gloas
125
- if (fork < ForkSeq.gloas && state.depositRequestsStartIndex === UNSET_DEPOSIT_REQUESTS_START_INDEX) {
131
+ // Pre-Gloas (Electra) path
132
+ if (state.depositRequestsStartIndex === UNSET_DEPOSIT_REQUESTS_START_INDEX) {
126
133
  state.depositRequestsStartIndex = depositRequest.index;
127
134
  }
128
135
 
@@ -136,28 +143,3 @@ export function processDepositRequest(
136
143
  });
137
144
  state.pendingDeposits.push(pendingDeposit);
138
145
  }
139
-
140
- /**
141
- * Build a set of pubkeys (hex-encoded) from pending deposits that have valid signatures.
142
- * This is computed once and passed to each processDepositRequest call to avoid
143
- * repeatedly iterating state.pendingDeposits.
144
- *
145
- * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.3/specs/gloas/beacon-chain.md#new-is_pending_validator
146
- */
147
- export function getPendingValidatorPubkeys(config: BeaconConfig, state: CachedBeaconStateGloas): Set<PubkeyHex> {
148
- const result = new Set<PubkeyHex>();
149
- for (const pendingDeposit of state.pendingDeposits.getAllReadonly()) {
150
- if (
151
- isValidDepositSignature(
152
- config,
153
- pendingDeposit.pubkey,
154
- pendingDeposit.withdrawalCredentials,
155
- pendingDeposit.amount,
156
- pendingDeposit.signature
157
- )
158
- ) {
159
- result.add(toPubkeyHex(pendingDeposit.pubkey));
160
- }
161
- }
162
- return result;
163
- }
@@ -0,0 +1,117 @@
1
+ import {ForkPostGloas, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
2
+ import {BeaconBlock, electra, ssz} from "@lodestar/types";
3
+ import {byteArrayEquals, toRootHex} from "@lodestar/utils";
4
+ import {CachedBeaconStateGloas} from "../types.js";
5
+ import {computeEpochAtSlot} from "../util/epoch.js";
6
+ import {PendingDepositsLookup} from "../util/pendingDepositsLookup.js";
7
+ import {processConsolidationRequest} from "./processConsolidationRequest.js";
8
+ import {processDepositRequest} from "./processDepositRequest.js";
9
+ import {processWithdrawalRequest} from "./processWithdrawalRequest.js";
10
+
11
+ /**
12
+ * Process parent execution payload effects as the first step of processBlock.
13
+ *
14
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/beacon-chain.md#new-process_parent_execution_payload
15
+ */
16
+ export function processParentExecutionPayload(state: CachedBeaconStateGloas, block: BeaconBlock<ForkPostGloas>): void {
17
+ const bid = block.body.signedExecutionPayloadBid.message;
18
+ const parentBid = state.latestExecutionPayloadBid;
19
+ const requests = block.body.parentExecutionRequests;
20
+
21
+ const isParentBlockFull = byteArrayEquals(bid.parentBlockHash, parentBid.blockHash);
22
+ if (!isParentBlockFull) {
23
+ // Parent was EMPTY -- no execution requests expected
24
+ assertEmptyExecutionRequests(requests);
25
+ return;
26
+ }
27
+
28
+ // Parent was FULL -- verify the bid commitment and apply the payload
29
+ const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(requests);
30
+ if (!byteArrayEquals(requestsRoot, parentBid.executionRequestsRoot)) {
31
+ throw new Error(
32
+ `Parent execution requests root mismatch actual=${toRootHex(requestsRoot)} expected=${toRootHex(parentBid.executionRequestsRoot)}`
33
+ );
34
+ }
35
+
36
+ applyParentExecutionPayload(state, requests);
37
+ }
38
+
39
+ /**
40
+ * Process the parent's execution requests, queue the builder payment, update payload availability,
41
+ * and update the latest block hash.
42
+ *
43
+ * Called from processParentExecutionPayload during block processing, and from the validator during
44
+ * block production before computing withdrawals.
45
+ *
46
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/beacon-chain.md#new-apply_parent_execution_payload
47
+ */
48
+ export function applyParentExecutionPayload(state: CachedBeaconStateGloas, requests: electra.ExecutionRequests): void {
49
+ const fork = state.config.getForkSeq(state.slot);
50
+ const parentBid = state.latestExecutionPayloadBid;
51
+ const parentSlot = parentBid.slot;
52
+ const parentEpoch = computeEpochAtSlot(parentSlot);
53
+ const currentEpoch = computeEpochAtSlot(state.slot);
54
+
55
+ // Process execution requests from parent's payload. The execution
56
+ // requests are processed at state.slot (child's slot), not the parent's slot.
57
+ if (requests.deposits.length > 0) {
58
+ const pendingDepositsLookup = PendingDepositsLookup.build(state);
59
+ for (const deposit of requests.deposits) {
60
+ processDepositRequest(fork, state, deposit, pendingDepositsLookup);
61
+ }
62
+ }
63
+
64
+ for (const withdrawal of requests.withdrawals) {
65
+ processWithdrawalRequest(fork, state, withdrawal);
66
+ }
67
+
68
+ for (const consolidation of requests.consolidations) {
69
+ processConsolidationRequest(state, consolidation);
70
+ }
71
+
72
+ // Settle the builder payment
73
+ if (parentEpoch === currentEpoch) {
74
+ settleBuilderPayment(state, SLOTS_PER_EPOCH + (parentSlot % SLOTS_PER_EPOCH));
75
+ } else if (parentEpoch === currentEpoch - 1) {
76
+ settleBuilderPayment(state, parentSlot % SLOTS_PER_EPOCH);
77
+ } else if (parentBid.value > 0) {
78
+ // Parent is older than the previous epoch, its payment entry has been evicted from
79
+ // builder_pending_payments. Append the withdrawal directly.
80
+ state.builderPendingWithdrawals.push(
81
+ ssz.gloas.BuilderPendingWithdrawal.toViewDU({
82
+ feeRecipient: parentBid.feeRecipient,
83
+ amount: parentBid.value,
84
+ builderIndex: parentBid.builderIndex,
85
+ })
86
+ );
87
+ }
88
+
89
+ // Update parent payload availability and latest block hash
90
+ state.executionPayloadAvailability.set(parentSlot % SLOTS_PER_HISTORICAL_ROOT, true);
91
+ state.latestBlockHash = parentBid.blockHash;
92
+ }
93
+
94
+ /**
95
+ * Settle a builder payment at the given index: move its withdrawal (if any) to the
96
+ * pending withdrawals list and clear the payment slot.
97
+ *
98
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/beacon-chain.md#new-settle_builder_payment
99
+ */
100
+ function settleBuilderPayment(state: CachedBeaconStateGloas, paymentIndex: number): void {
101
+ if (paymentIndex >= state.builderPendingPayments.length) {
102
+ throw new Error(
103
+ `Invalid builder payment index paymentIndex=${paymentIndex} limit=${state.builderPendingPayments.length}`
104
+ );
105
+ }
106
+ const payment = state.builderPendingPayments.get(paymentIndex).clone();
107
+ if (payment.withdrawal.amount > 0) {
108
+ state.builderPendingWithdrawals.push(payment.withdrawal);
109
+ }
110
+ state.builderPendingPayments.set(paymentIndex, ssz.gloas.BuilderPendingPayment.defaultViewDU());
111
+ }
112
+
113
+ function assertEmptyExecutionRequests(requests: electra.ExecutionRequests): void {
114
+ if (requests.deposits.length !== 0 || requests.withdrawals.length !== 0 || requests.consolidations.length !== 0) {
115
+ throw new Error("Parent execution requests must be empty when parent block is EMPTY");
116
+ }
117
+ }
@@ -15,7 +15,6 @@ import {
15
15
  convertBuilderIndexToValidatorIndex,
16
16
  convertValidatorIndexToBuilderIndex,
17
17
  isBuilderIndex,
18
- isParentBlockFull,
19
18
  } from "../util/gloas.js";
20
19
  import {
21
20
  decreaseBalance,
@@ -32,8 +31,15 @@ export function processWithdrawals(
32
31
  payload?: capella.FullOrBlindedExecutionPayload
33
32
  ): void {
34
33
  // Return early if the parent block is empty
35
- if (fork >= ForkSeq.gloas && !isParentBlockFull(state as CachedBeaconStateGloas)) {
36
- return;
34
+ if (fork >= ForkSeq.gloas) {
35
+ const stateGloas = state as CachedBeaconStateGloas;
36
+ const isParentBlockEmpty = !byteArrayEquals(
37
+ stateGloas.latestBlockHash,
38
+ stateGloas.latestExecutionPayloadBid.blockHash
39
+ );
40
+ if (isParentBlockEmpty) {
41
+ return;
42
+ }
37
43
  }
38
44
 
39
45
  // processedBuilderWithdrawalsCount is withdrawals coming from builder payment since gloas (EIP-7732)
@@ -48,7 +54,9 @@ export function processWithdrawals(
48
54
  } = getExpectedWithdrawals(fork, state);
49
55
  const numWithdrawals = expectedWithdrawals.length;
50
56
 
51
- // After gloas, withdrawals are verified later in processExecutionPayloadEnvelope
57
+ // Pre-gloas verifies the payload's withdrawals against expectedWithdrawals here.
58
+ // Post-gloas, the payload arrives later as an envelope and that consistency check
59
+ // happens in verifyExecutionPayloadEnvelope against state.payloadExpectedWithdrawals.
52
60
  if (fork < ForkSeq.gloas) {
53
61
  if (payload === undefined) {
54
62
  throw Error("payload is required for pre-gloas processWithdrawals");
@@ -38,9 +38,9 @@ import {
38
38
  computeEpochAtSlot,
39
39
  computeProposers,
40
40
  computeSyncPeriodAtEpoch,
41
- getActivationChurnLimit,
42
41
  getChurnLimit,
43
42
  getSeed,
43
+ getValidatorActivationChurnLimit,
44
44
  isActiveValidator,
45
45
  isAggregatorFromCommitteeLength,
46
46
  } from "../util/index.js";
@@ -478,7 +478,7 @@ export class EpochCache {
478
478
  // the first block of the epoch process_block() call. So churnLimit must be computed at the end of the before epoch
479
479
  // transition and the result is valid until the end of the next epoch transition
480
480
  const churnLimit = getChurnLimit(config, currentShuffling.activeIndices.length);
481
- const activationChurnLimit = getActivationChurnLimit(
481
+ const activationChurnLimit = getValidatorActivationChurnLimit(
482
482
  config,
483
483
  config.getForkSeq(state.slot),
484
484
  currentShuffling.activeIndices.length
@@ -652,7 +652,7 @@ export class EpochCache {
652
652
  // the first block of the epoch process_block() call. So churnLimit must be computed at the end of the before epoch
653
653
  // transition and the result is valid until the end of the next epoch transition
654
654
  this.churnLimit = getChurnLimit(this.config, this.currentShuffling.activeIndices.length);
655
- this.activationChurnLimit = getActivationChurnLimit(
655
+ this.activationChurnLimit = getValidatorActivationChurnLimit(
656
656
  this.config,
657
657
  this.config.getForkSeq(state.slot),
658
658
  this.currentShuffling.activeIndices.length
@@ -5,7 +5,7 @@ import {CachedBeaconStateElectra, EpochTransitionCache} from "../types.js";
5
5
  import {increaseBalance} from "../util/balance.js";
6
6
  import {hasCompoundingWithdrawalCredential, isValidatorKnown} from "../util/electra.js";
7
7
  import {computeStartSlotAtEpoch} from "../util/epoch.js";
8
- import {getActivationExitChurnLimit} from "../util/validator.js";
8
+ import {getActivationChurnLimit, getActivationExitChurnLimit} from "../util/validator.js";
9
9
 
10
10
  /**
11
11
  * Starting from Electra:
@@ -17,8 +17,11 @@ import {getActivationExitChurnLimit} from "../util/validator.js";
17
17
  * TODO Electra: Update ssz library to support batch push to `pendingDeposits`
18
18
  */
19
19
  export function processPendingDeposits(state: CachedBeaconStateElectra, cache: EpochTransitionCache): void {
20
+ const fork = state.config.getForkSeq(state.slot);
20
21
  const nextEpoch = state.epochCtx.epoch + 1;
21
- const availableForProcessing = state.depositBalanceToConsume + BigInt(getActivationExitChurnLimit(state.epochCtx));
22
+ const churnLimit =
23
+ fork >= ForkSeq.gloas ? getActivationChurnLimit(state.epochCtx) : getActivationExitChurnLimit(state.epochCtx);
24
+ const availableForProcessing = state.depositBalanceToConsume + BigInt(churnLimit);
22
25
  let processedAmount = 0;
23
26
  let nextDepositIndex = 0;
24
27
  const depositsToPostpone = [];
@@ -0,0 +1,101 @@
1
+ import {BeaconConfig} from "@lodestar/config";
2
+ import {UPDATE_TIMEOUT} from "@lodestar/params";
3
+ import {
4
+ LightClientBootstrap,
5
+ LightClientFinalityUpdate,
6
+ LightClientOptimisticUpdate,
7
+ LightClientUpdate,
8
+ Slot,
9
+ } from "@lodestar/types";
10
+ import {computeSyncPeriodAtSlot} from "../../util/epoch.js";
11
+ import {
12
+ type ProcessUpdateOpts,
13
+ getSyncCommitteeAtPeriod,
14
+ processLightClientUpdate,
15
+ } from "./processLightClientUpdate.js";
16
+ import {type ILightClientStore, LightClientStore, type LightClientStoreEvents} from "./store.js";
17
+ import {ZERO_HEADER, ZERO_SYNC_COMMITTEE, getZeroFinalityBranch, getZeroSyncCommitteeBranch} from "./utils.js";
18
+
19
+ export type {LightClientUpdateSummary} from "./isBetterUpdate.js";
20
+ export {isBetterUpdate, toLightClientUpdateSummary} from "./isBetterUpdate.js";
21
+ export {
22
+ type ProcessUpdateOpts,
23
+ getSyncCommitteeAtPeriod,
24
+ isSafeLightClientUpdate,
25
+ processLightClientUpdate,
26
+ } from "./processLightClientUpdate.js";
27
+ export {
28
+ type ILightClientStore,
29
+ LightClientStore,
30
+ type LightClientStoreEvents,
31
+ type LightClientUpdateWithSummary,
32
+ type SyncCommitteeFast,
33
+ } from "./store.js";
34
+ export {
35
+ getSafetyThreshold,
36
+ isFinalityUpdate,
37
+ isSyncCommitteeUpdate,
38
+ isValidLightClientHeader,
39
+ normalizeMerkleBranch,
40
+ upgradeLightClientFinalityUpdate,
41
+ upgradeLightClientHeader,
42
+ upgradeLightClientOptimisticUpdate,
43
+ upgradeLightClientStore,
44
+ upgradeLightClientUpdate,
45
+ } from "./utils.js";
46
+ export {validateLightClientBootstrap} from "./validateLightClientBootstrap.js";
47
+ export {validateLightClientUpdate} from "./validateLightClientUpdate.js";
48
+
49
+ export class LightclientSpec {
50
+ readonly store: ILightClientStore;
51
+ readonly config: BeaconConfig;
52
+
53
+ constructor(
54
+ config: BeaconConfig,
55
+ private readonly opts: ProcessUpdateOpts & LightClientStoreEvents,
56
+ bootstrap: LightClientBootstrap
57
+ ) {
58
+ this.store = new LightClientStore(config, bootstrap, opts);
59
+ this.config = config;
60
+ }
61
+
62
+ onUpdate(currentSlot: Slot, update: LightClientUpdate): void {
63
+ processLightClientUpdate(this.config, this.store, currentSlot, this.opts, update);
64
+ }
65
+
66
+ onFinalityUpdate(currentSlot: Slot, finalityUpdate: LightClientFinalityUpdate): void {
67
+ this.onUpdate(currentSlot, {
68
+ attestedHeader: finalityUpdate.attestedHeader,
69
+ nextSyncCommittee: ZERO_SYNC_COMMITTEE,
70
+ nextSyncCommitteeBranch: getZeroSyncCommitteeBranch(this.config.getForkName(finalityUpdate.signatureSlot)),
71
+ finalizedHeader: finalityUpdate.finalizedHeader,
72
+ finalityBranch: finalityUpdate.finalityBranch,
73
+ syncAggregate: finalityUpdate.syncAggregate,
74
+ signatureSlot: finalityUpdate.signatureSlot,
75
+ });
76
+ }
77
+
78
+ onOptimisticUpdate(currentSlot: Slot, optimisticUpdate: LightClientOptimisticUpdate): void {
79
+ this.onUpdate(currentSlot, {
80
+ attestedHeader: optimisticUpdate.attestedHeader,
81
+ 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)),
85
+ syncAggregate: optimisticUpdate.syncAggregate,
86
+ signatureSlot: optimisticUpdate.signatureSlot,
87
+ });
88
+ }
89
+
90
+ forceUpdate(currentSlot: Slot): void {
91
+ for (const bestValidUpdate of this.store.bestValidUpdates.values()) {
92
+ if (currentSlot > bestValidUpdate.update.finalizedHeader.beacon.slot + UPDATE_TIMEOUT) {
93
+ const updatePeriod = computeSyncPeriodAtSlot(bestValidUpdate.update.signatureSlot);
94
+ // Simulate process_light_client_store_force_update() by forcing to apply a bestValidUpdate
95
+ // https://github.com/ethereum/consensus-specs/blob/a57e15636013eeba3610ff3ade41781dba1bb0cd/specs/altair/light-client/sync-protocol.md?plain=1#L394
96
+ // Call for `updatePeriod + 1` to force the update at `update.signatureSlot` to be applied
97
+ getSyncCommitteeAtPeriod(this.store, updatePeriod + 1, this.opts);
98
+ }
99
+ }
100
+ }
101
+ }
@@ -0,0 +1,94 @@
1
+ import {SYNC_COMMITTEE_SIZE} from "@lodestar/params";
2
+ import {LightClientUpdate, Slot} from "@lodestar/types";
3
+ import {computeSyncPeriodAtSlot} from "../../util/epoch.js";
4
+ import {isFinalityUpdate, isSyncCommitteeUpdate, sumBits} from "./utils.js";
5
+
6
+ /**
7
+ * Wrapper type for `isBetterUpdate()` so we can apply its logic without requiring the full LightClientUpdate type.
8
+ */
9
+ export type LightClientUpdateSummary = {
10
+ activeParticipants: number;
11
+ attestedHeaderSlot: Slot;
12
+ signatureSlot: Slot;
13
+ finalizedHeaderSlot: Slot;
14
+ /** `if update.next_sync_committee_branch != [Bytes32() for _ in range(floorlog2(NEXT_SYNC_COMMITTEE_INDEX))]` */
15
+ isSyncCommitteeUpdate: boolean;
16
+ /** `if update.finality_branch != [Bytes32() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))]` */
17
+ isFinalityUpdate: boolean;
18
+ };
19
+
20
+ /**
21
+ * Returns the update with more bits. On ties, prevUpdate is the better
22
+ *
23
+ * https://github.com/ethereum/consensus-specs/blob/be3c774069e16e89145660be511c1b183056017e/specs/altair/light-client/sync-protocol.md#is_better_update
24
+ */
25
+ export function isBetterUpdate(newUpdate: LightClientUpdateSummary, oldUpdate: LightClientUpdateSummary): boolean {
26
+ // Compare supermajority (> 2/3) sync committee participation
27
+ const newNumActiveParticipants = newUpdate.activeParticipants;
28
+ const oldNumActiveParticipants = oldUpdate.activeParticipants;
29
+ const newHasSupermajority = newNumActiveParticipants * 3 >= SYNC_COMMITTEE_SIZE * 2;
30
+ const oldHasSupermajority = oldNumActiveParticipants * 3 >= SYNC_COMMITTEE_SIZE * 2;
31
+ if (newHasSupermajority !== oldHasSupermajority) {
32
+ return newHasSupermajority;
33
+ }
34
+ if (!newHasSupermajority && newNumActiveParticipants !== oldNumActiveParticipants) {
35
+ return newNumActiveParticipants > oldNumActiveParticipants;
36
+ }
37
+
38
+ // Compare presence of relevant sync committee
39
+ const newHasRelevantSyncCommittee =
40
+ newUpdate.isSyncCommitteeUpdate &&
41
+ computeSyncPeriodAtSlot(newUpdate.attestedHeaderSlot) === computeSyncPeriodAtSlot(newUpdate.signatureSlot);
42
+ const oldHasRelevantSyncCommittee =
43
+ oldUpdate.isSyncCommitteeUpdate &&
44
+ computeSyncPeriodAtSlot(oldUpdate.attestedHeaderSlot) === computeSyncPeriodAtSlot(oldUpdate.signatureSlot);
45
+ if (newHasRelevantSyncCommittee !== oldHasRelevantSyncCommittee) {
46
+ return newHasRelevantSyncCommittee;
47
+ }
48
+
49
+ // Compare indication of any finality
50
+ const newHasFinality = newUpdate.isFinalityUpdate;
51
+ const oldHasFinality = oldUpdate.isFinalityUpdate;
52
+ if (newHasFinality !== oldHasFinality) {
53
+ return newHasFinality;
54
+ }
55
+
56
+ // Compare sync committee finality
57
+ if (newHasFinality) {
58
+ const newHasSyncCommitteeFinality =
59
+ computeSyncPeriodAtSlot(newUpdate.finalizedHeaderSlot) === computeSyncPeriodAtSlot(newUpdate.attestedHeaderSlot);
60
+ const oldHasSyncCommitteeFinality =
61
+ computeSyncPeriodAtSlot(oldUpdate.finalizedHeaderSlot) === computeSyncPeriodAtSlot(oldUpdate.attestedHeaderSlot);
62
+ if (newHasSyncCommitteeFinality !== oldHasSyncCommitteeFinality) {
63
+ return newHasSyncCommitteeFinality;
64
+ }
65
+ }
66
+
67
+ // Tiebreaker 1: Sync committee participation beyond supermajority
68
+ if (newNumActiveParticipants !== oldNumActiveParticipants) {
69
+ return newNumActiveParticipants > oldNumActiveParticipants;
70
+ }
71
+
72
+ // Tiebreaker 2: Prefer older data (fewer changes to best)
73
+ if (newUpdate.attestedHeaderSlot !== oldUpdate.attestedHeaderSlot) {
74
+ return newUpdate.attestedHeaderSlot < oldUpdate.attestedHeaderSlot;
75
+ }
76
+ return newUpdate.signatureSlot < oldUpdate.signatureSlot;
77
+ }
78
+
79
+ export function isSafeLightClientUpdate(update: LightClientUpdateSummary): boolean {
80
+ return (
81
+ update.activeParticipants * 3 >= SYNC_COMMITTEE_SIZE * 2 && update.isFinalityUpdate && update.isSyncCommitteeUpdate
82
+ );
83
+ }
84
+
85
+ export function toLightClientUpdateSummary(update: LightClientUpdate): LightClientUpdateSummary {
86
+ return {
87
+ activeParticipants: sumBits(update.syncAggregate.syncCommitteeBits),
88
+ attestedHeaderSlot: update.attestedHeader.beacon.slot,
89
+ signatureSlot: update.signatureSlot,
90
+ finalizedHeaderSlot: update.finalizedHeader.beacon.slot,
91
+ isSyncCommitteeUpdate: isSyncCommitteeUpdate(update),
92
+ isFinalityUpdate: isFinalityUpdate(update),
93
+ };
94
+ }