@lodestar/state-transition 1.40.0-dev.3be9500fa9 → 1.40.0-dev.4acd3ce568

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 (50) hide show
  1. package/lib/block/index.d.ts +1 -0
  2. package/lib/block/index.d.ts.map +1 -1
  3. package/lib/block/index.js +1 -0
  4. package/lib/block/index.js.map +1 -1
  5. package/lib/block/processDepositRequest.d.ts +8 -2
  6. package/lib/block/processDepositRequest.d.ts.map +1 -1
  7. package/lib/block/processDepositRequest.js +81 -8
  8. package/lib/block/processDepositRequest.js.map +1 -1
  9. package/lib/block/processExecutionPayloadBid.d.ts.map +1 -1
  10. package/lib/block/processExecutionPayloadBid.js +14 -27
  11. package/lib/block/processExecutionPayloadBid.js.map +1 -1
  12. package/lib/block/processExecutionPayloadEnvelope.d.ts.map +1 -1
  13. package/lib/block/processExecutionPayloadEnvelope.js +25 -24
  14. package/lib/block/processExecutionPayloadEnvelope.js.map +1 -1
  15. package/lib/block/processOperations.js +1 -1
  16. package/lib/block/processOperations.js.map +1 -1
  17. package/lib/block/processVoluntaryExit.d.ts +1 -1
  18. package/lib/block/processVoluntaryExit.d.ts.map +1 -1
  19. package/lib/block/processVoluntaryExit.js +44 -2
  20. package/lib/block/processVoluntaryExit.js.map +1 -1
  21. package/lib/block/processWithdrawals.d.ts +1 -0
  22. package/lib/block/processWithdrawals.d.ts.map +1 -1
  23. package/lib/block/processWithdrawals.js +115 -66
  24. package/lib/block/processWithdrawals.js.map +1 -1
  25. package/lib/epoch/processBuilderPendingPayments.d.ts.map +1 -1
  26. package/lib/epoch/processBuilderPendingPayments.js +1 -4
  27. package/lib/epoch/processBuilderPendingPayments.js.map +1 -1
  28. package/lib/util/electra.d.ts.map +1 -1
  29. package/lib/util/electra.js +1 -2
  30. package/lib/util/electra.js.map +1 -1
  31. package/lib/util/gloas.d.ts +43 -3
  32. package/lib/util/gloas.d.ts.map +1 -1
  33. package/lib/util/gloas.js +93 -5
  34. package/lib/util/gloas.js.map +1 -1
  35. package/lib/util/validator.d.ts +5 -0
  36. package/lib/util/validator.d.ts.map +1 -1
  37. package/lib/util/validator.js +25 -2
  38. package/lib/util/validator.js.map +1 -1
  39. package/package.json +7 -7
  40. package/src/block/index.ts +1 -0
  41. package/src/block/processDepositRequest.ts +101 -8
  42. package/src/block/processExecutionPayloadBid.ts +18 -40
  43. package/src/block/processExecutionPayloadEnvelope.ts +33 -29
  44. package/src/block/processOperations.ts +1 -1
  45. package/src/block/processVoluntaryExit.ts +59 -4
  46. package/src/block/processWithdrawals.ts +162 -70
  47. package/src/epoch/processBuilderPendingPayments.ts +1 -5
  48. package/src/util/electra.ts +1 -4
  49. package/src/util/gloas.ts +109 -8
  50. package/src/util/validator.ts +31 -1
@@ -2,22 +2,29 @@ import {byteArrayEquals} from "@chainsafe/ssz";
2
2
  import {
3
3
  FAR_FUTURE_EPOCH,
4
4
  ForkSeq,
5
+ MAX_BUILDERS_PER_WITHDRAWALS_SWEEP,
5
6
  MAX_EFFECTIVE_BALANCE,
6
7
  MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP,
7
8
  MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP,
8
9
  MAX_WITHDRAWALS_PER_PAYLOAD,
9
10
  MIN_ACTIVATION_BALANCE,
10
11
  } from "@lodestar/params";
11
- import {ValidatorIndex, capella, ssz} from "@lodestar/types";
12
+ import {BuilderIndex, ValidatorIndex, capella, ssz} from "@lodestar/types";
12
13
  import {toRootHex} from "@lodestar/utils";
