Skip to content

Commit 0d5f647

Browse files
committed
fix: fixed churn limit post electra, fixed maxExitEpoch in past was negative
1 parent bc9cb9a commit 0d5f647

File tree

4 files changed

+51
-16
lines changed

4 files changed

+51
-16
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { BigNumber } from '@ethersproject/bignumber';
2+
3+
const MIN_PER_EPOCH_CHURN_LIMIT = BigNumber.from(4); // 4 validators
4+
const MIN_ACTIVATION_BALANCE = BigNumber.from('32000000000'); // 32 ETH in Gwei
5+
const MAX_PER_EPOCH_CHURN_LIMIT = BigNumber.from('256000000000'); // 256 ETH in Gwei
6+
const CHURN_LIMIT_QUOTIENT = BigNumber.from('65536');
7+
8+
/**
9+
* Calculates the churn limit (in Gwei) based on total active balance.
10+
* Pectra-style stake-based churn limit with min/max bounds.
11+
*/
12+
export function getChurnLimitGwei(totalActiveBalanceGwei: BigNumber): BigNumber {
13+
const minLimit = MIN_PER_EPOCH_CHURN_LIMIT.mul(MIN_ACTIVATION_BALANCE);
14+
const dynamicLimit = totalActiveBalanceGwei.div(CHURN_LIMIT_QUOTIENT);
15+
const maxedLimit = dynamicLimit.gt(minLimit) ? dynamicLimit : minLimit;
16+
17+
return maxedLimit.gt(MAX_PER_EPOCH_CHURN_LIMIT) ? MAX_PER_EPOCH_CHURN_LIMIT : maxedLimit;
18+
}
19+
20+
/**
21+
* Returns average number of validators for churn limit.
22+
*/
23+
export function getChurnLimit(totalActiveBalanceGwei: BigNumber) {
24+
return getChurnLimitGwei(totalActiveBalanceGwei).div(MIN_ACTIVATION_BALANCE);
25+
}

src/jobs/validators/validators.service.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { getValidatorWithdrawalTimestamp } from './utils/get-validator-withdrawa
2222
import { IndexedValidator, ResponseValidatorsData } from '../../common/consensus-provider/consensus-provider.types';
2323
import { SweepService } from '../../common/sweep';
2424
import { toEth } from '../../common/utils/to-eth';
25+
import { getChurnLimit } from './utils/getChurnLimit';
2526

