@lodestar/state-transition 1.44.0-dev.7418ddd9f7 → 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 (86) 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/epoch/processPendingDeposits.d.ts.map +1 -1
  36. package/lib/epoch/processPendingDeposits.js +4 -3
  37. package/lib/epoch/processPendingDeposits.js.map +1 -1
  38. package/lib/epoch/processProposerLookahead.d.ts.map +1 -1
  39. package/lib/epoch/processProposerLookahead.js +7 -2
  40. package/lib/epoch/processProposerLookahead.js.map +1 -1
  41. package/lib/slot/upgradeStateToGloas.js +35 -29
  42. package/lib/slot/upgradeStateToGloas.js.map +1 -1
  43. package/lib/stateView/beaconStateView.d.ts +4 -2
  44. package/lib/stateView/beaconStateView.d.ts.map +1 -1
  45. package/lib/stateView/beaconStateView.js +6 -0
  46. package/lib/stateView/beaconStateView.js.map +1 -1
  47. package/lib/stateView/interface.d.ts +29 -2
  48. package/lib/stateView/interface.d.ts.map +1 -1
  49. package/lib/stateView/interface.js.map +1 -1
  50. package/lib/stateView/nativeBeaconStateView.d.ts +230 -0
  51. package/lib/stateView/nativeBeaconStateView.d.ts.map +1 -0
  52. package/lib/stateView/nativeBeaconStateView.js +717 -0
  53. package/lib/stateView/nativeBeaconStateView.js.map +1 -0
  54. package/lib/stateView/stateViewFactory.d.ts.map +1 -1
  55. package/lib/stateView/stateViewFactory.js +2 -0
  56. package/lib/stateView/stateViewFactory.js.map +1 -1
  57. package/lib/util/epochShuffling.js +1 -1
  58. package/lib/util/epochShuffling.js.map +1 -1
  59. package/lib/util/gloas.d.ts +17 -0
  60. package/lib/util/gloas.d.ts.map +1 -1
  61. package/lib/util/gloas.js +59 -1
  62. package/lib/util/gloas.js.map +1 -1
  63. package/lib/util/shuffling.d.ts +2 -2
  64. package/lib/util/shuffling.d.ts.map +1 -1
  65. package/lib/util/shuffling.js +4 -3
  66. package/lib/util/shuffling.js.map +1 -1
  67. package/package.json +7 -8
  68. package/src/block/index.ts +6 -1
  69. package/src/block/processBuilderDepositRequest.ts +43 -0
  70. package/src/block/processBuilderExitRequest.ts +39 -0
  71. package/src/block/processDepositRequest.ts +4 -124
  72. package/src/block/processExecutionPayloadBid.ts +24 -9
  73. package/src/block/processOperations.ts +3 -2
  74. package/src/block/processParentExecutionPayload.ts +23 -11
  75. package/src/block/processProposerSlashing.ts +9 -4
  76. package/src/block/processVoluntaryExit.ts +2 -61
  77. package/src/epoch/processPendingDeposits.ts +2 -0
  78. package/src/epoch/processProposerLookahead.ts +8 -1
  79. package/src/slot/upgradeStateToGloas.ts +49 -33
  80. package/src/stateView/beaconStateView.ts +10 -1
  81. package/src/stateView/interface.ts +33 -1
  82. package/src/stateView/nativeBeaconStateView.ts +915 -0
  83. package/src/stateView/stateViewFactory.ts +2 -0
  84. package/src/util/epochShuffling.ts +1 -1
  85. package/src/util/gloas.ts +76 -1
  86. package/src/util/shuffling.ts +4 -3
@@ -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,
@@ -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
  }
