@lodestar/state-transition 1.44.0-dev.6ef8199cfa → 1.44.0-dev.762dd5f186

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 (90) hide show
  1. package/lib/block/index.d.ts +2 -0
  2. package/lib/block/index.d.ts.map +1 -1
  3. package/lib/block/index.js +3 -1
  4. package/lib/block/index.js.map +1 -1
  5. package/lib/block/processBuilderDepositRequest.d.ts +10 -0
  6. package/lib/block/processBuilderDepositRequest.d.ts.map +1 -0
  7. package/lib/block/processBuilderDepositRequest.js +27 -0
  8. package/lib/block/processBuilderDepositRequest.js.map +1 -0
  9. package/lib/block/processBuilderExitRequest.d.ts +13 -0
  10. package/lib/block/processBuilderExitRequest.d.ts.map +1 -0
  11. package/lib/block/processBuilderExitRequest.js +29 -0
  12. package/lib/block/processBuilderExitRequest.js.map +1 -0
  13. package/lib/block/processDepositRequest.d.ts +2 -8
  14. package/lib/block/processDepositRequest.d.ts.map +1 -1
  15. package/lib/block/processDepositRequest.js +3 -98
  16. package/lib/block/processDepositRequest.js.map +1 -1
  17. package/lib/block/processExecutionPayloadBid.d.ts +2 -3
  18. package/lib/block/processExecutionPayloadBid.d.ts.map +1 -1
  19. package/lib/block/processExecutionPayloadBid.js +16 -8
  20. package/lib/block/processExecutionPayloadBid.js.map +1 -1
  21. package/lib/block/processOperations.d.ts.map +1 -1
  22. package/lib/block/processOperations.js +3 -2
  23. package/lib/block/processOperations.js.map +1 -1
  24. package/lib/block/processParentExecutionPayload.d.ts +2 -2
  25. package/lib/block/processParentExecutionPayload.d.ts.map +1 -1
  26. package/lib/block/processParentExecutionPayload.js +16 -8
  27. package/lib/block/processParentExecutionPayload.js.map +1 -1
  28. package/lib/block/processProposerSlashing.d.ts.map +1 -1
  29. package/lib/block/processProposerSlashing.js +9 -1
  30. package/lib/block/processProposerSlashing.js.map +1 -1
  31. package/lib/block/processVoluntaryExit.d.ts +1 -1
  32. package/lib/block/processVoluntaryExit.d.ts.map +1 -1
  33. package/lib/block/processVoluntaryExit.js +1 -35
  34. package/lib/block/processVoluntaryExit.js.map +1 -1
  35. package/lib/cache/epochCache.d.ts.map +1 -1
  36. package/lib/cache/epochCache.js +10 -7
  37. package/lib/cache/epochCache.js.map +1 -1
  38. package/lib/epoch/processPendingDeposits.d.ts.map +1 -1
  39. package/lib/epoch/processPendingDeposits.js +4 -3
  40. package/lib/epoch/processPendingDeposits.js.map +1 -1
  41. package/lib/epoch/processProposerLookahead.d.ts.map +1 -1
  42. package/lib/epoch/processProposerLookahead.js +7 -2
  43. package/lib/epoch/processProposerLookahead.js.map +1 -1
  44. package/lib/slot/upgradeStateToGloas.js +35 -29
  45. package/lib/slot/upgradeStateToGloas.js.map +1 -1
  46. package/lib/stateView/beaconStateView.d.ts +5 -2
  47. package/lib/stateView/beaconStateView.d.ts.map +1 -1
  48. package/lib/stateView/beaconStateView.js +14 -0
  49. package/lib/stateView/beaconStateView.js.map +1 -1
  50. package/lib/stateView/interface.d.ts +30 -2
  51. package/lib/stateView/interface.d.ts.map +1 -1
  52. package/lib/stateView/interface.js.map +1 -1
  53. package/lib/stateView/nativeBeaconStateView.d.ts +230 -0
  54. package/lib/stateView/nativeBeaconStateView.d.ts.map +1 -0
  55. package/lib/stateView/nativeBeaconStateView.js +717 -0
  56. package/lib/stateView/nativeBeaconStateView.js.map +1 -0
  57. package/lib/stateView/stateViewFactory.d.ts.map +1 -1
  58. package/lib/stateView/stateViewFactory.js +2 -0
  59. package/lib/stateView/stateViewFactory.js.map +1 -1
  60. package/lib/util/epochShuffling.js +1 -1
  61. package/lib/util/epochShuffling.js.map +1 -1
  62. package/lib/util/gloas.d.ts +31 -0
  63. package/lib/util/gloas.d.ts.map +1 -1
  64. package/lib/util/gloas.js +83 -1
  65. package/lib/util/gloas.js.map +1 -1
  66. package/lib/util/shuffling.d.ts +2 -2
  67. package/lib/util/shuffling.d.ts.map +1 -1
  68. package/lib/util/shuffling.js +4 -3
  69. package/lib/util/shuffling.js.map +1 -1
  70. package/package.json +7 -8
  71. package/src/block/index.ts +6 -1
  72. package/src/block/processBuilderDepositRequest.ts +43 -0
  73. package/src/block/processBuilderExitRequest.ts +39 -0
  74. package/src/block/processDepositRequest.ts +4 -124
  75. package/src/block/processExecutionPayloadBid.ts +24 -9
  76. package/src/block/processOperations.ts +3 -2
  77. package/src/block/processParentExecutionPayload.ts +23 -11
  78. package/src/block/processProposerSlashing.ts +9 -4
  79. package/src/block/processVoluntaryExit.ts +2 -61
  80. package/src/cache/epochCache.ts +10 -7
  81. package/src/epoch/processPendingDeposits.ts +2 -0
  82. package/src/epoch/processProposerLookahead.ts +8 -1
  83. package/src/slot/upgradeStateToGloas.ts +49 -33
  84. package/src/stateView/beaconStateView.ts +18 -1
  85. package/src/stateView/interface.ts +34 -1
  86. package/src/stateView/nativeBeaconStateView.ts +915 -0
  87. package/src/stateView/stateViewFactory.ts +2 -0
  88. package/src/util/epochShuffling.ts +1 -1
  89. package/src/util/gloas.ts +104 -1
  90. package/src/util/shuffling.ts +4 -3
