From e43b01ac25870b31d22322542488791f20923fb2 Mon Sep 17 00:00:00 2001 From: Taras Alekhin Date: Wed, 21 Aug 2024 15:52:16 +0200 Subject: [PATCH 1/8] feat: added envs logs and metrics, added health check on start --- src/app/app.service.ts | 36 ++++++++++++++----- src/common/config/env.validation.ts | 18 ++++++++++ src/common/health/health.module.ts | 25 +++++++++++-- src/common/prometheus/prometheus.service.ts | 9 ++++- .../common/middleware/logger.middleware.ts | 7 ++-- 5 files changed, 78 insertions(+), 17 deletions(-) diff --git a/src/app/app.service.ts b/src/app/app.service.ts index c363dc8..79b801b 100644 --- a/src/app/app.service.ts +++ b/src/app/app.service.ts @@ -1,11 +1,12 @@ import { Inject, Injectable, LoggerService, OnModuleInit } from '@nestjs/common'; import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; -import { ConfigService } from 'common/config'; +import { ConfigService, ENV_KEYS, EnvironmentVariables } from 'common/config'; import { PrometheusService } from 'common/prometheus'; import { ConsensusProviderService } from 'common/consensus-provider'; import { ExecutionProviderService } from 'common/execution-provider'; import { APP_NAME, APP_VERSION } from './app.constants'; +import { commonPatterns, satanizer } from '@lidofinance/satanizer'; @Injectable() export class AppService implements OnModuleInit { @@ -20,14 +21,8 @@ export class AppService implements OnModuleInit { public async onModuleInit(): Promise { await this.validateNetwork(); - - const network = await this.executionProviderService.getNetworkName(); - const env = this.configService.get('NODE_ENV'); - const version = APP_VERSION; - const name = APP_NAME; - - this.prometheusService.buildInfo.labels({ env, network, name, version }).inc(); - this.logger.log('Init app', { env, network, name, version }); + await this.prometheusBuildInfoMetrics(); + this.prometheusEnvsInfoMetrics(); } /** @@ -43,4 +38,27 @@ export class AppService implements OnModuleInit { throw new Error('Chain ids do not match'); } } + + protected async prometheusBuildInfoMetrics() { + const network = await this.executionProviderService.getNetworkName(); + const env = this.configService.get('NODE_ENV'); + const version = APP_VERSION; + const name = APP_NAME; + + this.prometheusService.buildInfo.labels({ env, network, name, version }).inc(); + this.logger.log('Init app', { env, network, name, version }); + } + + protected prometheusEnvsInfoMetrics() { + const secrets = this.configService.secrets; + const mask = satanizer([...commonPatterns, ...secrets]); + + const allConfigEnvs = {}; + ENV_KEYS.forEach((key: keyof EnvironmentVariables) => { + allConfigEnvs[key] = mask(this.configService.get(key)); + }); + + this.prometheusService.envsInfo.labels(allConfigEnvs).inc(); + this.logger.log('Init app dumping envs', allConfigEnvs); + } } diff --git a/src/common/config/env.validation.ts b/src/common/config/env.validation.ts index 37ba99b..b2c09bd 100644 --- a/src/common/config/env.validation.ts +++ b/src/common/config/env.validation.ts @@ -96,3 +96,21 @@ export function validate(config: Record) { return validatedConfig; } + +export const ENV_KEYS = [ + 'NODE_ENV', + 'PORT', + 'CORS_WHITELIST_REGEXP', + 'GLOBAL_THROTTLE_TTL', + 'GLOBAL_THROTTLE_LIMIT', + 'GLOBAL_CACHE_TTL', + 'SENTRY_DSN', + 'LOG_LEVEL', + 'LOG_FORMAT', + 'JOB_INTERVAL_VALIDATORS', + 'JOB_INTERVAL_QUEUE_INFO', + 'JOB_INTERVAL_CONTRACT_CONFIG', + 'CL_API_URLS', + 'EL_RPC_URLS', + 'CHAIN_ID', +]; diff --git a/src/common/health/health.module.ts b/src/common/health/health.module.ts index 27c4bc2..f5a8851 100644 --- a/src/common/health/health.module.ts +++ b/src/common/health/health.module.ts @@ -1,13 +1,34 @@ -import { Module } from '@nestjs/common'; +import { Inject, LoggerService, Module, OnModuleInit } from '@nestjs/common'; import { TerminusModule } from '@nestjs/terminus'; import { HealthController } from './health.controller'; import { ExecutionProviderHealthIndicator } from './execution-provider.indicator'; import { ConsensusProviderIndicator } from './consensus-provider.indicator'; import { GenesisTimeModule } from '../genesis-time'; +import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; @Module({ providers: [ExecutionProviderHealthIndicator, ConsensusProviderIndicator], controllers: [HealthController], imports: [TerminusModule, GenesisTimeModule], }) -export class HealthModule {} +export class HealthModule implements OnModuleInit { + constructor( + @Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService, + protected readonly consensusProviderIndicator: ConsensusProviderIndicator, + protected readonly executionProviderIndicator: ExecutionProviderHealthIndicator, + ) {} + + async onModuleInit() { + await this.startUpChecks(); + } + + async startUpChecks() { + try { + await this.consensusProviderIndicator.isHealthy('consensusProvider'); + await this.executionProviderIndicator.isHealthy('executionProvider'); + this.logger.log(`Start up checks passed successfully`); + } catch (e) { + this.logger.error(`Start up checks failed with error: ${e}`); + } + } +} diff --git a/src/common/prometheus/prometheus.service.ts b/src/common/prometheus/prometheus.service.ts index 304bffb..5cfc215 100644 --- a/src/common/prometheus/prometheus.service.ts +++ b/src/common/prometheus/prometheus.service.ts @@ -2,6 +2,7 @@ import { getOrCreateMetric } from '@willsoto/nestjs-prometheus'; import { Options, Metrics, Metric } from './interfaces'; import { METRICS_PREFIX } from './prometheus.constants'; import { RequestSourceType } from '../../http/request-time/headers/request-source-type'; +import { ENV_KEYS } from '../config'; export class PrometheusService { protected prefix = METRICS_PREFIX; @@ -25,7 +26,13 @@ export class PrometheusService { public buildInfo = this.getOrCreateMetric('Gauge', { name: 'build_info', help: 'Build information', - labelNames: ['name', 'version', 'env', 'network', 'startSlot'], + labelNames: ['name', 'version', 'env', 'network'], + }); + + public envsInfo = this.getOrCreateMetric('Gauge', { + name: 'envs_info', + help: 'Environment variables information', + labelNames: ENV_KEYS, }); public clApiRequestDuration = this.getOrCreateMetric('Histogram', { diff --git a/src/http/common/middleware/logger.middleware.ts b/src/http/common/middleware/logger.middleware.ts index 270aba4..e203d13 100644 --- a/src/http/common/middleware/logger.middleware.ts +++ b/src/http/common/middleware/logger.middleware.ts @@ -1,7 +1,6 @@ import { Inject, Injectable, LoggerService, NestMiddleware } from '@nestjs/common'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { Request, Reply } from './interfaces'; -import { FastifyRequest } from 'fastify'; @Injectable() export class LoggerMiddleware implements NestMiddleware { @@ -10,15 +9,13 @@ export class LoggerMiddleware implements NestMiddleware { private readonly logger: LoggerService, ) {} - use(request: any, reply: Reply, next: () => void) { + use(request: Request, reply: Reply, next: () => void) { const { ip, method, headers, originalUrl } = request; const userAgent = headers['user-agent'] ?? ''; - const ips = request.ips ? request.ips : []; - reply.on('finish', () => { const { statusCode } = reply; - const log = { method, originalUrl, statusCode, userAgent, ip, ips }; + const log = { method, originalUrl, statusCode, userAgent, ip }; this.logger.log(JSON.stringify(log)); }); From 1835f611f6ef43fdbfda45de28aa9fb7478a3802 Mon Sep 17 00:00:00 2001 From: DiRaiks Date: Thu, 22 Aug 2024 16:11:42 +0300 Subject: [PATCH 2/8] fix: change way to get env keys --- src/common/config/env.validation.ts | 35 ++++++++--------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/src/common/config/env.validation.ts b/src/common/config/env.validation.ts index b2c09bd..12adf16 100644 --- a/src/common/config/env.validation.ts +++ b/src/common/config/env.validation.ts @@ -6,7 +6,7 @@ import { Environment, LogLevel, LogFormat } from './interfaces'; const toNumber = ({ defaultValue }) => ({ value }) => { - if (value === '' || value == null) return defaultValue; + if (value === '' || value == null || value === undefined) return defaultValue; return Number(value); }; @@ -18,7 +18,7 @@ export class EnvironmentVariables { @IsNumber() @Min(1) @Transform(toNumber({ defaultValue: 3000 })) - PORT: number; + PORT: number = undefined; @IsOptional() @IsString() @@ -49,16 +49,16 @@ export class EnvironmentVariables { @IsOptional() @IsEnum(LogLevel) @Transform(({ value }) => value || LogLevel.info) - LOG_LEVEL: LogLevel; + LOG_LEVEL: LogLevel = undefined; @IsOptional() @IsEnum(LogFormat) @Transform(({ value }) => value || LogFormat.json) - LOG_FORMAT: LogFormat; + LOG_FORMAT: LogFormat = undefined; @IsOptional() @IsString() - JOB_INTERVAL_VALIDATORS; + JOB_INTERVAL_VALIDATORS = undefined; @IsOptional() @IsString() @@ -71,17 +71,18 @@ export class EnvironmentVariables { @IsArray() @ArrayMinSize(1) @Transform(({ value }) => value.split(',')) - CL_API_URLS!: string[]; + CL_API_URLS: string[] = undefined; @IsArray() @ArrayMinSize(1) @Transform(({ value }) => value.split(',')) - EL_RPC_URLS!: string[]; + EL_RPC_URLS: string[] = undefined; @IsNumber() @Transform(({ value }) => Number(value)) - CHAIN_ID!: number; + CHAIN_ID: number = undefined; } +export const ENV_KEYS = Object.keys(new EnvironmentVariables()); export function validate(config: Record) { const validatedConfig = plainToClass(EnvironmentVariables, config); @@ -96,21 +97,3 @@ export function validate(config: Record) { return validatedConfig; } - -export const ENV_KEYS = [ - 'NODE_ENV', - 'PORT', - 'CORS_WHITELIST_REGEXP', - 'GLOBAL_THROTTLE_TTL', - 'GLOBAL_THROTTLE_LIMIT', - 'GLOBAL_CACHE_TTL', - 'SENTRY_DSN', - 'LOG_LEVEL', - 'LOG_FORMAT', - 'JOB_INTERVAL_VALIDATORS', - 'JOB_INTERVAL_QUEUE_INFO', - 'JOB_INTERVAL_CONTRACT_CONFIG', - 'CL_API_URLS', - 'EL_RPC_URLS', - 'CHAIN_ID', -]; From 05697c781a1eb4cf2bf1c9c2ede84a5495912177 Mon Sep 17 00:00:00 2001 From: DiRaiks Date: Thu, 22 Aug 2024 16:15:52 +0300 Subject: [PATCH 3/8] fix: clear decorator --- src/common/config/env.validation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/config/env.validation.ts b/src/common/config/env.validation.ts index 12adf16..d86489d 100644 --- a/src/common/config/env.validation.ts +++ b/src/common/config/env.validation.ts @@ -6,7 +6,7 @@ import { Environment, LogLevel, LogFormat } from './interfaces'; const toNumber = ({ defaultValue }) => ({ value }) => { - if (value === '' || value == null || value === undefined) return defaultValue; + if (value === '' || value == null) return defaultValue; return Number(value); }; From 038aec32dd40c9c7bcc27c65dee8f448936f61dd Mon Sep 17 00:00:00 2001 From: Taras Alekhin Date: Fri, 23 Aug 2024 13:18:41 +0200 Subject: [PATCH 4/8] fix: fix default envs values --- src/common/config/env.validation.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/common/config/env.validation.ts b/src/common/config/env.validation.ts index d86489d..5bf2da9 100644 --- a/src/common/config/env.validation.ts +++ b/src/common/config/env.validation.ts @@ -18,7 +18,7 @@ export class EnvironmentVariables { @IsNumber() @Min(1) @Transform(toNumber({ defaultValue: 3000 })) - PORT: number = undefined; + PORT: number = 3000; @IsOptional() @IsString() @@ -49,16 +49,16 @@ export class EnvironmentVariables { @IsOptional() @IsEnum(LogLevel) @Transform(({ value }) => value || LogLevel.info) - LOG_LEVEL: LogLevel = undefined; + LOG_LEVEL: LogLevel = null; @IsOptional() @IsEnum(LogFormat) @Transform(({ value }) => value || LogFormat.json) - LOG_FORMAT: LogFormat = undefined; + LOG_FORMAT: LogFormat = null; @IsOptional() @IsString() - JOB_INTERVAL_VALIDATORS = undefined; + JOB_INTERVAL_VALIDATORS = null; @IsOptional() @IsString() @@ -71,16 +71,16 @@ export class EnvironmentVariables { @IsArray() @ArrayMinSize(1) @Transform(({ value }) => value.split(',')) - CL_API_URLS: string[] = undefined; + CL_API_URLS: string[] = null; @IsArray() @ArrayMinSize(1) @Transform(({ value }) => value.split(',')) - EL_RPC_URLS: string[] = undefined; + EL_RPC_URLS: string[] = null; @IsNumber() @Transform(({ value }) => Number(value)) - CHAIN_ID: number = undefined; + CHAIN_ID: number = null; } export const ENV_KEYS = Object.keys(new EnvironmentVariables()); From e891568588c90a29def6810cb37b2a7907d9fbca Mon Sep 17 00:00:00 2001 From: Taras Alekhin Date: Tue, 27 Aug 2024 14:58:19 +0200 Subject: [PATCH 5/8] fix: added prefix --- src/common/prometheus/prometheus.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/prometheus/prometheus.service.ts b/src/common/prometheus/prometheus.service.ts index 5cfc215..c3e8d25 100644 --- a/src/common/prometheus/prometheus.service.ts +++ b/src/common/prometheus/prometheus.service.ts @@ -30,7 +30,7 @@ export class PrometheusService { }); public envsInfo = this.getOrCreateMetric('Gauge', { - name: 'envs_info', + name: METRICS_PREFIX + 'envs_info', help: 'Environment variables information', labelNames: ENV_KEYS, }); From 84140d5be1aa2ae76d0adae40522131b88495140 Mon Sep 17 00:00:00 2001 From: Taras Alekhin Date: Thu, 29 Aug 2024 16:52:26 +0200 Subject: [PATCH 6/8] feat: added validators info endpoint fix: fixed cache refill ether parsing, fixed validators balances case --- src/http/http.constants.ts | 1 + src/http/http.module.ts | 3 +- src/http/validators/dto/index.ts | 1 + src/http/validators/dto/validators.dto.ts | 27 +++++++++++++++++ src/http/validators/index.ts | 3 ++ src/http/validators/validators.controller.ts | 28 +++++++++++++++++ src/http/validators/validators.module.ts | 12 ++++++++ src/http/validators/validators.service.ts | 30 +++++++++++++++++++ .../validators/validators-cache.service.ts | 3 +- .../calculate-frame-by-validator-balances.ts | 11 +++---- 10 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 src/http/validators/dto/index.ts create mode 100644 src/http/validators/dto/validators.dto.ts create mode 100644 src/http/validators/index.ts create mode 100644 src/http/validators/validators.controller.ts create mode 100644 src/http/validators/validators.module.ts create mode 100644 src/http/validators/validators.service.ts diff --git a/src/http/http.constants.ts b/src/http/http.constants.ts index f839bee..f38eab0 100644 --- a/src/http/http.constants.ts +++ b/src/http/http.constants.ts @@ -1,6 +1,7 @@ export const HTTP_PATHS = { 1: { nft: 'nft', + validators: 'validators', 'request-time': 'request-time', 'estimate-gas': 'estimate-gas', }, diff --git a/src/http/http.module.ts b/src/http/http.module.ts index fd1d494..0980ffd 100644 --- a/src/http/http.module.ts +++ b/src/http/http.module.ts @@ -12,9 +12,10 @@ import { CacheModule, CacheControlHeadersInterceptor } from './common/cache'; import { RequestTimeModule } from './request-time'; import { NFTModule } from './nft'; import { EstimateModule } from './estimate'; +import { ValidatorsModule } from './validators'; @Module({ - imports: [RequestTimeModule, NFTModule, EstimateModule, CacheModule, ThrottlerModule], + imports: [RequestTimeModule, NFTModule, EstimateModule, ValidatorsModule, CacheModule, ThrottlerModule], providers: [ { provide: APP_GUARD, useClass: ThrottlerBehindProxyGuard }, { provide: APP_INTERCEPTOR, useClass: CacheControlHeadersInterceptor }, diff --git a/src/http/validators/dto/index.ts b/src/http/validators/dto/index.ts new file mode 100644 index 0000000..dfc0484 --- /dev/null +++ b/src/http/validators/dto/index.ts @@ -0,0 +1 @@ +export * from './validators.dto'; diff --git a/src/http/validators/dto/validators.dto.ts b/src/http/validators/dto/validators.dto.ts new file mode 100644 index 0000000..282ae8b --- /dev/null +++ b/src/http/validators/dto/validators.dto.ts @@ -0,0 +1,27 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ValidatorsDto { + @ApiProperty({ + example: 1658650005, + description: 'ms time when data was last updated at', + }) + lastUpdatedAt: number; + + @ApiProperty({ + example: 1724856617, + description: 'max exit epoch over all CL network', + }) + maxExitEpoch: number; + + @ApiProperty({ + example: '{}', + description: 'sum of balances Lido validators with withdrawable_epoch by frame', + }) + frameBalances: Record; + + @ApiProperty({ + example: 100000, + description: 'total number of validators in network', + }) + totalValidators: number; +} diff --git a/src/http/validators/index.ts b/src/http/validators/index.ts new file mode 100644 index 0000000..435ce4b --- /dev/null +++ b/src/http/validators/index.ts @@ -0,0 +1,3 @@ +export * from './validators.controller'; +export * from './validators.module'; +export * from './validators.service'; diff --git a/src/http/validators/validators.controller.ts b/src/http/validators/validators.controller.ts new file mode 100644 index 0000000..4c95652 --- /dev/null +++ b/src/http/validators/validators.controller.ts @@ -0,0 +1,28 @@ +import { + ClassSerializerInterceptor, + Controller, + Get, + HttpStatus, + UseInterceptors, + Version, + CacheTTL, +} from '@nestjs/common'; +import { ApiResponse, ApiTags } from '@nestjs/swagger'; +import { HTTP_PATHS } from 'http/http.constants'; +import { ValidatorsService } from './validators.service'; +import { ValidatorsDto } from './dto'; + +@Controller(HTTP_PATHS[1].validators) +@ApiTags('Validators') +@UseInterceptors(ClassSerializerInterceptor) +export class ValidatorsController { + constructor(protected readonly validatorsService: ValidatorsService) {} + + @Version('1') + @Get('/') + @CacheTTL(20 * 1000) + @ApiResponse({ status: HttpStatus.OK, type: ValidatorsDto }) + async validatorsV1(): Promise { + return this.validatorsService.getAllValidatorsInfo(); + } +} diff --git a/src/http/validators/validators.module.ts b/src/http/validators/validators.module.ts new file mode 100644 index 0000000..0ca4c8d --- /dev/null +++ b/src/http/validators/validators.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from 'common/config'; +import { ValidatorsController } from './validators.controller'; +import { ValidatorsService } from './validators.service'; +import { ValidatorsStorageModule } from '../../storage'; + +@Module({ + imports: [ConfigModule, ValidatorsStorageModule], + controllers: [ValidatorsController], + providers: [ValidatorsService], +}) +export class ValidatorsModule {} diff --git a/src/http/validators/validators.service.ts b/src/http/validators/validators.service.ts new file mode 100644 index 0000000..7f5b5c8 --- /dev/null +++ b/src/http/validators/validators.service.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from 'common/config'; +import { ValidatorsStorageService } from '../../storage'; + +@Injectable() +export class ValidatorsService { + constructor( + protected readonly configService: ConfigService, + protected readonly validatorsServiceStorage: ValidatorsStorageService, + ) {} + + getAllValidatorsInfo() { + const lastUpdatedAt = this.validatorsServiceStorage.getLastUpdate(); + const maxExitEpoch = Number(this.validatorsServiceStorage.getMaxExitEpoch()); + const frameBalancesBigNumber = this.validatorsServiceStorage.getFrameBalances(); + const totalValidators = this.validatorsServiceStorage.getTotal(); + + const frameBalances = Object.keys(frameBalancesBigNumber).reduce((acc, item) => { + acc[item] = frameBalancesBigNumber[item].toString(); + return acc; + }, {} as Record); + + return { + lastUpdatedAt, + maxExitEpoch, + frameBalances, + totalValidators, + }; + } +} diff --git a/src/storage/validators/validators-cache.service.ts b/src/storage/validators/validators-cache.service.ts index edd0a71..bf551cb 100644 --- a/src/storage/validators/validators-cache.service.ts +++ b/src/storage/validators/validators-cache.service.ts @@ -4,7 +4,6 @@ import * as path from 'path'; import { LOGGER_PROVIDER, LoggerService } from '../../common/logger'; import { ValidatorsStorageService } from './validators.service'; import { BigNumber } from '@ethersproject/bignumber'; -import { parseEther } from '@ethersproject/units'; @Injectable() export class ValidatorsCacheService { @@ -97,7 +96,7 @@ export class ValidatorsCacheService { protected parseFrameBalances(frameBalancesStr: string) { const frameBalances = JSON.parse(frameBalancesStr); return Object.keys(frameBalances).reduce((acc, key) => { - return { ...acc, [key]: parseEther(frameBalances[key]) }; + return { ...acc, [key]: BigNumber.from(frameBalances[key]) }; }, {}); } } diff --git a/src/waiting-time/utils/calculate-frame-by-validator-balances.ts b/src/waiting-time/utils/calculate-frame-by-validator-balances.ts index 76bfad0..35d6172 100644 --- a/src/waiting-time/utils/calculate-frame-by-validator-balances.ts +++ b/src/waiting-time/utils/calculate-frame-by-validator-balances.ts @@ -17,7 +17,7 @@ export const calculateFrameByValidatorBalances = (args: calculateFrameByValidato let lastFrame = BigNumber.from(currentFrame); const frames = Object.keys(frameBalances); - let result = null; + let result: BigNumber = null; for (let i = 0; i < frames.length; i++) { const frame = frames[i]; @@ -43,8 +43,9 @@ export const calculateFrameByValidatorBalances = (args: calculateFrameByValidato const sweepingMean = calculateSweepingMean(totalValidators).toNumber(); const framesOfSweepingMean = Math.ceil(sweepingMean / epochPerFrame); - const resultFrame = result.add(framesOfSweepingMean).toNumber(); - - // If withdrawable_epoch is less than current frame, should return next frame - return resultFrame < currentFrame ? currentFrame + 1 : resultFrame; + // todo: return back previous version of calculation after rework sweeping mean + // If resulted withdrawable_epoch is less than current frame, should return next frame + result = result.lt(currentFrame) ? BigNumber.from(currentFrame) : result; + const resultWithSweep = result.add(framesOfSweepingMean).toNumber(); + return resultWithSweep; }; From e6734f1105054bf7b5b82132c45306e4ef9ee6ed Mon Sep 17 00:00:00 2001 From: Taras Alekhin Date: Fri, 30 Aug 2024 19:58:15 +0200 Subject: [PATCH 7/8] feat: added validators balances metrics --- src/common/prometheus/prometheus.service.ts | 6 ++++++ .../validators/strigify-frame-balances.ts | 9 +++++++++ src/jobs/validators/validators.service.ts | 20 ++++++++++++++++--- .../validators/validators-cache.service.ts | 11 ++-------- 4 files changed, 34 insertions(+), 12 deletions(-) create mode 100644 src/common/validators/strigify-frame-balances.ts diff --git a/src/common/prometheus/prometheus.service.ts b/src/common/prometheus/prometheus.service.ts index c3e8d25..0fc7b6d 100644 --- a/src/common/prometheus/prometheus.service.ts +++ b/src/common/prometheus/prometheus.service.ts @@ -35,6 +35,12 @@ export class PrometheusService { labelNames: ENV_KEYS, }); + public validatorsState = this.getOrCreateMetric('Gauge', { + name: METRICS_PREFIX + 'validators_state', + help: 'balances of Lido validators with withdrawable_epoch by frames', + labelNames: ['frame', 'balance'], + }); + public clApiRequestDuration = this.getOrCreateMetric('Histogram', { name: METRICS_PREFIX + 'cl_api_requests_duration_seconds', help: 'CL API request duration', diff --git a/src/common/validators/strigify-frame-balances.ts b/src/common/validators/strigify-frame-balances.ts new file mode 100644 index 0000000..1c11b37 --- /dev/null +++ b/src/common/validators/strigify-frame-balances.ts @@ -0,0 +1,9 @@ +import { BigNumber } from '@ethersproject/bignumber'; + +export function stringifyFrameBalances(frameBalances: Record) { + return JSON.stringify( + Object.keys(frameBalances).reduce((acc, key) => { + return { ...acc, [key]: frameBalances[key].toString() }; + }, {}), + ); +} diff --git a/src/jobs/validators/validators.service.ts b/src/jobs/validators/validators.service.ts index b5d48c9..841b027 100644 --- a/src/jobs/validators/validators.service.ts +++ b/src/jobs/validators/validators.service.ts @@ -16,6 +16,8 @@ import { ResponseValidatorsData, Validator } from './validators.types'; import { parseGweiToWei } from '../../common/utils/parse-gwei-to-big-number'; import { ValidatorsCacheService } from 'storage/validators/validators-cache.service'; import { CronExpression } from '@nestjs/schedule'; +import { PrometheusService } from '../../common/prometheus'; +import { stringifyFrameBalances } from '../../common/validators/strigify-frame-balances'; export class ValidatorsService { static SERVICE_LOG_NAME = 'validators'; @@ -23,6 +25,7 @@ export class ValidatorsService { constructor( @Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService, + protected readonly prometheusService: PrometheusService, protected readonly consensusProviderService: ConsensusProviderService, protected readonly configService: ConfigService, protected readonly jobService: JobService, @@ -78,23 +81,34 @@ export class ValidatorsService { await unblock(); } - await this.setLidoValidatorsWithdrawableBalances(data); this.validatorsStorageService.setTotal(totalValidators); this.validatorsStorageService.setMaxExitEpoch(latestEpoch); this.validatorsStorageService.setLastUpdate(Math.floor(Date.now() / 1000)); + const frameBalances = await this.getLidoValidatorsWithdrawableBalances(data); + this.validatorsStorageService.setFrameBalances(frameBalances); await this.validatorsCacheService.saveDataToCache(); this.logger.log('End update validators', { service: ValidatorsService.SERVICE_LOG_NAME, totalValidators, latestEpoch, + frameBalances: stringifyFrameBalances(frameBalances), + }); + + Object.keys(frameBalances).forEach((frame) => { + this.prometheusService.validatorsState + .labels({ + frame, + balance: frameBalances[frame], + }) + .inc(); }); }, ); } - protected async setLidoValidatorsWithdrawableBalances(validators: Validator[]) { + protected async getLidoValidatorsWithdrawableBalances(validators: Validator[]) { const keysData = await this.lidoKeys.fetchLidoKeysData(); const lidoValidators = await this.lidoKeys.getLidoValidatorsByKeys(keysData.data, validators); @@ -111,6 +125,6 @@ export class ValidatorsService { await unblock(); } - this.validatorsStorageService.setFrameBalances(frameBalances); + return frameBalances; } } diff --git a/src/storage/validators/validators-cache.service.ts b/src/storage/validators/validators-cache.service.ts index bf551cb..a703ff5 100644 --- a/src/storage/validators/validators-cache.service.ts +++ b/src/storage/validators/validators-cache.service.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import { LOGGER_PROVIDER, LoggerService } from '../../common/logger'; import { ValidatorsStorageService } from './validators.service'; import { BigNumber } from '@ethersproject/bignumber'; +import { stringifyFrameBalances } from '../../common/validators/strigify-frame-balances'; @Injectable() export class ValidatorsCacheService { @@ -75,7 +76,7 @@ export class ValidatorsCacheService { this.validatorsStorage.getTotal(), this.validatorsStorage.getMaxExitEpoch(), this.validatorsStorage.getLastUpdate(), - this.stringifyFrameBalances(this.validatorsStorage.getFrameBalances()), + stringifyFrameBalances(this.validatorsStorage.getFrameBalances()), ].join(ValidatorsCacheService.CACHE_DATA_DIVIDER); await writeFile(cacheFileName, data); this.logger.log(`success save to file ${cacheFileName}`, { service: ValidatorsCacheService.SERVICE_LOG_NAME }); @@ -85,14 +86,6 @@ export class ValidatorsCacheService { return path.join(ValidatorsCacheService.CACHE_DIR, ValidatorsCacheService.CACHE_FILE_NAME); }; - protected stringifyFrameBalances(frameBalances: Record) { - return JSON.stringify( - Object.keys(frameBalances).reduce((acc, key) => { - return { ...acc, [key]: frameBalances[key].toString() }; - }, {}), - ); - } - protected parseFrameBalances(frameBalancesStr: string) { const frameBalances = JSON.parse(frameBalancesStr); return Object.keys(frameBalances).reduce((acc, key) => { From c878f0d1363739c73f4407aa872ced349a5cda6a Mon Sep 17 00:00:00 2001 From: Taras Alekhin Date: Mon, 2 Sep 2024 11:46:21 +0200 Subject: [PATCH 8/8] feat: updated validators info endpoint --- src/http/http.constants.ts | 2 +- src/http/validators/dto/validators.dto.ts | 6 ++++++ src/http/validators/validators.controller.ts | 6 +++--- src/http/validators/validators.module.ts | 3 ++- src/http/validators/validators.service.ts | 6 +++++- src/jobs/validators/validators.service.ts | 2 ++ .../utils/calculate-frame-by-validator-balances.ts | 9 ++++----- 7 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/http/http.constants.ts b/src/http/http.constants.ts index f38eab0..b754e63 100644 --- a/src/http/http.constants.ts +++ b/src/http/http.constants.ts @@ -1,7 +1,7 @@ export const HTTP_PATHS = { 1: { nft: 'nft', - validators: 'validators', + 'validators-info': 'validators-info', 'request-time': 'request-time', 'estimate-gas': 'estimate-gas', }, diff --git a/src/http/validators/dto/validators.dto.ts b/src/http/validators/dto/validators.dto.ts index 282ae8b..142db42 100644 --- a/src/http/validators/dto/validators.dto.ts +++ b/src/http/validators/dto/validators.dto.ts @@ -24,4 +24,10 @@ export class ValidatorsDto { description: 'total number of validators in network', }) totalValidators: number; + + @ApiProperty({ + example: 100000, + description: 'current frame', + }) + currentFrame: number; } diff --git a/src/http/validators/validators.controller.ts b/src/http/validators/validators.controller.ts index 4c95652..40077bd 100644 --- a/src/http/validators/validators.controller.ts +++ b/src/http/validators/validators.controller.ts @@ -12,17 +12,17 @@ import { HTTP_PATHS } from 'http/http.constants'; import { ValidatorsService } from './validators.service'; import { ValidatorsDto } from './dto'; -@Controller(HTTP_PATHS[1].validators) +@Controller() @ApiTags('Validators') @UseInterceptors(ClassSerializerInterceptor) export class ValidatorsController { constructor(protected readonly validatorsService: ValidatorsService) {} @Version('1') - @Get('/') + @Get(HTTP_PATHS[1]['validators-info']) @CacheTTL(20 * 1000) @ApiResponse({ status: HttpStatus.OK, type: ValidatorsDto }) async validatorsV1(): Promise { - return this.validatorsService.getAllValidatorsInfo(); + return this.validatorsService.getValidatorsInfo(); } } diff --git a/src/http/validators/validators.module.ts b/src/http/validators/validators.module.ts index 0ca4c8d..b772236 100644 --- a/src/http/validators/validators.module.ts +++ b/src/http/validators/validators.module.ts @@ -3,9 +3,10 @@ import { ConfigModule } from 'common/config'; import { ValidatorsController } from './validators.controller'; import { ValidatorsService } from './validators.service'; import { ValidatorsStorageModule } from '../../storage'; +import { GenesisTimeModule } from '../../common/genesis-time'; @Module({ - imports: [ConfigModule, ValidatorsStorageModule], + imports: [ConfigModule, ValidatorsStorageModule, GenesisTimeModule], controllers: [ValidatorsController], providers: [ValidatorsService], }) diff --git a/src/http/validators/validators.service.ts b/src/http/validators/validators.service.ts index 7f5b5c8..44c7e70 100644 --- a/src/http/validators/validators.service.ts +++ b/src/http/validators/validators.service.ts @@ -1,19 +1,22 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from 'common/config'; import { ValidatorsStorageService } from '../../storage'; +import { GenesisTimeService } from '../../common/genesis-time'; @Injectable() export class ValidatorsService { constructor( protected readonly configService: ConfigService, protected readonly validatorsServiceStorage: ValidatorsStorageService, + protected readonly genesisTimeService: GenesisTimeService, ) {} - getAllValidatorsInfo() { + getValidatorsInfo() { const lastUpdatedAt = this.validatorsServiceStorage.getLastUpdate(); const maxExitEpoch = Number(this.validatorsServiceStorage.getMaxExitEpoch()); const frameBalancesBigNumber = this.validatorsServiceStorage.getFrameBalances(); const totalValidators = this.validatorsServiceStorage.getTotal(); + const currentFrame = this.genesisTimeService.getFrameOfEpoch(this.genesisTimeService.getCurrentEpoch()); const frameBalances = Object.keys(frameBalancesBigNumber).reduce((acc, item) => { acc[item] = frameBalancesBigNumber[item].toString(); @@ -25,6 +28,7 @@ export class ValidatorsService { maxExitEpoch, frameBalances, totalValidators, + currentFrame, }; } } diff --git a/src/jobs/validators/validators.service.ts b/src/jobs/validators/validators.service.ts index 841b027..6ed224a 100644 --- a/src/jobs/validators/validators.service.ts +++ b/src/jobs/validators/validators.service.ts @@ -89,11 +89,13 @@ export class ValidatorsService { this.validatorsStorageService.setFrameBalances(frameBalances); await this.validatorsCacheService.saveDataToCache(); + const currentFrame = this.genesisTimeService.getFrameOfEpoch(this.genesisTimeService.getCurrentEpoch()); this.logger.log('End update validators', { service: ValidatorsService.SERVICE_LOG_NAME, totalValidators, latestEpoch, frameBalances: stringifyFrameBalances(frameBalances), + currentFrame, }); Object.keys(frameBalances).forEach((frame) => { diff --git a/src/waiting-time/utils/calculate-frame-by-validator-balances.ts b/src/waiting-time/utils/calculate-frame-by-validator-balances.ts index 35d6172..002fd35 100644 --- a/src/waiting-time/utils/calculate-frame-by-validator-balances.ts +++ b/src/waiting-time/utils/calculate-frame-by-validator-balances.ts @@ -43,9 +43,8 @@ export const calculateFrameByValidatorBalances = (args: calculateFrameByValidato const sweepingMean = calculateSweepingMean(totalValidators).toNumber(); const framesOfSweepingMean = Math.ceil(sweepingMean / epochPerFrame); - // todo: return back previous version of calculation after rework sweeping mean - // If resulted withdrawable_epoch is less than current frame, should return next frame - result = result.lt(currentFrame) ? BigNumber.from(currentFrame) : result; - const resultWithSweep = result.add(framesOfSweepingMean).toNumber(); - return resultWithSweep; + const resultFrame = result.add(framesOfSweepingMean).toNumber(); + + // If withdrawable_epoch is less than current frame, should return next frame + return resultFrame < currentFrame ? currentFrame + 1 : resultFrame; };