diff --git a/src/common/registry/main/abstract-registry.ts b/src/common/registry/main/abstract-registry.ts index 914be000..6abc95a2 100644 --- a/src/common/registry/main/abstract-registry.ts +++ b/src/common/registry/main/abstract-registry.ts @@ -148,7 +148,7 @@ export abstract class AbstractRegistryService { await entityManager .createQueryBuilder(RegistryKey) .insert(keysChunk) - .onConflict(['index', 'operator_index']) + .onConflict(['index', 'operator_index', 'module_address']) .merge() .execute(); }), @@ -184,7 +184,7 @@ export abstract class AbstractRegistryService { await entityManager .createQueryBuilder(RegistryOperator) .insert(operatorsChunk) - .onConflict('index') + .onConflict(['index', 'module_address']) .merge() .execute(); }), diff --git a/src/http/sr-modules-validators/entities/query.ts b/src/http/sr-modules-validators/entities/query.ts index 5688909b..2ffbd549 100644 --- a/src/http/sr-modules-validators/entities/query.ts +++ b/src/http/sr-modules-validators/entities/query.ts @@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsInt, IsOptional, Min } from 'class-validator'; import { Type } from 'class-transformer'; -export class Query { +export class ValidatorsQuery { @ApiProperty({ required: false, description: 'Number of validators to exit. If validators number less than amount, return all validators.', diff --git a/src/http/sr-modules-validators/sr-modules-validators.controller.ts b/src/http/sr-modules-validators/sr-modules-validators.controller.ts index 668f662f..e5756d0e 100644 --- a/src/http/sr-modules-validators/sr-modules-validators.controller.ts +++ b/src/http/sr-modules-validators/sr-modules-validators.controller.ts @@ -18,7 +18,7 @@ import { } from '@nestjs/swagger'; import { SRModulesValidatorsService } from './sr-modules-validators.service'; import { ModuleId } from 'http/common/entities/'; -import { Query as ValidatorsQuery } from './entities/query'; +import { ValidatorsQuery } from './entities/query'; import { ExitPresignMessageListResponse, ExitValidatorListResponse } from './entities'; import { OperatorIdParam } from 'http/common/entities/operator-id-param'; import { TooEarlyResponse } from 'http/common/entities/http-exceptions'; @@ -28,75 +28,75 @@ import { TooEarlyResponse } from 'http/common/entities/http-exceptions'; export class SRModulesValidatorsController { constructor(protected readonly validatorsService: SRModulesValidatorsService) {} - // @Version('1') - // @Get(':module_id/validators/validator-exits-to-prepare/:operator_id') - // @ApiOperation({ summary: 'Get list of N oldest lido validators' }) - // @ApiResponse({ - // status: 200, - // description: 'N oldest lido validators for operator.', - // type: ExitValidatorListResponse, - // }) - // @ApiResponse({ - // status: 425, - // description: "Meta is null, maybe data hasn't been written in db yet", - // type: TooEarlyResponse, - // }) - // @ApiNotFoundResponse({ - // status: HttpStatus.NOT_FOUND, - // description: 'Provided module or operator are not supported', - // type: NotFoundException, - // }) - // @ApiInternalServerErrorResponse({ - // status: HttpStatus.INTERNAL_SERVER_ERROR, - // description: 'Disabled endpoint/ Last Execution Layer block number in our database older than last Consensus Layer', - // type: InternalServerErrorException, - // }) - // @ApiParam({ - // name: 'module_id', - // example: '0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5', - // description: 'Staking router module_id or contract address.', - // }) - // getOldestValidators( - // @Param('module_id') moduleId: ModuleId, - // @Param() operator: OperatorIdParam, - // @Query() query: ValidatorsQuery, - // ) { - // return this.validatorsService.getOldestLidoValidators(moduleId, operator.operator_id, query); - // } + @Version('1') + @Get(':module_id/validators/validator-exits-to-prepare/:operator_id') + @ApiOperation({ summary: 'Get list of N oldest lido validators' }) + @ApiResponse({ + status: 200, + description: 'N oldest lido validators for operator.', + type: ExitValidatorListResponse, + }) + @ApiResponse({ + status: 425, + description: "Meta is null, maybe data hasn't been written in db yet", + type: TooEarlyResponse, + }) + @ApiNotFoundResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Provided module or operator are not supported', + type: NotFoundException, + }) + @ApiInternalServerErrorResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: 'Disabled endpoint/ Last Execution Layer block number in our database older than last Consensus Layer', + type: InternalServerErrorException, + }) + @ApiParam({ + name: 'module_id', + example: '0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5', + description: 'Staking router module_id or contract address.', + }) + getOldestValidators( + @Param('module_id') moduleId: ModuleId, + @Param() operator: OperatorIdParam, + @Query() query: ValidatorsQuery, + ) { + return this.validatorsService.getOldestLidoValidators(moduleId, operator.operator_id, query); + } - // @Version('1') - // @Get(':module_id/validators/generate-unsigned-exit-messages/:operator_id') - // @ApiOperation({ summary: 'Get list of exit messages for N oldest lido validators' }) - // @ApiResponse({ - // status: 200, - // description: 'Exit messages for N oldest lido validators of operator', - // type: ExitPresignMessageListResponse, - // }) - // @ApiResponse({ - // status: 425, - // description: "Meta is null, maybe data hasn't been written in db yet", - // type: TooEarlyResponse, - // }) - // @ApiNotFoundResponse({ - // status: HttpStatus.NOT_FOUND, - // description: 'Provided module or operator are not supported', - // type: NotFoundException, - // }) - // @ApiInternalServerErrorResponse({ - // status: HttpStatus.INTERNAL_SERVER_ERROR, - // description: 'Disabled endpoint/ Last Execution Layer block number in our database older than last Consensus Layer', - // type: InternalServerErrorException, - // }) - // @ApiParam({ - // name: 'module_id', - // example: '0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5', - // description: 'Staking router module_id or contract address.', - // }) - // getMessagesForOldestValidators( - // @Param('module_id') moduleId: ModuleId, - // @Param() operator: OperatorIdParam, - // @Query() query: ValidatorsQuery, - // ) { - // return this.validatorsService.getVoluntaryExitMessages(moduleId, operator.operator_id, query); - // } + @Version('1') + @Get(':module_id/validators/generate-unsigned-exit-messages/:operator_id') + @ApiOperation({ summary: 'Get list of exit messages for N oldest lido validators' }) + @ApiResponse({ + status: 200, + description: 'Exit messages for N oldest lido validators of operator', + type: ExitPresignMessageListResponse, + }) + @ApiResponse({ + status: 425, + description: "Meta is null, maybe data hasn't been written in db yet", + type: TooEarlyResponse, + }) + @ApiNotFoundResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Provided module or operator are not supported', + type: NotFoundException, + }) + @ApiInternalServerErrorResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: 'Disabled endpoint/ Last Execution Layer block number in our database older than last Consensus Layer', + type: InternalServerErrorException, + }) + @ApiParam({ + name: 'module_id', + example: '0x55032650b14df07b85bF18A3a3eC8E0Af2e028d5', + description: 'Staking router module_id or contract address.', + }) + getMessagesForOldestValidators( + @Param('module_id') moduleId: ModuleId, + @Param() operator: OperatorIdParam, + @Query() query: ValidatorsQuery, + ) { + return this.validatorsService.getVoluntaryExitMessages(moduleId, operator.operator_id, query); + } } diff --git a/src/http/sr-modules-validators/sr-modules-validators.service.ts b/src/http/sr-modules-validators/sr-modules-validators.service.ts index fea0450b..9947354b 100644 --- a/src/http/sr-modules-validators/sr-modules-validators.service.ts +++ b/src/http/sr-modules-validators/sr-modules-validators.service.ts @@ -5,7 +5,7 @@ import { ExitValidator, ExitPresignMessageListResponse, ExitPresignMessage, - Query as ValidatorsQuery, + ValidatorsQuery, } from './entities'; import { CLBlockSnapshot, ModuleId } from 'http/common/entities/'; import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; @@ -28,141 +28,67 @@ export class SRModulesValidatorsService { protected stakingRouterService: StakingRouterService, ) {} - // async getOldestLidoValidators( - // moduleId: ModuleId, - // operatorId: number, - // filters: ValidatorsQuery, - // ): Promise { - // if (this.disabledRegistry()) { - // this.logger.warn('ValidatorsRegistry is disabled in API'); - // throw new InternalServerErrorException(VALIDATORS_REGISTRY_DISABLED_ERROR); - // } - - // const stakingModule = await this.stakingRouterService.getStakingModule(moduleId); - - // if (!stakingModule) { - // throw new NotFoundException(`Module with moduleId ${moduleId} is not supported`); - // } - - // // We suppose if module in list, Keys API knows how to work with it - // // it is also important to have consistent module info and meta - - // if (stakingModule.type === STAKING_MODULE_TYPE.CURATED_ONCHAIN_V1_TYPE) { - // const { validators, meta: clMeta } = await this.getOperatorOldestValidators(operatorId, filters); - // const data = this.createExitValidatorList(validators); - // const clBlockSnapshot = new CLBlockSnapshot(clMeta); - - // return { - // data, - // meta: { - // clBlockSnapshot: clBlockSnapshot, - // }, - // }; - // } - - // throw new NotFoundException(`Modules with other types are not supported`); - // } - - // async getVoluntaryExitMessages( - // moduleId: ModuleId, - // operatorId: number, - // filters: ValidatorsQuery, - // ): Promise { - // if (this.disabledRegistry()) { - // this.logger.warn('ValidatorsRegistry is disabled in API'); - // throw new InternalServerErrorException(VALIDATORS_REGISTRY_DISABLED_ERROR); - // } - - // const stakingModule = await this.stakingRouterService.getStakingModule(moduleId); - - // if (!stakingModule) { - // throw new NotFoundException(`Module with moduleId ${moduleId} is not supported`); - // } - - // // We suppose if module in list, Keys API knows how to work with it - // // it is also important to have consistent module info and meta - - // if (stakingModule.type === STAKING_MODULE_TYPE.CURATED_ONCHAIN_V1_TYPE) { - // const { validators, meta: clMeta } = await this.getOperatorOldestValidators(operatorId, filters); - // const data = this.createExitPresignMessageList(validators, clMeta); - // const clBlockSnapshot = new CLBlockSnapshot(clMeta); - - // return { - // data, - // meta: { - // clBlockSnapshot: clBlockSnapshot, - // }, - // }; - // } - - // throw new NotFoundException(`Modules with other types are not supported`); - // } - - // private async getOperatorOldestValidators( - // operatorId: number, - // filters: ValidatorsQuery, - // ): Promise<{ validators: Validator[]; meta: ConsensusMeta }> { - // // get used keys for operator - // const { keys, meta: elMeta } = await this.curatedService.getKeysWithMeta({ - // used: true, - // operatorIndex: operatorId, - // }); - - // // check if elMeta is not null - // // if it is null, it means keys db is empty and Updating Keys Job is not finished yet - // if (!elMeta) { - // this.logger.warn(`EL meta is empty, maybe first Updating Keys Job is not finished yet.`); - // throw httpExceptionTooEarlyResp(); - // } - - // const pubkeys = keys.map((pubkey) => pubkey.key); - // const percent = - // filters?.max_amount == undefined && filters?.percent == undefined ? DEFAULT_EXIT_PERCENT : filters?.percent; - - // const result = await this.validatorsService.getOldestValidators({ - // pubkeys, - // statuses: VALIDATORS_STATUSES_FOR_EXIT, - // max_amount: filters?.max_amount, - // percent: percent, - // }); - - // if (!result) { - // // if result of this method is null it means Validators Registry is disabled - // throw new InternalServerErrorException(VALIDATORS_REGISTRY_DISABLED_ERROR); - // } - - // const { validators, meta: clMeta } = result; - - // // check if clMeta is not null - // // if it is null, it means keys db is empty and Updating Validators Job is not finished yet - // if (!clMeta) { - // this.logger.warn(`CL meta is empty, maybe first Updating Validators Job is not finished yet.`); - // throw httpExceptionTooEarlyResp(); - // } - - // // We need EL meta always be actual - // if (elMeta.blockNumber < clMeta.blockNumber) { - // this.logger.warn('Last Execution Layer block number in our database older than last Consensus Layer'); - // // add metric or alert on breaking el > cl condition - // // TODO: what answer will be better here? - // // TODO: describe in doc - // throw new InternalServerErrorException( - // 'Last Execution Layer block number in our database older than last Consensus Layer', - // ); - // } - - // return { validators, meta: clMeta }; - // } - - // private createExitValidatorList(validators: Validator[]): ExitValidator[] { - // return validators.map((v) => ({ validatorIndex: v.index, key: v.pubkey })); - // } - - // private createExitPresignMessageList(validators: Validator[], clMeta: ConsensusMeta): ExitPresignMessage[] { - // return validators.map((v) => ({ validator_index: String(v.index), epoch: String(clMeta.epoch) })); - // } - - // private disabledRegistry() { - // return !this.configService.get('VALIDATOR_REGISTRY_ENABLE'); - // } + async getOldestLidoValidators( + moduleId: ModuleId, + operatorId: number, + filters: ValidatorsQuery, + ): Promise { + if (this.disabledRegistry()) { + this.logger.warn('ValidatorsRegistry is disabled in API'); + throw new InternalServerErrorException(VALIDATORS_REGISTRY_DISABLED_ERROR); + } + + const { validators, meta } = await this.stakingRouterService.getOperatorOldestValidators( + moduleId, + operatorId, + filters, + ); + const data = this.createExitValidatorList(validators); + const clBlockSnapshot = new CLBlockSnapshot(meta); + + return { + data, + meta: { + clBlockSnapshot: clBlockSnapshot, + }, + }; + } + + async getVoluntaryExitMessages( + moduleId: ModuleId, + operatorId: number, + filters: ValidatorsQuery, + ): Promise { + if (this.disabledRegistry()) { + this.logger.warn('ValidatorsRegistry is disabled in API'); + throw new InternalServerErrorException(VALIDATORS_REGISTRY_DISABLED_ERROR); + } + + const { validators, meta } = await this.stakingRouterService.getOperatorOldestValidators( + moduleId, + operatorId, + filters, + ); + const data = this.createExitPresignMessageList(validators, meta); + const clBlockSnapshot = new CLBlockSnapshot(meta); + + return { + data, + meta: { + clBlockSnapshot: clBlockSnapshot, + }, + }; + } + + private createExitValidatorList(validators: Validator[]): ExitValidator[] { + return validators.map((v) => ({ validatorIndex: v.index, key: v.pubkey })); + } + + private createExitPresignMessageList(validators: Validator[], clMeta: ConsensusMeta): ExitPresignMessage[] { + return validators.map((v) => ({ validator_index: String(v.index), epoch: String(clMeta.epoch) })); + } + + private disabledRegistry() { + return !this.configService.get('VALIDATOR_REGISTRY_ENABLE'); + } } diff --git a/src/staking-router-modules/staking-router.module.ts b/src/staking-router-modules/staking-router.module.ts index 510937f9..71879376 100644 --- a/src/staking-router-modules/staking-router.module.ts +++ b/src/staking-router-modules/staking-router.module.ts @@ -2,6 +2,7 @@ import { Global, Module } from '@nestjs/common'; import { ExecutionProvider } from 'common/execution-provider'; import { KeyRegistryModule } from 'common/registry'; import { StorageModule } from 'storage/storage.module'; +import { ValidatorsModule } from 'validators'; import { CuratedModuleService } from './curated-module.service'; import { StakingRouterService } from './staking-router.service'; @@ -15,6 +16,7 @@ import { StakingRouterService } from './staking-router.service'; }, }), StorageModule, + ValidatorsModule, ], providers: [CuratedModuleService, StakingRouterService], exports: [CuratedModuleService, StakingRouterService], diff --git a/src/staking-router-modules/staking-router.service.ts b/src/staking-router-modules/staking-router.service.ts index e7590990..3ab67df8 100644 --- a/src/staking-router-modules/staking-router.service.ts +++ b/src/staking-router-modules/staking-router.service.ts @@ -1,5 +1,5 @@ import { EntityManager } from '@mikro-orm/knex'; -import { Inject, Injectable, LoggerService, NotFoundException } from '@nestjs/common'; +import { Inject, Injectable, InternalServerErrorException, LoggerService, NotFoundException } from '@nestjs/common'; import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; import { ModuleRef } from '@nestjs/core'; import { StakingRouterFetchService } from 'common/contracts'; @@ -24,6 +24,14 @@ import { import { SRModuleOperator } from 'http/common/entities/sr-module-operator'; import { SRModuleOperatorsKeysResponse } from 'http/sr-modules-operators-keys/entities'; import { isValidContractAddress } from './utils'; +import { ValidatorsQuery } from 'http/sr-modules-validators/entities'; +import { ConsensusMeta, Validator } from '@lido-nestjs/validators-registry'; +import { + DEFAULT_EXIT_PERCENT, + VALIDATORS_STATUSES_FOR_EXIT, + VALIDATORS_REGISTRY_DISABLED_ERROR, +} from 'validators/validators.constants'; +import { ValidatorsService } from 'validators'; @Injectable() export class StakingRouterService { @@ -35,6 +43,7 @@ export class StakingRouterService { protected readonly entityManager: EntityManager, protected readonly srModulesStorage: SRModuleStorageService, protected readonly elMetaStorage: ElMetaStorageService, + protected readonly validatorsService: ValidatorsService, ) {} // TODO: maybe add method to read modules and meta together @@ -633,40 +642,84 @@ export class StakingRouterService { }; } - // smth for validators - - // async getOldestLidoValidators( - // moduleId: ModuleId, - // operatorId: number, - // filters: ValidatorsQuery, - // ): Promise { - // if (this.disabledRegistry()) { - // this.logger.warn('ValidatorsRegistry is disabled in API'); - // throw new InternalServerErrorException(VALIDATORS_REGISTRY_DISABLED_ERROR); - // } - - // const stakingModule = await this.stakingRouterService.getStakingModule(moduleId); - - // if (!stakingModule) { - // throw new NotFoundException(`Module with moduleId ${moduleId} is not supported`); - // } - - // // We suppose if module in list, Keys API knows how to work with it - // // it is also important to have consistent module info and meta - - // if (stakingModule.type === STAKING_MODULE_TYPE.CURATED_ONCHAIN_V1_TYPE) { - // const { validators, meta: clMeta } = await this.getOperatorOldestValidators(operatorId, filters); - // const data = this.createExitValidatorList(validators); - // const clBlockSnapshot = new CLBlockSnapshot(clMeta); - - // return { - // data, - // meta: { - // clBlockSnapshot: clBlockSnapshot, - // }, - // }; - // } - - // throw new NotFoundException(`Modules with other types are not supported`); - // } + // Helper methods to return N oldest validators + + public async getOperatorOldestValidators( + moduleId: string, + operatorIndex: number, + filters: ValidatorsQuery, + ): Promise<{ validators: Validator[]; meta: ConsensusMeta }> { + const { validators, meta } = await this.entityManager.transactional( + async () => { + const stakingModule = await this.getStakingModule(moduleId); + + if (!stakingModule) { + throw new NotFoundException(`Module with moduleId ${moduleId} is not supported`); + } + + const elMeta = await this.getElBlockSnapshot(); + + if (!elMeta) { + this.logger.warn(`EL meta is empty, maybe first Updating Keys Job is not finished yet.`); + throw httpExceptionTooEarlyResp(); + } + + // read from config name of module that implement functions to fetch and store keys for type + // TODO: check what will happen if implementation is not a provider of StakingRouterModule + const impl = config[stakingModule.type]; + const moduleInstance = this.moduleRef.get(impl); + // TODO: use here method with streams + // TODO: add in select fields + // all keys should be returned with the same fields + + // this method of modules with StakingModuleInterface interface has common type of key + // /v1/keys return these common fields for all modules + // here should be request without module.stakingModuleAddress + const keys = await moduleInstance.getKeys({ operatorIndex }, stakingModule.stakingModuleAddress, { + populated: ['key'], + }); + + const pubkeys = keys.map((pubkey) => pubkey.key); + const percent = + filters?.max_amount == undefined && filters?.percent == undefined ? DEFAULT_EXIT_PERCENT : filters?.percent; + + const result = await this.validatorsService.getOldestValidators({ + pubkeys, + statuses: VALIDATORS_STATUSES_FOR_EXIT, + max_amount: filters?.max_amount, + percent: percent, + }); + + if (!result) { + // if result of this method is null it means Validators Registry is disabled + throw new InternalServerErrorException(VALIDATORS_REGISTRY_DISABLED_ERROR); + } + + const { validators, meta: clMeta } = result; + + // check if clMeta is not null + // if it is null, it means keys db is empty and Updating Validators Job is not finished yet + if (!clMeta) { + this.logger.warn(`CL meta is empty, maybe first Updating Validators Job is not finished yet.`); + throw httpExceptionTooEarlyResp(); + } + + // We need EL meta always be actual + if (elMeta.blockNumber < clMeta.blockNumber) { + this.logger.warn('Last Execution Layer block number in our database older than last Consensus Layer'); + // add metric or alert on breaking el > cl condition + // TODO: what answer will be better here? + // TODO: describe in doc + throw new InternalServerErrorException( + 'Last Execution Layer block number in our database older than last Consensus Layer', + ); + } + + return { validators, meta: clMeta }; + }, + { isolationLevel: IsolationLevel.READ_COMMITTED }, + ); + + return { validators, meta }; + } } diff --git a/src/validators/validators.constants.ts b/src/validators/validators.constants.ts new file mode 100644 index 00000000..8b0c3ab2 --- /dev/null +++ b/src/validators/validators.constants.ts @@ -0,0 +1,11 @@ +import { ValidatorStatus } from '@lido-nestjs/validators-registry'; + +export const VALIDATORS_STATUSES_FOR_EXIT = [ + ValidatorStatus.ACTIVE_ONGOING, + ValidatorStatus.PENDING_INITIALIZED, + ValidatorStatus.PENDING_QUEUED, +]; + +export const DEFAULT_EXIT_PERCENT = 10; + +export const VALIDATORS_REGISTRY_DISABLED_ERROR = 'Validators Registry is disabled. Check environment variables';