@@ -1,135 +1,15 @@
1
- import {FAR_FUTURE_EPOCH, ForkSeq, UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params";
2
- import {BLSPubkey, Bytes32, UintNum64, electra, ssz} from "@lodestar/types";
3
- import {toPubkeyHex} from "@lodestar/utils";
1
+ import {ForkSeq, UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params";
2
+ import {electra, ssz} from "@lodestar/types";
4
3
  import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
5
- import {findBuilderIndexByPubkey, isBuilderWithdrawalCredential} from "../util/gloas.js";
6
- import {computeEpochAtSlot, isValidatorKnown} from "../util/index.js";
7
- import {PendingDepositsLookup} from "../util/pendingDepositsLookup.js";
8
- import {isValidDepositSignature} from "./processDeposit.js";
9
4
 
10
- /**
11
- * Apply a deposit for a builder. Either increases balance for existing builder or adds new builder to registry.
12
- * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-apply_deposit_for_builder
13
- */
14
- export function applyDepositForBuilder(
15
- state: CachedBeaconStateGloas,
16
- pubkey: BLSPubkey,
17
- withdrawalCredentials: Bytes32,
18
- amount: UintNum64,
19
- signature: Bytes32,
20
- slot: UintNum64
21
- ): void {
22
- const builderIndex = findBuilderIndexByPubkey(state, pubkey);
23
-
24
- if (builderIndex !== null) {
25
- // Existing builder - increase balance
26
- const builder = state.builders.get(builderIndex);
27
- builder.balance += amount;
28
- } else {
29
- // New builder - verify signature and add to registry
30
- if (isValidDepositSignature(state.config, pubkey, withdrawalCredentials, amount, signature)) {
31
- addBuilderToRegistry(state, pubkey, withdrawalCredentials, amount, slot);
32
- }
33
- }
34
- }
35
-
36
- /**
37
- * Add a new builder to the builders registry.
38
- * Reuses slots from exited and fully withdrawn builders if available.
39
- */
40
- function addBuilderToRegistry(
41
- state: CachedBeaconStateGloas,
42
- pubkey: BLSPubkey,
43
- withdrawalCredentials: Bytes32,
44
- amount: UintNum64,
45
- slot: UintNum64
46
- ): void {
47
- const currentEpoch = computeEpochAtSlot(state.slot);
48
- const depositEpoch = computeEpochAtSlot(slot);
49
-
50
- // Try to find a reusable slot from an exited builder with zero balance
51
- let builderIndex = state.builders.length;
52
- for (let i = 0; i < state.builders.length; i++) {
53
- const builder = state.builders.getReadonly(i);
54
- if (builder.withdrawableEpoch <= currentEpoch && builder.balance === 0) {
55
- builderIndex = i;
56
- break;
57
- }
58
- }
59
-
60
- // Create new builder
61
- const newBuilder = ssz.gloas.Builder.toViewDU({
62
- pubkey,
63
- version: withdrawalCredentials[0],
64
- executionAddress: withdrawalCredentials.subarray(12),
65
- balance: amount,
66
- depositEpoch: depositEpoch,
67
- withdrawableEpoch: FAR_FUTURE_EPOCH,
68
- });
69
-
70
- if (builderIndex < state.builders.length) {
71
- // Reuse existing slot
72
- state.builders.set(builderIndex, newBuilder);
73
- } else {
74
- // Append to end
75
- state.builders.push(newBuilder);
76
- }
77
- }
78
-
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
- // See https://github.com/ChainSafe/lodestar/issues/9181
83
5
  export function processDepositRequest(
84
6
  fork: ForkSeq,
85
7
  state: CachedBeaconStateElectra | CachedBeaconStateGloas,
86
- depositRequest: electra.DepositRequest,
87
- pendingDepositsLookup?: PendingDepositsLookup
8
+ depositRequest: electra.DepositRequest
88
9
  ): void {
89
10
  const {pubkey, withdrawalCredentials, amount, signature} = depositRequest;
90
11
 
91
- if (fork >= ForkSeq.gloas) {
92
- const stateGloas = state as CachedBeaconStateGloas;
93
- const lookup = pendingDepositsLookup ?? PendingDepositsLookup.build(stateGloas);
94
- const pubkeyHex = toPubkeyHex(pubkey);
95
- const builderIndex = findBuilderIndexByPubkey(stateGloas, pubkey);
96
- const validatorIndex = state.epochCtx.getValidatorIndex(pubkey);
97
-
98
- const isBuilder = builderIndex !== null;
99
- const isValidator = isValidatorKnown(state, validatorIndex);
100
-
101
- if (isBuilder) {
102
- // Top up an existing builder regardless of withdrawal credential prefix
103
- applyDepositForBuilder(stateGloas, pubkey, withdrawalCredentials, amount, signature, state.slot);
104
- return;
105
- }
106
-
107
- // Only check the (expensive) "pending validator" condition when needed
108
- if (
109
- isBuilderWithdrawalCredential(withdrawalCredentials) &&
110
- !isValidator &&
111
- !lookup.hasPendingValidator(state.config, pubkeyHex)
112
- ) {
113
- applyDepositForBuilder(stateGloas, pubkey, withdrawalCredentials, amount, signature, state.slot);
114
- return;
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;
129
- }
130
-
131
- // Pre-Gloas (Electra) path
132
- if (state.depositRequestsStartIndex === UNSET_DEPOSIT_REQUESTS_START_INDEX) {
12
+ if (fork === ForkSeq.electra && state.depositRequestsStartIndex === UNSET_DEPOSIT_REQUESTS_START_INDEX) {
133
13
  state.depositRequestsStartIndex = depositRequest.index;
134
14
  }
135
15
 
@@ -1,15 +1,17 @@
1
1
  import {PublicKey, Signature, verify} from "@chainsafe/blst";
2
- import {BUILDER_INDEX_SELF_BUILD, ForkPostGloas, SLOTS_PER_EPOCH} from "@lodestar/params";
3
- import {BeaconBlock, gloas, ssz} from "@lodestar/types";
2
+ import {BUILDER_INDEX_SELF_BUILD, GENESIS_SLOT, PAYLOAD_BUILDER_VERSION, SLOTS_PER_EPOCH} from "@lodestar/params";
3
+ import {gloas, ssz} from "@lodestar/types";
4
4
  import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils";
5
5
  import {G2_POINT_AT_INFINITY} from "../constants/constants.js";
6
6
  import {getExecutionPayloadBidSigningRoot} from "../signatureSets/executionPayloadBid.js";
7
7
  import {CachedBeaconStateGloas} from "../types.js";
8
8
  import {canBuilderCoverBid, isActiveBuilder} from "../util/gloas.js";
9
- import {getCurrentEpoch, getRandaoMix} from "../util/index.js";
9
+ import {getBlockRootAtSlot, getCurrentEpoch, getRandaoMix} from "../util/index.js";
10
10
 
11
- export function processExecutionPayloadBid(state: CachedBeaconStateGloas, block: BeaconBlock<ForkPostGloas>): void {
12
- const signedBid = block.body.signedExecutionPayloadBid;
11
+ export function processExecutionPayloadBid(
12
+ state: CachedBeaconStateGloas,
13
+ signedBid: gloas.SignedExecutionPayloadBid
14
+ ): void {
13
15
  const bid = signedBid.message;
14
16
  const {builderIndex, value: amount} = bid;
15
17
 
@@ -31,6 +33,13 @@ export function processExecutionPayloadBid(state: CachedBeaconStateGloas, block:
31
33
  throw Error(`Invalid execution payload bid: builder ${builderIndex} is not active`);
32
34
  }
33
35
 
36
+ // Verify that the builder is a payload builder
37
+ if (builder.version !== PAYLOAD_BUILDER_VERSION) {
38
+ throw Error(
39
+ `Invalid execution payload bid: builder ${builderIndex} version ${builder.version} != ${PAYLOAD_BUILDER_VERSION}`
40
+ );
41
+ }
42
+
34
43
  // Verify that the builder has funds to cover the bid
35
44
  if (!canBuilderCoverBid(state, builderIndex, amount)) {
36
45
  throw Error(`Invalid execution payload bid: builder ${builderIndex} has insufficient balance`);
@@ -42,8 +51,12 @@ export function processExecutionPayloadBid(state: CachedBeaconStateGloas, block:
42
51
  }
43
52
  }
44
53
 
45
- if (bid.slot !== block.slot) {
46
- throw Error(`Bid slot ${bid.slot} does not match block slot ${block.slot}`);
54
+ if (bid.slot !== state.slot) {
55
+ throw Error(`Bid slot ${bid.slot} does not match state slot ${state.slot}`);
56
+ }
57
+
58
+ if (state.slot <= GENESIS_SLOT) {
59
+ throw Error(`Execution payload bid not allowed at genesis slot ${state.slot}`);
47
60
  }
48
61
 
49
62
  if (!byteArrayEquals(bid.parentBlockHash, state.latestBlockHash)) {
@@ -52,9 +65,10 @@ export function processExecutionPayloadBid(state: CachedBeaconStateGloas, block:
52
65
  );
53
66
  }
54
67
 
55
- if (!byteArrayEquals(bid.parentBlockRoot, block.parentRoot)) {
68
+ const parentBlockRoot = getBlockRootAtSlot(state, state.slot - 1);
69
+ if (!byteArrayEquals(bid.parentBlockRoot, parentBlockRoot)) {
56
70
  throw Error(
57
- `Parent block root ${toRootHex(bid.parentBlockRoot)} of bid does not match block's parent root ${toRootHex(block.parentRoot)}`
71
+ `Parent block root ${toRootHex(bid.parentBlockRoot)} of bid does not match state's parent block root ${toRootHex(parentBlockRoot)}`
58
72
  );
59
73
  }
60
74
 
@@ -79,6 +93,7 @@ export function processExecutionPayloadBid(state: CachedBeaconStateGloas, block:
79
93
  amount,
80
94
  builderIndex,
81
95
  }),
96
+ proposerIndex: state.epochCtx.getBeaconProposer(state.slot),
82
97
  });
83
98
 
84
99
  state.builderPendingPayments.set(SLOTS_PER_EPOCH + (bid.slot % SLOTS_PER_EPOCH), pendingPaymentView);
@@ -39,8 +39,9 @@ export function processOperations(
39
39
  opts: ProcessBlockOpts = {verifySignatures: true},
40
40
  metrics?: BeaconStateTransitionMetrics | null
41
41
  ): void {
42
- // verify that outstanding deposits are processed up to the maximum number of deposits
43
- const maxDeposits = getEth1DepositCount(state);
42
+ // verify that outstanding deposits are processed up to the maximum number of deposits.
43
+ // From Fulu the eth1 bridge deposit mechanism was removed, so blocks must not contain any deposits.
44
+ const maxDeposits = fork >= ForkSeq.fulu ? 0 : getEth1DepositCount(state);
44
45
  if (body.deposits.length !== maxDeposits) {
45
46
  throw new Error(
46
47
  `Block contains incorrect number of deposits: depositCount=${body.deposits.length} expected=${maxDeposits}`
@@ -1,9 +1,10 @@
1
1
  import {ForkPostGloas, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
2
- import {BeaconBlock, electra, ssz} from "@lodestar/types";
2
+ import {BeaconBlock, gloas, ssz} from "@lodestar/types";
3
3
  import {byteArrayEquals, toRootHex} from "@lodestar/utils";
4
4
  import {CachedBeaconStateGloas} from "../types.js";
5
5
  import {computeEpochAtSlot} from "../util/epoch.js";
6
- import {PendingDepositsLookup} from "../util/pendingDepositsLookup.js";
6
+ import {processBuilderDepositRequest} from "./processBuilderDepositRequest.js";
7
+ import {processBuilderExitRequest} from "./processBuilderExitRequest.js";
7
8
  import {processConsolidationRequest} from "./processConsolidationRequest.js";
8
9
  import {processDepositRequest} from "./processDepositRequest.js";
9
10
  import {processWithdrawalRequest} from "./processWithdrawalRequest.js";
@@ -26,7 +27,7 @@ export function processParentExecutionPayload(state: CachedBeaconStateGloas, blo
26
27
  }
27
28
 
28
29
  // Parent was FULL -- verify the bid commitment and apply the payload
29
- const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(requests);
30
+ const requestsRoot = ssz.gloas.ExecutionRequests.hashTreeRoot(requests);
30
31
  if (!byteArrayEquals(requestsRoot, parentBid.executionRequestsRoot)) {
31
32
  throw new Error(
32
33
  `Parent execution requests root mismatch actual=${toRootHex(requestsRoot)} expected=${toRootHex(parentBid.executionRequestsRoot)}`
@@ -45,7 +46,7 @@ export function processParentExecutionPayload(state: CachedBeaconStateGloas, blo
45
46
  *
46
47
  * 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
  */
48
- export function applyParentExecutionPayload(state: CachedBeaconStateGloas, requests: electra.ExecutionRequests): void {
49
+ export function applyParentExecutionPayload(state: CachedBeaconStateGloas, requests: gloas.ExecutionRequests): void {
49
50
  const fork = state.config.getForkSeq(state.slot);
50
51
  const parentBid = state.latestExecutionPayloadBid;
51
52
  const parentSlot = parentBid.slot;
@@ -54,11 +55,8 @@ export function applyParentExecutionPayload(state: CachedBeaconStateGloas, reque
54
55
 
55
56
  // Process execution requests from parent's payload. The execution
56
57
  // 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
- }
58
+ for (const deposit of requests.deposits) {
59
+ processDepositRequest(fork, state, deposit);
62
60
  }
63
61
 
64
62
  for (const withdrawal of requests.withdrawals) {
@@ -69,6 +67,14 @@ export function applyParentExecutionPayload(state: CachedBeaconStateGloas, reque
69
67
  processConsolidationRequest(state, consolidation);
70
68
  }
71
69
 
70
+ for (const builderDeposit of requests.builderDeposits) {
71
+ processBuilderDepositRequest(state, builderDeposit);
72
+ }
73
+
74
+ for (const builderExit of requests.builderExits) {
75
+ processBuilderExitRequest(state, builderExit);
76
+ }
77
+
72
78
  // Settle the builder payment
73
79
  if (parentEpoch === currentEpoch) {
74
80
  settleBuilderPayment(state, SLOTS_PER_EPOCH + (parentSlot % SLOTS_PER_EPOCH));
@@ -110,8 +116,14 @@ function settleBuilderPayment(state: CachedBeaconStateGloas, paymentIndex: numbe
110
116
  state.builderPendingPayments.set(paymentIndex, ssz.gloas.BuilderPendingPayment.defaultViewDU());
111
117
  }
112
118
 
113
- function assertEmptyExecutionRequests(requests: electra.ExecutionRequests): void {
114
- if (requests.deposits.length !== 0 || requests.withdrawals.length !== 0 || requests.consolidations.length !== 0) {
119
+ function assertEmptyExecutionRequests(requests: gloas.ExecutionRequests): void {
120
+ if (
121
+ requests.deposits.length !== 0 ||
122
+ requests.withdrawals.length !== 0 ||
123
+ requests.consolidations.length !== 0 ||
124
+ requests.builderDeposits.length !== 0 ||
125
+ requests.builderExits.length !== 0
126
+ ) {
115
127
  throw new Error("Parent execution requests must be empty when parent block is EMPTY");
116
128
  }
117
129
  }
@@ -32,6 +32,10 @@ export function processProposerSlashing(
32
32
  );
33
33
 
34
34
  if (fork >= ForkSeq.gloas) {
35
+ // Remove the BuilderPendingPayment corresponding to this proposal if it is still in the
36
+ // 2-epoch window. Only clear it when the slashed validator is the proposer associated with
37
+ // the payment; otherwise an unrelated same-slot equivocation could grief an honest proposer's
38
+ // payment.
35
39
  const slot = Number(proposerSlashing.signedHeader1.message.slot);
36
40
  const proposalEpoch = computeEpochAtSlot(slot);
37
41
  const currentEpoch = state.epochCtx.epoch;
@@ -45,10 +49,11 @@ export function processProposerSlashing(
45
49
  : undefined;
46
50
 
47
51
  if (paymentIndex !== undefined) {
48
- (state as CachedBeaconStateGloas).builderPendingPayments.set(
49
- paymentIndex,
50
- ssz.gloas.BuilderPendingPayment.defaultViewDU()
51
- );
52
+ const builderPendingPayments = (state as CachedBeaconStateGloas).builderPendingPayments;
53
+ const payment = builderPendingPayments.get(paymentIndex);
54
+ if (payment.proposerIndex === proposerSlashing.signedHeader1.message.proposerIndex) {
55
+ builderPendingPayments.set(paymentIndex, ssz.gloas.BuilderPendingPayment.defaultViewDU());
56
+ }
52
57
  }
53
58
  }
54
59
 
@@ -2,14 +2,7 @@ import {FAR_FUTURE_EPOCH, ForkSeq} from "@lodestar/params";
2
2
  import {phase0} from "@lodestar/types";
3
3
  import {verifyVoluntaryExitSignature} from "../signatureSets/index.js";
4
4
  import {BeaconStateView} from "../stateView/beaconStateView.js";
5
- import {CachedBeaconStateAllForks, CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
6
- import {
7
- convertValidatorIndexToBuilderIndex,
8
- getPendingBalanceToWithdrawForBuilder,
9
- initiateBuilderExit,
10
- isActiveBuilder,
11
- isBuilderIndex,
12
- } from "../util/gloas.js";
5
+ import {CachedBeaconStateAllForks, CachedBeaconStateElectra} from "../types.js";
13
6
  import {getPendingBalanceToWithdraw, isActiveValidator} from "../util/index.js";
14
7
  import {initiateValidatorExit} from "./index.js";
15
8
 
@@ -24,7 +17,7 @@ export enum VoluntaryExitValidity {
24
17
  }
25
18
 
26
19
  /**
27
- * Process a VoluntaryExit operation. Initiates the exit of a validator or builder.
20
+ * Process a VoluntaryExit operation. Initiates the exit of a validator.
28
21
  *
29
22
  * PERF: Work depends on number of VoluntaryExit per block. On regular networks the average is 0 / block.
30
23
  */
@@ -34,21 +27,11 @@ export function processVoluntaryExit(
34
27
  signedVoluntaryExit: phase0.SignedVoluntaryExit,
35
28
  verifySignature = true
36
29
  ): void {
37
- const voluntaryExit = signedVoluntaryExit.message;
38
-
39
30
  const validity = getVoluntaryExitValidity(fork, state, signedVoluntaryExit, verifySignature);
40
31
  if (validity !== VoluntaryExitValidity.valid) {
41
32
  throw Error(`Invalid voluntary exit at forkSeq=${fork} reason=${validity}`);
42
33
  }
43
34
 
44
- if (fork >= ForkSeq.gloas && isBuilderIndex(voluntaryExit.validatorIndex)) {
45
- initiateBuilderExit(
46
- state as CachedBeaconStateGloas,
47
- convertValidatorIndexToBuilderIndex(voluntaryExit.validatorIndex)
48
- );
49
- return;
50
- }
51
-
52
35
  const validator = state.validators.get(signedVoluntaryExit.message.validatorIndex);
53
36
  initiateValidatorExit(fork, state, validator);
54
37
  }
@@ -67,51 +50,9 @@ export function getVoluntaryExitValidity(
67
50
  return VoluntaryExitValidity.earlyEpoch;
68
51
  }
69
52
 
70
- // Check if this is a builder exit
71
- if (fork >= ForkSeq.gloas && isBuilderIndex(voluntaryExit.validatorIndex)) {
72
- return getBuilderVoluntaryExitValidity(state as CachedBeaconStateGloas, signedVoluntaryExit, verifySignature);
73
- }
74
-
75
53
  return getValidatorVoluntaryExitValidity(fork, state, signedVoluntaryExit, verifySignature);
76
54
  }
77
55
 
78
- function getBuilderVoluntaryExitValidity(
79
- state: CachedBeaconStateGloas,
80
- signedVoluntaryExit: phase0.SignedVoluntaryExit,
81
- verifySignature: boolean
82
- ): VoluntaryExitValidity {
83
- const {config, epochCtx} = state;
84
- const builderIndex = convertValidatorIndexToBuilderIndex(signedVoluntaryExit.message.validatorIndex);
85
-
86
- if (builderIndex >= state.builders.length) {
87
- return VoluntaryExitValidity.inactive;
88
- }
89
-
90
- const builder = state.builders.getReadonly(builderIndex);
91
-
92
- // Verify the builder is active
93
- if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) {
94
- return builder.withdrawableEpoch !== FAR_FUTURE_EPOCH
95
- ? VoluntaryExitValidity.alreadyExited
96
- : VoluntaryExitValidity.inactive;
97
- }
98
-
99
- // Only exit builder if it has no pending withdrawals in the queue
100
- if (getPendingBalanceToWithdrawForBuilder(state, builderIndex) !== 0) {
101
- return VoluntaryExitValidity.pendingWithdrawals;
102
- }
103
-
104
- // Verify signature
105
- if (
106
- verifySignature &&
107
- !verifyVoluntaryExitSignature(config, epochCtx.pubkeyCache, new BeaconStateView(state), signedVoluntaryExit)
108
- ) {
109
- return VoluntaryExitValidity.invalidSignature;
110
- }
111
-
112
- return VoluntaryExitValidity.valid;
113
- }
114
-
115
56
  function getValidatorVoluntaryExitValidity(
116
57
  fork: ForkSeq,
117
58
  state: CachedBeaconStateAllForks,
@@ -782,14 +782,17 @@ export class EpochCache {
782
782
  */
783
783
  getBeaconProposer(slot: Slot): ValidatorIndex {
784
784
  const epoch = computeEpochAtSlot(slot);
785
- if (epoch !== this.currentShuffling.epoch) {
786
- throw new EpochCacheError({
787
- code: EpochCacheErrorCode.PROPOSER_EPOCH_MISMATCH,
788
- currentEpoch: this.currentShuffling.epoch,
789
- requestedEpoch: epoch,
790
- });
785
+ if (epoch === this.currentShuffling.epoch) {
786
+ return this.proposers[slot % SLOTS_PER_EPOCH];
787
+ }
788
+ if (epoch === this.nextEpoch) {
789
+ return this.getBeaconProposersNextEpoch()[slot % SLOTS_PER_EPOCH];
791
790
  }
792
- return this.proposers[slot % SLOTS_PER_EPOCH];
791
+ throw new EpochCacheError({
792
+ code: EpochCacheErrorCode.PROPOSER_EPOCH_MISMATCH,
793
+ currentEpoch: this.currentShuffling.epoch,
794
+ requestedEpoch: epoch,
795
+ });
793
796
  }
794
797
 
795
798
  getBeaconProposers(): ValidatorIndex[] {
@@ -37,7 +37,9 @@ export function processPendingDeposits(state: CachedBeaconStateElectra, cache: E
37
37
 
38
38
  for (const deposit of deposits) {
39
39
  // Do not process deposit requests if Eth1 bridge deposits are not yet applied.
40
+ // The eth1 bridge deposit mechanism was removed in Fulu, so this gate only applies pre-Fulu.
40
41
  if (
42
+ fork < ForkSeq.fulu &&
41
43
  // Is deposit request
42
44
  deposit.slot > GENESIS_SLOT &&
43
45
  // There are pending Eth1 bridge deposits
@@ -1,6 +1,7 @@
1
1
  import {ForkSeq, MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params";
2
2
  import {ssz} from "@lodestar/types";
3
3
  import {CachedBeaconStateFulu, EpochTransitionCache} from "../types.js";
4
+ import {FLAG_UNSLASHED} from "../util/attesterStatus.js";
4
5
  import {computeEpochShuffling} from "../util/epochShuffling.js";
5
6
  import {computeProposerIndices} from "../util/seed.js";
6
7
 
@@ -26,7 +27,13 @@ export function processProposerLookahead(
26
27
  // Save shuffling to cache so afterProcessEpoch can reuse it instead of recomputing
27
28
  cache.nextShuffling = shuffling;
28
29
 
29
- const lastEpochProposerLookahead = computeProposerIndices(fork, state, shuffling, epoch);
30
+ const activeIndices =
31
+ fork >= ForkSeq.gloas
32
+ ? // Exclude slashed validators from proposing (EIP-8045)
33
+ shuffling.activeIndices.filter((index) => (cache.flags[index] & FLAG_UNSLASHED) !== 0)
34
+ : shuffling.activeIndices;
35
+
36
+ const lastEpochProposerLookahead = computeProposerIndices(fork, state, {activeIndices}, epoch);
30
37
 
31
38
  state.proposerLookahead = ssz.fulu.ProposerLookahead.toViewDU([
32
39
  ...remainingProposerLookahead,
@@ -1,10 +1,10 @@
1
- import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
1
+ import {PAYLOAD_BUILDER_VERSION, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
2
2
  import {ssz} from "@lodestar/types";
3
3
  import {toPubkeyHex} from "@lodestar/utils";
4
- import {applyDepositForBuilder} from "../block/processDepositRequest.js";
4
+ import {isValidDepositSignature} from "../block/processDeposit.js";
5
5
  import {getCachedBeaconState} from "../cache/stateCache.js";
6
6
  import {CachedBeaconStateFulu, CachedBeaconStateGloas} from "../types.js";
7
- import {initializePtcWindow, isBuilderWithdrawalCredential} from "../util/gloas.js";
7
+ import {addBuilderToRegistry, initializePtcWindow, isBuilderWithdrawalCredential} from "../util/gloas.js";
8
8
  import {isValidatorKnown} from "../util/index.js";
9
9
  import {PendingDepositsLookup} from "../util/pendingDepositsLookup.js";
10
10
 
@@ -48,9 +48,9 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea
48
48
  stateGloasView.currentSyncCommittee = stateGloasCloned.currentSyncCommittee;
49
49
  stateGloasView.nextSyncCommittee = stateGloasCloned.nextSyncCommittee;
50
50
  stateGloasView.latestExecutionPayloadBid.blockHash = stateFulu.latestExecutionPayloadHeader.blockHash;
51
- stateGloasView.latestExecutionPayloadBid.gasLimit = BigInt(stateFulu.latestExecutionPayloadHeader.gasLimit);
52
- stateGloasView.latestExecutionPayloadBid.executionRequestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(
53
- ssz.electra.ExecutionRequests.defaultValue()
51
+ stateGloasView.latestExecutionPayloadBid.gasLimit = stateFulu.latestExecutionPayloadHeader.gasLimit;
52
+ stateGloasView.latestExecutionPayloadBid.executionRequestsRoot = ssz.gloas.ExecutionRequests.hashTreeRoot(
53
+ ssz.gloas.ExecutionRequests.defaultValue()
54
54
  );
55
55
  stateGloasView.nextWithdrawalIndex = stateGloasCloned.nextWithdrawalIndex;
56
56
  stateGloasView.nextWithdrawalValidatorIndex = stateGloasCloned.nextWithdrawalValidatorIndex;
@@ -90,7 +90,8 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea
90
90
  * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.8/specs/gloas/fork.md#new-onboard_builders_from_pending_deposits
91
91
  */
92
92
  function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void {
93
- // Track pubkeys of new builders added when applying deposits
93
+ // Track pubkeys of new builders added when applying deposits. `state.builders` starts empty
94
+ // at the fork, so every builder pubkey here is one added in an earlier iteration.
94
95
  const builderPubkeys = new Set<string>();
95
96
 
96
97
  const pendingDeposits = ssz.electra.PendingDeposits.defaultViewDU();
@@ -109,40 +110,55 @@ function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void
109
110
  continue;
110
111
  }
111
112
 
112
- // `applyDepositForBuilder` can mutate the state and add a builder to the registry, so
113
- // the set of builder pubkeys must be recomputed each iteration. `builderPubkeys` stands
114
- // in for the spec's `[b.pubkey for b in state.builders]`: `state.builders` starts empty
115
- // at the fork, so every builder is one added in a previous iteration of this loop.
116
- if (!builderPubkeys.has(pubkeyHex)) {
117
- // Deposits for non-builders stay in the pending queue. If there is a valid pending
118
- // deposit for a new validator with this pubkey, keep this deposit in the pending
119
- // queue to be applied to that validator later.
120
- if (!isBuilderWithdrawalCredential(deposit.withdrawalCredentials)) {
121
- pendingDeposits.push(deposit);
122
- pendingDepositsLookup.add(deposit, pubkeyHex);
123
- continue;
124
- }
125
- if (pendingDepositsLookup.hasPendingValidator(state.config, pubkeyHex)) {
126
- pendingDeposits.push(deposit);
127
- pendingDepositsLookup.add(deposit, pubkeyHex);
128
- continue;
113
+ if (builderPubkeys.has(pubkeyHex)) {
114
+ // Top up an already-onboarded builder
115
+ // TODO GLOAS: linear search; consider builder pubkey cache when we drop the upgrade-time set
116
+ for (let j = 0; j < state.builders.length; j++) {
117
+ if (toPubkeyHex(state.builders.getReadonly(j).pubkey) === pubkeyHex) {
118
+ state.builders.get(j).balance += deposit.amount;
119
+ break;
120
+ }
129
121
  }
122
+ continue;
130
123
  }
131
124
 
132
- const buildersLenBefore = state.builders.length;
133
- // TODO GLOAS: handle 20k 1ETH deposits on time
134
- // there is a note in the spec https://github.com/ethereum/consensus-specs/pull/5227
135
- applyDepositForBuilder(
125
+ // Deposits for non-builders stay in the pending queue. If there is a valid pending
126
+ // deposit for a new validator with this pubkey, keep this deposit pending so the validator
127
+ // can pick it up later.
128
+ if (!isBuilderWithdrawalCredential(deposit.withdrawalCredentials)) {
129
+ pendingDeposits.push(deposit);
130
+ pendingDepositsLookup.add(deposit, pubkeyHex);
131
+ continue;
132
+ }
133
+ if (pendingDepositsLookup.hasPendingValidator(state.config, pubkeyHex)) {
134
+ pendingDeposits.push(deposit);
135
+ pendingDepositsLookup.add(deposit, pubkeyHex);
136
+ continue;
137
+ }
138
+
139
+ // Verify the deposit signature (proof of possession). If invalid the deposit is silently
140
+ // dropped — stake is forfeited, matching the validator deposit contract behavior.
141
+ if (
142
+ !isValidDepositSignature(
143
+ state.config,
144
+ deposit.pubkey,
145
+ deposit.withdrawalCredentials,
146
+ deposit.amount,
147
+ deposit.signature
148
+ )
149
+ ) {
150
+ continue;
151
+ }
152
+
153
+ addBuilderToRegistry(
136
154
  state,
137
155
  deposit.pubkey,
138
- deposit.withdrawalCredentials,
156
+ PAYLOAD_BUILDER_VERSION,
157
+ deposit.withdrawalCredentials.subarray(12),
139
158
  deposit.amount,
140
- deposit.signature,
141
159
  deposit.slot
142
160
  );
143
- if (state.builders.length > buildersLenBefore) {
144
- builderPubkeys.add(pubkeyHex);
145
- }
161
+ builderPubkeys.add(pubkeyHex);
146
162
  }
147
163
 
148
164
  state.pendingDeposits = pendingDeposits;
@@ -8,6 +8,7 @@ import {
8
8
  BlindedBeaconBlock,
9
9
  BuilderIndex,
10
10
  Bytes32,
11
+ CommitteeIndex,
11
12
  Epoch,
12
13
  ExecutionPayloadBid,
13
14
  ExecutionPayloadHeader,
@@ -452,6 +453,14 @@ export class BeaconStateView implements IBeaconStateViewLatestFork {
452
453
  return this.cachedState.epochCtx.getShufflingAtEpoch(epoch);
453
454
  }
454
455
 
456
+ getBeaconCommittee(slot: Slot, index: CommitteeIndex): Uint32Array {
457
+ return this.cachedState.epochCtx.getBeaconCommittee(slot, index);
458
+ }
459
+
460
+ getBeaconCommitteeCountPerSlot(epoch: Epoch): number {
461
+ return this.cachedState.epochCtx.getCommitteeCountPerSlot(epoch);
462
+ }
463
+
455
464
  get previousDecisionRoot(): RootHex {
456
465
  return this.cachedState.epochCtx.previousDecisionRoot;
457
466
  }
@@ -498,6 +507,14 @@ export class BeaconStateView implements IBeaconStateViewLatestFork {
498
507
  return this.cachedState.epochCtx.getBeaconProposer(slot);
499
508
  }
500
509
 
510
+ getBeaconProposerOrNull(slot: Slot): ValidatorIndex | null {
511
+ try {
512
+ return this.cachedState.epochCtx.getBeaconProposer(slot);
513
+ } catch {
514
+ return null;
515
+ }
516
+ }
517
+
501
518
  computeAnchorCheckpoint(): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} {
502
519
  return computeAnchorCheckpoint(this.config, this.cachedState);
503
520
  }
@@ -814,7 +831,7 @@ export class BeaconStateView implements IBeaconStateViewLatestFork {
814
831
  /**
815
832
  * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/validator.md#executionpayload
816
833
  */
817
- withParentPayloadApplied(executionRequests: electra.ExecutionRequests): IBeaconStateViewGloas {
834
+ withParentPayloadApplied(executionRequests: gloas.ExecutionRequests): IBeaconStateViewGloas {
818
835
  if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.gloas) {
819
836
  throw new Error("withParentPayloadApplied is not available before Gloas");
820
837
  }