@@ -822,7 +831,7 @@ export class BeaconStateView implements IBeaconStateViewLatestFork {
822
831
  /**
823
832
  * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/validator.md#executionpayload
824
833
  */
825
- withParentPayloadApplied(executionRequests: electra.ExecutionRequests): IBeaconStateViewGloas {
834
+ withParentPayloadApplied(executionRequests: gloas.ExecutionRequests): IBeaconStateViewGloas {
826
835
  if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.gloas) {
827
836
  throw new Error("withParentPayloadApplied is not available before Gloas");
828
837
  }
@@ -23,6 +23,7 @@ import {
23
23
  BlindedBeaconBlock,
24
24
  BuilderIndex,
25
25
  Bytes32,
26
+ CommitteeIndex,
26
27
  Epoch,
27
28
  ExecutionPayloadBid,
28
29
  ExecutionPayloadHeader,
@@ -75,6 +76,8 @@ export interface IBeaconStateView {
75
76
 
76
77
  // Shuffling and committees
77
78
  getShufflingAtEpoch(epoch: Epoch): EpochShuffling;
79
+ getBeaconCommittee(slot: Slot, index: CommitteeIndex): Uint32Array;
80
+ getBeaconCommitteeCountPerSlot(epoch: Epoch): number;
78
81
  // Decision roots
79
82
  previousDecisionRoot: RootHex;
80
83
  currentDecisionRoot: RootHex;
@@ -261,7 +264,7 @@ export interface IBeaconStateViewGloas extends IBeaconStateViewFulu {
261
264
  * operation selection (e.g. voluntary exits) see the same post-apply state that the block
262
265
  * processor will see at import.
263
266
  */
264
- withParentPayloadApplied(executionRequests: electra.ExecutionRequests): IBeaconStateViewGloas;
267
+ withParentPayloadApplied(executionRequests: gloas.ExecutionRequests): IBeaconStateViewGloas;
265
268
  }
266
269
 
267
270
  /**
@@ -278,6 +281,35 @@ export type IBeaconStateViewLatestFork = Omit<
278
281
  latestExecutionPayloadHeader: ExecutionPayloadHeader;
279
282
  payloadBlockNumber: number;
280
283
  };
284
+
285
+ /**
286
+ * Contract a BeaconStateView backing implementation must satisfy.
287
+ *
288
+ * Differs from `IBeaconStateViewLatestFork` in two ways:
289
+ * - `executionPayloadAvailability` is a raw `{uint8Array, bitLen}` POJO — a
290
+ * native (`.node`) binding cannot construct a `BitArray` across FFI.
291
+ * `NativeBeaconStateView` lifts it back to `BitArray` for beacon-node.
292
+ * - Methods that produce another view (`stateTransition`, `processSlots`,
293
+ * `loadOtherState`, `withParentPayloadApplied`) return `IBeaconStateViewNative`
294
+ * so callers can re-wrap without an `as unknown` cast. Param lists are reused
295
+ * via `Parameters<...>` to avoid duplicating signatures.
296
+ *
297
+ * The TS-side `BeaconStateView` also structurally satisfies this contract since
298
+ * `BitArray` exposes `uint8Array` and `bitLen`.
299
+ */
300
+ export type IBeaconStateViewNative = Omit<
301
+ IBeaconStateViewLatestFork,
302
+ "executionPayloadAvailability" | "loadOtherState" | "stateTransition" | "processSlots" | "withParentPayloadApplied"
303
+ > & {
304
+ executionPayloadAvailability: {uint8Array: Uint8Array; bitLen: number};
305
+ loadOtherState(...args: Parameters<IBeaconStateViewLatestFork["loadOtherState"]>): IBeaconStateViewNative;
306
+ stateTransition(...args: Parameters<IBeaconStateViewLatestFork["stateTransition"]>): IBeaconStateViewNative;
307
+ processSlots(...args: Parameters<IBeaconStateViewLatestFork["processSlots"]>): IBeaconStateViewNative;
308
+ withParentPayloadApplied(
309
+ ...args: Parameters<IBeaconStateViewLatestFork["withParentPayloadApplied"]>
310
+ ): IBeaconStateViewNative;
311
+ };
312
+
281
313
  export function isStatePostAltair(state: IBeaconStateView): state is IBeaconStateViewAltair {
282
314
  return isForkPostAltair(state.forkName);
283
315
  }