2627
export class ValidatorsService {
2728
static SERVICE_LOG_NAME = 'validators';
@@ -91,20 +92,22 @@ export class ValidatorsService {
9192
const indexedValidators: ResponseValidatorsData = await processValidatorsStream(stream);
9293
const currentEpoch = this.genesisTimeService.getCurrentEpoch();
9394

94-
let activeValidatorCount = 0;
95-
let latestEpoch = `${currentEpoch + MAX_SEED_LOOKAHEAD + 1}`;
96-
9795
const sweepMeanEpochs = await this.sweepService.getSweepDelayInEpochs(indexedValidators, currentEpoch);
9896
this.validatorsStorageService.setSweepMeanEpochs(sweepMeanEpochs);
9997

98+
let activeValidatorCount = 0;
99+
let maxExitEpoch = `${currentEpoch + MAX_SEED_LOOKAHEAD + 1}`;
100+
let totalActiveBalance = BigNumber.from(0);
101+
100102
for (const item of indexedValidators) {
101103
if (['active_ongoing', 'active_exiting', 'active_slashed'].includes(item.status)) {
102104
activeValidatorCount++;
105+
totalActiveBalance = totalActiveBalance.add(item.balance);
103106
}
104107

105108
if (item.validator.exit_epoch !== FAR_FUTURE_EPOCH.toString()) {
106-
if (BigNumber.from(item.validator.exit_epoch).gt(BigNumber.from(latestEpoch))) {
107-
latestEpoch = item.validator.exit_epoch;
109+
if (BigNumber.from(item.validator.exit_epoch).gt(BigNumber.from(maxExitEpoch))) {
110+
maxExitEpoch = item.validator.exit_epoch;
108111
}
109112
}
110113

@@ -122,8 +125,9 @@ export class ValidatorsService {
122125
);
123126

124127
this.validatorsStorageService.setActiveValidatorsCount(activeValidatorCount);
128+
this.validatorsStorageService.setChurnLimit(getChurnLimit(totalActiveBalance).toNumber());
125129
this.validatorsStorageService.setTotalValidatorsCount(indexedValidators.length);
126-
this.validatorsStorageService.setMaxExitEpoch(latestEpoch);
130+
this.validatorsStorageService.setMaxExitEpoch(maxExitEpoch);
127131
await this.findAndSetLidoValidatorsWithdrawableBalances(indexedValidators);
128132
await this.validatorsCacheService.saveDataToCache();
129133
this.validatorsStorageService.setLastUpdate(Math.floor(Date.now() / 1000));
@@ -135,7 +139,7 @@ export class ValidatorsService {
135139
this.logger.log('End update validators', {
136140
service: ValidatorsService.SERVICE_LOG_NAME,
137141
activeValidatorCount,
138-
latestEpoch,
142+
maxExitEpoch,
139143
frameBalances: stringifyFrameBalances(frameBalances),
140144
currentFrame,
141145
});

src/storage/validators/validators.service.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export class ValidatorsStorageService {
99
protected lastUpdate: number;
1010
protected frameBalances: Record<string, BigNumber>;
1111
protected sweepMeanEpochs: number;
12+
protected churnLimit: number;
1213
protected withdrawableLidoValidatorIds: string[] = [];
1314

1415
/**
@@ -91,6 +92,14 @@ export class ValidatorsStorageService {
9192
return this.sweepMeanEpochs;
9293
}
9394

95+
public setChurnLimit(churnLimit: number) {
96+
this.churnLimit = churnLimit;
97+
}
98+
99+
public getChurnLimit() {
100+
return this.churnLimit;
101+
}
102+
94103
public setWithdrawableLidoValidatorIds(withdrawableLidoValidators: string[]) {
95104
this.withdrawableLidoValidatorIds = withdrawableLidoValidators;
96105
}

src/waiting-time/waiting-time.service.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
} from './waiting-time.types';
4141
import { SimpleFallbackJsonRpcBatchProvider } from '@lido-nestjs/execution';
4242
import { toEth } from '../common/utils/to-eth';
43+
import { MAX_SEED_LOOKAHEAD } from '../jobs/validators';
4344

4445
@Injectable()
4546
export class WaitingTimeService {
@@ -137,10 +138,9 @@ export class WaitingTimeService {
137138
const queueStETH = calculateUnfinalizedEthToRequestId(requests, request);
138139
const requestTimestamp = request.timestamp.toNumber() * 1000;
139140

140-
const currentExitValidatorsDiffEpochs = Number(maxExitEpoch) - currentEpoch;
141+
const currentExitValidatorsDiffEpochs = Math.max(Number(maxExitEpoch) - currentEpoch, MAX_SEED_LOOKAHEAD);
141142
const maxExitEpochInPast =
142-
this.genesisTimeService.getEpochByTimestamp(request.timestamp.toNumber() * 1000) +
143-
currentExitValidatorsDiffEpochs;
143+
this.genesisTimeService.getEpochByTimestamp(requestTimestamp) + currentExitValidatorsDiffEpochs;
144144

145145
const { frame, type: precalculatedType } = await this.calculateWithdrawalFrame({
146146
unfinalized: queueStETH,
@@ -267,15 +267,12 @@ export class WaitingTimeService {
267267
unfinalizedETH: BigNumber,
268268
latestEpoch: string,
269269
): Promise<number> {
270-
// latest epoch of most late to exit validators
271-
const totalValidators = this.validators.getActiveValidatorsCount();
272-
273-
const churnLimit = Math.max(MIN_PER_EPOCH_CHURN_LIMIT, totalValidators / CHURN_LIMIT_QUOTIENT);
270+
const churnLimit = this.validators.getChurnLimit();
274271
const epochPerFrame = this.contractConfig.getEpochsPerFrame();
275272

276273
// calculate additional source of eth, rewards accumulated each epoch
277-
const rewardsPerDay = await this.rewardsStorage.getRewardsPerFrame();
278-
const rewardsPerEpoch = rewardsPerDay.div(epochPerFrame);
274+
const rewardsPerFrame = this.rewardsStorage.getRewardsPerFrame();
275+
const rewardsPerEpoch = rewardsPerFrame.div(epochPerFrame);
279276

280277
const maxValidatorExitRequestsPerFrameVEBO = this.contractConfig.getMaxValidatorExitRequestsPerReport();
281278
const epochsPerFrameVEBO = this.contractConfig.getEpochsPerFrameVEBO();

0 commit comments

Comments
 (0)