@lodestar/state-transition 1.39.0-dev.b6bba4cb8c → 1.39.0-dev.b6d377a93c
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.
- package/lib/block/processWithdrawals.d.ts +3 -3
- package/lib/block/processWithdrawals.d.ts.map +1 -1
- package/lib/block/processWithdrawals.js +152 -105
- package/lib/block/processWithdrawals.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/rewards/attestationsRewards.d.ts +6 -0
- package/lib/rewards/attestationsRewards.d.ts.map +1 -0
- package/lib/rewards/attestationsRewards.js +113 -0
- package/lib/rewards/attestationsRewards.js.map +1 -0
- package/lib/rewards/blockRewards.d.ts +13 -0
- package/lib/rewards/blockRewards.d.ts.map +1 -0
- package/lib/rewards/blockRewards.js +95 -0
- package/lib/rewards/blockRewards.js.map +1 -0
- package/lib/rewards/index.d.ts +4 -0
- package/lib/rewards/index.d.ts.map +1 -0
- package/lib/rewards/index.js +4 -0
- package/lib/rewards/index.js.map +1 -0
- package/lib/rewards/syncCommitteeRewards.d.ts +6 -0
- package/lib/rewards/syncCommitteeRewards.d.ts.map +1 -0
- package/lib/rewards/syncCommitteeRewards.js +36 -0
- package/lib/rewards/syncCommitteeRewards.js.map +1 -0
- package/lib/signatureSets/proposer.d.ts +3 -4
- package/lib/signatureSets/proposer.d.ts.map +1 -1
- package/lib/signatureSets/proposer.js +5 -6
- package/lib/signatureSets/proposer.js.map +1 -1
- package/package.json +14 -11
- package/src/block/processWithdrawals.ts +230 -135
- package/src/index.ts +1 -0
- package/src/rewards/attestationsRewards.ts +200 -0
- package/src/rewards/blockRewards.ts +147 -0
- package/src/rewards/index.ts +3 -0
- package/src/rewards/syncCommitteeRewards.ts +59 -0
- package/src/signatureSets/proposer.ts +6 -7
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
MIN_ACTIVATION_BALANCE,
|
|
10
10
|
} from "@lodestar/params";
|
|
11
11
|
import {ValidatorIndex, capella, ssz} from "@lodestar/types";
|
|
12
|
-
import {
|
|
12
|
+
import {toRootHex} from "@lodestar/utils";
|
|
13
13
|
import {CachedBeaconStateCapella, CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
|
|
14
14
|
import {isBuilderPaymentWithdrawable, isParentBlockFull} from "../util/gloas.ts";
|
|
15
15
|
import {
|
|
@@ -30,13 +30,11 @@ export function processWithdrawals(
|
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// processedPartialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002)
|
|
34
33
|
// processedBuilderWithdrawalsCount is withdrawals coming from builder payment since gloas (EIP-7732)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
} = getExpectedWithdrawals(fork, state);
|
|
34
|
+
// processedPartialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002)
|
|
35
|
+
// processedValidatorSweepCount is withdrawals coming from validator sweep
|
|
36
|
+
const {expectedWithdrawals, processedBuilderWithdrawalsCount, processedPartialWithdrawalsCount} =
|
|
37
|
+
getExpectedWithdrawals(fork, state);
|
|
40
38
|
const numWithdrawals = expectedWithdrawals.length;
|
|
41
39
|
|
|
42
40
|
// After gloas, withdrawals are verified later in processExecutionPayloadEnvelope
|
|
@@ -68,12 +66,10 @@ export function processWithdrawals(
|
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
const withdrawal = expectedWithdrawals[i];
|
|
73
|
-
decreaseBalance(state, withdrawal.validatorIndex, Number(withdrawal.amount));
|
|
74
|
-
}
|
|
69
|
+
applyWithdrawals(state, expectedWithdrawals);
|
|
75
70
|
|
|
76
71
|
if (fork >= ForkSeq.electra) {
|
|
72
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.0/specs/electra/beacon-chain.md#new-update_pending_partial_withdrawals
|
|
77
73
|
const stateElectra = state as CachedBeaconStateElectra;
|
|
78
74
|
stateElectra.pendingPartialWithdrawals = stateElectra.pendingPartialWithdrawals.sliceFrom(
|
|
79
75
|
processedPartialWithdrawalsCount
|
|
@@ -97,13 +93,14 @@ export function processWithdrawals(
|
|
|
97
93
|
...remainingWithdrawals,
|
|
98
94
|
]);
|
|
99
95
|
}
|
|
100
|
-
|
|
101
96
|
// Update the nextWithdrawalIndex
|
|
97
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.0/specs/capella/beacon-chain.md#new-update_next_withdrawal_index
|
|
102
98
|
const latestWithdrawal = expectedWithdrawals.at(-1);
|
|
103
99
|
if (latestWithdrawal) {
|
|
104
100
|
state.nextWithdrawalIndex = latestWithdrawal.index + 1;
|
|
105
101
|
}
|
|
106
102
|
|
|
103
|
+
// https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.0/specs/capella/beacon-chain.md#new-update_next_withdrawal_validator_index
|
|
107
104
|
// Update the nextWithdrawalValidatorIndex
|
|
108
105
|
if (latestWithdrawal && expectedWithdrawals.length === MAX_WITHDRAWALS_PER_PAYLOAD) {
|
|
109
106
|
// All slots filled, nextWithdrawalValidatorIndex should be validatorIndex having next turn
|
|
@@ -116,142 +113,163 @@ export function processWithdrawals(
|
|
|
116
113
|
}
|
|
117
114
|
}
|
|
118
115
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
processedPartialWithdrawalsCount: number;
|
|
126
|
-
processedBuilderWithdrawalsCount: number;
|
|
127
|
-
} {
|
|
128
|
-
if (fork < ForkSeq.capella) {
|
|
129
|
-
throw new Error(`getExpectedWithdrawals not supported at forkSeq=${fork} < ForkSeq.capella`);
|
|
130
|
-
}
|
|
131
|
-
|
|
116
|
+
function getBuilderWithdrawals(
|
|
117
|
+
state: CachedBeaconStateGloas,
|
|
118
|
+
withdrawalIndex: number,
|
|
119
|
+
balanceAfterWithdrawals: Map<ValidatorIndex, number>
|
|
120
|
+
): {builderWithdrawals: capella.Withdrawal[]; withdrawalIndex: number; processedCount: number} {
|
|
121
|
+
const builderWithdrawals: capella.Withdrawal[] = [];
|
|
132
122
|
const epoch = state.epochCtx.epoch;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const allBuilderPendingWithdrawals =
|
|
149
|
-
stateGloas.builderPendingWithdrawals.length <= MAX_WITHDRAWALS_PER_PAYLOAD
|
|
150
|
-
? stateGloas.builderPendingWithdrawals.getAllReadonly()
|
|
151
|
-
: null;
|
|
123
|
+
const allBuilderPendingWithdrawals =
|
|
124
|
+
state.builderPendingWithdrawals.length <= MAX_WITHDRAWALS_PER_PAYLOAD
|
|
125
|
+
? state.builderPendingWithdrawals.getAllReadonly()
|
|
126
|
+
: null;
|
|
127
|
+
|
|
128
|
+
let processedCount = 0;
|
|
129
|
+
for (let i = 0; i < state.builderPendingWithdrawals.length; i++) {
|
|
130
|
+
const withdrawal = allBuilderPendingWithdrawals
|
|
131
|
+
? allBuilderPendingWithdrawals[i]
|
|
132
|
+
: state.builderPendingWithdrawals.getReadonly(i);
|
|
133
|
+
|
|
134
|
+
if (withdrawal.withdrawableEpoch > epoch || builderWithdrawals.length === MAX_WITHDRAWALS_PER_PAYLOAD) {
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
152
137
|
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
: stateGloas.builderPendingWithdrawals.getReadonly(i);
|
|
138
|
+
if (isBuilderPaymentWithdrawable(state, withdrawal)) {
|
|
139
|
+
const builderIndex = withdrawal.builderIndex;
|
|
140
|
+
const builder = state.validators.get(withdrawal.builderIndex);
|
|
157
141
|
|
|
158
|
-
|
|
159
|
-
|
|
142
|
+
let balance = balanceAfterWithdrawals.get(builderIndex);
|
|
143
|
+
if (balance === undefined) {
|
|
144
|
+
balance = state.balances.get(builderIndex);
|
|
145
|
+
balanceAfterWithdrawals.set(builderIndex, balance);
|
|
160
146
|
}
|
|
161
147
|
|
|
162
|
-
|
|
163
|
-
const totalWithdrawn = withdrawnBalances.getOrDefault(withdrawal.builderIndex);
|
|
164
|
-
const balance = state.balances.get(withdrawal.builderIndex) - totalWithdrawn;
|
|
165
|
-
const builder = state.validators.get(withdrawal.builderIndex);
|
|
166
|
-
|
|
167
|
-
let withdrawableBalance = 0;
|
|
148
|
+
let withdrawableBalance = 0;
|
|
168
149
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
}
|
|
175
156
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
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);
|
|
186
166
|
}
|
|
187
|
-
processedBuilderWithdrawalsCount++;
|
|
188
167
|
}
|
|
168
|
+
processedCount++;
|
|
189
169
|
}
|
|
190
170
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const partialWithdrawalBound = Math.min(
|
|
194
|
-
withdrawals.length + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP,
|
|
195
|
-
MAX_WITHDRAWALS_PER_PAYLOAD - 1
|
|
196
|
-
);
|
|
197
|
-
const stateElectra = state as CachedBeaconStateElectra;
|
|
171
|
+
return {builderWithdrawals, withdrawalIndex, processedCount};
|
|
172
|
+
}
|
|
198
173
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
174
|
+
function getPendingPartialWithdrawals(
|
|
175
|
+
state: CachedBeaconStateElectra,
|
|
176
|
+
withdrawalIndex: number,
|
|
177
|
+
numPriorWithdrawal: number,
|
|
178
|
+
balanceAfterWithdrawals: Map<ValidatorIndex, number>
|
|
179
|
+
): {pendingPartialWithdrawals: capella.Withdrawal[]; withdrawalIndex: number; processedCount: number} {
|
|
180
|
+
const epoch = state.epochCtx.epoch;
|
|
181
|
+
const pendingPartialWithdrawals: capella.Withdrawal[] = [];
|
|
182
|
+
const validators = state.validators;
|
|
183
|
+
|
|
184
|
+
// In pre-gloas, partialWithdrawalBound == MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP
|
|
185
|
+
const partialWithdrawalBound = Math.min(
|
|
186
|
+
numPriorWithdrawal + MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP,
|
|
187
|
+
MAX_WITHDRAWALS_PER_PAYLOAD - 1
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP = 8, PENDING_PARTIAL_WITHDRAWALS_LIMIT: 134217728 so we should only call getAllReadonly() if it makes sense
|
|
191
|
+
// pendingPartialWithdrawals comes from EIP-7002 smart contract where it takes fee so it's more likely than not validator is in correct condition to withdraw
|
|
192
|
+
// also we may break early if withdrawableEpoch > epoch
|
|
193
|
+
const allPendingPartialWithdrawals =
|
|
194
|
+
state.pendingPartialWithdrawals.length <= partialWithdrawalBound
|
|
195
|
+
? state.pendingPartialWithdrawals.getAllReadonly()
|
|
196
|
+
: null;
|
|
197
|
+
|
|
198
|
+
// EIP-7002: Execution layer triggerable withdrawals
|
|
199
|
+
let processedCount = 0;
|
|
200
|
+
for (let i = 0; i < state.pendingPartialWithdrawals.length; i++) {
|
|
201
|
+
const withdrawal = allPendingPartialWithdrawals
|
|
202
|
+
? allPendingPartialWithdrawals[i]
|
|
203
|
+
: state.pendingPartialWithdrawals.getReadonly(i);
|
|
204
|
+
if (
|
|
205
|
+
withdrawal.withdrawableEpoch > epoch ||
|
|
206
|
+
pendingPartialWithdrawals.length + numPriorWithdrawal === partialWithdrawalBound
|
|
207
|
+
) {
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
215
210
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
validator.effectiveBalance >= MIN_ACTIVATION_BALANCE &&
|
|
223
|
-
balance > MIN_ACTIVATION_BALANCE
|
|
224
|
-
) {
|
|
225
|
-
const balanceOverMinActivationBalance = BigInt(balance - MIN_ACTIVATION_BALANCE);
|
|
226
|
-
const withdrawableBalance =
|
|
227
|
-
balanceOverMinActivationBalance < withdrawal.amount ? balanceOverMinActivationBalance : withdrawal.amount;
|
|
228
|
-
withdrawals.push({
|
|
229
|
-
index: withdrawalIndex,
|
|
230
|
-
validatorIndex: withdrawal.validatorIndex,
|
|
231
|
-
address: validator.withdrawalCredentials.subarray(12),
|
|
232
|
-
amount: withdrawableBalance,
|
|
233
|
-
});
|
|
234
|
-
withdrawalIndex++;
|
|
235
|
-
withdrawnBalances.set(withdrawal.validatorIndex, totalWithdrawn + Number(withdrawableBalance));
|
|
236
|
-
}
|
|
237
|
-
processedPartialWithdrawalsCount++;
|
|
211
|
+
const validatorIndex = withdrawal.validatorIndex;
|
|
212
|
+
const validator = validators.getReadonly(validatorIndex);
|
|
213
|
+
let balance = balanceAfterWithdrawals.get(validatorIndex);
|
|
214
|
+
if (balance === undefined) {
|
|
215
|
+
balance = state.balances.get(validatorIndex);
|
|
216
|
+
balanceAfterWithdrawals.set(validatorIndex, balance);
|
|
238
217
|
}
|
|
218
|
+
|
|
219
|
+
if (
|
|
220
|
+
validator.exitEpoch === FAR_FUTURE_EPOCH &&
|
|
221
|
+
validator.effectiveBalance >= MIN_ACTIVATION_BALANCE &&
|
|
222
|
+
balance > MIN_ACTIVATION_BALANCE
|
|
223
|
+
) {
|
|
224
|
+
const balanceOverMinActivationBalance = BigInt(balance - MIN_ACTIVATION_BALANCE);
|
|
225
|
+
const withdrawableBalance =
|
|
226
|
+
balanceOverMinActivationBalance < withdrawal.amount ? balanceOverMinActivationBalance : withdrawal.amount;
|
|
227
|
+
pendingPartialWithdrawals.push({
|
|
228
|
+
index: withdrawalIndex,
|
|
229
|
+
validatorIndex,
|
|
230
|
+
address: validator.withdrawalCredentials.subarray(12),
|
|
231
|
+
amount: withdrawableBalance,
|
|
232
|
+
});
|
|
233
|
+
withdrawalIndex++;
|
|
234
|
+
balanceAfterWithdrawals.set(validatorIndex, balance - Number(withdrawableBalance));
|
|
235
|
+
}
|
|
236
|
+
processedCount++;
|
|
239
237
|
}
|
|
240
238
|
|
|
241
|
-
|
|
242
|
-
|
|
239
|
+
return {pendingPartialWithdrawals, withdrawalIndex, processedCount};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function getValidatorsSweepWithdrawals(
|
|
243
|
+
fork: ForkSeq,
|
|
244
|
+
state: CachedBeaconStateCapella | CachedBeaconStateElectra | CachedBeaconStateGloas,
|
|
245
|
+
withdrawalIndex: number,
|
|
246
|
+
numPriorWithdrawal: number,
|
|
247
|
+
balanceAfterWithdrawals: Map<ValidatorIndex, number>
|
|
248
|
+
): {sweepWithdrawals: capella.Withdrawal[]; processedCount: number} {
|
|
249
|
+
const sweepWithdrawals: capella.Withdrawal[] = [];
|
|
250
|
+
const epoch = state.epochCtx.epoch;
|
|
251
|
+
const {validators, balances, nextWithdrawalValidatorIndex} = state;
|
|
252
|
+
const isPostElectra = fork >= ForkSeq.electra;
|
|
253
|
+
|
|
254
|
+
const validatorsLimit = Math.min(state.validators.length, MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP);
|
|
255
|
+
let processedCount = 0;
|
|
243
256
|
// Just run a bounded loop max iterating over all withdrawals
|
|
244
257
|
// however breaks out once we have MAX_WITHDRAWALS_PER_PAYLOAD
|
|
245
|
-
for (n = 0; n <
|
|
258
|
+
for (let n = 0; n < validatorsLimit; n++) {
|
|
259
|
+
if (sweepWithdrawals.length + numPriorWithdrawal === MAX_WITHDRAWALS_PER_PAYLOAD) {
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
|
|
246
263
|
// Get next validator in turn
|
|
247
264
|
const validatorIndex = (nextWithdrawalValidatorIndex + n) % validators.length;
|
|
248
265
|
|
|
249
266
|
const validator = validators.getReadonly(validatorIndex);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
267
|
+
let balance = balanceAfterWithdrawals.get(validatorIndex);
|
|
268
|
+
if (balance === undefined) {
|
|
269
|
+
balance = balances.get(validatorIndex);
|
|
270
|
+
balanceAfterWithdrawals.set(validatorIndex, balance);
|
|
271
|
+
}
|
|
272
|
+
|
|
255
273
|
const {withdrawableEpoch, withdrawalCredentials, effectiveBalance} = validator;
|
|
256
274
|
const hasWithdrawableCredentials = isPostElectra
|
|
257
275
|
? hasExecutionWithdrawalCredential(withdrawalCredentials)
|
|
@@ -259,40 +277,117 @@ export function getExpectedWithdrawals(
|
|
|
259
277
|
// early skip for balance = 0 as its now more likely that validator has exited/slashed with
|
|
260
278
|
// balance zero than not have withdrawal credentials set
|
|
261
279
|
if (balance === 0 || !hasWithdrawableCredentials) {
|
|
280
|
+
processedCount++;
|
|
262
281
|
continue;
|
|
263
282
|
}
|
|
264
283
|
|
|
265
284
|
// capella full withdrawal
|
|
266
285
|
if (withdrawableEpoch <= epoch) {
|
|
267
|
-
|
|
286
|
+
sweepWithdrawals.push({
|
|
268
287
|
index: withdrawalIndex,
|
|
269
288
|
validatorIndex,
|
|
270
289
|
address: validator.withdrawalCredentials.subarray(12),
|
|
271
290
|
amount: BigInt(balance),
|
|
272
291
|
});
|
|
273
292
|
withdrawalIndex++;
|
|
274
|
-
|
|
293
|
+
balanceAfterWithdrawals.set(validatorIndex, 0);
|
|
275
294
|
} else if (
|
|
276
295
|
effectiveBalance === (isPostElectra ? getMaxEffectiveBalance(withdrawalCredentials) : MAX_EFFECTIVE_BALANCE) &&
|
|
277
296
|
balance > effectiveBalance
|
|
278
297
|
) {
|
|
279
298
|
// capella partial withdrawal
|
|
280
299
|
const partialAmount = balance - effectiveBalance;
|
|
281
|
-
|
|
300
|
+
sweepWithdrawals.push({
|
|
282
301
|
index: withdrawalIndex,
|
|
283
302
|
validatorIndex,
|
|
284
303
|
address: validator.withdrawalCredentials.subarray(12),
|
|
285
304
|
amount: BigInt(partialAmount),
|
|
286
305
|
});
|
|
287
306
|
withdrawalIndex++;
|
|
288
|
-
|
|
307
|
+
balanceAfterWithdrawals.set(validatorIndex, balance - partialAmount);
|
|
289
308
|
}
|
|
309
|
+
processedCount++;
|
|
310
|
+
}
|
|
290
311
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
312
|
+
return {sweepWithdrawals, processedCount};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function applyWithdrawals(
|
|
316
|
+
state: CachedBeaconStateCapella | CachedBeaconStateElectra | CachedBeaconStateGloas,
|
|
317
|
+
withdrawals: capella.Withdrawal[]
|
|
318
|
+
): void {
|
|
319
|
+
for (const withdrawal of withdrawals) {
|
|
320
|
+
decreaseBalance(state, withdrawal.validatorIndex, Number(withdrawal.amount));
|
|
295
321
|
}
|
|
322
|
+
}
|
|
296
323
|
|
|
297
|
-
|
|
324
|
+
export function getExpectedWithdrawals(
|
|
325
|
+
fork: ForkSeq,
|
|
326
|
+
state: CachedBeaconStateCapella | CachedBeaconStateElectra | CachedBeaconStateGloas
|
|
327
|
+
): {
|
|
328
|
+
expectedWithdrawals: capella.Withdrawal[];
|
|
329
|
+
processedBuilderWithdrawalsCount: number;
|
|
330
|
+
processedPartialWithdrawalsCount: number;
|
|
331
|
+
processedValidatorSweepCount: number;
|
|
332
|
+
} {
|
|
333
|
+
if (fork < ForkSeq.capella) {
|
|
334
|
+
throw new Error(`getExpectedWithdrawals not supported at forkSeq=${fork} < ForkSeq.capella`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let withdrawalIndex = state.nextWithdrawalIndex;
|
|
338
|
+
|
|
339
|
+
const expectedWithdrawals: capella.Withdrawal[] = [];
|
|
340
|
+
// Map to track balances after applying withdrawals
|
|
341
|
+
// 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>();
|
|
343
|
+
// partialWithdrawalsCount is withdrawals coming from EL since electra (EIP-7002)
|
|
344
|
+
let processedPartialWithdrawalsCount = 0;
|
|
345
|
+
// builderWithdrawalsCount is withdrawals coming from builder payments since Gloas (EIP-7732)
|
|
346
|
+
let processedBuilderWithdrawalsCount = 0;
|
|
347
|
+
|
|
348
|
+
if (fork >= ForkSeq.gloas) {
|
|
349
|
+
const {
|
|
350
|
+
builderWithdrawals,
|
|
351
|
+
withdrawalIndex: newWithdrawalIndex,
|
|
352
|
+
processedCount,
|
|
353
|
+
} = getBuilderWithdrawals(state as CachedBeaconStateGloas, withdrawalIndex, balanceAfterWithdrawals);
|
|
354
|
+
|
|
355
|
+
expectedWithdrawals.push(...builderWithdrawals);
|
|
356
|
+
withdrawalIndex = newWithdrawalIndex;
|
|
357
|
+
processedBuilderWithdrawalsCount = processedCount;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (fork >= ForkSeq.electra) {
|
|
361
|
+
const {
|
|
362
|
+
pendingPartialWithdrawals,
|
|
363
|
+
withdrawalIndex: newWithdrawalIndex,
|
|
364
|
+
processedCount,
|
|
365
|
+
} = getPendingPartialWithdrawals(
|
|
366
|
+
state as CachedBeaconStateElectra,
|
|
367
|
+
withdrawalIndex,
|
|
368
|
+
expectedWithdrawals.length,
|
|
369
|
+
balanceAfterWithdrawals
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
expectedWithdrawals.push(...pendingPartialWithdrawals);
|
|
373
|
+
withdrawalIndex = newWithdrawalIndex;
|
|
374
|
+
processedPartialWithdrawalsCount = processedCount;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const {sweepWithdrawals, processedCount: processedValidatorSweepCount} = getValidatorsSweepWithdrawals(
|
|
378
|
+
fork,
|
|
379
|
+
state,
|
|
380
|
+
withdrawalIndex,
|
|
381
|
+
expectedWithdrawals.length,
|
|
382
|
+
balanceAfterWithdrawals
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
expectedWithdrawals.push(...sweepWithdrawals);
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
expectedWithdrawals,
|
|
389
|
+
processedBuilderWithdrawalsCount,
|
|
390
|
+
processedPartialWithdrawalsCount,
|
|
391
|
+
processedValidatorSweepCount,
|
|
392
|
+
};
|
|
298
393
|
}
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ export {
|
|
|
40
40
|
export * from "./constants/index.js";
|
|
41
41
|
export type {EpochTransitionStep} from "./epoch/index.js";
|
|
42
42
|
export {type BeaconStateTransitionMetrics, getMetrics} from "./metrics.js";
|
|
43
|
+
export * from "./rewards/index.js";
|
|
43
44
|
export * from "./signatureSets/index.js";
|
|
44
45
|
export * from "./stateTransition.js";
|
|
45
46
|
export type {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {PubkeyIndexMap} from "@chainsafe/pubkey-index-map";
|
|
2
|
+
import {BeaconConfig} from "@lodestar/config";
|
|
3
|
+
import {
|
|
4
|
+
EFFECTIVE_BALANCE_INCREMENT,
|
|
5
|
+
ForkName,
|
|
6
|
+
INACTIVITY_PENALTY_QUOTIENT_ALTAIR,
|
|
7
|
+
MAX_EFFECTIVE_BALANCE,
|
|
8
|
+
MAX_EFFECTIVE_BALANCE_ELECTRA,
|
|
9
|
+
PARTICIPATION_FLAG_WEIGHTS,
|
|
10
|
+
TIMELY_HEAD_FLAG_INDEX,
|
|
11
|
+
TIMELY_SOURCE_FLAG_INDEX,
|
|
12
|
+
TIMELY_TARGET_FLAG_INDEX,
|
|
13
|
+
WEIGHT_DENOMINATOR,
|
|
14
|
+
isForkPostElectra,
|
|
15
|
+
} from "@lodestar/params";
|
|
16
|
+
import {ValidatorIndex, rewards} from "@lodestar/types";
|
|
17
|
+
import {fromHex} from "@lodestar/utils";
|
|
18
|
+
import {EpochTransitionCache, beforeProcessEpoch} from "../cache/epochTransitionCache.js";
|
|
19
|
+
import {CachedBeaconStateAllForks, CachedBeaconStateAltair} from "../types.js";
|
|
20
|
+
import {
|
|
21
|
+
FLAG_ELIGIBLE_ATTESTER,
|
|
22
|
+
FLAG_PREV_HEAD_ATTESTER_UNSLASHED,
|
|
23
|
+
FLAG_PREV_SOURCE_ATTESTER_UNSLASHED,
|
|
24
|
+
FLAG_PREV_TARGET_ATTESTER_UNSLASHED,
|
|
25
|
+
hasMarkers,
|
|
26
|
+
isInInactivityLeak,
|
|
27
|
+
} from "../util/index.js";
|
|
28
|
+
|
|
29
|
+
/** Attestations penalty with respect to effective balance in Gwei */
|
|
30
|
+
type AttestationsPenalty = {target: number; source: number; effectiveBalance: number};
|
|
31
|
+
|
|
32
|
+
const defaultAttestationsReward = {head: 0, target: 0, source: 0, inclusionDelay: 0, inactivity: 0};
|
|
33
|
+
const defaultAttestationsPenalty = {target: 0, source: 0};
|
|
34
|
+
|
|
35
|
+
export async function computeAttestationsRewards(
|
|
36
|
+
config: BeaconConfig,
|
|
37
|
+
pubkey2index: PubkeyIndexMap,
|
|
38
|
+
state: CachedBeaconStateAllForks,
|
|
39
|
+
validatorIds?: (ValidatorIndex | string)[]
|
|
40
|
+
): Promise<rewards.AttestationsRewards> {
|
|
41
|
+
const fork = config.getForkName(state.slot);
|
|
42
|
+
if (fork === ForkName.phase0) {
|
|
43
|
+
throw Error("Unsupported fork. Attestations rewards calculation is not available in phase0");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const stateAltair = state as CachedBeaconStateAltair;
|
|
47
|
+
const transitionCache = beforeProcessEpoch(stateAltair);
|
|
48
|
+
|
|
49
|
+
const [idealRewards, penalties] = computeIdealAttestationsRewardsAndPenaltiesAltair(
|
|
50
|
+
config,
|
|
51
|
+
stateAltair,
|
|
52
|
+
transitionCache
|
|
53
|
+
);
|
|
54
|
+
const totalRewards = computeTotalAttestationsRewardsAltair(
|
|
55
|
+
config,
|
|
56
|
+
pubkey2index,
|
|
57
|
+
stateAltair,
|
|
58
|
+
transitionCache,
|
|
59
|
+
idealRewards,
|
|
60
|
+
penalties,
|
|
61
|
+
validatorIds
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return {idealRewards, totalRewards};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function computeIdealAttestationsRewardsAndPenaltiesAltair(
|
|
68
|
+
config: BeaconConfig,
|
|
69
|
+
state: CachedBeaconStateAllForks,
|
|
70
|
+
transitionCache: EpochTransitionCache
|
|
71
|
+
): [rewards.IdealAttestationsReward[], AttestationsPenalty[]] {
|
|
72
|
+
const baseRewardPerIncrement = transitionCache.baseRewardPerIncrement;
|
|
73
|
+
const activeBalanceByIncrement = transitionCache.totalActiveStakeByIncrement;
|
|
74
|
+
const fork = config.getForkName(state.slot);
|
|
75
|
+
const maxEffectiveBalance = isForkPostElectra(fork) ? MAX_EFFECTIVE_BALANCE_ELECTRA : MAX_EFFECTIVE_BALANCE;
|
|
76
|
+
const maxEffectiveBalanceByIncrement = Math.floor(maxEffectiveBalance / EFFECTIVE_BALANCE_INCREMENT);
|
|
77
|
+
|
|
78
|
+
const idealRewards = Array.from({length: maxEffectiveBalanceByIncrement + 1}, (_, effectiveBalanceByIncrement) => ({
|
|
79
|
+
...defaultAttestationsReward,
|
|
80
|
+
effectiveBalance: effectiveBalanceByIncrement * EFFECTIVE_BALANCE_INCREMENT,
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
const attestationsPenalties: AttestationsPenalty[] = Array.from(
|
|
84
|
+
{length: maxEffectiveBalanceByIncrement + 1},
|
|
85
|
+
(_, effectiveBalanceByIncrement) => ({
|
|
86
|
+
...defaultAttestationsPenalty,
|
|
87
|
+
effectiveBalance: effectiveBalanceByIncrement * EFFECTIVE_BALANCE_INCREMENT,
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < PARTICIPATION_FLAG_WEIGHTS.length; i++) {
|
|
92
|
+
const weight = PARTICIPATION_FLAG_WEIGHTS[i];
|
|
93
|
+
|
|
94
|
+
let unslashedStakeByIncrement: number;
|
|
95
|
+
let flagName: keyof rewards.IdealAttestationsReward;
|
|
96
|
+
|
|
97
|
+
switch (i) {
|
|
98
|
+
case TIMELY_SOURCE_FLAG_INDEX: {
|
|
99
|
+
unslashedStakeByIncrement = transitionCache.prevEpochUnslashedStake.sourceStakeByIncrement;
|
|
100
|
+
flagName = "source";
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case TIMELY_TARGET_FLAG_INDEX: {
|
|
104
|
+
unslashedStakeByIncrement = transitionCache.prevEpochUnslashedStake.targetStakeByIncrement;
|
|
105
|
+
flagName = "target";
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
case TIMELY_HEAD_FLAG_INDEX: {
|
|
109
|
+
unslashedStakeByIncrement = transitionCache.prevEpochUnslashedStake.headStakeByIncrement;
|
|
110
|
+
flagName = "head";
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
default: {
|
|
114
|
+
throw Error(`Unable to retrieve unslashed stake. Unknown participation flag index: ${i}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (
|
|
119
|
+
let effectiveBalanceByIncrement = 0;
|
|
120
|
+
effectiveBalanceByIncrement <= maxEffectiveBalanceByIncrement;
|
|
121
|
+
effectiveBalanceByIncrement++
|
|
122
|
+
) {
|
|
123
|
+
const baseReward = effectiveBalanceByIncrement * baseRewardPerIncrement;
|
|
124
|
+
const rewardNumerator = baseReward * weight * unslashedStakeByIncrement;
|
|
125
|
+
// Both idealReward and penalty are rounded to nearest integer. Loss of precision is minimal as unit is gwei
|
|
126
|
+
const idealReward = Math.round(rewardNumerator / activeBalanceByIncrement / WEIGHT_DENOMINATOR);
|
|
127
|
+
const penalty = Math.round((baseReward * weight) / WEIGHT_DENOMINATOR); // Positive number indicates penalty
|
|
128
|
+
|
|
129
|
+
const idealAttestationsReward = idealRewards[effectiveBalanceByIncrement];
|
|
130
|
+
idealAttestationsReward[flagName] = isInInactivityLeak(state) ? 0 : idealReward; // No attestations rewards during inactivity leak
|
|
131
|
+
|
|
132
|
+
if (flagName !== "head") {
|
|
133
|
+
const attestationPenalty = attestationsPenalties[effectiveBalanceByIncrement];
|
|
134
|
+
attestationPenalty[flagName] = penalty;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return [idealRewards, attestationsPenalties];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Same calculation as `getRewardsAndPenaltiesAltair` but returns the breakdown of rewards instead of aggregated
|
|
143
|
+
function computeTotalAttestationsRewardsAltair(
|
|
144
|
+
config: BeaconConfig,
|
|
145
|
+
pubkey2index: PubkeyIndexMap,
|
|
146
|
+
state: CachedBeaconStateAltair,
|
|
147
|
+
transitionCache: EpochTransitionCache,
|
|
148
|
+
idealRewards: rewards.IdealAttestationsReward[],
|
|
149
|
+
penalties: AttestationsPenalty[],
|
|
150
|
+
validatorIds: (ValidatorIndex | string)[] = []
|
|
151
|
+
): rewards.TotalAttestationsReward[] {
|
|
152
|
+
const rewards = [];
|
|
153
|
+
const {flags} = transitionCache;
|
|
154
|
+
const {epochCtx} = state;
|
|
155
|
+
const validatorIndices = validatorIds
|
|
156
|
+
.map((id) => (typeof id === "number" ? id : pubkey2index.get(fromHex(id))))
|
|
157
|
+
.filter((index) => index !== undefined); // Validator indices to include in the result
|
|
158
|
+
|
|
159
|
+
const inactivityPenaltyDenominator = config.INACTIVITY_SCORE_BIAS * INACTIVITY_PENALTY_QUOTIENT_ALTAIR;
|
|
160
|
+
|
|
161
|
+
for (let i = 0; i < flags.length; i++) {
|
|
162
|
+
if (validatorIndices.length && !validatorIndices.includes(i)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const flag = flags[i];
|
|
167
|
+
if (!hasMarkers(flag, FLAG_ELIGIBLE_ATTESTER)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const effectiveBalanceIncrement = epochCtx.effectiveBalanceIncrements[i];
|
|
172
|
+
|
|
173
|
+
const currentRewards = {...defaultAttestationsReward, validatorIndex: i};
|
|
174
|
+
|
|
175
|
+
if (hasMarkers(flag, FLAG_PREV_SOURCE_ATTESTER_UNSLASHED)) {
|
|
176
|
+
currentRewards.source = idealRewards[effectiveBalanceIncrement].source;
|
|
177
|
+
} else {
|
|
178
|
+
currentRewards.source = penalties[effectiveBalanceIncrement].source * -1; // Negative reward to indicate penalty
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (hasMarkers(flag, FLAG_PREV_TARGET_ATTESTER_UNSLASHED)) {
|
|
182
|
+
currentRewards.target = idealRewards[effectiveBalanceIncrement].target;
|
|
183
|
+
} else {
|
|
184
|
+
currentRewards.target = penalties[effectiveBalanceIncrement].target * -1;
|
|
185
|
+
|
|
186
|
+
// Also incur inactivity penalty if not voting target correctly
|
|
187
|
+
const inactivityPenaltyNumerator =
|
|
188
|
+
effectiveBalanceIncrement * EFFECTIVE_BALANCE_INCREMENT * state.inactivityScores.get(i);
|
|
189
|
+
currentRewards.inactivity = Math.floor(inactivityPenaltyNumerator / inactivityPenaltyDenominator) * -1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (hasMarkers(flag, FLAG_PREV_HEAD_ATTESTER_UNSLASHED)) {
|
|
193
|
+
currentRewards.head = idealRewards[effectiveBalanceIncrement].head;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
rewards.push(currentRewards);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return rewards;
|
|
200
|
+
}
|