From 20ce6564d68cc82bfaea23fd38d9508d3733ae54 Mon Sep 17 00:00:00 2001 From: Taras Alekhin Date: Fri, 27 Sep 2024 15:05:53 +0200 Subject: [PATCH 1/5] fix: fixed initialization server --- src/http/validators/validators.service.ts | 4 ++++ src/jobs/validators/validators.service.ts | 3 ++- src/storage/validators/validators-cache.service.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/http/validators/validators.service.ts b/src/http/validators/validators.service.ts index 5c2b82f..48aab16 100644 --- a/src/http/validators/validators.service.ts +++ b/src/http/validators/validators.service.ts @@ -23,6 +23,10 @@ export class ValidatorsService { return acc; }, {} as Record); + if (!lastUpdatedAt) { + return null; + } + return { lastUpdatedAt, maxExitEpoch, diff --git a/src/jobs/validators/validators.service.ts b/src/jobs/validators/validators.service.ts index e21b785..ec466d4 100644 --- a/src/jobs/validators/validators.service.ts +++ b/src/jobs/validators/validators.service.ts @@ -88,7 +88,6 @@ export class ValidatorsService { this.validatorsStorageService.setActiveValidatorsCount(activeValidatorCount); this.validatorsStorageService.setTotalValidatorsCount(data.length); this.validatorsStorageService.setMaxExitEpoch(latestEpoch); - this.validatorsStorageService.setLastUpdate(Math.floor(Date.now() / 1000)); const frameBalances = await this.getLidoValidatorsWithdrawableBalances(data); this.validatorsStorageService.setFrameBalances(frameBalances); @@ -111,6 +110,8 @@ export class ValidatorsService { }) .inc(); }); + + this.validatorsStorageService.setLastUpdate(Math.floor(Date.now() / 1000)); }, ); } diff --git a/src/storage/validators/validators-cache.service.ts b/src/storage/validators/validators-cache.service.ts index 9a7182d..d888649 100644 --- a/src/storage/validators/validators-cache.service.ts +++ b/src/storage/validators/validators-cache.service.ts @@ -52,8 +52,8 @@ export class ValidatorsCacheService { this.validatorsStorage.setActiveValidatorsCount(Number(data[0])); this.validatorsStorage.setMaxExitEpoch(data[1]); - this.validatorsStorage.setLastUpdate(Number(data[2])); this.validatorsStorage.setFrameBalances(this.parseFrameBalances(data[3])); + this.validatorsStorage.setLastUpdate(Number(data[2])); this.logger.log(`success initialize from cache file ${cacheFileName}`, { service: ValidatorsCacheService.SERVICE_LOG_NAME, From 8d07ee7528e27b56e7a9f3009a294e138158b607 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 24 Nov 2024 07:35:23 +0000 Subject: [PATCH 2/5] chore(deps): bump cross-spawn from 7.0.3 to 7.0.6 Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6. - [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md) - [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6) --- updated-dependencies: - dependency-name: cross-spawn dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index cc7f6d1..3126bce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2950,9 +2950,9 @@ cron@2.2.0: luxon "^3.2.1" cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" From 8b52137aea4a6e01302b0db7371f88680e91076a Mon Sep 17 00:00:00 2001 From: Taras Alekhin Date: Tue, 10 Dec 2024 10:48:14 +0100 Subject: [PATCH 3/5] feat: added strict block number for requests, added min rewards for week --- src/events/rewards/rewards.service.ts | 129 +++++++++++++---------- src/waiting-time/waiting-time.service.ts | 19 ++-- 2 files changed, 84 insertions(+), 64 deletions(-) diff --git a/src/events/rewards/rewards.service.ts b/src/events/rewards/rewards.service.ts index 7325e81..e092733 100644 --- a/src/events/rewards/rewards.service.ts +++ b/src/events/rewards/rewards.service.ts @@ -61,7 +61,7 @@ export class RewardsService { } protected async updateRewards(): Promise { - const rewardsPerFrame = await this.getLastTotalRewardsPerFrame(); + const rewardsPerFrame = await this.getMinLastTotalRewardsPerFrame(); if (!rewardsPerFrame) { return; @@ -72,15 +72,11 @@ export class RewardsService { this.rewardsStorage.setElRewardsPerFrame(rewardsPerFrame.elRewards); } - public async getLastTotalRewardsPerFrame(): Promise<{ - clRewards: BigNumber; - elRewards: BigNumber; - allRewards: BigNumber; - } | null> { - const framesFromLastReport = await this.getFramesFromLastReport(); - if (framesFromLastReport === null) { + public async getMinLastTotalRewardsPerFrame() { + const framesFromLastReports = await this.getFramesFromLastReports(); + if (framesFromLastReports === null) { this.logger.warn( - 'last rewards was not updated because last TokenRebase events were not found during last 48 hours.', + 'last reward reports were not found because last TokenRebase events were not found during last week.', { service: RewardsService.SERVICE_LOG_NAME }, ); return { @@ -90,41 +86,57 @@ export class RewardsService { }; } - const { blockNumber, frames } = framesFromLastReport; + const rewards = await Promise.all( + framesFromLastReports.map(async ({ blockNumber, frames }) => { + const { clRewards, elRewards } = await this.getRewardsByBlockNumber(blockNumber, frames); - if (frames.eq(0)) { - this.logger.warn('last rewards set to 0 because frames passed from last event is 0.', { - service: RewardsService.SERVICE_LOG_NAME, - }); - return { - clRewards: BigNumber.from(0), - elRewards: BigNumber.from(0), - allRewards: BigNumber.from(0), - }; - } + return { + clRewards: clRewards, + elRewards: elRewards, + }; + }), + ); - const { preCLBalance, postCLBalance } = await this.getEthDistributed(blockNumber); - const elRewards = (await this.getElRewards(blockNumber)) ?? BigNumber.from(0); - const withdrawalsReceived = (await this.getWithdrawalsReceived(blockNumber)) ?? BigNumber.from(0); + let minCL = rewards[0].clRewards; + let minEL = rewards[0].elRewards; - const clValidatorsBalanceDiff = postCLBalance.sub(preCLBalance); - const clRewards = clValidatorsBalanceDiff.add(withdrawalsReceived); + rewards.forEach((r) => { + if (minCL.lt(r.clRewards)) { + minCL = r.clRewards; + } + + if (minEL.lt(r.elRewards)) { + minEL = r.elRewards; + } + }); + + const allRewards = minEL.add(minEL); - const allRewards = clRewards.add(elRewards).div(frames); this.logger.log(`rewardsPerFrame are updated to ${allRewards.toString()}`, { service: RewardsService.SERVICE_LOG_NAME, }); return { - clRewards: clRewards.div(frames), - elRewards: elRewards.div(frames), + clRewards: minCL, + elRewards: minEL, allRewards, }; } - protected async get48HoursAgoBlock() { + protected async getRewardsByBlockNumber(blockNumber: number, framesPassed: BigNumber) { + const { preCLBalance, postCLBalance } = await this.getEthDistributed(blockNumber); + const elRewards = (await this.getElRewards(blockNumber)) ?? BigNumber.from(0); + const withdrawalsReceived = (await this.getWithdrawalsReceived(blockNumber)) ?? BigNumber.from(0); + + const clValidatorsBalanceDiff = postCLBalance.sub(preCLBalance); + const clRewards = clValidatorsBalanceDiff.add(withdrawalsReceived); + + return { clRewards: clRewards.div(framesPassed), elRewards: elRewards.div(framesPassed) }; + } + + protected async getHoursAgoBlock(hours: number) { const currentBlock = await this.provider.getBlockNumber(); - return currentBlock - Math.ceil((2 * 24 * 60 * 60) / SECONDS_PER_SLOT); + return currentBlock - Math.ceil((hours * 60 * 60) / SECONDS_PER_SLOT); } protected async getElRewards(fromBlock: number): Promise { @@ -135,7 +147,7 @@ export class RewardsService { fromBlock, address: res.address, }); - const lastLog = logs[logs.length - 1]; + const lastLog = logs[0]; if (!lastLog) { return BigNumber.from(0); @@ -166,7 +178,7 @@ export class RewardsService { this.logger.log('ETHDistributed event logs', { service: RewardsService.SERVICE_LOG_NAME, logsCount: logs.length }); - const lastLog = logs[logs.length - 1]; + const lastLog = logs[0]; if (!lastLog) { this.logger.warn('ETHDistributed event is not found for CL balance.', { @@ -215,7 +227,7 @@ export class RewardsService { logsCount: logs.length, }); - const lastLog = logs[logs.length - 1]; + const lastLog = logs[0]; if (!lastLog) { return BigNumber.from(0); } @@ -233,11 +245,8 @@ export class RewardsService { } // reports can be skipped, so we need timeElapsed (time from last report) - protected async getFramesFromLastReport(): Promise<{ - blockNumber: number; - frames: BigNumber; - } | null> { - const last48HoursAgoBlock = await this.get48HoursAgoBlock(); + protected async getFramesFromLastReports() { + const weekAgoBlock = await this.getHoursAgoBlock(24 * 7); const res = this.contractLido.filters.TokenRebased(); @@ -246,51 +255,55 @@ export class RewardsService { { topics: res.topics, toBlock: 'latest', - fromBlock: last48HoursAgoBlock, + fromBlock: weekAgoBlock, address: res.address, }, this.logger, 'TokenRebased', ); - this.logger.log('TokenRebase event logs for last 48 hours', { + this.logger.log('TokenRebase event logs for last week', { service: RewardsService.SERVICE_LOG_NAME, logsCount: logs.length, }); if (logs.length === 0) { - this.logger.warn('TokenRebase events are not found for last 48 hours.', { + this.logger.warn('TokenRebase events are not found for last week.', { service: RewardsService.SERVICE_LOG_NAME, }); return null; } - const lastLog = logs[logs.length - 1]; - const parser = new Interface([LIDO_TOKEN_REBASED_EVENT]); - const parsedData = parser.parseLog(lastLog); + const rewardsBlocks = logs.map((log) => { + const parser = new Interface([LIDO_TOKEN_REBASED_EVENT]); + const parsedData = parser.parseLog(log); - this.logger.log('last TokenRebase event for last 48 hours', { + return { + blockNumber: log.blockNumber, + frames: BigNumber.from(parsedData.args.getValue('timeElapsed')).div( + SECONDS_PER_SLOT * SLOTS_PER_EPOCH * this.contractConfig.getEpochsPerFrame(), + ), + }; + }); + + this.logger.log('last TokenRebase events for last week', { service: RewardsService.SERVICE_LOG_NAME, - args: parsedData.args, - timeElapsed: parsedData.args.getValue('timeElapsed'), - blockNumber: lastLog.blockNumber, + rewardsBlocks, }); - return { - blockNumber: lastLog.blockNumber, - frames: BigNumber.from(parsedData.args.getValue('timeElapsed')).div( - SECONDS_PER_SLOT * SLOTS_PER_EPOCH * this.contractConfig.getEpochsPerFrame(), - ), - }; + return rewardsBlocks; } // it includes WithdrawalVault balance and diff between rewards and cached rewards from previous report - async getVaultsBalance() { + async getVaultsBalance(blockNumber: number) { const chainId = this.configService.get('CHAIN_ID'); - const withdrawalVaultAddress = await this.lidoLocator.withdrawalVault(); - const withdrawalVaultBalance = await this.provider.getBalance(withdrawalVaultAddress); - const rewardsVaultBalance = await this.provider.getBalance(EXECUTION_REWARDS_VAULT_CONTRACT_ADDRESSES[chainId]); + const withdrawalVaultAddress = await this.lidoLocator.withdrawalVault({ blockTag: blockNumber }); + const withdrawalVaultBalance = await this.provider.getBalance(withdrawalVaultAddress, blockNumber); + const rewardsVaultBalance = await this.provider.getBalance( + EXECUTION_REWARDS_VAULT_CONTRACT_ADDRESSES[chainId], + blockNumber, + ); const elRewards = this.rewardsStorage.getElRewardsPerFrame(); const clRewards = this.rewardsStorage.getClRewardsPerFrame(); diff --git a/src/waiting-time/waiting-time.service.ts b/src/waiting-time/waiting-time.service.ts index a971c23..e6c27c7 100644 --- a/src/waiting-time/waiting-time.service.ts +++ b/src/waiting-time/waiting-time.service.ts @@ -38,6 +38,7 @@ import { GetWaitingTimeInfoV2Args, GetWaitingTimeInfoV2Result, } from './waiting-time.types'; +import { SimpleFallbackJsonRpcBatchProvider } from '@lido-nestjs/execution'; @Injectable() export class WaitingTimeService { @@ -51,6 +52,7 @@ export class WaitingTimeService { protected readonly genesisTimeService: GenesisTimeService, protected readonly rewardsService: RewardsService, protected readonly queueInfo: QueueInfoStorageService, + protected readonly provider: SimpleFallbackJsonRpcBatchProvider, ) {} // preparing all needed number for calculation withdrawal time @@ -60,12 +62,14 @@ export class WaitingTimeService { // nextCalculationAt not needed anymore due to runtime queries to contract const nextCalculationAt = this.queueInfo.getNextUpdate().toISOString(); const validatorsLastUpdate = this.validators.getLastUpdate(); + const block = await this.provider.getBlock('safe'); + const blockNumber = block.number; const [unfinalized, buffer, vaultsBalance] = !cached ? await Promise.all([ - this.contractWithdrawal.unfinalizedStETH(), - this.contractLido.getBufferedEther(), - this.rewardsService.getVaultsBalance(), + this.contractWithdrawal.unfinalizedStETH({ blockTag: blockNumber }), + this.contractLido.getBufferedEther({ blockTag: blockNumber }), + this.rewardsService.getVaultsBalance(blockNumber), ]) : [cached.unfinalized, cached.buffer, cached.vaultsBalance]; @@ -296,10 +300,13 @@ export class WaitingTimeService { } public async calculateRequestsTime(ids: string[]) { + const block = await this.provider.getBlock('safe'); + const blockNumber = block.number; + const [unfinalized, buffer, vaultsBalance] = await Promise.all([ - this.contractWithdrawal.unfinalizedStETH(), - this.contractLido.getBufferedEther(), - this.rewardsService.getVaultsBalance(), + this.contractWithdrawal.unfinalizedStETH({ blockTag: blockNumber }), + this.contractLido.getBufferedEther({ blockTag: blockNumber }), + this.rewardsService.getVaultsBalance(blockNumber), ]); return Promise.all( From 009d6e7cf88cfeee3c6d197a65ef0392eebe952f Mon Sep 17 00:00:00 2001 From: Taras Alekhin Date: Tue, 10 Dec 2024 12:36:50 +0100 Subject: [PATCH 4/5] feat: updated tests --- src/waiting-time/waiting-time.service.spec.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/waiting-time/waiting-time.service.spec.ts b/src/waiting-time/waiting-time.service.spec.ts index aa25bc7..529bdf4 100644 --- a/src/waiting-time/waiting-time.service.spec.ts +++ b/src/waiting-time/waiting-time.service.spec.ts @@ -14,6 +14,7 @@ import { RewardsService } from 'events/rewards/rewards.service'; import { SECONDS_PER_SLOT, SLOTS_PER_EPOCH } from 'common/genesis-time'; import { WaitingTimeCalculationType } from './waiting-time.types'; +import { SimpleFallbackJsonRpcBatchProvider } from '@lido-nestjs/execution'; jest.mock('common/config', () => ({})); @@ -24,6 +25,7 @@ describe('WaitingTimeService', () => { let contractConfig: ContractConfigStorageService; let genesisTimeService: GenesisTimeService; let validatorsStorage: ValidatorsStorageService; + let rpcBatchProvider: SimpleFallbackJsonRpcBatchProvider; // constants const genesisTime = 1606824023; @@ -108,6 +110,12 @@ describe('WaitingTimeService', () => { getFrameBalances: jest.fn(), }, }, + { + provide: SimpleFallbackJsonRpcBatchProvider, + useValue: { + getBlock: jest.fn(), + }, + }, { provide: GenesisTimeService, useValue: { @@ -130,6 +138,7 @@ describe('WaitingTimeService', () => { contractConfig = moduleRef.get(ContractConfigStorageService); genesisTimeService = moduleRef.get(GenesisTimeService); validatorsStorage = moduleRef.get(ValidatorsStorageService); + rpcBatchProvider = moduleRef.get(SimpleFallbackJsonRpcBatchProvider); // mocks jest.spyOn(contractConfig, 'getInitialEpoch').mockReturnValue(initialEpoch); @@ -145,6 +154,8 @@ describe('WaitingTimeService', () => { jest.spyOn(validatorsStorage, 'getActiveValidatorsCount').mockReturnValue(10000); jest.spyOn(validatorsStorage, 'getFrameBalances').mockReturnValue({}); jest.spyOn(service, 'getFrameIsBunker').mockReturnValue(null); + // needed for mock only block number + jest.spyOn(rpcBatchProvider, 'getBlock').mockResolvedValue({ number: 21367114 } as any); }); afterEach(async () => { From bd98d88f477ac88223e8e0903965a1db812fe11e Mon Sep 17 00:00:00 2001 From: Taras Alekhin Date: Fri, 13 Dec 2024 11:57:32 +0100 Subject: [PATCH 5/5] fix: code fixes --- src/events/rewards/rewards.constants.ts | 2 ++ src/events/rewards/rewards.service.ts | 10 ++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/events/rewards/rewards.constants.ts b/src/events/rewards/rewards.constants.ts index b25bde2..5208949 100644 --- a/src/events/rewards/rewards.constants.ts +++ b/src/events/rewards/rewards.constants.ts @@ -4,3 +4,5 @@ export const LIDO_EL_REWARDS_RECEIVED_EVENT = 'event ELRewardsReceived(uint256 a export const LIDO_WITHDRAWALS_RECEIVED_EVENT = 'event WithdrawalsReceived(uint256 amount)'; export const LIDO_TOKEN_REBASED_EVENT = 'event TokenRebased(uint256 indexed reportTimestamp, uint256 timeElapsed, uint256 preTotalShares, uint256 preTotalEther, uint256 postTotalShares, uint256 postTotalEther, uint256 sharesMintedAsFees)'; + +export const ONE_WEEK_HOURS = 24 * 7; diff --git a/src/events/rewards/rewards.service.ts b/src/events/rewards/rewards.service.ts index e092733..2be730d 100644 --- a/src/events/rewards/rewards.service.ts +++ b/src/events/rewards/rewards.service.ts @@ -14,6 +14,7 @@ import { LIDO_ETH_DESTRIBUTED_EVENT, LIDO_TOKEN_REBASED_EVENT, LIDO_WITHDRAWALS_RECEIVED_EVENT, + ONE_WEEK_HOURS, } from './rewards.constants'; import { BigNumber } from '@ethersproject/bignumber'; import { LOGGER_PROVIDER, LoggerService } from '../../common/logger'; @@ -91,8 +92,8 @@ export class RewardsService { const { clRewards, elRewards } = await this.getRewardsByBlockNumber(blockNumber, frames); return { - clRewards: clRewards, - elRewards: elRewards, + clRewards, + elRewards, }; }), ); @@ -100,6 +101,7 @@ export class RewardsService { let minCL = rewards[0].clRewards; let minEL = rewards[0].elRewards; + // find minimum for last week rewards.forEach((r) => { if (minCL.lt(r.clRewards)) { minCL = r.clRewards; @@ -110,7 +112,7 @@ export class RewardsService { } }); - const allRewards = minEL.add(minEL); + const allRewards = minEL.add(minCL); this.logger.log(`rewardsPerFrame are updated to ${allRewards.toString()}`, { service: RewardsService.SERVICE_LOG_NAME, @@ -246,7 +248,7 @@ export class RewardsService { // reports can be skipped, so we need timeElapsed (time from last report) protected async getFramesFromLastReports() { - const weekAgoBlock = await this.getHoursAgoBlock(24 * 7); + const weekAgoBlock = await this.getHoursAgoBlock(ONE_WEEK_HOURS); const res = this.contractLido.filters.TokenRebased();