13
14
  import {CachedBeaconStateCapella, CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
14
- import {isBuilderPaymentWithdrawable, isParentBlockFull} from "../util/gloas.ts";
15
+ import {
16
+ convertBuilderIndexToValidatorIndex,
17
+ convertValidatorIndexToBuilderIndex,
18
+ isBuilderIndex,
19
+ isParentBlockFull,
20
+ } from "../util/gloas.ts";
15
21
  import {
16
22
  decreaseBalance,
17
23
  getMaxEffectiveBalance,
18
24
  hasEth1WithdrawalCredential,
19
25
  hasExecutionWithdrawalCredential,
20
26
  isCapellaPayloadHeader,
27
+ isPartiallyWithdrawableValidator,
21
28
  } from "../util/index.js";
22
29
 
23
30
  export function processWithdrawals(
@@ -32,9 +39,14 @@ export function processWithdrawals(
32
39
 
33
40
  // processedBuilderWithdrawalsCount is withdrawals coming from builder payment since gloas (EIP-7732)
34
41
  // processedPartialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002)
42
+ // processedBuildersSweepCount is withdrawals from builder sweep since gloas (EIP-7732)
35
43
  // processedValidatorSweepCount is withdrawals coming from validator sweep
36
- const {expectedWithdrawals, processedBuilderWithdrawalsCount, processedPartialWithdrawalsCount} =
37
- getExpectedWithdrawals(fork, state);
44
+ const {
45
+ expectedWithdrawals,
46
+ processedBuilderWithdrawalsCount,
47
+ processedPartialWithdrawalsCount,
48
+ processedBuildersSweepCount,
49
+ } = getExpectedWithdrawals(fork, state);
38
50
  const numWithdrawals = expectedWithdrawals.length;
39
51
 
40
52
  // After gloas, withdrawals are verified later in processExecutionPayloadEnvelope
@@ -78,20 +90,20 @@ export function processWithdrawals(
78
90
 
79
91
  if (fork >= ForkSeq.gloas) {
80
92
  const stateGloas = state as CachedBeaconStateGloas;
81
- stateGloas.latestWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(expectedWithdrawals);
82
-
83
- const unprocessedWithdrawals = stateGloas.builderPendingWithdrawals
84
- .getAllReadonly()
85
- .slice(0, processedBuilderWithdrawalsCount)
86
- .filter((w) => !isBuilderPaymentWithdrawable(stateGloas, w));
87
- const remainingWithdrawals = stateGloas.builderPendingWithdrawals
88
- .sliceFrom(processedBuilderWithdrawalsCount)
89
- .getAllReadonly();
90
-
91
- stateGloas.builderPendingWithdrawals = ssz.gloas.BeaconState.fields.builderPendingWithdrawals.toViewDU([
92
- ...unprocessedWithdrawals,
93
- ...remainingWithdrawals,
94
- ]);
93
+
94
+ // Store expected withdrawals for verification
95
+ stateGloas.payloadExpectedWithdrawals = ssz.capella.Withdrawals.toViewDU(expectedWithdrawals);
96
+
97
+ // Update builder pending withdrawals queue
98
+ stateGloas.builderPendingWithdrawals = stateGloas.builderPendingWithdrawals.sliceFrom(
99
+ processedBuilderWithdrawalsCount
100
+ );
101
+
102
+ // Update next builder index for sweep
103
+ if (stateGloas.builders.length > 0) {
104
+ const nextIndex = stateGloas.nextWithdrawalBuilderIndex + processedBuildersSweepCount;
105
+ stateGloas.nextWithdrawalBuilderIndex = nextIndex % stateGloas.builders.length;
106
+ }
95
107
  }
96
108
  // Update the nextWithdrawalIndex
97
109
  // https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.0/specs/capella/beacon-chain.md#new-update_next_withdrawal_index
@@ -116,10 +128,11 @@ export function processWithdrawals(
116
128
  function getBuilderWithdrawals(
117
129
  state: CachedBeaconStateGloas,
118
130
  withdrawalIndex: number,
119
- balanceAfterWithdrawals: Map<ValidatorIndex, number>
131
+ priorWithdrawals: capella.Withdrawal[],
132
+ builderBalanceAfterWithdrawals: Map<number, number>
120
133
  ): {builderWithdrawals: capella.Withdrawal[]; withdrawalIndex: number; processedCount: number} {
134
+ const withdrawalsLimit = MAX_WITHDRAWALS_PER_PAYLOAD - 1;
121
135
  const builderWithdrawals: capella.Withdrawal[] = [];
122
- const epoch = state.epochCtx.epoch;
123
136
  const allBuilderPendingWithdrawals =
124
137
  state.builderPendingWithdrawals.length <= MAX_WITHDRAWALS_PER_PAYLOAD
125
138
  ? state.builderPendingWithdrawals.getAllReadonly()
@@ -127,55 +140,100 @@ function getBuilderWithdrawals(
127
140
 
128
141
  let processedCount = 0;
129
142
  for (let i = 0; i < state.builderPendingWithdrawals.length; i++) {
143
+ // Check combined length against limit
144
+ const allWithdrawals = priorWithdrawals.length + builderWithdrawals.length;
145
+ if (allWithdrawals >= withdrawalsLimit) {
146
+ break;
147
+ }
148
+
130
149
  const withdrawal = allBuilderPendingWithdrawals
131
150
  ? allBuilderPendingWithdrawals[i]
132
151
  : state.builderPendingWithdrawals.getReadonly(i);
133
152
 
134
- if (withdrawal.withdrawableEpoch > epoch || builderWithdrawals.length === MAX_WITHDRAWALS_PER_PAYLOAD) {
135
- break;
153
+ const builderIndex = withdrawal.builderIndex;
154
+
155
+ // Get builder balance (from builder.balance, not state.balances)
156
+ let balance = builderBalanceAfterWithdrawals.get(builderIndex);
157
+ if (balance === undefined) {
158
+ balance = state.builders.getReadonly(builderIndex).balance;
159
+ builderBalanceAfterWithdrawals.set(builderIndex, balance);
136
160
  }
137
161
 
138
- if (isBuilderPaymentWithdrawable(state, withdrawal)) {
139
- const builderIndex = withdrawal.builderIndex;
140
- const builder = state.validators.get(withdrawal.builderIndex);
162
+ // Use the withdrawal amount directly as specified in the spec
163
+ builderWithdrawals.push({
164
+ index: withdrawalIndex,
165
+ validatorIndex: convertBuilderIndexToValidatorIndex(builderIndex),
166
+ address: withdrawal.feeRecipient,
167
+ amount: BigInt(withdrawal.amount),
168
+ });
169
+ withdrawalIndex++;
170
+ builderBalanceAfterWithdrawals.set(builderIndex, balance - withdrawal.amount);
141
171
 
142
- let balance = balanceAfterWithdrawals.get(builderIndex);
143
- if (balance === undefined) {
144
- balance = state.balances.get(builderIndex);
145
- balanceAfterWithdrawals.set(builderIndex, balance);
146
- }
172
+ processedCount++;
173
+ }
147
174
 
148
- let withdrawableBalance = 0;
175
+ return {builderWithdrawals, withdrawalIndex, processedCount};
176
+ }
149
177
 
150
- if (builder.slashed) {
151
- withdrawableBalance = balance < withdrawal.amount ? balance : withdrawal.amount;
152
- } else if (balance > MIN_ACTIVATION_BALANCE) {
153
- withdrawableBalance =
154
- balance - MIN_ACTIVATION_BALANCE < withdrawal.amount ? balance - MIN_ACTIVATION_BALANCE : withdrawal.amount;
155
- }
178
+ function getBuildersSweepWithdrawals(
179
+ state: CachedBeaconStateGloas,
180
+ withdrawalIndex: number,
181
+ numPriorWithdrawal: number,
182
+ builderBalanceAfterWithdrawals: Map<number, number>
183
+ ): {buildersSweepWithdrawals: capella.Withdrawal[]; withdrawalIndex: number; processedCount: number} {
184
+ const withdrawalsLimit = MAX_WITHDRAWALS_PER_PAYLOAD - 1;
185
+ const buildersSweepWithdrawals: capella.Withdrawal[] = [];
186
+ const epoch = state.epochCtx.epoch;
187
+ const builders = state.builders;
156
188
 
157
- if (withdrawableBalance > 0) {
158
- builderWithdrawals.push({
159
- index: withdrawalIndex,
160
- validatorIndex: withdrawal.builderIndex,
161
- address: withdrawal.feeRecipient,
162
- amount: BigInt(withdrawableBalance),
163
- });
164
- withdrawalIndex++;
165
- balanceAfterWithdrawals.set(builderIndex, balance - withdrawableBalance);
166
- }
189
+ // Return early if no builders
190
+ if (builders.length === 0) {
191
+ return {buildersSweepWithdrawals, withdrawalIndex, processedCount: 0};
192
+ }
193
+
194
+ const buildersLimit = Math.min(builders.length, MAX_BUILDERS_PER_WITHDRAWALS_SWEEP);
195
+ let processedCount = 0;
196
+
197
+ for (let n = 0; n < buildersLimit; n++) {
198
+ if (buildersSweepWithdrawals.length + numPriorWithdrawal >= withdrawalsLimit) {
199
+ break;
200
+ }
201
+
202
+ // Get next builder in turn
203
+ const builderIndex = (state.nextWithdrawalBuilderIndex + n) % builders.length;
204
+ const builder = builders.getReadonly(builderIndex);
205
+
206
+ // Get builder balance
207
+ let balance = builderBalanceAfterWithdrawals.get(builderIndex);
208
+ if (balance === undefined) {
209
+ balance = builder.balance;
210
+ builderBalanceAfterWithdrawals.set(builderIndex, balance);
167
211
  }
212
+
213
+ // Check if builder is withdrawable and has balance
214
+ if (builder.withdrawableEpoch <= epoch && balance > 0) {
215
+ // Withdraw full balance to builder's execution address
216
+ buildersSweepWithdrawals.push({
217
+ index: withdrawalIndex,
218
+ validatorIndex: convertBuilderIndexToValidatorIndex(builderIndex),
219
+ address: builder.executionAddress,
220
+ amount: BigInt(balance),
221
+ });
222
+ withdrawalIndex++;
223
+ builderBalanceAfterWithdrawals.set(builderIndex, 0);
224
+ }
225
+
168
226
  processedCount++;
169
227
  }
170
228
 
171
- return {builderWithdrawals, withdrawalIndex, processedCount};
229
+ return {buildersSweepWithdrawals, withdrawalIndex, processedCount};
172
230
  }
173
231
 
174
232
  function getPendingPartialWithdrawals(
175
233
  state: CachedBeaconStateElectra,
176
234
  withdrawalIndex: number,
177
235
  numPriorWithdrawal: number,
178
- balanceAfterWithdrawals: Map<ValidatorIndex, number>
236
+ validatorBalanceAfterWithdrawals: Map<ValidatorIndex, number>
179
237
  ): {pendingPartialWithdrawals: capella.Withdrawal[]; withdrawalIndex: number; processedCount: number} {
180
238
  const epoch = state.epochCtx.epoch;
181
239
  const pendingPartialWithdrawals: capella.Withdrawal[] = [];
@@ -203,17 +261,17 @@ function getPendingPartialWithdrawals(
203
261
  : state.pendingPartialWithdrawals.getReadonly(i);
204
262
  if (
205
263
  withdrawal.withdrawableEpoch > epoch ||
206
- pendingPartialWithdrawals.length + numPriorWithdrawal === partialWithdrawalBound
264
+ pendingPartialWithdrawals.length + numPriorWithdrawal >= partialWithdrawalBound
207
265
  ) {
208
266
  break;
209
267
  }
210
268
 
211
269
  const validatorIndex = withdrawal.validatorIndex;
212
270
  const validator = validators.getReadonly(validatorIndex);
213
- let balance = balanceAfterWithdrawals.get(validatorIndex);
271
+ let balance = validatorBalanceAfterWithdrawals.get(validatorIndex);
214
272
  if (balance === undefined) {
215
273
  balance = state.balances.get(validatorIndex);
216
- balanceAfterWithdrawals.set(validatorIndex, balance);
274
+ validatorBalanceAfterWithdrawals.set(validatorIndex, balance);
217
275
  }
218
276
 
219
277
  if (
@@ -231,7 +289,7 @@ function getPendingPartialWithdrawals(
231
289
  amount: withdrawableBalance,
232
290
  });
233
291
  withdrawalIndex++;
234
- balanceAfterWithdrawals.set(validatorIndex, balance - Number(withdrawableBalance));
292
+ validatorBalanceAfterWithdrawals.set(validatorIndex, balance - Number(withdrawableBalance));
235
293
  }
236
294
  processedCount++;
237
295
  }
@@ -244,7 +302,7 @@ function getValidatorsSweepWithdrawals(
244
302
  state: CachedBeaconStateCapella | CachedBeaconStateElectra | CachedBeaconStateGloas,
245
303
  withdrawalIndex: number,
246
304
  numPriorWithdrawal: number,
247
- balanceAfterWithdrawals: Map<ValidatorIndex, number>
305
+ validatorBalanceAfterWithdrawals: Map<ValidatorIndex, number>
248
306
  ): {sweepWithdrawals: capella.Withdrawal[]; processedCount: number} {
249
307
  const sweepWithdrawals: capella.Withdrawal[] = [];
250
308
  const epoch = state.epochCtx.epoch;
@@ -264,13 +322,13 @@ function getValidatorsSweepWithdrawals(
264
322
  const validatorIndex = (nextWithdrawalValidatorIndex + n) % validators.length;
265
323
 
266
324
  const validator = validators.getReadonly(validatorIndex);
267
- let balance = balanceAfterWithdrawals.get(validatorIndex);
325
+ let balance = validatorBalanceAfterWithdrawals.get(validatorIndex);
268
326
  if (balance === undefined) {
269
327
  balance = balances.get(validatorIndex);
270
- balanceAfterWithdrawals.set(validatorIndex, balance);
328
+ validatorBalanceAfterWithdrawals.set(validatorIndex, balance);
271
329
  }
272
330
 
273
- const {withdrawableEpoch, withdrawalCredentials, effectiveBalance} = validator;
331
+ const {withdrawableEpoch, withdrawalCredentials} = validator;
274
332
  const hasWithdrawableCredentials = isPostElectra
275
333
  ? hasExecutionWithdrawalCredential(withdrawalCredentials)
276
334
  : hasEth1WithdrawalCredential(withdrawalCredentials);
@@ -290,13 +348,11 @@ function getValidatorsSweepWithdrawals(
290
348
  amount: BigInt(balance),
291
349
  });
292
350
  withdrawalIndex++;
293
- balanceAfterWithdrawals.set(validatorIndex, 0);
294
- } else if (
295
- effectiveBalance === (isPostElectra ? getMaxEffectiveBalance(withdrawalCredentials) : MAX_EFFECTIVE_BALANCE) &&
296
- balance > effectiveBalance
297
- ) {
351
+ validatorBalanceAfterWithdrawals.set(validatorIndex, 0);
352
+ } else if (isPartiallyWithdrawableValidator(fork, validator, balance)) {
298
353
  // capella partial withdrawal
299
- const partialAmount = balance - effectiveBalance;
354
+ const maxEffectiveBalance = isPostElectra ? getMaxEffectiveBalance(withdrawalCredentials) : MAX_EFFECTIVE_BALANCE;
355
+ const partialAmount = balance - maxEffectiveBalance;
300
356
  sweepWithdrawals.push({
301
357
  index: withdrawalIndex,
302
358
  validatorIndex,
@@ -304,7 +360,7 @@ function getValidatorsSweepWithdrawals(
304
360
  amount: BigInt(partialAmount),
305
361
  });
306
362
  withdrawalIndex++;
307
- balanceAfterWithdrawals.set(validatorIndex, balance - partialAmount);
363
+ validatorBalanceAfterWithdrawals.set(validatorIndex, balance - partialAmount);
308
364
  }
309
365
  processedCount++;
310
366
  }
@@ -317,7 +373,16 @@ function applyWithdrawals(
317
373
  withdrawals: capella.Withdrawal[]
318
374
  ): void {
319
375
  for (const withdrawal of withdrawals) {
320
- decreaseBalance(state, withdrawal.validatorIndex, Number(withdrawal.amount));
376
+ if (isBuilderIndex(withdrawal.validatorIndex)) {
377
+ // Handle builder withdrawal
378
+ const builderIndex = convertValidatorIndexToBuilderIndex(withdrawal.validatorIndex);
379
+ const builder = (state as CachedBeaconStateGloas).builders.get(builderIndex);
380
+ const withdrawalAmount = Number(withdrawal.amount);
381
+ builder.balance -= Math.min(withdrawalAmount, builder.balance);
382
+ } else {
383
+ // Handle validator withdrawal
384
+ decreaseBalance(state, withdrawal.validatorIndex, Number(withdrawal.amount));
385
+ }
321
386
  }
322
387
  }
323
388
 
@@ -328,6 +393,7 @@ export function getExpectedWithdrawals(
328
393
  expectedWithdrawals: capella.Withdrawal[];
329
394
  processedBuilderWithdrawalsCount: number;
330
395
  processedPartialWithdrawalsCount: number;
396
+ processedBuildersSweepCount: number;
331
397
  processedValidatorSweepCount: number;
332
398
  } {
333
399
  if (fork < ForkSeq.capella) {
@@ -337,20 +403,28 @@ export function getExpectedWithdrawals(
337
403
  let withdrawalIndex = state.nextWithdrawalIndex;
338
404
 
339
405
  const expectedWithdrawals: capella.Withdrawal[] = [];
340
- // Map to track balances after applying withdrawals
406
+ // Separate maps to track balances after applying withdrawals
341
407
  // https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.0/specs/capella/beacon-chain.md#new-get_balance_after_withdrawals
342
- const balanceAfterWithdrawals = new Map<ValidatorIndex, number>();
408
+ const builderBalanceAfterWithdrawals = new Map<BuilderIndex, number>();
409
+ const validatorBalanceAfterWithdrawals = new Map<ValidatorIndex, number>();
343
410
  // partialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002)
344
411
  let processedPartialWithdrawalsCount = 0;
345
412
  // builderWithdrawalsCount is withdrawals coming from builder payments since Gloas (EIP-7732)
346
413
  let processedBuilderWithdrawalsCount = 0;
414
+ // buildersSweepCount is withdrawals from builder sweep since Gloas (EIP-7732)
415
+ let processedBuildersSweepCount = 0;
347
416
 
348
417
  if (fork >= ForkSeq.gloas) {
349
418
  const {
350
419
  builderWithdrawals,
351
420
  withdrawalIndex: newWithdrawalIndex,
352
421
  processedCount,
353
- } = getBuilderWithdrawals(state as CachedBeaconStateGloas, withdrawalIndex, balanceAfterWithdrawals);
422
+ } = getBuilderWithdrawals(
423
+ state as CachedBeaconStateGloas,
424
+ withdrawalIndex,
425
+ expectedWithdrawals,
426
+ builderBalanceAfterWithdrawals
427
+ );
354
428
 
355
429
  expectedWithdrawals.push(...builderWithdrawals);
356
430
  withdrawalIndex = newWithdrawalIndex;
@@ -366,7 +440,7 @@ export function getExpectedWithdrawals(
366
440
  state as CachedBeaconStateElectra,
367
441
  withdrawalIndex,
368
442
  expectedWithdrawals.length,
369
- balanceAfterWithdrawals
443
+ validatorBalanceAfterWithdrawals
370
444
  );
371
445
 
372
446
  expectedWithdrawals.push(...pendingPartialWithdrawals);
@@ -374,12 +448,29 @@ export function getExpectedWithdrawals(
374
448
  processedPartialWithdrawalsCount = processedCount;
375
449
  }
376
450
 
451
+ if (fork >= ForkSeq.gloas) {
452
+ const {
453
+ buildersSweepWithdrawals,
454
+ withdrawalIndex: newWithdrawalIndex,
455
+ processedCount,
456
+ } = getBuildersSweepWithdrawals(
457
+ state as CachedBeaconStateGloas,
458
+ withdrawalIndex,
459
+ expectedWithdrawals.length,
460
+ builderBalanceAfterWithdrawals
461
+ );
462
+
463
+ expectedWithdrawals.push(...buildersSweepWithdrawals);
464
+ withdrawalIndex = newWithdrawalIndex;
465
+ processedBuildersSweepCount = processedCount;
466
+ }
467
+
377
468
  const {sweepWithdrawals, processedCount: processedValidatorSweepCount} = getValidatorsSweepWithdrawals(
378
469
  fork,
379
470
  state,
380
471
  withdrawalIndex,
381
472
  expectedWithdrawals.length,
382
- balanceAfterWithdrawals
473
+ validatorBalanceAfterWithdrawals
383
474
  );
384
475
 
385
476
  expectedWithdrawals.push(...sweepWithdrawals);
@@ -388,6 +479,7 @@ export function getExpectedWithdrawals(
388
479
  expectedWithdrawals,
389
480
  processedBuilderWithdrawalsCount,
390
481
  processedPartialWithdrawalsCount,
482
+ processedBuildersSweepCount,
391
483
  processedValidatorSweepCount,
392
484
  };
393
485
  }
@@ -1,7 +1,6 @@
1
1
  import {SLOTS_PER_EPOCH} from "@lodestar/params";
2
2
  import {ssz} from "@lodestar/types";
3
3
  import {CachedBeaconStateGloas} from "../types.ts";
4
- import {computeExitEpochAndUpdateChurn} from "../util/epoch.ts";
5
4
  import {getBuilderPaymentQuorumThreshold} from "../util/gloas.ts";
6
5
 
7
6
  /**
@@ -12,10 +11,7 @@ export function processBuilderPendingPayments(state: CachedBeaconStateGloas): vo
12
11
 
13
12
  for (let i = 0; i < SLOTS_PER_EPOCH; i++) {
14
13
  const payment = state.builderPendingPayments.get(i);
15
- if (payment.weight > quorum) {
16
- const exitQueueEpoch = computeExitEpochAndUpdateChurn(state, BigInt(payment.withdrawal.amount));
17
- payment.withdrawal.withdrawableEpoch = exitQueueEpoch + state.config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY;
18
-
14
+ if (payment.weight >= quorum) {
19
15
  state.builderPendingWithdrawals.push(payment.withdrawal);
20
16
  }
21
17
  }
@@ -3,12 +3,9 @@ import {ValidatorIndex, ssz} from "@lodestar/types";
3
3
  import {G2_POINT_AT_INFINITY} from "../constants/constants.js";
4
4
  import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
5
5
  import {hasEth1WithdrawalCredential} from "./capella.js";
6
- import {hasBuilderWithdrawalCredential} from "./gloas.ts";
7
6
 
8
7
  export function hasCompoundingWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean {
9
- return (
10
- withdrawalCredentials[0] === COMPOUNDING_WITHDRAWAL_PREFIX || hasBuilderWithdrawalCredential(withdrawalCredentials)
11
- );
8
+ return withdrawalCredentials[0] === COMPOUNDING_WITHDRAWAL_PREFIX;
12
9
  }
13
10
 
14
11
  export function hasExecutionWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean {
package/src/util/gloas.ts CHANGED
@@ -1,19 +1,21 @@
1
1
  import {byteArrayEquals} from "@chainsafe/ssz";
2
2
  import {
3
+ BUILDER_INDEX_FLAG,
3
4
  BUILDER_PAYMENT_THRESHOLD_DENOMINATOR,
4
5
  BUILDER_PAYMENT_THRESHOLD_NUMERATOR,
5
6
  BUILDER_WITHDRAWAL_PREFIX,
6
7
  EFFECTIVE_BALANCE_INCREMENT,
8
+ FAR_FUTURE_EPOCH,
9
+ MIN_DEPOSIT_AMOUNT,
7
10
  SLOTS_PER_EPOCH,
8
11
  } from "@lodestar/params";
9
- import {gloas} from "@lodestar/types";
10
12
  import {AttestationData} from "@lodestar/types/phase0";
11
13
  import {CachedBeaconStateGloas} from "../types.ts";
12
14
  import {getBlockRootAtSlot} from "./blockRoot.ts";
13
15
  import {computeEpochAtSlot} from "./epoch.ts";
14
16
  import {RootCache} from "./rootCache.ts";
15
17
 
16
- export function hasBuilderWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean {
18
+ export function isBuilderWithdrawalCredential(withdrawalCredentials: Uint8Array): boolean {
17
19
  return withdrawalCredentials[0] === BUILDER_WITHDRAWAL_PREFIX;
18
20
  }
19
21
 
@@ -25,14 +27,113 @@ export function getBuilderPaymentQuorumThreshold(state: CachedBeaconStateGloas):
25
27
  return Math.floor(quorum / BUILDER_PAYMENT_THRESHOLD_DENOMINATOR);
26
28
  }
27
29
 
28
- export function isBuilderPaymentWithdrawable(
29
- state: CachedBeaconStateGloas,
30
- withdrawal: gloas.BuilderPendingWithdrawal
31
- ): boolean {
32
- const builder = state.validators.getReadonly(withdrawal.builderIndex);
30
+ /**
31
+ * Check if a validator index represents a builder (has the builder flag set).
32
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-is_builder_index
33
+ */
34
+ export function isBuilderIndex(validatorIndex: number): boolean {
35
+ return (validatorIndex & BUILDER_INDEX_FLAG) !== 0;
36
+ }
37
+
38
+ /**
39
+ * Convert a builder index to a flagged validator index for use in Withdrawal containers.
40
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-convert_builder_index_to_validator_index
41
+ */
42
+ export function convertBuilderIndexToValidatorIndex(builderIndex: number): number {
43
+ return builderIndex | BUILDER_INDEX_FLAG;
44
+ }
45
+
46
+ /**
47
+ * Convert a flagged validator index back to a builder index.
48
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-convert_validator_index_to_builder_index
49
+ */
50
+ export function convertValidatorIndexToBuilderIndex(validatorIndex: number): number {
51
+ return validatorIndex & ~BUILDER_INDEX_FLAG;
52
+ }
53
+
54
+ /**
55
+ * Check if a builder is active (deposited and not yet withdrawable).
56
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#isactivebuilder
57
+ */
58
+ export function isActiveBuilder(state: CachedBeaconStateGloas, builderIndex: number): boolean {
59
+ const builder = state.builders.getReadonly(builderIndex);
60
+ const finalizedEpoch = state.finalizedCheckpoint.epoch;
61
+
62
+ return builder.depositEpoch < finalizedEpoch && builder.withdrawableEpoch === FAR_FUTURE_EPOCH;
63
+ }
64
+
65
+ /**
66
+ * Get the total pending balance to withdraw for a builder (from withdrawals + payments).
67
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-get_pending_balance_to_withdraw_for_builder
68
+ */
69
+ export function getPendingBalanceToWithdrawForBuilder(state: CachedBeaconStateGloas, builderIndex: number): number {
70
+ let pendingBalance = 0;
71
+
72
+ // Sum pending withdrawals
73
+ for (let i = 0; i < state.builderPendingWithdrawals.length; i++) {
74
+ const withdrawal = state.builderPendingWithdrawals.getReadonly(i);
75
+ if (withdrawal.builderIndex === builderIndex) {
76
+ pendingBalance += withdrawal.amount;
77
+ }
78
+ }
79
+
80
+ // Sum pending payments
81
+ for (let i = 0; i < state.builderPendingPayments.length; i++) {
82
+ const payment = state.builderPendingPayments.getReadonly(i);
83
+ if (payment.withdrawal.builderIndex === builderIndex) {
84
+ pendingBalance += payment.withdrawal.amount;
85
+ }
86
+ }
87
+
88
+ return pendingBalance;
89
+ }
90
+
91
+ /**
92
+ * Check if a builder has sufficient balance to cover a bid amount.
93
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-can_builder_cover_bid
94
+ */
95
+ export function canBuilderCoverBid(state: CachedBeaconStateGloas, builderIndex: number, bidAmount: number): boolean {
96
+ const builder = state.builders.getReadonly(builderIndex);
97
+ const pendingBalance = getPendingBalanceToWithdrawForBuilder(state, builderIndex);
98
+ const minBalance = MIN_DEPOSIT_AMOUNT + pendingBalance;
99
+
100
+ if (builder.balance < minBalance) {
101
+ return false;
102
+ }
103
+
104
+ return builder.balance - minBalance >= bidAmount;
105
+ }
106
+
107
+ /**
108
+ * Initiate a builder exit by setting their withdrawable epoch.
109
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-initiate_builder_exit
110
+ */
111
+ export function initiateBuilderExit(state: CachedBeaconStateGloas, builderIndex: number): void {
112
+ const builder = state.builders.get(builderIndex);
113
+
114
+ // Return if builder already initiated exit
115
+ if (builder.withdrawableEpoch !== FAR_FUTURE_EPOCH) {
116
+ return;
117
+ }
118
+
119
+ // Set builder exit epoch
33
120
  const currentEpoch = computeEpochAtSlot(state.slot);
121
+ builder.withdrawableEpoch = currentEpoch + state.config.MIN_BUILDER_WITHDRAWABILITY_DELAY;
122
+ }
34
123
 
35
- return builder.withdrawableEpoch >= currentEpoch || !builder.slashed;
124
+ /**
125
+ * Find the index of a builder by their public key.
126
+ * Returns null if not found.
127
+ *
128
+ * May consider builder pubkey cache if performance becomes an issue.
129
+ */
130
+ export function findBuilderIndexByPubkey(state: CachedBeaconStateGloas, pubkey: Uint8Array): number | null {
131
+ for (let i = 0; i < state.builders.length; i++) {
132
+ if (byteArrayEquals(state.builders.getReadonly(i).pubkey, pubkey)) {
133
+ return i;
134
+ }
135
+ }
136
+ return null;
36
137
  }
37
138
 
38
139
  export function isAttestationSameSlot(state: CachedBeaconStateGloas, data: AttestationData): boolean {
@@ -2,13 +2,15 @@ import {ChainForkConfig} from "@lodestar/config";
2
2
  import {
3
3
  EFFECTIVE_BALANCE_INCREMENT,
4
4
  ForkSeq,
5
+ MAX_EFFECTIVE_BALANCE,
5
6
  MAX_EFFECTIVE_BALANCE_ELECTRA,
6
7
  MIN_ACTIVATION_BALANCE,
7
8
  } from "@lodestar/params";
8
9
  import {Epoch, ValidatorIndex, phase0} from "@lodestar/types";
9
10
  import {intDiv} from "@lodestar/utils";
10
11
  import {BeaconStateAllForks, CachedBeaconStateElectra, CachedBeaconStateGloas, EpochCache} from "../types.js";
11
- import {hasCompoundingWithdrawalCredential} from "./electra.js";
12
+ import {hasEth1WithdrawalCredential} from "./capella.js";
13
+ import {hasCompoundingWithdrawalCredential, hasExecutionWithdrawalCredential} from "./electra.js";
12
14
 
13
15
  /**
14
16
  * Check if [[validator]] is active
@@ -94,6 +96,34 @@ export function getMaxEffectiveBalance(withdrawalCredentials: Uint8Array): numbe
94
96
  return MIN_ACTIVATION_BALANCE;
95
97
  }
96
98
 
99
+ /**
100
+ * Check if validator is partially withdrawable.
101
+ * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/electra/beacon-chain.md#modified-is_partially_withdrawable_validator
102
+ */
103
+ export function isPartiallyWithdrawableValidator(fork: ForkSeq, validator: phase0.Validator, balance: number): boolean {
104
+ const isPostElectra = fork >= ForkSeq.electra;
105
+
106
+ // Check withdrawal credentials
107
+ const hasWithdrawableCredentials = isPostElectra
108
+ ? hasExecutionWithdrawalCredential(validator.withdrawalCredentials)
109
+ : hasEth1WithdrawalCredential(validator.withdrawalCredentials);
110
+
111
+ if (!hasWithdrawableCredentials) {
112
+ return false;
113
+ }
114
+
115
+ // Get max effective balance based on fork
116
+ const maxEffectiveBalance = isPostElectra
117
+ ? getMaxEffectiveBalance(validator.withdrawalCredentials)
118
+ : MAX_EFFECTIVE_BALANCE;
119
+
120
+ // Check if at max effective balance and has excess balance
121
+ const hasMaxEffectiveBalance = validator.effectiveBalance === maxEffectiveBalance;
122
+ const hasExcessBalance = balance > maxEffectiveBalance;
123
+
124
+ return hasMaxEffectiveBalance && hasExcessBalance;
125
+ }
126
+
97
127
  export function getPendingBalanceToWithdraw(
98
128
  fork: ForkSeq,
99
129
  state: CachedBeaconStateElectra | CachedBeaconStateGloas,