Skip to content

Commit 275f82e

Browse files
committed
fix: improve processEffectiveBalanceUpdates
1 parent 499cf57 commit 275f82e

11 files changed

Lines changed: 63 additions & 19 deletions

File tree

packages/beacon-node/src/chain/historicalState/worker.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ if (metricsRegister) {
8282
buckets: [0.05, 0.1, 0.2, 0.5, 1, 1.5],
8383
labelNames: ["source"],
8484
}),
85+
numEffectiveBalanceUpdates: metricsRegister.gauge({
86+
name: "lodestar_historical_state_stfn_num_effective_balance_updates_count",
87+
help: "Count of effective balance updates in epoch transition",
88+
}),
8589
preStateBalancesNodesPopulatedMiss: metricsRegister.gauge<{source: StateCloneSource}>({
8690
name: "lodestar_historical_state_stfn_balances_nodes_populated_miss_total",
8791
help: "Total count state.balances nodesPopulated is false on stfn",

packages/beacon-node/src/metrics/metrics/lodestar.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,10 @@ export function createLodestarMetrics(
332332
buckets: [0.05, 0.1, 0.2, 0.5, 1, 1.5],
333333
labelNames: ["source"],
334334
}),
335+
numEffectiveBalanceUpdates: register.gauge({
336+
name: "lodestar_stfn_effective_balance_updates_count",
337+
help: "Total count of effective balance updates",
338+
}),
335339
preStateBalancesNodesPopulatedMiss: register.gauge<{source: StateCloneSource}>({
336340
name: "lodestar_stfn_balances_nodes_populated_miss_total",
337341
help: "Total count state.balances nodesPopulated is false on stfn",

packages/state-transition/src/cache/epochTransitionCache.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Epoch, ValidatorIndex} from "@lodestar/types";
1+
import {Epoch, ValidatorIndex, phase0} from "@lodestar/types";
22
import {intDiv} from "@lodestar/utils";
33
import {EPOCHS_PER_SLASHINGS_VECTOR, FAR_FUTURE_EPOCH, ForkSeq, MIN_ACTIVATION_BALANCE} from "@lodestar/params";
44

@@ -127,6 +127,18 @@ export interface EpochTransitionCache {
127127

128128
flags: number[];
129129

130+
/**
131+
* Validators in the current epoch, should use it for read-only value instead of accessing state.validators directly.
132+
* Note that during epoch processing, validators could be updated so need to use it with care.
133+
*/
134+
validators: phase0.Validator[];
135+
136+
/**
137+
* This is for electra only
138+
* Validators that're switched to compounding during processPendingConsolidations(), not available in beforeProcessEpoch()
139+
*/
140+
newCompoundingValidators?: Set<ValidatorIndex>;
141+
130142
/**
131143
* balances array will be populated by processRewardsAndPenalties() and consumed by processEffectiveBalanceUpdates().
132144
* processRewardsAndPenalties() already has a regular Javascript array of balances.
@@ -481,7 +493,9 @@ export function beforeProcessEpoch(
481493
proposerIndices,
482494
inclusionDelays,
483495
flags,
484-
496+
validators,
497+
// will be assigned in processPendingConsolidations()
498+
newCompoundingValidators: undefined,
485499
// Will be assigned in processRewardsAndPenalties()
486500
balances: undefined,
487501
};

packages/state-transition/src/epoch/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,9 @@ export function processEpoch(
150150
const timer = metrics?.epochTransitionStepTime.startTimer({
151151
step: EpochTransitionStep.processEffectiveBalanceUpdates,
152152
});
153-
processEffectiveBalanceUpdates(fork, state, cache);
153+
const numUpdate = processEffectiveBalanceUpdates(fork, state, cache);
154154
timer?.();
155+
metrics?.numEffectiveBalanceUpdates.set(numUpdate);
155156
}
156157

157158
processSlashingsReset(state, cache);

packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ const TIMELY_TARGET = 1 << TIMELY_TARGET_FLAG_INDEX;
2323
*
2424
* - On normal mainnet conditions 0 validators change their effective balance
2525
* - In case of big innactivity event a medium portion of validators may have their effectiveBalance updated
26+
*
27+
* Return number of validators updated
2628
*/
2729
export function processEffectiveBalanceUpdates(
2830
fork: ForkSeq,
2931
state: CachedBeaconStateAllForks,
3032
cache: EpochTransitionCache
31-
): void {
33+
): number {
3234
const HYSTERESIS_INCREMENT = EFFECTIVE_BALANCE_INCREMENT / HYSTERESIS_QUOTIENT;
3335
const DOWNWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_DOWNWARD_MULTIPLIER;
3436
const UPWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_UPWARD_MULTIPLIER;
@@ -43,34 +45,38 @@ export function processEffectiveBalanceUpdates(
4345
// and updated in processPendingBalanceDeposits() and processPendingConsolidations()
4446
// so it's recycled here for performance.
4547
const balances = cache.balances ?? state.balances.getAll();
48+
const currentEpochValidators = cache.validators;
49+
const newCompoundingValidators = cache.newCompoundingValidators ?? new Set();
4650

51+
let numUpdate = 0;
4752
for (let i = 0, len = balances.length; i < len; i++) {
4853
const balance = balances[i];
4954

5055
// PERF: It's faster to access to get() every single element (4ms) than to convert to regular array then loop (9ms)
5156
let effectiveBalanceIncrement = effectiveBalanceIncrements[i];
5257
let effectiveBalance = effectiveBalanceIncrement * EFFECTIVE_BALANCE_INCREMENT;
53-
let effectiveBalanceLimit;
58+
59+
let effectiveBalanceLimit: number;
60+
if (fork < ForkSeq.electra) {
61+
effectiveBalanceLimit = MAX_EFFECTIVE_BALANCE;
62+
} else {
63+
// from electra, effectiveBalanceLimit is per validator
64+
const isCompoundingValidator =
65+
hasCompoundingWithdrawalCredential(currentEpochValidators[i].withdrawalCredentials) ||
66+
newCompoundingValidators.has(i);
67+
effectiveBalanceLimit = isCompoundingValidator ? MAX_EFFECTIVE_BALANCE_ELECTRA : MIN_ACTIVATION_BALANCE;
68+
}
5469

5570
if (
5671
// Too big
5772
effectiveBalance > balance + DOWNWARD_THRESHOLD ||
5873
// Too small. Check effectiveBalance < MAX_EFFECTIVE_BALANCE to prevent unnecessary updates
59-
effectiveBalance + UPWARD_THRESHOLD < balance
74+
(effectiveBalance < effectiveBalanceLimit && effectiveBalance + UPWARD_THRESHOLD < balance)
6075
) {
6176
// Update the state tree
6277
// Should happen rarely, so it's fine to update the tree
6378
const validator = validators.get(i);
6479

65-
if (fork < ForkSeq.electra) {
66-
effectiveBalanceLimit = MAX_EFFECTIVE_BALANCE;
67-
} else {
68-
// Electra or after
69-
effectiveBalanceLimit = hasCompoundingWithdrawalCredential(validator.withdrawalCredentials)
70-
? MAX_EFFECTIVE_BALANCE_ELECTRA
71-
: MIN_ACTIVATION_BALANCE;
72-
}
73-
7480
effectiveBalance = Math.min(balance - (balance % EFFECTIVE_BALANCE_INCREMENT), effectiveBalanceLimit);
7581
validator.effectiveBalance = effectiveBalance;
7682
// Also update the fast cached version
@@ -95,6 +101,7 @@ export function processEffectiveBalanceUpdates(
95101

96102
effectiveBalanceIncrement = newEffectiveBalanceIncrement;
97103
effectiveBalanceIncrements[i] = effectiveBalanceIncrement;
104+
numUpdate++;
98105
}
99106

100107
// TODO: Do this in afterEpochTransitionCache, looping a Uint8Array should be very cheap
@@ -105,4 +112,5 @@ export function processEffectiveBalanceUpdates(
105112
}
106113

107114
cache.nextEpochTotalActiveBalanceByIncrement = nextEpochTotalActiveBalanceByIncrement;
115+
return numUpdate;
108116
}

packages/state-transition/src/epoch/processPendingConsolidations.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {ValidatorIndex} from "@lodestar/types";
12
import {CachedBeaconStateElectra, EpochTransitionCache} from "../types.js";
23
import {decreaseBalance, increaseBalance} from "../util/balance.js";
34
import {getActiveBalance} from "../util/validator.js";
@@ -20,6 +21,7 @@ export function processPendingConsolidations(state: CachedBeaconStateElectra, ca
2021
let nextPendingConsolidation = 0;
2122
const validators = state.validators;
2223
const cachedBalances = cache.balances;
24+
const newCompoundingValidators = new Set<ValidatorIndex>();
2325

2426
for (const pendingConsolidation of state.pendingConsolidations.getAllReadonly()) {
2527
const {sourceIndex, targetIndex} = pendingConsolidation;
@@ -35,6 +37,7 @@ export function processPendingConsolidations(state: CachedBeaconStateElectra, ca
3537
}
3638
// Churn any target excess active balance of target and raise its max
3739
switchToCompoundingValidator(state, targetIndex);
40+
newCompoundingValidators.add(targetIndex);
3841
// Move active balance to target. Excess balance is withdrawable.
3942
const activeBalance = getActiveBalance(state, sourceIndex);
4043
decreaseBalance(state, sourceIndex, activeBalance);
@@ -47,5 +50,6 @@ export function processPendingConsolidations(state: CachedBeaconStateElectra, ca
4750
nextPendingConsolidation++;
4851
}
4952

53+
cache.newCompoundingValidators = newCompoundingValidators;
5054
state.pendingConsolidations = state.pendingConsolidations.sliceFrom(nextPendingConsolidation);
5155
}

packages/state-transition/src/metrics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type BeaconStateTransitionMetrics = {
1111
processBlockTime: Histogram;
1212
processBlockCommitTime: Histogram;
1313
stateHashTreeRootTime: Histogram<{source: StateHashTreeRootSource}>;
14+
numEffectiveBalanceUpdates: Gauge;
1415
preStateBalancesNodesPopulatedMiss: Gauge<{source: StateCloneSource}>;
1516
preStateBalancesNodesPopulatedHit: Gauge<{source: StateCloneSource}>;
1617
preStateValidatorsNodesPopulatedMiss: Gauge<{source: StateCloneSource}>;

packages/state-transition/test/perf/epoch/epochAltair.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue<CachedBeaconStateAllForks>
141141
itBench({
142142
id: `${stateId} - altair processEffectiveBalanceUpdates`,
143143
beforeEach: () => stateOg.value.clone(),
144-
fn: (state) => processEffectiveBalanceUpdates(ForkSeq.altair, state, cache.value),
144+
fn: (state) => {
145+
processEffectiveBalanceUpdates(ForkSeq.altair, state, cache.value);
146+
},
145147
});
146148

147149
itBench({

packages/state-transition/test/perf/epoch/epochCapella.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ function benchmarkAltairEpochSteps(stateOg: LazyValue<CachedBeaconStateAllForks>
120120
itBench({
121121
id: `${stateId} - capella processEffectiveBalanceUpdates`,
122122
beforeEach: () => stateOg.value.clone(),
123-
fn: (state) => processEffectiveBalanceUpdates(ForkSeq.capella, state, cache.value),
123+
fn: (state) => {
124+
processEffectiveBalanceUpdates(ForkSeq.capella, state, cache.value);
125+
},
124126
});
125127

126128
itBench({

packages/state-transition/test/perf/epoch/epochPhase0.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ function benchmarkPhase0EpochSteps(stateOg: LazyValue<CachedBeaconStateAllForks>
123123
itBench({
124124
id: `${stateId} - phase0 processEffectiveBalanceUpdates`,
125125
beforeEach: () => stateOg.value.clone(),
126-
fn: (state) => processEffectiveBalanceUpdates(ForkSeq.phase0, state, cache.value),
126+
fn: (state) => {
127+
processEffectiveBalanceUpdates(ForkSeq.phase0, state, cache.value);
128+
},
127129
});
128130

129131
itBench({

0 commit comments

Comments
 (0)