From 9c158bcfbc2462c64192a3db4e219f3d224889b4 Mon Sep 17 00:00:00 2001 From: Alex Movsunov Date: Wed, 6 Sep 2023 11:42:17 +0400 Subject: [PATCH 01/75] avro streams pipelines clean code restore cr --- .vscode/launch.json | 20 ++++++++ package.json | 2 + src/app/app.module.ts | 1 + src/common/registry/storage/key.storage.ts | 9 ++++ .../registry/storage/operator.storage.ts | 9 ++++ src/common/streams/index.ts | 1 + src/common/streams/streamify.ts | 30 +++++++++++ src/http/keys/keys.controller.ts | 2 +- .../sr-modules-keys.controller.ts | 2 +- .../sr-module-operators-keys.response.ts | 29 +++++++++++ src/http/sr-modules-operators-keys/index.ts | 1 + .../sr-modules-operators-keys.controller.ts | 32 ++++++++++-- .../sr-modules-operators-keys.service.ts | 49 +++++++++++++++--- .../sr-modules-operators-keys.types.ts | 12 +++++ .../curated-module.service.ts | 50 +++++++++++++------ .../interfaces/staking-module.interface.ts | 2 + yarn.lock | 19 +++++++ 17 files changed, 245 insertions(+), 25 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 src/common/streams/index.ts create mode 100644 src/common/streams/streamify.ts create mode 100644 src/http/sr-modules-operators-keys/sr-modules-operators-keys.types.ts diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..488d2a6a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Debug Nest Framework", + "runtimeExecutable": "yarn", + "runtimeArgs": [ + "start:debug", + ], + "cwd": "${workspaceFolder}/src", + "autoAttachChildProcesses": true, + "restart": true, + "sourceMaps": true, + "stopOnEntry": false, + "console": "integratedTerminal", + } + ] +} diff --git a/package.json b/package.json index f5d56e40..2dffc3d1 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "ethers": "^5.5.4", "fastify-swagger": "^4.13.1", "jsonstream": "^1.0.3", + "pg-query-stream": "^4.5.3", "prom-client": "^14.0.1", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", @@ -81,6 +82,7 @@ "@nestjs/testing": "^8.2.5", "@types/cache-manager": "^3.4.2", "@types/jest": "^27.4.0", + "@types/jsonstream": "^0.8.31", "@types/node": "^17.0.9", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.10.0", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 9efe2c5a..a6cd8a12 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -38,6 +38,7 @@ import { StakingRouterModule } from 'staking-router-modules'; autoLoadEntities: false, cache: { enabled: false }, debug: false, + safe: true, registerRequestContext: true, allowGlobalContext: false, }; diff --git a/src/common/registry/storage/key.storage.ts b/src/common/registry/storage/key.storage.ts index 09b785ab..dd3ffcba 100644 --- a/src/common/registry/storage/key.storage.ts +++ b/src/common/registry/storage/key.storage.ts @@ -16,6 +16,15 @@ export class RegistryKeyStorageService { return await this.repository.find(where, options); } + findStream(where: FilterQuery, fields?: string[]): AsyncIterable { + const knex = this.repository.getKnex(); + return knex + .select(fields || '*') + .from('registry_key') + .where(where) + .stream(); + } + /** find all keys */ async findAll(moduleAddress: string): Promise { return await this.repository.find( diff --git a/src/common/registry/storage/operator.storage.ts b/src/common/registry/storage/operator.storage.ts index 347a9c88..d71fad1e 100644 --- a/src/common/registry/storage/operator.storage.ts +++ b/src/common/registry/storage/operator.storage.ts @@ -16,6 +16,15 @@ export class RegistryOperatorStorageService { return await this.repository.find(where, options); } + findStream(where: FilterQuery, fields?: string[]): AsyncIterable { + const knex = this.repository.getKnex(); + return knex + .select(fields || '*') + .from('registry_operator') + .where(where) + .stream(); + } + /** find all operators */ async findAll(moduleAddress: string): Promise { return await this.repository.find( diff --git a/src/common/streams/index.ts b/src/common/streams/index.ts new file mode 100644 index 00000000..84d8fd0f --- /dev/null +++ b/src/common/streams/index.ts @@ -0,0 +1 @@ +export * from './streamify'; diff --git a/src/common/streams/streamify.ts b/src/common/streams/streamify.ts new file mode 100644 index 00000000..d4f02849 --- /dev/null +++ b/src/common/streams/streamify.ts @@ -0,0 +1,30 @@ +import { Readable, ReadableOptions } from 'stream'; + +class GeneratorToStream extends Readable { + constructor(options: ReadableOptions, protected readonly generator: AsyncGenerator) { + super(options); + } + + _read() { + try { + this.generator + .next() + .then((result) => { + if (!result.done) { + this.push(result.value); + } else { + this.push(null); + } + }) + .catch((e) => { + this.emit('error', e); + }); + } catch (e) { + this.emit('error', e); + } + } +} + +export function streamify(generator: AsyncGenerator) { + return new GeneratorToStream({ objectMode: true }, generator); +} diff --git a/src/http/keys/keys.controller.ts b/src/http/keys/keys.controller.ts index 45a950cb..d51bf89b 100644 --- a/src/http/keys/keys.controller.ts +++ b/src/http/keys/keys.controller.ts @@ -53,7 +53,7 @@ export class KeysController { try { for (const keysGenerator of keysGenerators) { for await (const keysBatch of keysGenerator) { - jsonStream.write(keysBatch); + jsonStream.write(JSON.stringify(keysBatch)); } } } finally { diff --git a/src/http/sr-modules-keys/sr-modules-keys.controller.ts b/src/http/sr-modules-keys/sr-modules-keys.controller.ts index d916319c..8f51cc45 100644 --- a/src/http/sr-modules-keys/sr-modules-keys.controller.ts +++ b/src/http/sr-modules-keys/sr-modules-keys.controller.ts @@ -71,7 +71,7 @@ export class SRModulesKeysController { reply.type('application/json').send(jsonStream); for await (const keysBatch of keysGenerator) { - jsonStream.write(keysBatch); + jsonStream.write(JSON.stringify(keysBatch)); } jsonStream.end(); diff --git a/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts b/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts index 05c679eb..27ef46db 100644 --- a/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts +++ b/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts @@ -45,3 +45,32 @@ export class SRModuleOperatorsKeysResponse { }) meta!: ELMeta; } + +export class SRModulesOperatorsKeysStreamResponse { + @ApiProperty({ + type: 'array', + items: { oneOf: [{ $ref: getSchemaPath(CuratedOperator) }] }, + description: 'Operators of staking router module', + }) + operators?: SRModuleOperator[]; + + @ApiProperty({ + type: 'array', + items: { oneOf: [{ $ref: getSchemaPath(CuratedKey) }] }, + description: 'Keys of staking router module', + }) + keys?: SRModuleKey[]; + + @ApiProperty({ + type: 'array', + items: { oneOf: [{ $ref: getSchemaPath(SRModule) }] }, + description: 'List of Staking Router', + }) + modules?: SRModule[]; + + @ApiProperty({ + nullable: true, + type: () => ELMeta, + }) + meta?: ELMeta; +} diff --git a/src/http/sr-modules-operators-keys/index.ts b/src/http/sr-modules-operators-keys/index.ts index 9ef30a0a..dbf0531b 100644 --- a/src/http/sr-modules-operators-keys/index.ts +++ b/src/http/sr-modules-operators-keys/index.ts @@ -1,3 +1,4 @@ export * from './sr-modules-operators-keys.module'; export * from './sr-modules-operators-keys.controller'; export * from './sr-modules-operators-keys.service'; +export * from './sr-modules-operators-keys.types'; diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts index c02f5c14..b2e0d622 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts @@ -1,13 +1,15 @@ +import { pipeline } from 'node:stream/promises'; +import { IsolationLevel } from '@mikro-orm/core'; import { Controller, Get, Version, Param, Query, NotFoundException, HttpStatus, Res } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags, ApiParam, ApiNotFoundResponse } from '@nestjs/swagger'; -import { SRModuleOperatorsKeysResponse } from './entities'; +import { SRModuleOperatorsKeysResponse, SRModulesOperatorsKeysStreamResponse } from './entities'; import { ModuleId, KeyQuery } from 'http/common/entities/'; import { SRModulesOperatorsKeysService } from './sr-modules-operators-keys.service'; import { TooEarlyResponse } from 'http/common/entities/http-exceptions'; import { EntityManager } from '@mikro-orm/knex'; import * as JSONStream from 'jsonstream'; import type { FastifyReply } from 'fastify'; -import { IsolationLevel } from '@mikro-orm/core'; +import { streamify } from 'common/streams'; @Controller('/modules') @ApiTags('operators-keys') @@ -63,7 +65,7 @@ export class SRModulesOperatorsKeysController { reply.type('application/json').send(jsonStream); for await (const keysBatch of keysGenerator) { - jsonStream.write(keysBatch); + jsonStream.write(JSON.stringify(keysBatch)); } jsonStream.end(); @@ -71,4 +73,28 @@ export class SRModulesOperatorsKeysController { { isolationLevel: IsolationLevel.REPEATABLE_READ }, ); } + + @Version('2') + @ApiOperation({ summary: 'Comprehensive stream for staking router modules, operators and their keys' }) + @ApiResponse({ + status: 200, + description: 'Stream of all SR modules, operators and keys', + type: SRModulesOperatorsKeysStreamResponse, + }) + @ApiResponse({ + status: 425, + description: 'Meta has not exist yet, maybe data was not written in db yet', + type: TooEarlyResponse, + }) + @Get('operators/keys') + async getModulesOperatorsKeysStream(@Res() reply: FastifyReply) { + const jsonStream = JSONStream.stringify(); + + reply.type('application/json').send(jsonStream); + + await this.entityManager.transactional( + () => pipeline([streamify(this.srModulesOperatorsKeys.getModulesOperatorsKeysGenerator()), jsonStream]), + { isolationLevel: IsolationLevel.REPEATABLE_READ }, + ); + } } diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.service.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.service.ts index cab680ea..f2930471 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.service.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.service.ts @@ -6,14 +6,15 @@ import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; import { StakingRouterService } from 'staking-router-modules/staking-router.service'; import { KeyEntity, OperatorEntity } from 'staking-router-modules/interfaces/staking-module.interface'; import { EntityManager } from '@mikro-orm/knex'; +import { MetaStreamRecord, ModulesOperatorsKeysRecord } from './sr-modules-operators-keys.types'; @Injectable() export class SRModulesOperatorsKeysService { constructor( @Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService, protected readonly configService: ConfigService, - protected stakingRouterService: StakingRouterService, - protected readonly entityManager: EntityManager, + protected readonly stakingRouterService: StakingRouterService, + readonly entityManager: EntityManager, ) {} public async get( @@ -29,13 +30,49 @@ export class SRModulesOperatorsKeysService { const moduleInstance = this.stakingRouterService.getStakingRouterModuleImpl(module.type); - const keysGenerator: AsyncGenerator = await moduleInstance.getKeysStream( - module.stakingModuleAddress, - filters, - ); + const keysGenerator: AsyncGenerator = moduleInstance.getKeysStream(module.stakingModuleAddress, filters); const operatorsFilter = filters.operatorIndex ? { index: filters.operatorIndex } : {}; const operators: OperatorEntity[] = await moduleInstance.getOperators(module.stakingModuleAddress, operatorsFilter); return { operators, keysGenerator, module, meta: { elBlockSnapshot } }; } + + public async *getModulesOperatorsKeysGenerator(): AsyncGenerator { + const { stakingModules, elBlockSnapshot } = await this.stakingRouterService.getStakingModulesAndMeta(); + + const meta: MetaStreamRecord = { elBlockSnapshot }; + for (const stakingModule of stakingModules) { + const moduleInstance = this.stakingRouterService.getStakingRouterModuleImpl(stakingModule.type); + + const keysGenerator = moduleInstance.getKeysStream(stakingModule.stakingModuleAddress, {}); + const operatorsGenerator = moduleInstance.getOperatorsStream(stakingModule.stakingModuleAddress, {}); + + let nextKey = await keysGenerator.next(); + let nextOperator = await operatorsGenerator.next(); + + yield { + stakingModule, + meta, + key: nextKey.value || null, + operator: nextOperator.value || null, + }; + + do { + if (!nextKey.done) { + nextKey = await keysGenerator.next(); + } + + if (!nextOperator.done) { + nextOperator = await operatorsGenerator.next(); + } + + yield { + stakingModule: null, + meta: null, + key: nextKey.value || null, + operator: nextOperator.value || null, + }; + } while (!nextKey.done || !nextOperator.done); + } + } } diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.types.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.types.ts new file mode 100644 index 00000000..b229bac3 --- /dev/null +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.types.ts @@ -0,0 +1,12 @@ +import { ELBlockSnapshot } from 'http/common/entities'; +import { KeyEntity, OperatorEntity } from 'staking-router-modules/interfaces/staking-module.interface'; +import { SrModuleEntity } from 'storage/sr-module.entity'; + +export type MetaStreamRecord = { elBlockSnapshot: ELBlockSnapshot } | null; + +export type ModulesOperatorsKeysRecord = { + stakingModule: SrModuleEntity | null; + key: KeyEntity | null; + operator: OperatorEntity | null; + meta: MetaStreamRecord; +}; diff --git a/src/staking-router-modules/curated-module.service.ts b/src/staking-router-modules/curated-module.service.ts index a3726fca..fb73e29a 100644 --- a/src/staking-router-modules/curated-module.service.ts +++ b/src/staking-router-modules/curated-module.service.ts @@ -56,29 +56,51 @@ export class CuratedModuleService implements StakingModuleInterface { public async *getKeysStream(contractAddress: string, filters: KeysFilter): AsyncGenerator { const where = {}; if (filters.operatorIndex != undefined) { - where['operatorIndex'] = filters.operatorIndex; + where['operator_index'] = filters.operatorIndex; } if (filters.used != undefined) { where['used'] = filters.used; } - where['moduleAddress'] = contractAddress; - - const batchSize = 10000; - let offset = 0; + where['module_address'] = contractAddress; - while (true) { - const chunk = await this.keyStorageService.find(where, { limit: batchSize, offset }); - if (chunk.length === 0) { - break; - } + const keyStream = this.keyStorageService.findStream(where, [ + 'index', + 'operator_index as operatorIndex', + 'key', + 'deposit_signature as depositSignature', + 'used', + 'module_address as moduleAddress', + ]); - offset += batchSize; + for await (const record of keyStream) { + yield record; + } + } - for (const record of chunk) { - yield record; - } + public async *getOperatorsStream(moduleAddress: string, filters?: OperatorsFilter): AsyncGenerator { + const where = {}; + if (filters?.index != undefined) { + where['index'] = filters.index; + } + // we store operators of modules with the same impl at the same table + where['module_address'] = moduleAddress; + + const operatorStream = this.operatorStorageService.findStream(where, [ + 'index', + 'active', + 'name', + 'reward_address as rewardAddress', + 'staking_limit as stakingLimit', + 'stopped_validators as stoppedValidators', + 'total_signing_keys as totalSigningKeys', + 'used_signing_keys as usedSigningKeys', + 'module_address as moduleAddress', + ]); + + for await (const record of operatorStream) { + yield record; } } diff --git a/src/staking-router-modules/interfaces/staking-module.interface.ts b/src/staking-router-modules/interfaces/staking-module.interface.ts index 377e1c75..1cc22012 100644 --- a/src/staking-router-modules/interfaces/staking-module.interface.ts +++ b/src/staking-router-modules/interfaces/staking-module.interface.ts @@ -47,6 +47,8 @@ export interface StakingModuleInterface { getOperators(moduleAddress: string, filters?: OperatorsFilter): Promise; + getOperatorsStream(moduleAddress: string, filters?: OperatorsFilter): AsyncGenerator; + getOperator(moduleAddress: string, index: number): Promise; getCurrentNonce(moduleAddress: string, blockHash: string): Promise; diff --git a/yarn.lock b/yarn.lock index 53277f6e..ca8dacf7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1612,6 +1612,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/jsonstream@^0.8.31": + version "0.8.31" + resolved "https://registry.yarnpkg.com/@types/jsonstream/-/jsonstream-0.8.31.tgz#8df1c36c80dbb36e40794217d782022455d1e89a" + integrity sha512-32nJ7wl+q0lebxHo8iXqpmgLmkj6lYNHKp97+h8qteQ6O6IKnY+GyD/Eitqi4ul2Bbw3MScfRKtaHxt8gpC98w== + dependencies: + "@types/node" "*" + "@types/node-fetch@^2.5.12": version "2.6.2" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" @@ -5528,6 +5535,11 @@ pg-connection-string@2.5.0, pg-connection-string@^2.5.0: resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.5.0.tgz#538cadd0f7e603fc09a12590f3b8a452c2c0cf34" integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== +pg-cursor@^2.10.3: + version "2.10.3" + resolved "https://registry.yarnpkg.com/pg-cursor/-/pg-cursor-2.10.3.tgz#4b44fbaede168a4785def56b8ac195e7df354472" + integrity sha512-rDyBVoqPVnx/PTmnwQAYgusSeAKlTL++gmpf5klVK+mYMFEqsOc6VHHZnPKc/4lOvr4r6fiMuoxSFuBF1dx4FQ== + pg-int8@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" @@ -5543,6 +5555,13 @@ pg-protocol@^1.5.0: resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.5.0.tgz#b5dd452257314565e2d54ab3c132adc46565a6a0" integrity sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ== +pg-query-stream@^4.5.3: + version "4.5.3" + resolved "https://registry.yarnpkg.com/pg-query-stream/-/pg-query-stream-4.5.3.tgz#841ce414064d7b14bd2540d2267bdf40779d26f2" + integrity sha512-ufa94r/lHJdjAm3+zPZEO0gXAmCb4tZPaOt7O76mjcxdL/HxwTuryy76km+u0odBBgtfdKFYq/9XGfiYeQF0yA== + dependencies: + pg-cursor "^2.10.3" + pg-types@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" From 0f1970456fd17f4b76ad6ff0a23d705be3fe0a0c Mon Sep 17 00:00:00 2001 From: Alex Movsunov Date: Mon, 18 Sep 2023 14:15:25 +0400 Subject: [PATCH 02/75] fix e2e --- .vscode/launch.json | 35 +++++++++++++++++++ package.json | 1 - src/http/keys/keys.controller.ts | 2 +- src/http/keys/keys.e2e-spec.ts | 14 +++++++- .../sr-modules-operators-keys.controller.ts | 2 +- yarn.lock | 7 ---- 6 files changed, 50 insertions(+), 11 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 488d2a6a..5a07de47 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,6 +15,41 @@ "sourceMaps": true, "stopOnEntry": false, "console": "integratedTerminal", + }, + { + "type": "node", + "request": "launch", + "name": "Debug Jest e2e", + "runtimeExecutable": "yarn", + "runtimeArgs": [ + "test:e2e", + ], + "cwd": "${workspaceFolder}/src", + "autoAttachChildProcesses": true, + "restart": true, + "sourceMaps": true, + "stopOnEntry": false, + "console": "integratedTerminal", + }, + { + "name": "Jest file", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/jest", + "args": [ + "${fileBasenameNoExtension}", + "--runInBand", + "--watch", + "--coverage=false", + "--no-cache" + ], + "cwd": "${workspaceRoot}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "sourceMaps": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + } } ] } diff --git a/package.json b/package.json index 2dffc3d1..5e855efc 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,6 @@ "@nestjs/testing": "^8.2.5", "@types/cache-manager": "^3.4.2", "@types/jest": "^27.4.0", - "@types/jsonstream": "^0.8.31", "@types/node": "^17.0.9", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.10.0", diff --git a/src/http/keys/keys.controller.ts b/src/http/keys/keys.controller.ts index d51bf89b..45a950cb 100644 --- a/src/http/keys/keys.controller.ts +++ b/src/http/keys/keys.controller.ts @@ -53,7 +53,7 @@ export class KeysController { try { for (const keysGenerator of keysGenerators) { for await (const keysBatch of keysGenerator) { - jsonStream.write(JSON.stringify(keysBatch)); + jsonStream.write(keysBatch); } } } finally { diff --git a/src/http/keys/keys.e2e-spec.ts b/src/http/keys/keys.e2e-spec.ts index 9cc733fe..7334b881 100644 --- a/src/http/keys/keys.e2e-spec.ts +++ b/src/http/keys/keys.e2e-spec.ts @@ -3,11 +3,12 @@ import { Test } from '@nestjs/testing'; import { Global, INestApplication, Module, ValidationPipe, VersioningType } from '@nestjs/common'; import { KeyRegistryService, + RegistryKey, RegistryKeyStorageService, RegistryStorageModule, RegistryStorageService, } from '../../common/registry'; -import { MikroORM } from '@mikro-orm/core'; +import { FilterQuery, MikroORM } from '@mikro-orm/core'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { KeysController } from './keys.controller'; import { StakingRouterModule } from '../../staking-router-modules/staking-router.module'; @@ -131,6 +132,15 @@ describe('KeyController (e2e)', () => { } } + class RegistryKeyStorageServiceMock extends RegistryKeyStorageService { + async *findStream(where: FilterQuery, fields?: string[] | undefined): AsyncIterable { + const result = await this.find(where); + for (const key of result) { + yield key; + } + } + } + beforeAll(async () => { const imports = [ // sqlite3 only supports serializable transactions, ignoring the isolation level param @@ -151,6 +161,8 @@ describe('KeyController (e2e)', () => { const moduleRef = await Test.createTestingModule({ imports, controllers, providers }) .overrideProvider(KeyRegistryService) .useClass(KeysRegistryServiceMock) + .overrideProvider(RegistryKeyStorageService) + .useClass(RegistryKeyStorageServiceMock) .compile(); elMetaStorageService = moduleRef.get(ElMetaStorageService); diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts index b2e0d622..03d71717 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts @@ -65,7 +65,7 @@ export class SRModulesOperatorsKeysController { reply.type('application/json').send(jsonStream); for await (const keysBatch of keysGenerator) { - jsonStream.write(JSON.stringify(keysBatch)); + jsonStream.write(keysBatch); } jsonStream.end(); diff --git a/yarn.lock b/yarn.lock index ca8dacf7..75a8f25b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1612,13 +1612,6 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/jsonstream@^0.8.31": - version "0.8.31" - resolved "https://registry.yarnpkg.com/@types/jsonstream/-/jsonstream-0.8.31.tgz#8df1c36c80dbb36e40794217d782022455d1e89a" - integrity sha512-32nJ7wl+q0lebxHo8iXqpmgLmkj6lYNHKp97+h8qteQ6O6IKnY+GyD/Eitqi4ul2Bbw3MScfRKtaHxt8gpC98w== - dependencies: - "@types/node" "*" - "@types/node-fetch@^2.5.12": version "2.6.2" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" From e017147ed489b630d5ab4f9a5b069566ce106027 Mon Sep 17 00:00:00 2001 From: Alex Movsunov Date: Mon, 25 Sep 2023 19:54:58 +0400 Subject: [PATCH 03/75] pull new code --- .../entities/sr-module-operators-keys.response.ts | 14 +++++++------- .../sr-modules-operators-keys.types.ts | 7 +++---- yarn.lock | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts b/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts index 583d3096..65ab6f80 100644 --- a/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts +++ b/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts @@ -1,4 +1,4 @@ -import { ApiProperty, ApiExtraModels } from '@nestjs/swagger'; +import { ApiProperty, ApiExtraModels, getSchemaPath } from '@nestjs/swagger'; import { Key, Operator, StakingModuleResponse, ELMeta } from '../../common/entities/'; @ApiExtraModels(Operator) @@ -41,24 +41,24 @@ export class SRModuleOperatorsKeysResponse { export class SRModulesOperatorsKeysStreamResponse { @ApiProperty({ type: 'array', - items: { oneOf: [{ $ref: getSchemaPath(CuratedOperator) }] }, + items: { oneOf: [{ $ref: getSchemaPath(Operator) }] }, description: 'Operators of staking router module', }) - operators?: SRModuleOperator[]; + operators?: Operator[]; @ApiProperty({ type: 'array', - items: { oneOf: [{ $ref: getSchemaPath(CuratedKey) }] }, + items: { oneOf: [{ $ref: getSchemaPath(Key) }] }, description: 'Keys of staking router module', }) - keys?: SRModuleKey[]; + keys?: Key[]; @ApiProperty({ type: 'array', - items: { oneOf: [{ $ref: getSchemaPath(SRModule) }] }, + items: { oneOf: [{ $ref: getSchemaPath(StakingModuleResponse) }] }, description: 'List of Staking Router', }) - modules?: SRModule[]; + modules?: StakingModuleResponse[]; @ApiProperty({ nullable: true, diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.types.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.types.ts index b229bac3..2cc6ea5a 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.types.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.types.ts @@ -1,12 +1,11 @@ -import { ELBlockSnapshot } from 'http/common/entities'; -import { KeyEntity, OperatorEntity } from 'staking-router-modules/interfaces/staking-module.interface'; +import { ELBlockSnapshot, Key, Operator } from 'http/common/entities'; import { SrModuleEntity } from 'storage/sr-module.entity'; export type MetaStreamRecord = { elBlockSnapshot: ELBlockSnapshot } | null; export type ModulesOperatorsKeysRecord = { stakingModule: SrModuleEntity | null; - key: KeyEntity | null; - operator: OperatorEntity | null; + key: Key | null; + operator: Operator | null; meta: MetaStreamRecord; }; diff --git a/yarn.lock b/yarn.lock index a1e8baba..bf91dfd7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -85,7 +85,7 @@ "@aragon/apps-vault" "^4.0.0" "@aragon/os" "4.2.0" -"@aragon/apps-lido@github:lidofinance/aragon-apps#master": +"@aragon/apps-lido@lidofinance/aragon-apps#master": version "1.0.0" resolved "https://codeload.github.com/lidofinance/aragon-apps/tar.gz/b09834d29c0db211ddd50f50905cbeff257fc8e0" From 24cc07d3a32a372a212ceeacbf7c07812f951628 Mon Sep 17 00:00:00 2001 From: Alex Movsunov Date: Tue, 26 Sep 2023 10:39:28 +0400 Subject: [PATCH 04/75] fix e2e tests --- src/http/keys/keys.e2e-spec.ts | 2 +- .../sr-modules-keys.controller.ts | 2 +- .../sr-modules-keys.e2e-spec.ts | 11 +++++++++++ .../sr-modules-operators-keys.e2e-spec.ts | 11 +++++++++++ .../sr-modules-operators.e2e-spec.ts | 12 ++++++++++++ .../sr-modules-validators.e2e-spec.ts | 11 +++++++++++ src/http/sr-modules/sr-modules.e2e-spec.ts | 18 +++++++++++++++++- 7 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/http/keys/keys.e2e-spec.ts b/src/http/keys/keys.e2e-spec.ts index e9344bf1..25030106 100644 --- a/src/http/keys/keys.e2e-spec.ts +++ b/src/http/keys/keys.e2e-spec.ts @@ -54,7 +54,7 @@ describe('KeyController (e2e)', () => { } class RegistryKeyStorageServiceMock extends RegistryKeyStorageService { - async *findStream(where: FilterQuery, fields?: string[] | undefined): AsyncIterable { + async *findStream(where: FilterQuery, fields): AsyncIterable { const result = await this.find(where); for (const key of result) { yield key; diff --git a/src/http/sr-modules-keys/sr-modules-keys.controller.ts b/src/http/sr-modules-keys/sr-modules-keys.controller.ts index 6c5b9747..e2d9a806 100644 --- a/src/http/sr-modules-keys/sr-modules-keys.controller.ts +++ b/src/http/sr-modules-keys/sr-modules-keys.controller.ts @@ -87,7 +87,7 @@ export class SRModulesKeysController { reply.type('application/json').send(jsonStream); for await (const keysBatch of keysGenerator) { - jsonStream.write(JSON.stringify(keysBatch)); + jsonStream.write(keysBatch); } jsonStream.end(); diff --git a/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts b/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts index cecdc37c..80b298f6 100644 --- a/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts +++ b/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts @@ -63,6 +63,15 @@ describe('SRModulesKeysController (e2e)', () => { } } + class RegistryKeyStorageServiceMock extends RegistryKeyStorageService { + async *findStream(where, fields): AsyncIterable { + const result = await this.find(where); + for (const key of result) { + yield key; + } + } + } + beforeAll(async () => { const imports = [ // sqlite3 only supports serializable transactions, ignoring the isolation level param @@ -83,6 +92,8 @@ describe('SRModulesKeysController (e2e)', () => { const moduleRef = await Test.createTestingModule({ imports, controllers, providers }) .overrideProvider(KeyRegistryService) .useClass(KeysRegistryServiceMock) + .overrideProvider(RegistryKeyStorageService) + .useClass(RegistryKeyStorageServiceMock) .compile(); elMetaStorageService = moduleRef.get(ElMetaStorageService); diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts index 44833846..99a2fbf1 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts @@ -55,6 +55,15 @@ describe('SRModulesOperatorsKeysController (e2e)', () => { } } + class RegistryKeyStorageServiceMock extends RegistryKeyStorageService { + async *findStream(where, fields): AsyncIterable { + const result = await this.find(where); + for (const key of result) { + yield key; + } + } + } + beforeAll(async () => { const imports = [ // sqlite3 only supports serializable transactions, ignoring the isolation level param @@ -75,6 +84,8 @@ describe('SRModulesOperatorsKeysController (e2e)', () => { const moduleRef = await Test.createTestingModule({ imports, controllers, providers }) .overrideProvider(KeyRegistryService) .useClass(KeysRegistryServiceMock) + .overrideProvider(RegistryKeyStorageService) + .useClass(RegistryKeyStorageServiceMock) .compile(); elMetaStorageService = moduleRef.get(ElMetaStorageService); diff --git a/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts b/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts index d20d9c07..af290b7f 100644 --- a/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts +++ b/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts @@ -3,6 +3,7 @@ import { Test } from '@nestjs/testing'; import { Global, INestApplication, Module, ValidationPipe, VersioningType } from '@nestjs/common'; import { KeyRegistryService, + RegistryKeyStorageService, RegistryOperatorStorageService, RegistryStorageModule, RegistryStorageService, @@ -69,6 +70,15 @@ describe('SRModuleOperatorsController (e2e)', () => { } } + class RegistryKeyStorageServiceMock extends RegistryKeyStorageService { + async *findStream(where, fields): AsyncIterable { + const result = await this.find(where); + for (const key of result) { + yield key; + } + } + } + beforeAll(async () => { const imports = [ // sqlite3 only supports serializable transactions, ignoring the isolation level param @@ -89,6 +99,8 @@ describe('SRModuleOperatorsController (e2e)', () => { const moduleRef = await Test.createTestingModule({ imports, controllers, providers }) .overrideProvider(KeyRegistryService) .useClass(KeysRegistryServiceMock) + .overrideProvider(RegistryKeyStorageService) + .useClass(RegistryKeyStorageServiceMock) .compile(); elMetaStorageService = moduleRef.get(ElMetaStorageService); diff --git a/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts b/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts index c6a3e86a..87719cd9 100644 --- a/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts +++ b/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts @@ -82,6 +82,15 @@ describe('SRModulesValidatorsController (e2e)', () => { } } + class RegistryKeyStorageServiceMock extends RegistryKeyStorageService { + async *findStream(where, fields): AsyncIterable { + const result = await this.find(where); + for (const key of result) { + yield key; + } + } + } + const consensusServiceMock = { getBlockV2: (args: { blockId: string | number }) => { return block; @@ -135,6 +144,8 @@ describe('SRModulesValidatorsController (e2e)', () => { }) .overrideProvider(ConsensusService) .useValue(consensusServiceMock) + .overrideProvider(RegistryKeyStorageService) + .useClass(RegistryKeyStorageServiceMock) .compile(); elMetaStorageService = moduleRef.get(ElMetaStorageService); diff --git a/src/http/sr-modules/sr-modules.e2e-spec.ts b/src/http/sr-modules/sr-modules.e2e-spec.ts index 16d4794f..f0c48da7 100644 --- a/src/http/sr-modules/sr-modules.e2e-spec.ts +++ b/src/http/sr-modules/sr-modules.e2e-spec.ts @@ -4,7 +4,12 @@ import { Global, INestApplication, Module, ValidationPipe, VersioningType } from import { MikroORM } from '@mikro-orm/core'; import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { KeyRegistryService, RegistryStorageModule, RegistryStorageService } from '../../common/registry'; +import { + KeyRegistryService, + RegistryKeyStorageService, + RegistryStorageModule, + RegistryStorageService, +} from '../../common/registry'; import { StakingRouterModule } from '../../staking-router-modules/staking-router.module'; import { dvtModule, curatedModule, dvtModuleResp, curatedModuleResp, dvtModuleInUpperCase } from '../module.fixture'; import { SRModuleStorageService } from '../../storage/sr-module.storage'; @@ -45,6 +50,15 @@ describe('SRModulesController (e2e)', () => { } } + class RegistryKeyStorageServiceMock extends RegistryKeyStorageService { + async *findStream(where, fields): AsyncIterable { + const result = await this.find(where); + for (const key of result) { + yield key; + } + } + } + beforeAll(async () => { const imports = [ // sqlite3 only supports serializable transactions, ignoring the isolation level param @@ -66,6 +80,8 @@ describe('SRModulesController (e2e)', () => { const moduleRef = await Test.createTestingModule({ imports, controllers, providers }) .overrideProvider(KeyRegistryService) .useClass(KeysRegistryServiceMock) + .overrideProvider(RegistryKeyStorageService) + .useClass(RegistryKeyStorageServiceMock) .compile(); elMetaStorageService = moduleRef.get(ElMetaStorageService); From 9f02fe4b88a392687f30856425a4ced669f228d2 Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Fri, 20 Oct 2023 09:18:23 +0400 Subject: [PATCH 05/75] VAL-369: Use postgres in e2e --- .dockerignore | 3 - .vscode/launch.json | 2 +- Dockerfile.e2e | 15 +++++ docker-compose.e2e.yml | 43 +++++++++++++ package.json | 4 +- src/app/app-testing.module.ts | 25 ++++++-- src/app/database-testing.module.ts | 29 +++++++++ src/app/index.ts | 1 + src/http/keys/keys.e2e-spec.ts | 28 ++------- .../sr-modules-keys.e2e-spec.ts | 25 ++------ .../sr-modules-operators-keys.e2e-spec.ts | 14 +---- .../sr-modules-operators.e2e-spec.ts | 25 ++------ .../sr-modules-validators.e2e-spec.ts | 62 +++++-------------- src/http/sr-modules/sr-modules.e2e-spec.ts | 34 ++-------- 14 files changed, 147 insertions(+), 163 deletions(-) create mode 100644 Dockerfile.e2e create mode 100644 docker-compose.e2e.yml create mode 100644 src/app/database-testing.module.ts diff --git a/.dockerignore b/.dockerignore index fc1211d6..4230b796 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,9 +2,6 @@ /dist /node_modules -# Env -.env - # Logs logs *.log diff --git a/.vscode/launch.json b/.vscode/launch.json index 5a07de47..2a6f524b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,7 +22,7 @@ "name": "Debug Jest e2e", "runtimeExecutable": "yarn", "runtimeArgs": [ - "test:e2e", + "test:e2e:docker:debug", ], "cwd": "${workspaceFolder}/src", "autoAttachChildProcesses": true, diff --git a/Dockerfile.e2e b/Dockerfile.e2e new file mode 100644 index 00000000..4d871920 --- /dev/null +++ b/Dockerfile.e2e @@ -0,0 +1,15 @@ +FROM node:18.14.2-alpine3.16 as building + +RUN apk add --no-cache git=2.36.6-r0 + +WORKDIR /app + +COPY package.json yarn.lock chronix.config.ts .env ./ +COPY jest* ./ +COPY ./tsconfig*.json ./ + +RUN yarn install --frozen-lockfile --non-interactive && yarn cache clean +COPY ./src ./src +RUN yarn typechain + +CMD ["yarn", "test:e2e:docker"] diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml new file mode 100644 index 00000000..0e84c19f --- /dev/null +++ b/docker-compose.e2e.yml @@ -0,0 +1,43 @@ +version: '3.7' + +services: + e2e_pgdb: + container_name: e2e_pgdb + image: postgres:14-alpine + restart: unless-stopped + environment: + - POSTGRES_DB=${DB_NAME} + - POSTGRES_USER=${DB_USER} + - POSTGRES_PASSWORD=${DB_PASSWORD} + expose: + - 5432:5432 + volumes: + - ./.volumes/pgdata-${CHAIN_ID}/:/var/lib/postgresql/data + + e2e_keys_api: + container_name: e2e_keys_api + build: + context: ./ + dockerfile: Dockerfile.e2e + environment: + - NODE_ENV=production + - PORT=${PORT} + - CORS_WHITELIST_REGEXP=${CORS_WHITELIST_REGEXP} + - GLOBAL_THROTTLE_TTL=${GLOBAL_THROTTLE_TTL} + - GLOBAL_THROTTLE_LIMIT=${GLOBAL_THROTTLE_LIMIT} + - GLOBAL_CACHE_TTL=${GLOBAL_CACHE_TTL} + - LOG_LEVEL=${LOG_LEVEL} + - LOG_FORMAT=${LOG_FORMAT} + - PROVIDERS_URLS=${PROVIDERS_URLS} + - CL_API_URLS=${CL_API_URLS} + - CHAIN_ID=${CHAIN_ID} + - DB_NAME=${DB_NAME} + - DB_PORT=5432 + - DB_HOST=e2e_pgdb + - DB_USER=${DB_USER} + - DB_PASSWORD=${DB_PASSWORD} + - JOB_INTERVAL_REGISTRY=${JOB_INTERVAL_REGISTRY} + - VALIDATOR_REGISTRY_ENABLE=${VALIDATOR_REGISTRY_ENABLE} + - JOB_INTERVAL_VALIDATORS_REGISTRY=${JOB_INTERVAL_VALIDATORS_REGISTRY} + depends_on: + - e2e_pgdb diff --git a/package.json b/package.json index ac9ae448..ed990a82 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,9 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config jest-e2e.json && chronix test", + "test:e2e": "docker-compose --env-file=./.env -f docker-compose.e2e.yml up --build --abort-on-container-exit", + "test:e2e:docker": "mikro-orm schema:drop -r && mikro-orm migration:up && jest -i --config jest-e2e.json && chronix test", + "test:e2e:docker:debug": "mikro-orm migration:up && node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest -i --config jest-e2e.json && chronix test", "typechain": "typechain --target=ethers-v5 --out-dir ./src/generated ./src/staking-router-modules/contracts/abi/*.json", "chronix:compile": "chronix compile", "chronix:test": "chronix test" diff --git a/src/app/app-testing.module.ts b/src/app/app-testing.module.ts index 8de87552..f9e150fd 100644 --- a/src/app/app-testing.module.ts +++ b/src/app/app-testing.module.ts @@ -1,7 +1,8 @@ import { APP_INTERCEPTOR } from '@nestjs/core'; import { Module } from '@nestjs/common'; import { PrometheusModule } from '../common/prometheus'; -import { ConfigModule } from '../common/config'; +import { ConfigModule, ConfigService } from '../common/config'; +import config from 'mikro-orm.config'; import { SentryInterceptor } from '../common/sentry'; import { HealthModule } from '../common/health'; import { AppService } from './app.service'; @@ -22,11 +23,23 @@ import { KeysUpdateModule } from 'jobs/keys-update'; ConfigModule, ExecutionProviderModule, ConsensusProviderModule, - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], + MikroOrmModule.forRootAsync({ + async useFactory(configService: ConfigService) { + return { + ...config, + dbName: configService.get('DB_NAME'), + host: configService.get('DB_HOST'), + port: configService.get('DB_PORT'), + user: configService.get('DB_USER'), + password: configService.get('DB_PASSWORD'), + autoLoadEntities: false, + cache: { enabled: false }, + debug: false, + registerRequestContext: true, + allowGlobalContext: true, + }; + }, + inject: [ConfigService], }), LoggerModule.forRoot({ transports: [nullTransport()] }), ScheduleModule.forRoot(), diff --git a/src/app/database-testing.module.ts b/src/app/database-testing.module.ts new file mode 100644 index 00000000..4e9d4bc8 --- /dev/null +++ b/src/app/database-testing.module.ts @@ -0,0 +1,29 @@ +import { Module } from '@nestjs/common'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import config from 'mikro-orm.config'; +import { ConfigModule, ConfigService } from 'common/config'; + +@Module({ + imports: [ + ConfigModule, + MikroOrmModule.forRootAsync({ + async useFactory(configService: ConfigService) { + return { + ...config, + dbName: configService.get('DB_NAME'), + host: configService.get('DB_HOST'), + port: configService.get('DB_PORT'), + user: configService.get('DB_USER'), + password: configService.get('DB_PASSWORD'), + autoLoadEntities: false, + cache: { enabled: false }, + debug: false, + registerRequestContext: true, + allowGlobalContext: true, + }; + }, + inject: [ConfigService], + }), + ], +}) +export class DatabaseTestingModule {} diff --git a/src/app/index.ts b/src/app/index.ts index 006ae467..e8af5d49 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -1,3 +1,4 @@ export * from './app.constants'; export * from './app.module'; export * from './app.service'; +export * from './database-testing.module'; diff --git a/src/http/keys/keys.e2e-spec.ts b/src/http/keys/keys.e2e-spec.ts index ac2b5627..3001b5b4 100644 --- a/src/http/keys/keys.e2e-spec.ts +++ b/src/http/keys/keys.e2e-spec.ts @@ -3,13 +3,11 @@ import { Test } from '@nestjs/testing'; import { Global, INestApplication, Module, ValidationPipe, VersioningType } from '@nestjs/common'; import { KeyRegistryService, - RegistryKey, RegistryKeyStorageService, RegistryStorageModule, RegistryStorageService, } from '../../common/registry'; -import { FilterQuery, MikroORM } from '@mikro-orm/core'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { MikroORM } from '@mikro-orm/core'; import { KeysController } from './keys.controller'; import { StakingRouterModule } from '../../staking-router-modules/staking-router.module'; @@ -22,6 +20,7 @@ import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify import { elMeta } from '../el-meta.fixture'; import { curatedKeyWithDuplicate, curatedModule, curatedModuleKeys, dvtModule, keys } from '../db.fixtures'; import { curatedModuleKeysResponse, dvtModuleKeysResponse } from 'http/keys.fixtures'; +import { DatabaseTestingModule } from 'app'; describe('KeyController (e2e)', () => { let app: INestApplication; @@ -51,25 +50,9 @@ describe('KeyController (e2e)', () => { } } - class RegistryKeyStorageServiceMock extends RegistryKeyStorageService { - async *findStream(where: FilterQuery, fields): AsyncIterable { - const result = await this.find(where); - for (const key of result) { - yield key; - } - } - } - beforeAll(async () => { const imports = [ - // sqlite3 only supports serializable transactions, ignoring the isolation level param - // TODO: use postgres - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + DatabaseTestingModule, LoggerModule.forRoot({ transports: [nullTransport()] }), KeyRegistryModule, StakingRouterModule, @@ -80,8 +63,6 @@ describe('KeyController (e2e)', () => { const moduleRef = await Test.createTestingModule({ imports, controllers, providers }) .overrideProvider(KeyRegistryService) .useClass(KeysRegistryServiceMock) - .overrideProvider(RegistryKeyStorageService) - .useClass(RegistryKeyStorageServiceMock) .compile(); elMetaStorageService = moduleRef.get(ElMetaStorageService); @@ -90,7 +71,8 @@ describe('KeyController (e2e)', () => { registryStorage = moduleRef.get(RegistryStorageService); const generator = moduleRef.get(MikroORM).getSchemaGenerator(); - await generator.updateSchema(); + await generator.refreshDatabase(); + await generator.clearDatabase(); app = moduleRef.createNestApplication(new FastifyAdapter()); app.enableVersioning({ type: VersioningType.URI }); diff --git a/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts b/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts index 4cf2a523..5b505373 100644 --- a/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts +++ b/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts @@ -8,7 +8,6 @@ import { RegistryStorageService, } from '../../common/registry'; import { MikroORM } from '@mikro-orm/core'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; import { StakingRouterModule } from '../../staking-router-modules/staking-router.module'; import { SRModuleStorageService } from '../../storage/sr-module.storage'; @@ -22,6 +21,7 @@ import { curatedModule, dvtModule, keys } from '../db.fixtures'; import { dvtModuleResp, curatedModuleResp } from '../module.fixture'; import { curatedModuleKeysResponse, dvtModuleKeysResponse } from '../keys.fixtures'; import { elMeta } from '../el-meta.fixture'; +import { DatabaseTestingModule } from 'app'; describe('SRModulesKeysController (e2e)', () => { let app: INestApplication; @@ -62,25 +62,9 @@ describe('SRModulesKeysController (e2e)', () => { } } - class RegistryKeyStorageServiceMock extends RegistryKeyStorageService { - async *findStream(where, fields): AsyncIterable { - const result = await this.find(where); - for (const key of result) { - yield key; - } - } - } - beforeAll(async () => { const imports = [ - // sqlite3 only supports serializable transactions, ignoring the isolation level param - // TODO: use postgres - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + DatabaseTestingModule, LoggerModule.forRoot({ transports: [nullTransport()] }), KeyRegistryModule, StakingRouterModule, @@ -91,8 +75,6 @@ describe('SRModulesKeysController (e2e)', () => { const moduleRef = await Test.createTestingModule({ imports, controllers, providers }) .overrideProvider(KeyRegistryService) .useClass(KeysRegistryServiceMock) - .overrideProvider(RegistryKeyStorageService) - .useClass(RegistryKeyStorageServiceMock) .compile(); elMetaStorageService = moduleRef.get(ElMetaStorageService); @@ -101,7 +83,8 @@ describe('SRModulesKeysController (e2e)', () => { registryStorage = moduleRef.get(RegistryStorageService); const generator = moduleRef.get(MikroORM).getSchemaGenerator(); - await generator.updateSchema(); + await generator.refreshDatabase(); + await generator.clearDatabase(); app = moduleRef.createNestApplication(new FastifyAdapter()); app.enableVersioning({ type: VersioningType.URI }); diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts index a1532bb4..13f05bca 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts @@ -56,15 +56,6 @@ describe('SRModulesOperatorsKeysController (e2e)', () => { } } - class RegistryKeyStorageServiceMock extends RegistryKeyStorageService { - async *findStream(where, fields): AsyncIterable { - const result = await this.find(where); - for (const key of result) { - yield key; - } - } - } - beforeAll(async () => { const imports = [ // sqlite3 only supports serializable transactions, ignoring the isolation level param @@ -85,8 +76,6 @@ describe('SRModulesOperatorsKeysController (e2e)', () => { const moduleRef = await Test.createTestingModule({ imports, controllers, providers }) .overrideProvider(KeyRegistryService) .useClass(KeysRegistryServiceMock) - .overrideProvider(RegistryKeyStorageService) - .useClass(RegistryKeyStorageServiceMock) .compile(); elMetaStorageService = moduleRef.get(ElMetaStorageService); @@ -96,7 +85,8 @@ describe('SRModulesOperatorsKeysController (e2e)', () => { operatorsStorageService = moduleRef.get(RegistryOperatorStorageService); const generator = moduleRef.get(MikroORM).getSchemaGenerator(); - await generator.updateSchema(); + await generator.refreshDatabase(); + await generator.clearDatabase(); app = moduleRef.createNestApplication(new FastifyAdapter()); app.enableVersioning({ type: VersioningType.URI }); diff --git a/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts b/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts index ebb1fb09..2d27abcc 100644 --- a/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts +++ b/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts @@ -3,7 +3,6 @@ import { Test } from '@nestjs/testing'; import { Global, INestApplication, Module, ValidationPipe, VersioningType } from '@nestjs/common'; import { KeyRegistryService, - RegistryKeyStorageService, RegistryOperatorStorageService, RegistryStorageModule, RegistryStorageService, @@ -24,6 +23,7 @@ import { elMeta } from '../el-meta.fixture'; import { operators, dvtModule, curatedModule, srModules } from '../db.fixtures'; import { dvtModuleResp, curatedModuleResp } from '../module.fixture'; import { dvtOperatorsResp, curatedOperatorsResp } from '../operator.fixtures'; +import { DatabaseTestingModule } from 'app'; describe('SRModuleOperatorsController (e2e)', () => { let app: INestApplication; @@ -64,25 +64,9 @@ describe('SRModuleOperatorsController (e2e)', () => { } } - class RegistryKeyStorageServiceMock extends RegistryKeyStorageService { - async *findStream(where, fields): AsyncIterable { - const result = await this.find(where); - for (const key of result) { - yield key; - } - } - } - beforeAll(async () => { const imports = [ - // sqlite3 only supports serializable transactions, ignoring the isolation level param - // TODO: use postgres - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + DatabaseTestingModule, LoggerModule.forRoot({ transports: [nullTransport()] }), KeyRegistryModule, StakingRouterModule, @@ -93,8 +77,6 @@ describe('SRModuleOperatorsController (e2e)', () => { const moduleRef = await Test.createTestingModule({ imports, controllers, providers }) .overrideProvider(KeyRegistryService) .useClass(KeysRegistryServiceMock) - .overrideProvider(RegistryKeyStorageService) - .useClass(RegistryKeyStorageServiceMock) .compile(); elMetaStorageService = moduleRef.get(ElMetaStorageService); @@ -103,7 +85,8 @@ describe('SRModuleOperatorsController (e2e)', () => { registryStorage = moduleRef.get(RegistryStorageService); const generator = moduleRef.get(MikroORM).getSchemaGenerator(); - await generator.updateSchema(); + await generator.refreshDatabase(); + await generator.clearDatabase(); app = moduleRef.createNestApplication(new FastifyAdapter()); app.enableVersioning({ type: VersioningType.URI }); diff --git a/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts b/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts index 56b2b1c1..58aae8aa 100644 --- a/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts +++ b/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts @@ -1,16 +1,12 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ import { Test } from '@nestjs/testing'; import { Global, INestApplication, Module, ValidationPipe, VersioningType } from '@nestjs/common'; import { KeyRegistryService, - RegistryKey, RegistryKeyStorageService, - RegistryOperator, RegistryStorageModule, RegistryStorageService, } from '../../common/registry'; import { MikroORM } from '@mikro-orm/core'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; import { SRModulesValidatorsController } from './sr-modules-validators.controller'; import { StakingRouterModule } from '../../staking-router-modules/staking-router.module'; @@ -25,17 +21,10 @@ import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify import { elMeta } from '../el-meta.fixture'; import { keys, dvtModule, curatedModule } from '../db.fixtures'; import { ConfigService } from '../../common/config'; -import { - ConsensusMetaEntity, - ConsensusValidatorEntity, - ValidatorsRegistryInterface, -} from '@lido-nestjs/validators-registry'; +import { ValidatorsRegistryInterface } from '@lido-nestjs/validators-registry'; import { ConsensusModule, ConsensusService } from '@lido-nestjs/consensus'; import { FetchModule } from '@lido-nestjs/fetch'; -import { ConfigModule } from '../../common/config'; import { ValidatorsModule } from '../../validators'; -import { SrModuleEntity } from '../../storage/sr-module.entity'; -import { ElMetaEntity } from '../../storage/el-meta.entity'; import { block, header, @@ -51,6 +40,7 @@ import { dvtOpOneRespExitMessages20percent, dvtOpOneRespExitMessages5maxAmount, } from '../consensus.fixtures'; +import { DatabaseTestingModule } from 'app'; describe('SRModulesValidatorsController (e2e)', () => { let app: INestApplication; @@ -60,6 +50,7 @@ describe('SRModulesValidatorsController (e2e)', () => { let elMetaStorageService: ElMetaStorageService; let registryStorage: RegistryStorageService; let validatorsRegistry: ValidatorsRegistryInterface; + let configService: ConfigService; async function cleanDB() { await keysStorageService.removeAll(); @@ -81,15 +72,6 @@ describe('SRModulesValidatorsController (e2e)', () => { } } - class RegistryKeyStorageServiceMock extends RegistryKeyStorageService { - async *findStream(where, fields): AsyncIterable { - const result = await this.find(where); - for (const key of result) { - yield key; - } - } - } - const consensusServiceMock = { getBlockV2: (args: { blockId: string | number }) => { return block; @@ -104,25 +86,10 @@ describe('SRModulesValidatorsController (e2e)', () => { beforeAll(async () => { const imports = [ - // sqlite3 only supports serializable transactions, ignoring the isolation level param - // TODO: use postgres - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: [ - RegistryKey, - RegistryOperator, - ConsensusValidatorEntity, - ConsensusMetaEntity, - SrModuleEntity, - ElMetaEntity, - ], - }), + DatabaseTestingModule, LoggerModule.forRoot({ transports: [nullTransport()] }), KeyRegistryModule, StakingRouterModule, - ConfigModule, ConsensusModule.forRoot({ imports: [FetchModule], }), @@ -134,28 +101,29 @@ describe('SRModulesValidatorsController (e2e)', () => { const moduleRef = await Test.createTestingModule({ imports, controllers, providers }) .overrideProvider(KeyRegistryService) .useClass(KeysRegistryServiceMock) - .overrideProvider(ConfigService) - .useValue({ - get(path) { - const conf = { VALIDATOR_REGISTRY_ENABLE: true }; - return conf[path]; - }, - }) .overrideProvider(ConsensusService) .useValue(consensusServiceMock) - .overrideProvider(RegistryKeyStorageService) - .useClass(RegistryKeyStorageServiceMock) .compile(); elMetaStorageService = moduleRef.get(ElMetaStorageService); + configService = moduleRef.get(ConfigService); keysStorageService = moduleRef.get(RegistryKeyStorageService); moduleStorageService = moduleRef.get(SRModuleStorageService); registryStorage = moduleRef.get(RegistryStorageService); // validatorsStorage = moduleRef.get(StorageService); validatorsRegistry = moduleRef.get(ValidatorsRegistryInterface); + jest.spyOn(configService, 'get').mockImplementation((path) => { + if (path === 'VALIDATOR_REGISTRY_ENABLE') { + return true; + } + + return configService.get(path); + }); + const generator = moduleRef.get(MikroORM).getSchemaGenerator(); - await generator.updateSchema(); + await generator.refreshDatabase(); + await generator.clearDatabase(); app = moduleRef.createNestApplication(new FastifyAdapter()); app.enableVersioning({ type: VersioningType.URI }); diff --git a/src/http/sr-modules/sr-modules.e2e-spec.ts b/src/http/sr-modules/sr-modules.e2e-spec.ts index 816d1b1f..47b3de5c 100644 --- a/src/http/sr-modules/sr-modules.e2e-spec.ts +++ b/src/http/sr-modules/sr-modules.e2e-spec.ts @@ -2,14 +2,8 @@ import { Test } from '@nestjs/testing'; import { Global, INestApplication, Module, ValidationPipe, VersioningType } from '@nestjs/common'; import { MikroORM } from '@mikro-orm/core'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; - -import { - KeyRegistryService, - RegistryKeyStorageService, - RegistryStorageModule, - RegistryStorageService, -} from '../../common/registry'; + +import { KeyRegistryService, RegistryStorageModule, RegistryStorageService } from '../../common/registry'; import { StakingRouterModule } from '../../staking-router-modules/staking-router.module'; import { dvtModuleResp, curatedModuleResp, dvtModuleAddressWithChecksum } from '../module.fixture'; import { SRModuleStorageService } from '../../storage/sr-module.storage'; @@ -24,6 +18,7 @@ import { SRModulesService } from './sr-modules.service'; import { elMeta } from '../el-meta.fixture'; import { curatedModule, dvtModule } from '../db.fixtures'; +import { DatabaseTestingModule } from 'app'; describe('SRModulesController (e2e)', () => { let app: INestApplication; @@ -51,25 +46,9 @@ describe('SRModulesController (e2e)', () => { } } - class RegistryKeyStorageServiceMock extends RegistryKeyStorageService { - async *findStream(where, fields): AsyncIterable { - const result = await this.find(where); - for (const key of result) { - yield key; - } - } - } - beforeAll(async () => { const imports = [ - // sqlite3 only supports serializable transactions, ignoring the isolation level param - // TODO: use postgres - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + DatabaseTestingModule, LoggerModule.forRoot({ transports: [nullTransport()] }), KeyRegistryModule, StakingRouterModule, @@ -81,8 +60,6 @@ describe('SRModulesController (e2e)', () => { const moduleRef = await Test.createTestingModule({ imports, controllers, providers }) .overrideProvider(KeyRegistryService) .useClass(KeysRegistryServiceMock) - .overrideProvider(RegistryKeyStorageService) - .useClass(RegistryKeyStorageServiceMock) .compile(); elMetaStorageService = moduleRef.get(ElMetaStorageService); @@ -90,7 +67,8 @@ describe('SRModulesController (e2e)', () => { registryStorage = moduleRef.get(RegistryStorageService); const generator = moduleRef.get(MikroORM).getSchemaGenerator(); - await generator.updateSchema(); + await generator.refreshDatabase(); + await generator.clearDatabase(); app = moduleRef.createNestApplication(new FastifyAdapter()); app.enableVersioning({ type: VersioningType.URI }); From 7e729eb160f4c7a652e6e71336a11935a44e2737 Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Mon, 23 Oct 2023 08:52:44 +0300 Subject: [PATCH 06/75] feat: github service-container for e2e tests --- .github/workflows/test.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 837f30b3..4767aaf3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,6 +3,20 @@ on: pull_request jobs: test: runs-on: ubuntu-latest + + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + POSTGRES_DB: node_operator_keys_service_db + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - name: Checkout repo uses: actions/checkout@v2.3.5 @@ -20,8 +34,13 @@ jobs: - name: Run tests run: yarn test - name: Run tests - run: yarn test:e2e + run: yarn test:e2e:docker env: PROVIDERS_URLS: ${{ secrets.PROVIDERS_URLS }} CHAIN_ID: ${{ secrets.CHAIN_ID }} CHRONIX_PROVIDER_MAINNET_URL: ${{ secrets.CHRONIX_PROVIDER_MAINNET_URL }} + DB_NAME: node_operator_keys_service_db + DB_PORT: 5432 + DB_HOST: postgres + DB_USER: postgres + DB_PASSWORD: postgres \ No newline at end of file From da5001bb09b9b5c5df4347c4b7144251ad00753f Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Mon, 23 Oct 2023 11:00:43 +0300 Subject: [PATCH 07/75] fix: ci --- .github/workflows/test.yml | 8 +++++--- .../sr-modules-operators/sr-modules-operators.e2e-spec.ts | 1 - .../sr-modules-validators.e2e-spec.ts | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4767aaf3..4fd40fff 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,6 +16,8 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + ports: + - 5432:5432 steps: - name: Checkout repo @@ -33,7 +35,7 @@ jobs: run: yarn lint - name: Run tests run: yarn test - - name: Run tests + - name: Run E2E tests run: yarn test:e2e:docker env: PROVIDERS_URLS: ${{ secrets.PROVIDERS_URLS }} @@ -41,6 +43,6 @@ jobs: CHRONIX_PROVIDER_MAINNET_URL: ${{ secrets.CHRONIX_PROVIDER_MAINNET_URL }} DB_NAME: node_operator_keys_service_db DB_PORT: 5432 - DB_HOST: postgres + DB_HOST: localhost DB_USER: postgres - DB_PASSWORD: postgres \ No newline at end of file + DB_PASSWORD: postgres diff --git a/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts b/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts index 2d27abcc..11a3c76a 100644 --- a/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts +++ b/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts @@ -8,7 +8,6 @@ import { RegistryStorageService, } from '../../common/registry'; import { MikroORM } from '@mikro-orm/core'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; import { StakingRouterModule } from '../../staking-router-modules/staking-router.module'; import { SRModuleStorageService } from '../../storage/sr-module.storage'; diff --git a/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts b/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts index 58aae8aa..fc743328 100644 --- a/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts +++ b/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { Test } from '@nestjs/testing'; import { Global, INestApplication, Module, ValidationPipe, VersioningType } from '@nestjs/common'; import { From 044369207d2afd702fc40ed2271678bcd285db79 Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Mon, 23 Oct 2023 18:20:10 +0300 Subject: [PATCH 08/75] fix tests --- .github/workflows/test.yml | 1 + docker-compose.e2e.yml | 2 +- package.json | 2 +- .../sr-modules-operators-keys.e2e-spec.ts | 10 ++-------- .../sr-modules-validators.e2e-spec.ts | 8 -------- 5 files changed, 5 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4fd40fff..6d13a9fe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,3 +46,4 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASSWORD: postgres + VALIDATOR_REGISTRY_ENABLE: true diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml index 0e84c19f..4818af83 100644 --- a/docker-compose.e2e.yml +++ b/docker-compose.e2e.yml @@ -37,7 +37,7 @@ services: - DB_USER=${DB_USER} - DB_PASSWORD=${DB_PASSWORD} - JOB_INTERVAL_REGISTRY=${JOB_INTERVAL_REGISTRY} - - VALIDATOR_REGISTRY_ENABLE=${VALIDATOR_REGISTRY_ENABLE} + - VALIDATOR_REGISTRY_ENABLE=true - JOB_INTERVAL_VALIDATORS_REGISTRY=${JOB_INTERVAL_VALIDATORS_REGISTRY} depends_on: - e2e_pgdb diff --git a/package.json b/package.json index ed990a82..e9819b3a 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "docker-compose --env-file=./.env -f docker-compose.e2e.yml up --build --abort-on-container-exit", "test:e2e:docker": "mikro-orm schema:drop -r && mikro-orm migration:up && jest -i --config jest-e2e.json && chronix test", - "test:e2e:docker:debug": "mikro-orm migration:up && node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest -i --config jest-e2e.json && chronix test", + "test:e2e:docker:debug": "mikro-orm schema:drop -r && mikro-orm migration:up && node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest -i --config jest-e2e.json && chronix test", "typechain": "typechain --target=ethers-v5 --out-dir ./src/generated ./src/staking-router-modules/contracts/abi/*.json", "chronix:compile": "chronix compile", "chronix:test": "chronix test" diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts index 13f05bca..4953d20c 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts @@ -26,6 +26,7 @@ import { elMeta } from '../el-meta.fixture'; import { keys, operators, dvtModule, curatedModule } from '../db.fixtures'; import { dvtModuleKeysResponse } from '../keys.fixtures'; import { dvtOperatorsResp } from '../operator.fixtures'; +import { DatabaseTestingModule } from 'app'; describe('SRModulesOperatorsKeysController (e2e)', () => { let app: INestApplication; @@ -58,14 +59,7 @@ describe('SRModulesOperatorsKeysController (e2e)', () => { beforeAll(async () => { const imports = [ - // sqlite3 only supports serializable transactions, ignoring the isolation level param - // TODO: use postgres - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + DatabaseTestingModule, LoggerModule.forRoot({ transports: [nullTransport()] }), KeyRegistryModule, StakingRouterModule, diff --git a/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts b/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts index fc743328..b781a725 100644 --- a/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts +++ b/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts @@ -114,14 +114,6 @@ describe('SRModulesValidatorsController (e2e)', () => { // validatorsStorage = moduleRef.get(StorageService); validatorsRegistry = moduleRef.get(ValidatorsRegistryInterface); - jest.spyOn(configService, 'get').mockImplementation((path) => { - if (path === 'VALIDATOR_REGISTRY_ENABLE') { - return true; - } - - return configService.get(path); - }); - const generator = moduleRef.get(MikroORM).getSchemaGenerator(); await generator.refreshDatabase(); await generator.clearDatabase(); From f14ee771692bc0c1cbd4c6c1406ca65f49dd613d Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Mon, 23 Oct 2023 19:11:48 +0300 Subject: [PATCH 09/75] revert changes --- .github/workflows/test.yml | 2 +- src/app/app-testing.module.ts | 25 +++++-------------- .../sr-modules-operators-keys.e2e-spec.ts | 1 - .../sr-modules-validators.e2e-spec.ts | 8 ++++++ 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6d13a9fe..5ffaae05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,6 +39,7 @@ jobs: run: yarn test:e2e:docker env: PROVIDERS_URLS: ${{ secrets.PROVIDERS_URLS }} + CL_API_URLS: "https://e2e-test.lido.fi," CHAIN_ID: ${{ secrets.CHAIN_ID }} CHRONIX_PROVIDER_MAINNET_URL: ${{ secrets.CHRONIX_PROVIDER_MAINNET_URL }} DB_NAME: node_operator_keys_service_db @@ -46,4 +47,3 @@ jobs: DB_HOST: localhost DB_USER: postgres DB_PASSWORD: postgres - VALIDATOR_REGISTRY_ENABLE: true diff --git a/src/app/app-testing.module.ts b/src/app/app-testing.module.ts index f9e150fd..8de87552 100644 --- a/src/app/app-testing.module.ts +++ b/src/app/app-testing.module.ts @@ -1,8 +1,7 @@ import { APP_INTERCEPTOR } from '@nestjs/core'; import { Module } from '@nestjs/common'; import { PrometheusModule } from '../common/prometheus'; -import { ConfigModule, ConfigService } from '../common/config'; -import config from 'mikro-orm.config'; +import { ConfigModule } from '../common/config'; import { SentryInterceptor } from '../common/sentry'; import { HealthModule } from '../common/health'; import { AppService } from './app.service'; @@ -23,23 +22,11 @@ import { KeysUpdateModule } from 'jobs/keys-update'; ConfigModule, ExecutionProviderModule, ConsensusProviderModule, - MikroOrmModule.forRootAsync({ - async useFactory(configService: ConfigService) { - return { - ...config, - dbName: configService.get('DB_NAME'), - host: configService.get('DB_HOST'), - port: configService.get('DB_PORT'), - user: configService.get('DB_USER'), - password: configService.get('DB_PASSWORD'), - autoLoadEntities: false, - cache: { enabled: false }, - debug: false, - registerRequestContext: true, - allowGlobalContext: true, - }; - }, - inject: [ConfigService], + MikroOrmModule.forRoot({ + dbName: ':memory:', + type: 'sqlite', + allowGlobalContext: true, + entities: ['./**/*.entity.ts'], }), LoggerModule.forRoot({ transports: [nullTransport()] }), ScheduleModule.forRoot(), diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts index 4953d20c..80d2f281 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts @@ -9,7 +9,6 @@ import { RegistryStorageService, } from '../../common/registry'; import { MikroORM } from '@mikro-orm/core'; -import { MikroOrmModule } from '@mikro-orm/nestjs'; import { SRModulesOperatorsKeysController } from './sr-modules-operators-keys.controller'; import { StakingRouterModule } from '../../staking-router-modules/staking-router.module'; diff --git a/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts b/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts index b781a725..fc743328 100644 --- a/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts +++ b/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts @@ -114,6 +114,14 @@ describe('SRModulesValidatorsController (e2e)', () => { // validatorsStorage = moduleRef.get(StorageService); validatorsRegistry = moduleRef.get(ValidatorsRegistryInterface); + jest.spyOn(configService, 'get').mockImplementation((path) => { + if (path === 'VALIDATOR_REGISTRY_ENABLE') { + return true; + } + + return configService.get(path); + }); + const generator = moduleRef.get(MikroORM).getSchemaGenerator(); await generator.refreshDatabase(); await generator.clearDatabase(); From 01634ac71212cc6a49d678969790c35cc35e2e56 Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Mon, 23 Oct 2023 19:36:19 +0300 Subject: [PATCH 10/75] fix: get rid of console.log --- docker-compose.e2e.yml | 2 +- src/common/registry/test/fetch/operator.fetch.spec.ts | 1 - .../sr-modules-operators/sr-modules-operators.controller.ts | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml index 4818af83..0e84c19f 100644 --- a/docker-compose.e2e.yml +++ b/docker-compose.e2e.yml @@ -37,7 +37,7 @@ services: - DB_USER=${DB_USER} - DB_PASSWORD=${DB_PASSWORD} - JOB_INTERVAL_REGISTRY=${JOB_INTERVAL_REGISTRY} - - VALIDATOR_REGISTRY_ENABLE=true + - VALIDATOR_REGISTRY_ENABLE=${VALIDATOR_REGISTRY_ENABLE} - JOB_INTERVAL_VALIDATORS_REGISTRY=${JOB_INTERVAL_VALIDATORS_REGISTRY} depends_on: - e2e_pgdb diff --git a/src/common/registry/test/fetch/operator.fetch.spec.ts b/src/common/registry/test/fetch/operator.fetch.spec.ts index 1f8b3d25..5ff8a939 100644 --- a/src/common/registry/test/fetch/operator.fetch.spec.ts +++ b/src/common/registry/test/fetch/operator.fetch.spec.ts @@ -79,7 +79,6 @@ describe('Operators', () => { // operatorFields(operator); return iface.encodeFunctionResult('getNodeOperator', operatorFields(operator)); }); - console.log('aaaaa', expected); const result = await fetchService.fetch(address); expect(result).toEqual([expected]); diff --git a/src/http/sr-modules-operators/sr-modules-operators.controller.ts b/src/http/sr-modules-operators/sr-modules-operators.controller.ts index 95eed771..cc67ad11 100644 --- a/src/http/sr-modules-operators/sr-modules-operators.controller.ts +++ b/src/http/sr-modules-operators/sr-modules-operators.controller.ts @@ -83,7 +83,6 @@ export class SRModulesOperatorsController { }) @Get('modules/:module_id/operators/:operator_id') getModuleOperator(@Param('module_id', ModuleIdPipe) module_id: string | number, @Param() operator: OperatorId) { - console.log(module_id, typeof module_id); return this.srModulesOperators.getModuleOperator(module_id, operator.operator_id); } } From 478ea5bbefb0192048c65ea571ce350805310407 Mon Sep 17 00:00:00 2001 From: infloop Date: Fri, 3 Nov 2023 18:17:54 +0500 Subject: [PATCH 11/75] chore: review things --- .../registry/fetch/utils/batches.spec.ts | 65 +++++++++++++++++++ src/common/registry/fetch/utils/batches.ts | 18 +++++ .../registry/fetch/utils/split-hex.spec.ts | 21 ++++++ src/common/registry/fetch/utils/split-hex.ts | 33 ++++++++++ 4 files changed, 137 insertions(+) create mode 100644 src/common/registry/fetch/utils/batches.spec.ts create mode 100644 src/common/registry/fetch/utils/batches.ts create mode 100644 src/common/registry/fetch/utils/split-hex.spec.ts create mode 100644 src/common/registry/fetch/utils/split-hex.ts diff --git a/src/common/registry/fetch/utils/batches.spec.ts b/src/common/registry/fetch/utils/batches.spec.ts new file mode 100644 index 00000000..9292d077 --- /dev/null +++ b/src/common/registry/fetch/utils/batches.spec.ts @@ -0,0 +1,65 @@ +import { makeBatches } from './batches'; + +describe('batch', () => { + test('total <<< batch, offset 0, total 0', () => { + const dataBatches = makeBatches(200, 0, 0); + + expect(dataBatches).toEqual([]); + }); + + test('total >>> batch, offset 800, total 800', () => { + const dataBatches = makeBatches(200, 800, 800); + + // strange + expect(dataBatches).toEqual([]); + }); + + test('total <<< batch, offset 0, total 1', () => { + const dataBatches = makeBatches(200, 0, 1); + + expect(dataBatches).toEqual([{ batchSize: 1, offset: 0 }]); + }); + + test('total <<< batch, offset 1, total 1', () => { + const dataBatches = makeBatches(200, 1, 1); + + expect(dataBatches).toEqual([{ batchSize: 1, offset: 1 }]); + }); + + test('total === batch, offset 1, total 10', () => { + const dataBatches = makeBatches(10, 1, 10); + + // thought that batchSize is 9 + expect(dataBatches).toEqual([{ batchSize: 9, offset: 1 }]); + }); + + test('total < batch, offset 1, total 3', () => { + const dataBatches = makeBatches(10, 1, 3); + + // thought that batchSize here is 2 + expect(dataBatches).toEqual([{ batchSize: 2, offset: 1 }]); + }); + + test('total === batch, offset 1, total 3', () => { + const dataBatches = makeBatches(3, 1, 3); + + // thought that batchSize here is 2 + expect(dataBatches).toEqual([{ batchSize: 2, offset: 1 }]); + }); + + test('total > batch, offset 0, total 199', () => { + const dataBatches = makeBatches(100, 0, 199); + + expect(dataBatches).toEqual([ + { batchSize: 100, offset: 0 }, + { batchSize: 99, offset: 100 }, + ]); + }); + + test('total > batch, offset 100, total 199', () => { + const dataBatches = makeBatches(100, 100, 199); + + // why? + expect(dataBatches).toEqual([{ batchSize: 99, offset: 100 }]); + }); +}); diff --git a/src/common/registry/fetch/utils/batches.ts b/src/common/registry/fetch/utils/batches.ts new file mode 100644 index 00000000..96b414ce --- /dev/null +++ b/src/common/registry/fetch/utils/batches.ts @@ -0,0 +1,18 @@ +import { CallOverrides } from '../interfaces/overrides.interface'; + +export const makeBatches = ( + batchSize: number, + offset: number, + totalAmount: number, +) => { + const numberOfBatches = Math.ceil(totalAmount / batchSize); + const batches: { offset: number; batchSize: number }[] = []; + + for (let i = 0; i < numberOfBatches; i++) { + const currentOffset = offset + i * batchSize; + const currentBatchSize = Math.min(batchSize, totalAmount - i * batchSize); + batches.push({ offset: currentOffset, batchSize: currentBatchSize }); + } + + return batches; +}; diff --git a/src/common/registry/fetch/utils/split-hex.spec.ts b/src/common/registry/fetch/utils/split-hex.spec.ts new file mode 100644 index 00000000..9b08f7b3 --- /dev/null +++ b/src/common/registry/fetch/utils/split-hex.spec.ts @@ -0,0 +1,21 @@ +import { splitHex } from './split-hex'; + +describe('split-merged-record', () => { + test('test1', () => { + const parts = splitHex('0x123456789', 3); + + expect(parts).toEqual(['0x123', '0x456', '0x789']); + }); + + test('test2', () => { + const parts = splitHex('0x1234567890', 3); + + expect(parts).toEqual(['0x123', '0x456', '0x789']); + }); + + test('test3', () => { + const parts = splitHex('123456789', 3); + + expect(parts).toEqual(['0x123', '0x456', '0x789']); + }); +}); diff --git a/src/common/registry/fetch/utils/split-hex.ts b/src/common/registry/fetch/utils/split-hex.ts new file mode 100644 index 00000000..e45bd5dc --- /dev/null +++ b/src/common/registry/fetch/utils/split-hex.ts @@ -0,0 +1,33 @@ +/** + * Split one big string into array of strings + * `0x${key1}{key2}...` -> `[`0x${key1}`, `0x${key2}`]` + * + * example record: + * 0x81b4ae61a898396903897f94bea0e062c3a6925ee93d30f4d4aee93b533b49551ac337da78ff2ab0cfbb0adb380cad94953805708367b0b5f6710d41608ccdd0d5a67938e10e68dd010890d4bfefdcde874370423b0af0d0a053b7b98ae2d6ed + * + * 0x81b4ae61a898396903897f94bea0e062c3a6925ee93d30f4d4aee93b533b49551ac337da78ff2ab0cfbb0adb380cad94 + * @param record pubkey or signature merged string + * @param capacity 96 or 192 + * @returns array of keys or signatures + */ +export const splitHex = (hexString: string, chunkLength: number) => { + if (chunkLength < 1) { + throw new RangeError('chunkLength should be positive'); + } + + if (typeof hexString !== 'string' || hexString.substring(0, 2) !== '0x') { + throw new Error('not a hex-like string'); + } + + const parts: string[] = []; + let part = ''; + // start from index 2 because each record beginning from 0x + for (let i = 2; i < hexString.length; i++) { + part += hexString[i]; + if (part.length === chunkLength) { + parts.push(`0x${part}`); + part = ''; + } + } + return parts; +}; From dd89a98340c32d735d7191a8a7b78817c377d666 Mon Sep 17 00:00:00 2001 From: Eddort Date: Fri, 3 Nov 2023 16:34:37 +0100 Subject: [PATCH 12/75] wip: batches spec --- src/common/registry/fetch/utils/batches.spec.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/common/registry/fetch/utils/batches.spec.ts b/src/common/registry/fetch/utils/batches.spec.ts index 9292d077..f49eb1ce 100644 --- a/src/common/registry/fetch/utils/batches.spec.ts +++ b/src/common/registry/fetch/utils/batches.spec.ts @@ -8,10 +8,10 @@ describe('batch', () => { }); test('total >>> batch, offset 800, total 800', () => { - const dataBatches = makeBatches(200, 800, 800); + const dataBatches = makeBatches(200, 799, 1); // strange - expect(dataBatches).toEqual([]); + expect(dataBatches).toEqual([{ batchSize: 1, offset: 799 }]); }); test('total <<< batch, offset 0, total 1', () => { @@ -29,22 +29,22 @@ describe('batch', () => { test('total === batch, offset 1, total 10', () => { const dataBatches = makeBatches(10, 1, 10); - // thought that batchSize is 9 - expect(dataBatches).toEqual([{ batchSize: 9, offset: 1 }]); + // thought that batchSize is 10 + expect(dataBatches).toEqual([{ batchSize: 10, offset: 1 }]); }); test('total < batch, offset 1, total 3', () => { const dataBatches = makeBatches(10, 1, 3); // thought that batchSize here is 2 - expect(dataBatches).toEqual([{ batchSize: 2, offset: 1 }]); + expect(dataBatches).toEqual([{ batchSize: 3, offset: 1 }]); }); test('total === batch, offset 1, total 3', () => { const dataBatches = makeBatches(3, 1, 3); // thought that batchSize here is 2 - expect(dataBatches).toEqual([{ batchSize: 2, offset: 1 }]); + expect(dataBatches).toEqual([{ batchSize: 3, offset: 1 }]); }); test('total > batch, offset 0, total 199', () => { @@ -60,6 +60,9 @@ describe('batch', () => { const dataBatches = makeBatches(100, 100, 199); // why? - expect(dataBatches).toEqual([{ batchSize: 99, offset: 100 }]); + expect(dataBatches).toEqual([ + { batchSize: 100, offset: 100 }, + { batchSize: 99, offset: 200 }, + ]); }); }); From a197e9c2d2671f7b9bc698820a9328fe057699f0 Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Tue, 7 Nov 2023 13:25:55 +0400 Subject: [PATCH 13/75] cr --- src/app/app.module.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0a2c479d..657a4890 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -38,7 +38,6 @@ import { StakingRouterModule } from '../staking-router-modules'; autoLoadEntities: false, cache: { enabled: false }, debug: false, - safe: true, registerRequestContext: true, allowGlobalContext: false, }; From 9d0897a2b111ec469018b418ce80f55f7a65060d Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 7 Nov 2023 10:59:05 +0100 Subject: [PATCH 14/75] feat: stream json error handling --- src/http/keys/keys.controller.ts | 9 +++++++-- src/http/sr-modules-keys/sr-modules-keys.controller.ts | 7 ++++++- .../sr-modules-operators-keys.controller.ts | 7 ++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/http/keys/keys.controller.ts b/src/http/keys/keys.controller.ts index 0defea68..a29d6486 100644 --- a/src/http/keys/keys.controller.ts +++ b/src/http/keys/keys.controller.ts @@ -48,7 +48,7 @@ export class KeysController { const jsonStream = JSONStream.stringify('{ "meta": ' + JSON.stringify(meta) + ', "data": [', ',', ']}'); reply.type('application/json').send(jsonStream); - // TODO: is it necessary to check the error? or 'finally' is ok? + try { for (const keysGenerator of keysGenerators) { for await (const key of keysGenerator) { @@ -56,8 +56,13 @@ export class KeysController { jsonStream.write(keyReponse); } } - } finally { jsonStream.end(); + } catch (streamError) { + // Handle the error during streaming. + console.error('Error during streaming:', streamError); + // destroy method closes the stream without ']' and corrupt the result + // https://github.com/dominictarr/through/blob/master/index.js#L78 + jsonStream.destroy(); } }, { isolationLevel: IsolationLevel.REPEATABLE_READ }, diff --git a/src/http/sr-modules-keys/sr-modules-keys.controller.ts b/src/http/sr-modules-keys/sr-modules-keys.controller.ts index ed70003f..972200dc 100644 --- a/src/http/sr-modules-keys/sr-modules-keys.controller.ts +++ b/src/http/sr-modules-keys/sr-modules-keys.controller.ts @@ -98,8 +98,13 @@ export class SRModulesKeysController { jsonStream.write(keyReponse); } - } finally { jsonStream.end(); + } catch (streamError) { + // Handle the error during streaming. + console.error('Error during streaming:', streamError); + // destroy method closes the stream without ']' and corrupt the result + // https://github.com/dominictarr/through/blob/master/index.js#L78 + jsonStream.destroy(); } }, { isolationLevel: IsolationLevel.REPEATABLE_READ }, diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts index 64b4c309..9519b7d9 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts @@ -76,8 +76,13 @@ export class SRModulesOperatorsKeysController { const keyResponse = new Key(key); jsonStream.write(keyResponse); } - } finally { jsonStream.end(); + } catch (streamError) { + // Handle the error during streaming. + console.error('Error during streaming:', streamError); + // destroy method closes the stream without ']' and corrupt the result + // https://github.com/dominictarr/through/blob/master/index.js#L78 + jsonStream.destroy(); } }, { isolationLevel: IsolationLevel.REPEATABLE_READ }, From 3a05f6101b5af5fc46858fc535dfc90d56a6719d Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Wed, 8 Nov 2023 08:00:37 +0400 Subject: [PATCH 15/75] cr 1 --- docker-compose.e2e.yml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml index 0e84c19f..265db991 100644 --- a/docker-compose.e2e.yml +++ b/docker-compose.e2e.yml @@ -21,23 +21,7 @@ services: dockerfile: Dockerfile.e2e environment: - NODE_ENV=production - - PORT=${PORT} - - CORS_WHITELIST_REGEXP=${CORS_WHITELIST_REGEXP} - - GLOBAL_THROTTLE_TTL=${GLOBAL_THROTTLE_TTL} - - GLOBAL_THROTTLE_LIMIT=${GLOBAL_THROTTLE_LIMIT} - - GLOBAL_CACHE_TTL=${GLOBAL_CACHE_TTL} - - LOG_LEVEL=${LOG_LEVEL} - - LOG_FORMAT=${LOG_FORMAT} - - PROVIDERS_URLS=${PROVIDERS_URLS} - - CL_API_URLS=${CL_API_URLS} - - CHAIN_ID=${CHAIN_ID} - - DB_NAME=${DB_NAME} - DB_PORT=5432 - DB_HOST=e2e_pgdb - - DB_USER=${DB_USER} - - DB_PASSWORD=${DB_PASSWORD} - - JOB_INTERVAL_REGISTRY=${JOB_INTERVAL_REGISTRY} - - VALIDATOR_REGISTRY_ENABLE=${VALIDATOR_REGISTRY_ENABLE} - - JOB_INTERVAL_VALIDATORS_REGISTRY=${JOB_INTERVAL_VALIDATORS_REGISTRY} depends_on: - e2e_pgdb From 17ed1ee55c5dbf091f74c4ebdd23df861fea15c0 Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Wed, 8 Nov 2023 13:08:40 +0400 Subject: [PATCH 16/75] experiment --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ffaae05..4bbab83a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,7 @@ jobs: run: yarn test:e2e:docker env: PROVIDERS_URLS: ${{ secrets.PROVIDERS_URLS }} - CL_API_URLS: "https://e2e-test.lido.fi," + # CL_API_URLS: "https://e2e-test.lido.fi," CHAIN_ID: ${{ secrets.CHAIN_ID }} CHRONIX_PROVIDER_MAINNET_URL: ${{ secrets.CHRONIX_PROVIDER_MAINNET_URL }} DB_NAME: node_operator_keys_service_db From 1c36f23c3e1698c57a8f1be8ee9586b6bbfcdf61 Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Wed, 8 Nov 2023 13:19:19 +0400 Subject: [PATCH 17/75] VALIDATOR_REGISTRY_ENABLE=false --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4bbab83a..76093cff 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,7 @@ jobs: run: yarn test:e2e:docker env: PROVIDERS_URLS: ${{ secrets.PROVIDERS_URLS }} - # CL_API_URLS: "https://e2e-test.lido.fi," + VALIDATOR_REGISTRY_ENABLE: false CHAIN_ID: ${{ secrets.CHAIN_ID }} CHRONIX_PROVIDER_MAINNET_URL: ${{ secrets.CHRONIX_PROVIDER_MAINNET_URL }} DB_NAME: node_operator_keys_service_db From be43c98993a68e17efa6237d88654a6a944da031 Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Wed, 8 Nov 2023 13:25:39 +0400 Subject: [PATCH 18/75] rollback --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 76093cff..5ffaae05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,7 +39,7 @@ jobs: run: yarn test:e2e:docker env: PROVIDERS_URLS: ${{ secrets.PROVIDERS_URLS }} - VALIDATOR_REGISTRY_ENABLE: false + CL_API_URLS: "https://e2e-test.lido.fi," CHAIN_ID: ${{ secrets.CHAIN_ID }} CHRONIX_PROVIDER_MAINNET_URL: ${{ secrets.CHRONIX_PROVIDER_MAINNET_URL }} DB_NAME: node_operator_keys_service_db From cf06652f97fe060455739d1f606a8d8f7ab94abe Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 8 Nov 2023 12:41:47 +0100 Subject: [PATCH 19/75] feat: add timeout to keys and operators stream --- src/common/registry/storage/key.storage.ts | 7 ++++++- .../registry/storage/operator.storage.ts | 7 ++++++- src/common/registry/utils/stream.utils.ts | 21 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 src/common/registry/utils/stream.utils.ts diff --git a/src/common/registry/storage/key.storage.ts b/src/common/registry/storage/key.storage.ts index dd3ffcba..441a190f 100644 --- a/src/common/registry/storage/key.storage.ts +++ b/src/common/registry/storage/key.storage.ts @@ -1,6 +1,7 @@ import { QueryOrder } from '@mikro-orm/core'; import { FilterQuery, FindOptions } from '@mikro-orm/core'; import { Injectable } from '@nestjs/common'; +import { addTimeoutToStream } from '../utils/stream.utils'; import { RegistryKey } from './key.entity'; import { RegistryKeyRepository } from './key.repository'; @@ -18,11 +19,15 @@ export class RegistryKeyStorageService { findStream(where: FilterQuery, fields?: string[]): AsyncIterable { const knex = this.repository.getKnex(); - return knex + const stream = knex .select(fields || '*') .from('registry_key') .where(where) .stream(); + + addTimeoutToStream(stream, 60_000); + + return stream; } /** find all keys */ diff --git a/src/common/registry/storage/operator.storage.ts b/src/common/registry/storage/operator.storage.ts index d71fad1e..2230416e 100644 --- a/src/common/registry/storage/operator.storage.ts +++ b/src/common/registry/storage/operator.storage.ts @@ -1,6 +1,7 @@ import { QueryOrder } from '@mikro-orm/core'; import { FilterQuery, FindOptions } from '@mikro-orm/core'; import { Injectable } from '@nestjs/common'; +import { addTimeoutToStream } from '../utils/stream.utils'; import { RegistryOperator } from './operator.entity'; import { RegistryOperatorRepository } from './operator.repository'; @@ -18,11 +19,15 @@ export class RegistryOperatorStorageService { findStream(where: FilterQuery, fields?: string[]): AsyncIterable { const knex = this.repository.getKnex(); - return knex + const stream = knex .select(fields || '*') .from('registry_operator') .where(where) .stream(); + + addTimeoutToStream(stream, 60_000); + + return stream; } /** find all operators */ diff --git a/src/common/registry/utils/stream.utils.ts b/src/common/registry/utils/stream.utils.ts new file mode 100644 index 00000000..24eb1c0f --- /dev/null +++ b/src/common/registry/utils/stream.utils.ts @@ -0,0 +1,21 @@ +import { PassThrough } from 'stream'; + +export const addTimeoutToStream = (stream: PassThrough, ms: number) => { + let timeout = setTimeout(() => { + stream.destroy(); + }, ms); + + const debounce = () => { + clearTimeout(timeout); + timeout = setTimeout(() => { + stream.destroy(); + }, ms); + }; + + const debounceTimeoutHandler = stream.on('data', debounce); + + stream.once('close', () => { + clearTimeout(timeout); + debounceTimeoutHandler.off('data', debounce); + }); +}; From 4dd16ab1f229c10186b8e9c377debdda4f71c82b Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 9 Nov 2023 11:15:02 +0100 Subject: [PATCH 20/75] fix: replace console logs by error --- src/http/keys/keys.controller.ts | 2 +- src/http/sr-modules-keys/sr-modules-keys.controller.ts | 2 +- .../sr-modules-operators-keys.controller.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/http/keys/keys.controller.ts b/src/http/keys/keys.controller.ts index a29d6486..c795d86b 100644 --- a/src/http/keys/keys.controller.ts +++ b/src/http/keys/keys.controller.ts @@ -59,10 +59,10 @@ export class KeysController { jsonStream.end(); } catch (streamError) { // Handle the error during streaming. - console.error('Error during streaming:', streamError); // destroy method closes the stream without ']' and corrupt the result // https://github.com/dominictarr/through/blob/master/index.js#L78 jsonStream.destroy(); + throw streamError; } }, { isolationLevel: IsolationLevel.REPEATABLE_READ }, diff --git a/src/http/sr-modules-keys/sr-modules-keys.controller.ts b/src/http/sr-modules-keys/sr-modules-keys.controller.ts index 972200dc..9e7b20e1 100644 --- a/src/http/sr-modules-keys/sr-modules-keys.controller.ts +++ b/src/http/sr-modules-keys/sr-modules-keys.controller.ts @@ -101,10 +101,10 @@ export class SRModulesKeysController { jsonStream.end(); } catch (streamError) { // Handle the error during streaming. - console.error('Error during streaming:', streamError); // destroy method closes the stream without ']' and corrupt the result // https://github.com/dominictarr/through/blob/master/index.js#L78 jsonStream.destroy(); + throw streamError; } }, { isolationLevel: IsolationLevel.REPEATABLE_READ }, diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts index 9519b7d9..e99e1ba0 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts @@ -79,10 +79,10 @@ export class SRModulesOperatorsKeysController { jsonStream.end(); } catch (streamError) { // Handle the error during streaming. - console.error('Error during streaming:', streamError); // destroy method closes the stream without ']' and corrupt the result // https://github.com/dominictarr/through/blob/master/index.js#L78 jsonStream.destroy(); + throw streamError; } }, { isolationLevel: IsolationLevel.REPEATABLE_READ }, From bf16d5b040f281207f51b6639f84aa5c225817e9 Mon Sep 17 00:00:00 2001 From: Eddort Date: Sun, 12 Nov 2023 19:27:14 +0100 Subject: [PATCH 21/75] feat: better error handling --- src/common/registry/storage/key.storage.ts | 2 +- .../registry/storage/operator.storage.ts | 2 +- src/common/registry/utils/stream.utils.ts | 15 ++++++++++++--- src/http/keys/keys.controller.ts | 12 ++++++++++-- .../sr-modules-keys.controller.ts | 8 ++++++-- .../sr-modules-operators-keys.controller.ts | 18 ++++++++++++++++-- 6 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/common/registry/storage/key.storage.ts b/src/common/registry/storage/key.storage.ts index 441a190f..bb3a2f28 100644 --- a/src/common/registry/storage/key.storage.ts +++ b/src/common/registry/storage/key.storage.ts @@ -25,7 +25,7 @@ export class RegistryKeyStorageService { .where(where) .stream(); - addTimeoutToStream(stream, 60_000); + addTimeoutToStream(stream, 60_000, 'A timeout occurred loading keys from the database'); return stream; } diff --git a/src/common/registry/storage/operator.storage.ts b/src/common/registry/storage/operator.storage.ts index 2230416e..a642fd01 100644 --- a/src/common/registry/storage/operator.storage.ts +++ b/src/common/registry/storage/operator.storage.ts @@ -25,7 +25,7 @@ export class RegistryOperatorStorageService { .where(where) .stream(); - addTimeoutToStream(stream, 60_000); + addTimeoutToStream(stream, 60_000, 'A timeout occurred loading operators from the database'); return stream; } diff --git a/src/common/registry/utils/stream.utils.ts b/src/common/registry/utils/stream.utils.ts index 24eb1c0f..dc98a156 100644 --- a/src/common/registry/utils/stream.utils.ts +++ b/src/common/registry/utils/stream.utils.ts @@ -1,17 +1,26 @@ import { PassThrough } from 'stream'; -export const addTimeoutToStream = (stream: PassThrough, ms: number) => { +/** + * This util adds a timeout for streaming, + * preventing errors related to stream hangs + * this can happen when working with knex + * @param stream nodejs stream + * @param ms timeout in ms + * @param errorMessage error text to be displayed when the stream is closed + */ +export const addTimeoutToStream = (stream: PassThrough, ms: number, errorMessage: string) => { let timeout = setTimeout(() => { - stream.destroy(); + stream.destroy(new Error(errorMessage)); }, ms); const debounce = () => { clearTimeout(timeout); timeout = setTimeout(() => { - stream.destroy(); + stream.destroy(new Error(errorMessage)); }, ms); }; + // we should stop streaming if more than "ms" has elapsed since the last receipt of data. const debounceTimeoutHandler = stream.on('data', debounce); stream.once('close', () => { diff --git a/src/http/keys/keys.controller.ts b/src/http/keys/keys.controller.ts index c795d86b..77526c01 100644 --- a/src/http/keys/keys.controller.ts +++ b/src/http/keys/keys.controller.ts @@ -10,7 +10,10 @@ import { HttpStatus, NotFoundException, Res, + LoggerService, + Inject, } from '@nestjs/common'; +import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; import type { FastifyReply } from 'fastify'; import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { KeysService } from './keys.service'; @@ -25,7 +28,11 @@ import { IsolationLevel } from '@mikro-orm/core'; @Controller('keys') @ApiTags('keys') export class KeysController { - constructor(protected readonly keysService: KeysService, protected readonly entityManager: EntityManager) {} + constructor( + @Inject(LOGGER_PROVIDER) protected logger: LoggerService, + protected readonly keysService: KeysService, + protected readonly entityManager: EntityManager, + ) {} @Version('1') @Get() @@ -56,13 +63,14 @@ export class KeysController { jsonStream.write(keyReponse); } } + jsonStream.end(); } catch (streamError) { // Handle the error during streaming. + this.logger.log('keys streaming error', streamError); // destroy method closes the stream without ']' and corrupt the result // https://github.com/dominictarr/through/blob/master/index.js#L78 jsonStream.destroy(); - throw streamError; } }, { isolationLevel: IsolationLevel.REPEATABLE_READ }, diff --git a/src/http/sr-modules-keys/sr-modules-keys.controller.ts b/src/http/sr-modules-keys/sr-modules-keys.controller.ts index 9e7b20e1..2970d631 100644 --- a/src/http/sr-modules-keys/sr-modules-keys.controller.ts +++ b/src/http/sr-modules-keys/sr-modules-keys.controller.ts @@ -10,7 +10,10 @@ import { HttpStatus, Res, HttpCode, + LoggerService, + Inject, } from '@nestjs/common'; +import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; import { ApiNotFoundResponse, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { SRModuleKeyListResponse, GroupedByModuleKeyListResponse } from './entities'; import { SRModulesKeysService } from './sr-modules-keys.service'; @@ -27,6 +30,7 @@ import { ModuleIdPipe } from '../common/pipeline/module-id-pipe'; @ApiTags('sr-module-keys') export class SRModulesKeysController { constructor( + @Inject(LOGGER_PROVIDER) protected logger: LoggerService, protected readonly srModulesKeysService: SRModulesKeysService, protected readonly entityManager: EntityManager, ) {} @@ -95,16 +99,16 @@ export class SRModulesKeysController { try { for await (const key of keysGenerator) { const keyReponse = new Key(key); - jsonStream.write(keyReponse); } + jsonStream.end(); } catch (streamError) { // Handle the error during streaming. + this.logger.error('module-keys streaming error', streamError); // destroy method closes the stream without ']' and corrupt the result // https://github.com/dominictarr/through/blob/master/index.js#L78 jsonStream.destroy(); - throw streamError; } }, { isolationLevel: IsolationLevel.REPEATABLE_READ }, diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts index e99e1ba0..db426359 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts @@ -1,6 +1,17 @@ import { pipeline } from 'node:stream/promises'; import { IsolationLevel } from '@mikro-orm/core'; -import { Controller, Get, Version, Param, Query, NotFoundException, HttpStatus, Res } from '@nestjs/common'; +import { + Controller, + Get, + Version, + Param, + Query, + NotFoundException, + HttpStatus, + Res, + LoggerService, + Inject, +} from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags, ApiParam, ApiNotFoundResponse } from '@nestjs/swagger'; import { SRModuleOperatorsKeysResponse, SRModulesOperatorsKeysStreamResponse } from './entities'; import { KeyQuery, Key } from 'http/common/entities/'; @@ -11,11 +22,13 @@ import * as JSONStream from 'jsonstream'; import type { FastifyReply } from 'fastify'; import { streamify } from 'common/streams'; import { ModuleIdPipe } from 'http/common/pipeline/module-id-pipe'; +import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; @Controller('/modules') @ApiTags('operators-keys') export class SRModulesOperatorsKeysController { constructor( + @Inject(LOGGER_PROVIDER) protected logger: LoggerService, protected readonly srModulesOperatorsKeys: SRModulesOperatorsKeysService, protected readonly entityManager: EntityManager, ) {} @@ -76,13 +89,14 @@ export class SRModulesOperatorsKeysController { const keyResponse = new Key(key); jsonStream.write(keyResponse); } + jsonStream.end(); } catch (streamError) { // Handle the error during streaming. + this.logger.error('operators-keys streaming error', streamError); // destroy method closes the stream without ']' and corrupt the result // https://github.com/dominictarr/through/blob/master/index.js#L78 jsonStream.destroy(); - throw streamError; } }, { isolationLevel: IsolationLevel.REPEATABLE_READ }, From 850f9b85587835db658cc0bb1a1bde0d93c994b5 Mon Sep 17 00:00:00 2001 From: Eddort Date: Sun, 12 Nov 2023 20:51:20 +0100 Subject: [PATCH 22/75] feat: keys batch tests --- src/common/registry/fetch/key-batch.fetch.ts | 69 ++++---------- .../registry/fetch/utils/batches.spec.ts | 89 +++++++++---------- src/common/registry/fetch/utils/batches.ts | 9 +- .../registry/fetch/utils/split-hex.spec.ts | 37 +++++--- 4 files changed, 88 insertions(+), 116 deletions(-) diff --git a/src/common/registry/fetch/key-batch.fetch.ts b/src/common/registry/fetch/key-batch.fetch.ts index 75306b59..0a424417 100644 --- a/src/common/registry/fetch/key-batch.fetch.ts +++ b/src/common/registry/fetch/key-batch.fetch.ts @@ -5,6 +5,8 @@ import { KeyBatchRecord, RegistryKey } from './interfaces/key.interface'; import { RegistryOperatorFetchService } from './operator.fetch'; import { KEYS_BATCH_SIZE, KEYS_LENGTH, SIGNATURE_LENGTH } from './key-batch.constants'; import { RegistryFetchOptions, REGISTRY_FETCH_OPTIONS_TOKEN } from './interfaces/module.interface'; +import { splitHex } from './utils/split-hex'; +import { makeBatches } from './utils/batches'; @Injectable() export class RegistryKeyBatchFetchService { @@ -18,38 +20,12 @@ export class RegistryKeyBatchFetchService { return this.contract.attach(moduleAddress); } - /** - * Split one big string into array of strings - * `0x${key1}{key2}...` -> `[`0x${key1}`, `0x${key2}`]` - * - * example record: - * 0x81b4ae61a898396903897f94bea0e062c3a6925ee93d30f4d4aee93b533b49551ac337da78ff2ab0cfbb0adb380cad94953805708367b0b5f6710d41608ccdd0d5a67938e10e68dd010890d4bfefdcde874370423b0af0d0a053b7b98ae2d6ed - * - * 0x81b4ae61a898396903897f94bea0e062c3a6925ee93d30f4d4aee93b533b49551ac337da78ff2ab0cfbb0adb380cad94 - * @param record pubkey or signature merged string - * @param capacity 96 or 192 - * @returns array of keys or signatures - */ - protected splitMergedRecord(record: string, capacity: number) { - const parts: string[] = []; - let part = ''; - // start from index 2 because each record beginning from 0x - for (let i = 2; i < record.length; i++) { - part += record[i]; - if (part.length === capacity) { - parts.push(`0x${part}`); - part = ''; - } - } - return parts; - } - protected unformattedSignaturesToArray(unformattedSignatures: string) { - return this.splitMergedRecord(unformattedSignatures, SIGNATURE_LENGTH); + return splitHex(unformattedSignatures, SIGNATURE_LENGTH); } protected unformattedKeysToArray(unformattedKeys: string) { - return this.splitMergedRecord(unformattedKeys, KEYS_LENGTH); + return splitHex(unformattedKeys, KEYS_LENGTH); } public formatKeys( @@ -61,7 +37,7 @@ export class RegistryKeyBatchFetchService { const keys = this.unformattedKeysToArray(unformattedRecords[0]); const signatures = this.unformattedSignaturesToArray(unformattedRecords[1]); const usedStatuses = unformattedRecords[2]; - // TODO: do we need this? + if (keys.length !== signatures.length || keys.length !== usedStatuses.length) { throw new Error('format keys error'); } @@ -112,30 +88,23 @@ export class RegistryKeyBatchFetchService { public async fetchSigningKeysInBatches( moduleAddress: string, operatorIndex: number, - offset: number, + defaultOffset: number, totalAmount: number, overrides: CallOverrides, ) { - const batchSize = this.options.keysBatchSize || KEYS_BATCH_SIZE; - - const numberOfBatches = Math.ceil(totalAmount / batchSize); - const promises: Promise[] = []; - - for (let i = 0; i < numberOfBatches; i++) { - const currentOffset = offset + i * batchSize; - const currentBatchSize = Math.min(batchSize, totalAmount - i * batchSize); - const promise = (async () => { - const keys = await this.getContract(moduleAddress).getSigningKeys( - operatorIndex, - currentOffset, - currentBatchSize, - overrides as any, - ); - return this.formatKeys(moduleAddress, operatorIndex, keys, currentOffset); - })(); - - promises.push(promise); - } + const defaultBatchSize = this.options.keysBatchSize || KEYS_BATCH_SIZE; + const batches = makeBatches(defaultBatchSize, defaultOffset, totalAmount); + + const promises = batches.map(async ({ offset, batchSize }) => { + const keys = await this.getContract(moduleAddress).getSigningKeys( + operatorIndex, + offset, + batchSize, + overrides as any, + ); + + return this.formatKeys(moduleAddress, operatorIndex, keys, offset); + }); const results = await Promise.all(promises); return results.flat(); diff --git a/src/common/registry/fetch/utils/batches.spec.ts b/src/common/registry/fetch/utils/batches.spec.ts index f49eb1ce..c5d96c01 100644 --- a/src/common/registry/fetch/utils/batches.spec.ts +++ b/src/common/registry/fetch/utils/batches.spec.ts @@ -1,68 +1,61 @@ import { makeBatches } from './batches'; -describe('batch', () => { - test('total <<< batch, offset 0, total 0', () => { - const dataBatches = makeBatches(200, 0, 0); - - expect(dataBatches).toEqual([]); - }); - - test('total >>> batch, offset 800, total 800', () => { - const dataBatches = makeBatches(200, 799, 1); - - // strange - expect(dataBatches).toEqual([{ batchSize: 1, offset: 799 }]); +describe('makeBatches util', () => { + test('should create batches with correct offset and batchSize', () => { + const batchSize = 3; + const offset = 0; + const totalAmount = 10; + + const result = makeBatches(batchSize, offset, totalAmount); + + expect(result).toEqual([ + { offset: 0, batchSize: 3 }, + { offset: 3, batchSize: 3 }, + { offset: 6, batchSize: 3 }, + { offset: 9, batchSize: 1 }, + ]); }); - test('total <<< batch, offset 0, total 1', () => { - const dataBatches = makeBatches(200, 0, 1); - - expect(dataBatches).toEqual([{ batchSize: 1, offset: 0 }]); - }); + test('should create a single batch when totalAmount is less than batchSize', () => { + const batchSize = 10; + const offset = 2; + const totalAmount = 8; - test('total <<< batch, offset 1, total 1', () => { - const dataBatches = makeBatches(200, 1, 1); + const result = makeBatches(batchSize, offset, totalAmount); - expect(dataBatches).toEqual([{ batchSize: 1, offset: 1 }]); + expect(result).toEqual([{ offset: 2, batchSize: 8 }]); }); - test('total === batch, offset 1, total 10', () => { - const dataBatches = makeBatches(10, 1, 10); - - // thought that batchSize is 10 - expect(dataBatches).toEqual([{ batchSize: 10, offset: 1 }]); - }); + test('should handle cases when totalAmount is 0', () => { + const batchSize = 5; + const offset = 0; + const totalAmount = 0; - test('total < batch, offset 1, total 3', () => { - const dataBatches = makeBatches(10, 1, 3); + const result = makeBatches(batchSize, offset, totalAmount); - // thought that batchSize here is 2 - expect(dataBatches).toEqual([{ batchSize: 3, offset: 1 }]); + expect(result).toEqual([]); }); - test('total === batch, offset 1, total 3', () => { - const dataBatches = makeBatches(3, 1, 3); + test('should throw error when batchSize is 0', () => { + const batchSize = 0; + const offset = 2; + const totalAmount = 8; - // thought that batchSize here is 2 - expect(dataBatches).toEqual([{ batchSize: 3, offset: 1 }]); + expect(() => makeBatches(batchSize, offset, totalAmount)).toThrowError('batchSize must be greater than 0'); }); - test('total > batch, offset 0, total 199', () => { - const dataBatches = makeBatches(100, 0, 199); - - expect(dataBatches).toEqual([ - { batchSize: 100, offset: 0 }, - { batchSize: 99, offset: 100 }, - ]); - }); + test('should create batches correct offset and batchSize when totalAmount is a multiple of batchSize', () => { + const batchSize = 4; + const offset = 2; + const totalAmount = 16; - test('total > batch, offset 100, total 199', () => { - const dataBatches = makeBatches(100, 100, 199); + const result = makeBatches(batchSize, offset, totalAmount); - // why? - expect(dataBatches).toEqual([ - { batchSize: 100, offset: 100 }, - { batchSize: 99, offset: 200 }, + expect(result).toEqual([ + { offset: 2, batchSize: 4 }, + { offset: 6, batchSize: 4 }, + { offset: 10, batchSize: 4 }, + { offset: 14, batchSize: 4 }, ]); }); }); diff --git a/src/common/registry/fetch/utils/batches.ts b/src/common/registry/fetch/utils/batches.ts index 96b414ce..1c6c14f6 100644 --- a/src/common/registry/fetch/utils/batches.ts +++ b/src/common/registry/fetch/utils/batches.ts @@ -1,10 +1,5 @@ -import { CallOverrides } from '../interfaces/overrides.interface'; - -export const makeBatches = ( - batchSize: number, - offset: number, - totalAmount: number, -) => { +export const makeBatches = (batchSize: number, offset: number, totalAmount: number) => { + if (batchSize < 1) throw new Error('batchSize must be greater than 0'); const numberOfBatches = Math.ceil(totalAmount / batchSize); const batches: { offset: number; batchSize: number }[] = []; diff --git a/src/common/registry/fetch/utils/split-hex.spec.ts b/src/common/registry/fetch/utils/split-hex.spec.ts index 9b08f7b3..610322f4 100644 --- a/src/common/registry/fetch/utils/split-hex.spec.ts +++ b/src/common/registry/fetch/utils/split-hex.spec.ts @@ -1,21 +1,36 @@ import { splitHex } from './split-hex'; -describe('split-merged-record', () => { - test('test1', () => { - const parts = splitHex('0x123456789', 3); - - expect(parts).toEqual(['0x123', '0x456', '0x789']); +describe('splitHex util', () => { + test('splits a hex string into chunks of specified length', () => { + const hexString = '0x123456789abcdefg'; // 15 characters + const chunkLength = 4; + const result = splitHex(hexString, chunkLength); + expect(result).toEqual(['0x1234', '0x5678', '0x9abc', '0xdefg']); }); - test('test2', () => { - const parts = splitHex('0x1234567890', 3); + test('throws RangeError if chunkLength is less than 1', () => { + const hexString = '0xabcdef'; + const chunkLength = 0; + expect(() => splitHex(hexString, chunkLength)).toThrowError('chunkLength should be positive'); + }); - expect(parts).toEqual(['0x123', '0x456', '0x789']); + test('throws Error if input is not a hex-like string', () => { + const hexString = 'abcdef'; // Missing '0x' prefix + const chunkLength = 2; + expect(() => splitHex(hexString, chunkLength)).toThrowError('not a hex-like string'); }); - test('test3', () => { - const parts = splitHex('123456789', 3); + test('handles empty input string', () => { + const hexString = '0x'; + const chunkLength = 2; + const result = splitHex(hexString, chunkLength); + expect(result).toEqual([]); + }); - expect(parts).toEqual(['0x123', '0x456', '0x789']); + test('handles input string with odd length', () => { + const hexString = '0x12345'; // 5 characters + const chunkLength = 2; + const result = splitHex(hexString, chunkLength); + expect(result).toEqual(['0x12', '0x34']); }); }); From f89211ac7f8fe42b34df5cd6af59c4c4eb09fff8 Mon Sep 17 00:00:00 2001 From: infloop Date: Mon, 13 Nov 2023 13:48:12 +0500 Subject: [PATCH 23/75] fix: add jsdoc fix for splitHex function --- src/common/registry/fetch/utils/split-hex.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/registry/fetch/utils/split-hex.ts b/src/common/registry/fetch/utils/split-hex.ts index e45bd5dc..c560f1d8 100644 --- a/src/common/registry/fetch/utils/split-hex.ts +++ b/src/common/registry/fetch/utils/split-hex.ts @@ -1,14 +1,14 @@ /** - * Split one big string into array of strings + * Split one big hex-like string into array of hex-like strings * `0x${key1}{key2}...` -> `[`0x${key1}`, `0x${key2}`]` * * example record: * 0x81b4ae61a898396903897f94bea0e062c3a6925ee93d30f4d4aee93b533b49551ac337da78ff2ab0cfbb0adb380cad94953805708367b0b5f6710d41608ccdd0d5a67938e10e68dd010890d4bfefdcde874370423b0af0d0a053b7b98ae2d6ed * * 0x81b4ae61a898396903897f94bea0e062c3a6925ee93d30f4d4aee93b533b49551ac337da78ff2ab0cfbb0adb380cad94 - * @param record pubkey or signature merged string - * @param capacity 96 or 192 - * @returns array of keys or signatures + * @param hexString hex-like string + * @param chunkLength chunk length + * @returns array of hex-like strings */ export const splitHex = (hexString: string, chunkLength: number) => { if (chunkLength < 1) { From 91bc76f8a9a476922e35cebc6ef2c4d9dd66c835 Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Mon, 13 Nov 2023 21:09:49 +0400 Subject: [PATCH 24/75] VAL-422: Make production KAPI docker image without npm dev dependencies --- Dockerfile | 2 +- nest-cli.json | 5 ++++- package.json | 2 +- tsconfig.build.json | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4b43b206..f1046041 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ RUN apk add --no-cache git=2.36.6-r0 WORKDIR /app -COPY package.json yarn.lock chronix.config.ts ./ +COPY package.json yarn.lock ./ COPY ./tsconfig*.json ./ COPY ./src ./src diff --git a/nest-cli.json b/nest-cli.json index 56167b36..6b2c268b 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -1,4 +1,7 @@ { "collection": "@nestjs/schematics", - "sourceRoot": "src" + "sourceRoot": "src", + "compilerOptions": { + "tsConfigPath": "./tsconfig.build.json" + } } diff --git a/package.json b/package.json index db319a36..7aafaaef 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "url": "https://github.com/lidofinance/lido-keys-api/issues" }, "scripts": { - "postinstall": "rimraf ./node_modules/@ethersproject/abstract-provider/src.ts/*.ts && yarn chronix:compile", + "postinstall": "rimraf ./node_modules/@ethersproject/abstract-provider/src.ts/*.ts", "mikro-orm": "./node_modules/mikro-orm", "prebuild": "rimraf dist", "build": "nest build", diff --git a/tsconfig.build.json b/tsconfig.build.json index 64f86c6b..4cc41b2d 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] + "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "**/*e2e-chain.ts"] } From c054b44c30264b01e008adff368ac7c942aa5627 Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Mon, 13 Nov 2023 21:16:41 +0400 Subject: [PATCH 25/75] fix tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7aafaaef..d060b6d2 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config jest-e2e.json && chronix test", + "test:e2e": "jest --config jest-e2e.json && chronix compile && chronix test", "typechain": "typechain --target=ethers-v5 --out-dir ./src/generated ./src/staking-router-modules/contracts/abi/*.json", "chronix:compile": "chronix compile", "chronix:test": "chronix test", From 789948ca42de69af573bbdde89367b6fd77bab0a Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Mon, 20 Nov 2023 20:47:06 +0400 Subject: [PATCH 26/75] chore: code review --- .github/workflows/test.yml | 2 +- src/common/registry/storage/key.storage.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5ffaae05..9d85514e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ jobs: services: postgres: - image: postgres + image: postgres:16-alpine env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres diff --git a/src/common/registry/storage/key.storage.ts b/src/common/registry/storage/key.storage.ts index bb3a2f28..5a81cb25 100644 --- a/src/common/registry/storage/key.storage.ts +++ b/src/common/registry/storage/key.storage.ts @@ -23,6 +23,10 @@ export class RegistryKeyStorageService { .select(fields || '*') .from('registry_key') .where(where) + .orderBy([ + { column: 'operatorIndex', order: 'asc' }, + { column: 'index', order: 'asc' }, + ]) .stream(); addTimeoutToStream(stream, 60_000, 'A timeout occurred loading keys from the database'); From dd6d2d33680bf30e9c216ef8b90a023cca86c86d Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Mon, 20 Nov 2023 20:50:51 +0400 Subject: [PATCH 27/75] chore: code review 2 --- src/common/registry/storage/operator.storage.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/common/registry/storage/operator.storage.ts b/src/common/registry/storage/operator.storage.ts index a642fd01..90229d52 100644 --- a/src/common/registry/storage/operator.storage.ts +++ b/src/common/registry/storage/operator.storage.ts @@ -23,6 +23,10 @@ export class RegistryOperatorStorageService { .select(fields || '*') .from('registry_operator') .where(where) + .orderBy([ + { column: 'operatorIndex', order: 'asc' }, + { column: 'index', order: 'asc' }, + ]) .stream(); addTimeoutToStream(stream, 60_000, 'A timeout occurred loading operators from the database'); From 89dd289467cf12d836addd63b41baf88f6bf587b Mon Sep 17 00:00:00 2001 From: infloop Date: Tue, 21 Nov 2023 06:40:48 +0500 Subject: [PATCH 28/75] fix: improve tests and edge-cases --- .../registry/fetch/utils/batches.spec.ts | 83 +++++++++++++++---- src/common/registry/fetch/utils/batches.ts | 16 +++- .../registry/fetch/utils/split-hex.spec.ts | 20 ++++- src/common/registry/fetch/utils/split-hex.ts | 8 +- 4 files changed, 101 insertions(+), 26 deletions(-) diff --git a/src/common/registry/fetch/utils/batches.spec.ts b/src/common/registry/fetch/utils/batches.spec.ts index c5d96c01..54461d4c 100644 --- a/src/common/registry/fetch/utils/batches.spec.ts +++ b/src/common/registry/fetch/utils/batches.spec.ts @@ -1,12 +1,12 @@ import { makeBatches } from './batches'; describe('makeBatches util', () => { - test('should create batches with correct offset and batchSize', () => { + test('should create batches with correct zero offset and batchSize', () => { const batchSize = 3; const offset = 0; - const totalAmount = 10; + const limit = 10; - const result = makeBatches(batchSize, offset, totalAmount); + const result = makeBatches(batchSize, offset, limit); expect(result).toEqual([ { offset: 0, batchSize: 3 }, @@ -16,22 +16,37 @@ describe('makeBatches util', () => { ]); }); - test('should create a single batch when totalAmount is less than batchSize', () => { + test('should create batches with correct not zero offset and batchSize', () => { + const batchSize = 3; + const offset = 1; + const limit = 10; + + const result = makeBatches(batchSize, offset, limit); + + expect(result).toEqual([ + { offset: 1, batchSize: 3 }, + { offset: 4, batchSize: 3 }, + { offset: 7, batchSize: 3 }, + { offset: 10, batchSize: 1 }, + ]); + }); + + test('should create a single batch when limit is less than batchSize', () => { const batchSize = 10; const offset = 2; - const totalAmount = 8; + const limit = 8; - const result = makeBatches(batchSize, offset, totalAmount); + const result = makeBatches(batchSize, offset, limit); expect(result).toEqual([{ offset: 2, batchSize: 8 }]); }); - test('should handle cases when totalAmount is 0', () => { + test('should handle cases when limit is 0', () => { const batchSize = 5; const offset = 0; - const totalAmount = 0; + const limit = 0; - const result = makeBatches(batchSize, offset, totalAmount); + const result = makeBatches(batchSize, offset, limit); expect(result).toEqual([]); }); @@ -39,17 +54,57 @@ describe('makeBatches util', () => { test('should throw error when batchSize is 0', () => { const batchSize = 0; const offset = 2; - const totalAmount = 8; + const limit = 8; + + expect(() => makeBatches(batchSize, offset, limit)).toThrowError('batchSize must be greater than 0 and integer'); + }); + + test('should throw error when batchSize is not a number', () => { + const batchSize: any = 'test'; + const offset = 2; + const limit = 8; + + expect(() => makeBatches(batchSize, offset, limit)).toThrowError('batchSize must be greater than 0 and integer'); + }); + + test('should throw error when limit is not a number', () => { + const batchSize = 10; + const offset = 2; + const limit: any = 'test'; + + expect(() => makeBatches(batchSize, offset, limit)).toThrowError('limit should be positive integer'); + }); + + test('should throw error when limit is a negative number', () => { + const batchSize = 10; + const offset = 2; + const limit = -1; + + expect(() => makeBatches(batchSize, offset, limit)).toThrowError('limit should be positive integer'); + }); + + test('should throw error when offset is not a number', () => { + const batchSize = 10; + const offset: any = 'test'; + const limit = 2; + + expect(() => makeBatches(batchSize, offset, limit)).toThrowError('offset should be positive integer'); + }); + + test('should throw error when offset is a negative number', () => { + const batchSize = 10; + const offset = -2; + const limit = 10; - expect(() => makeBatches(batchSize, offset, totalAmount)).toThrowError('batchSize must be greater than 0'); + expect(() => makeBatches(batchSize, offset, limit)).toThrowError('offset should be positive integer'); }); - test('should create batches correct offset and batchSize when totalAmount is a multiple of batchSize', () => { + test('should create batches correct offset and batchSize when limit is a multiple of batchSize', () => { const batchSize = 4; const offset = 2; - const totalAmount = 16; + const limit = 16; - const result = makeBatches(batchSize, offset, totalAmount); + const result = makeBatches(batchSize, offset, limit); expect(result).toEqual([ { offset: 2, batchSize: 4 }, diff --git a/src/common/registry/fetch/utils/batches.ts b/src/common/registry/fetch/utils/batches.ts index 1c6c14f6..1b860311 100644 --- a/src/common/registry/fetch/utils/batches.ts +++ b/src/common/registry/fetch/utils/batches.ts @@ -1,11 +1,19 @@ -export const makeBatches = (batchSize: number, offset: number, totalAmount: number) => { - if (batchSize < 1) throw new Error('batchSize must be greater than 0'); - const numberOfBatches = Math.ceil(totalAmount / batchSize); +export const makeBatches = (batchSize: number, offset: number, limit: number) => { + if (!Number.isInteger(offset) || offset < 0) { + throw new RangeError('offset should be positive integer'); + } + if (!Number.isInteger(limit) || limit < 0) { + throw new RangeError('limit should be positive integer'); + } + if (!Number.isInteger(batchSize) || batchSize < 1) { + throw new RangeError('batchSize must be greater than 0 and integer'); + } + const numberOfBatches = Math.ceil(limit / batchSize); const batches: { offset: number; batchSize: number }[] = []; for (let i = 0; i < numberOfBatches; i++) { const currentOffset = offset + i * batchSize; - const currentBatchSize = Math.min(batchSize, totalAmount - i * batchSize); + const currentBatchSize = Math.min(batchSize, limit - i * batchSize); batches.push({ offset: currentOffset, batchSize: currentBatchSize }); } diff --git a/src/common/registry/fetch/utils/split-hex.spec.ts b/src/common/registry/fetch/utils/split-hex.spec.ts index 610322f4..07bfbd2a 100644 --- a/src/common/registry/fetch/utils/split-hex.spec.ts +++ b/src/common/registry/fetch/utils/split-hex.spec.ts @@ -2,22 +2,34 @@ import { splitHex } from './split-hex'; describe('splitHex util', () => { test('splits a hex string into chunks of specified length', () => { - const hexString = '0x123456789abcdefg'; // 15 characters + const hexString = '0x123456789abcdefABCDEF012'; // 24 characters const chunkLength = 4; const result = splitHex(hexString, chunkLength); - expect(result).toEqual(['0x1234', '0x5678', '0x9abc', '0xdefg']); + expect(result).toEqual(['0x1234', '0x5678', '0x9abc', '0xdefA', '0xBCDE', '0xF012']); }); test('throws RangeError if chunkLength is less than 1', () => { const hexString = '0xabcdef'; const chunkLength = 0; - expect(() => splitHex(hexString, chunkLength)).toThrowError('chunkLength should be positive'); + expect(() => splitHex(hexString, chunkLength)).toThrowError('chunkLength should be positive integer'); }); test('throws Error if input is not a hex-like string', () => { const hexString = 'abcdef'; // Missing '0x' prefix const chunkLength = 2; - expect(() => splitHex(hexString, chunkLength)).toThrowError('not a hex-like string'); + expect(() => splitHex(hexString, chunkLength)).toThrowError('hexString is not a hex-like string'); + }); + + test('throws Error if input is bad hex string', () => { + const hexString = '0x01234567890AbCdEfG'; // G symbol + const chunkLength = 2; + expect(() => splitHex(hexString, chunkLength)).toThrowError('hexString is not a hex-like string'); + }); + + test('throws Error if input is not a string', () => { + const hexString: string = 1245; + const chunkLength = 2; + expect(() => splitHex(hexString, chunkLength)).toThrowError('hexString is not a hex-like string'); }); test('handles empty input string', () => { diff --git a/src/common/registry/fetch/utils/split-hex.ts b/src/common/registry/fetch/utils/split-hex.ts index c560f1d8..c3cc0e97 100644 --- a/src/common/registry/fetch/utils/split-hex.ts +++ b/src/common/registry/fetch/utils/split-hex.ts @@ -11,12 +11,12 @@ * @returns array of hex-like strings */ export const splitHex = (hexString: string, chunkLength: number) => { - if (chunkLength < 1) { - throw new RangeError('chunkLength should be positive'); + if (!Number.isInteger(chunkLength) || chunkLength < 1) { + throw new RangeError('chunkLength should be positive integer'); } - if (typeof hexString !== 'string' || hexString.substring(0, 2) !== '0x') { - throw new Error('not a hex-like string'); + if (typeof hexString !== 'string' || !hexString.match(/^0x[0-9A-Fa-f]*$/)) { + throw new Error('hexString is not a hex-like string'); } const parts: string[] = []; From d8c7488b9f934f8597233d5d784223b9b71f8bb4 Mon Sep 17 00:00:00 2001 From: infloop Date: Tue, 21 Nov 2023 06:55:36 +0500 Subject: [PATCH 29/75] fix: add test for batching --- src/common/registry/fetch/utils/batches.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/common/registry/fetch/utils/batches.spec.ts b/src/common/registry/fetch/utils/batches.spec.ts index 54461d4c..707f0295 100644 --- a/src/common/registry/fetch/utils/batches.spec.ts +++ b/src/common/registry/fetch/utils/batches.spec.ts @@ -31,6 +31,22 @@ describe('makeBatches util', () => { ]); }); + test('should create batches with limit not fully divisible by batch size', () => { + const batchSize = 501; + const offset = 297; + const limit = 2125; + + const result = makeBatches(batchSize, offset, limit); + + expect(result).toEqual([ + { offset: 297, batchSize: 501 }, + { offset: 798, batchSize: 501 }, + { offset: 1299, batchSize: 501 }, + { offset: 1800, batchSize: 501 }, + { offset: 2301, batchSize: 121 }, + ]); + }); + test('should create a single batch when limit is less than batchSize', () => { const batchSize = 10; const offset = 2; From 18f757a60d713c206b3dd4e6445812d2477ab419 Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Wed, 22 Nov 2023 15:14:52 +0400 Subject: [PATCH 30/75] VAL-436: Enhance KAPI mev-m stream endpoint --- .../registry/storage/operator.storage.ts | 2 +- .../sr-module-operators-keys.response.ts | 18 +++--- .../sr-modules-operators-keys.controller.ts | 14 +++-- .../sr-modules-operators-keys.e2e-spec.ts | 59 ++++++++++++++++++- .../sr-modules-operators-keys.service.ts | 19 ++++-- .../sr-modules-operators-keys.types.ts | 5 +- 6 files changed, 91 insertions(+), 26 deletions(-) diff --git a/src/common/registry/storage/operator.storage.ts b/src/common/registry/storage/operator.storage.ts index 90229d52..af3615ca 100644 --- a/src/common/registry/storage/operator.storage.ts +++ b/src/common/registry/storage/operator.storage.ts @@ -24,7 +24,7 @@ export class RegistryOperatorStorageService { .from('registry_operator') .where(where) .orderBy([ - { column: 'operatorIndex', order: 'asc' }, + { column: 'moduleAddress', order: 'asc' }, { column: 'index', order: 'asc' }, ]) .stream(); diff --git a/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts b/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts index c79dcdfa..bbd91e4c 100644 --- a/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts +++ b/src/http/sr-modules-operators-keys/entities/sr-module-operators-keys.response.ts @@ -38,26 +38,26 @@ export class SRModuleOperatorsKeysResponse { export class SRModulesOperatorsKeysStreamResponse { @ApiProperty({ - type: () => [Operator], - description: 'Operators of staking router module', + type: () => Operator, + description: 'Operator of staking router module', }) - operators?: Operator[]; + operator!: Operator | null; @ApiProperty({ - type: () => [Key], - description: 'Keys of staking router module', + type: () => Key, + description: 'Key of staking router module', }) - keys?: Key[]; + key!: Key | null; @ApiProperty({ type: () => StakingModuleResponse, - description: 'List of Staking Router', + description: 'Staking Router module', }) - modules?: StakingModuleResponse[]; + stakingModule!: StakingModuleResponse | null; @ApiProperty({ nullable: true, type: () => ELMeta, }) - meta?: ELMeta; + meta!: ELMeta | null; } diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts index db426359..c85d7840 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.controller.ts @@ -109,6 +109,7 @@ export class SRModulesOperatorsKeysController { status: 200, description: 'Stream of all SR modules, operators and keys', type: SRModulesOperatorsKeysStreamResponse, + isArray: true, }) @ApiResponse({ status: 425, @@ -121,9 +122,14 @@ export class SRModulesOperatorsKeysController { reply.type('application/json').send(jsonStream); - await this.entityManager.transactional( - () => pipeline([streamify(this.srModulesOperatorsKeys.getModulesOperatorsKeysGenerator()), jsonStream]), - { isolationLevel: IsolationLevel.REPEATABLE_READ }, - ); + try { + await this.entityManager.transactional( + () => pipeline([streamify(this.srModulesOperatorsKeys.getModulesOperatorsKeysGenerator()), jsonStream]), + { isolationLevel: IsolationLevel.REPEATABLE_READ }, + ); + } catch (error) { + this.logger.error('modules-operators-keys error', error); + jsonStream.destroy(); + } } } diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts index 80d2f281..4aa1ddb3 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts @@ -20,12 +20,13 @@ import { nullTransport, LoggerModule } from '@lido-nestjs/logger'; import * as request from 'supertest'; import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'; -import { dvtModuleResp } from '../module.fixture'; +import { curatedModuleResp, dvtModuleResp } from '../module.fixture'; import { elMeta } from '../el-meta.fixture'; import { keys, operators, dvtModule, curatedModule } from '../db.fixtures'; -import { dvtModuleKeysResponse } from '../keys.fixtures'; -import { dvtOperatorsResp } from '../operator.fixtures'; +import { curatedModuleKeysResponse, dvtModuleKeysResponse } from '../keys.fixtures'; +import { curatedOperatorsResp, dvtOperatorsResp } from '../operator.fixtures'; import { DatabaseTestingModule } from 'app'; +import { ModulesOperatorsKeysRecord } from './sr-modules-operators-keys.types'; describe('SRModulesOperatorsKeysController (e2e)', () => { let app: INestApplication; @@ -113,6 +114,58 @@ describe('SRModulesOperatorsKeysController (e2e)', () => { await cleanDB(); }); + describe('The /keys request', () => { + it('should return all modules, operators and keys', async () => { + const resp = await request(app.getHttpServer()).get(`/v2/modules/operators/keys`); + + expect(resp.status).toEqual(200); + + const expectedResponse: ModulesOperatorsKeysRecord[] = [ + // dvt module + { + stakingModule: dvtModuleResp, + meta: { + elBlockSnapshot: { + blockNumber: elMeta.number, + blockHash: elMeta.hash, + timestamp: elMeta.timestamp, + }, + }, + operator: dvtOperatorsResp[0], + key: dvtModuleKeysResponse[0], + }, + ...dvtOperatorsResp.slice(1).map((operator, i) => ({ + stakingModule: null, + meta: null, + operator, + key: dvtModuleKeysResponse[i + 1], + })), + ...dvtModuleKeysResponse + .slice(dvtOperatorsResp.length) + .map((key) => ({ stakingModule: null, meta: null, operator: null, key })), + + // curated module + { + stakingModule: curatedModuleResp, + meta: null, + operator: curatedOperatorsResp[0], + key: curatedModuleKeysResponse[0], + }, + ...curatedOperatorsResp.slice(1).map((operator, i) => ({ + stakingModule: null, + meta: null, + operator, + key: curatedModuleKeysResponse[i + 1], + })), + ...curatedModuleKeysResponse + .slice(curatedOperatorsResp.length) + .map((key) => ({ stakingModule: null, meta: null, operator: null, key })), + ]; + + expect(resp.body).toEqual(expectedResponse); + }); + }); + it('should return all keys for request without filters', async () => { const resp = await request(app.getHttpServer()).get(`/v1/modules/${dvtModule.moduleId}/operators/keys`); diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.service.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.service.ts index df665cc0..45bba03b 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.service.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.service.ts @@ -56,6 +56,7 @@ export class SRModulesOperatorsKeysService { const { stakingModules, elBlockSnapshot } = await this.stakingRouterService.getStakingModulesAndMeta(); const meta: MetaStreamRecord = { elBlockSnapshot }; + let metaHasSent = false; for (const stakingModule of stakingModules) { const moduleInstance = this.stakingRouterService.getStakingRouterModuleImpl(stakingModule.type); @@ -66,12 +67,14 @@ export class SRModulesOperatorsKeysService { let nextOperator = await operatorsGenerator.next(); yield { - stakingModule, - meta, - key: nextKey.value || null, - operator: nextOperator.value || null, + stakingModule: new StakingModuleResponse(stakingModule), + meta: !metaHasSent ? meta : null, + key: !nextKey.value ? null : new Key(nextKey.value), + operator: !nextOperator.value ? null : new Operator(nextOperator.value), }; + metaHasSent = true; + do { if (!nextKey.done) { nextKey = await keysGenerator.next(); @@ -81,11 +84,15 @@ export class SRModulesOperatorsKeysService { nextOperator = await operatorsGenerator.next(); } + if (!nextKey.value && !nextOperator.value) { + break; + } + yield { stakingModule: null, meta: null, - key: nextKey.value || null, - operator: nextOperator.value || null, + key: !nextKey.value ? null : new Key(nextKey.value), + operator: !nextOperator.value ? null : new Operator(nextOperator.value), }; } while (!nextKey.done || !nextOperator.done); } diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.types.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.types.ts index 2cc6ea5a..d3817768 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.types.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.types.ts @@ -1,10 +1,9 @@ -import { ELBlockSnapshot, Key, Operator } from 'http/common/entities'; -import { SrModuleEntity } from 'storage/sr-module.entity'; +import { ELBlockSnapshot, Key, Operator, StakingModuleResponse } from 'http/common/entities'; export type MetaStreamRecord = { elBlockSnapshot: ELBlockSnapshot } | null; export type ModulesOperatorsKeysRecord = { - stakingModule: SrModuleEntity | null; + stakingModule: StakingModuleResponse | null; key: Key | null; operator: Operator | null; meta: MetaStreamRecord; From 331637cbbb3d2dc2474472a96b6fe16aecd79a93 Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Wed, 22 Nov 2023 20:29:16 +0400 Subject: [PATCH 31/75] cr 1 --- src/common/registry/storage/operator.storage.ts | 4 ---- src/staking-router-modules/curated-module.service.ts | 11 +++++------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/common/registry/storage/operator.storage.ts b/src/common/registry/storage/operator.storage.ts index af3615ca..a642fd01 100644 --- a/src/common/registry/storage/operator.storage.ts +++ b/src/common/registry/storage/operator.storage.ts @@ -23,10 +23,6 @@ export class RegistryOperatorStorageService { .select(fields || '*') .from('registry_operator') .where(where) - .orderBy([ - { column: 'moduleAddress', order: 'asc' }, - { column: 'index', order: 'asc' }, - ]) .stream(); addTimeoutToStream(stream, 60_000, 'A timeout occurred loading operators from the database'); diff --git a/src/staking-router-modules/curated-module.service.ts b/src/staking-router-modules/curated-module.service.ts index df4d15af..028be742 100644 --- a/src/staking-router-modules/curated-module.service.ts +++ b/src/staking-router-modules/curated-module.service.ts @@ -85,7 +85,10 @@ export class CuratedModuleService implements StakingModuleInterface { } } - public async *getOperatorsStream(moduleAddress: string, filters?: OperatorsFilter): AsyncGenerator { + public async *getOperatorsStream( + moduleAddress: string, + filters?: OperatorsFilter, + ): AsyncGenerator { const where = {}; if (filters?.index != undefined) { where['index'] = filters.index; @@ -93,7 +96,7 @@ export class CuratedModuleService implements StakingModuleInterface { // we store operators of modules with the same impl at the same table where['module_address'] = moduleAddress; - const operatorStream = this.operatorStorageService.findStream(where, [ + yield* this.operatorStorageService.findStream(where, [ 'index', 'active', 'name', @@ -104,10 +107,6 @@ export class CuratedModuleService implements StakingModuleInterface { 'used_signing_keys as usedSigningKeys', 'module_address as moduleAddress', ]); - - for await (const record of operatorStream) { - yield record; - } } public async getKeysByPubKeys(moduleAddress: string, pubKeys: string[]): Promise { From 84bb7b6166d0bf0cf1ce8e42717ac0136195f5dd Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 23 Nov 2023 02:33:24 +0400 Subject: [PATCH 32/75] fix: use logger instead console.log --- src/common/registry/main/abstract-registry.ts | 2 -- src/jobs/jobs.service.ts | 2 +- src/mikro-orm.config.ts | 6 ++++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/common/registry/main/abstract-registry.ts b/src/common/registry/main/abstract-registry.ts index 60fc4107..4eeb5fbc 100644 --- a/src/common/registry/main/abstract-registry.ts +++ b/src/common/registry/main/abstract-registry.ts @@ -99,8 +99,6 @@ export abstract class AbstractRegistryService { currentOperators: RegistryOperator[], blockHash: string, ) { - // TODO: disable console time after testing - console.time('FETCH_OPERATORS'); /** * TODO: optimize a number of queries * it's possible to update keys faster by using different strategies depending on the reason for the update diff --git a/src/jobs/jobs.service.ts b/src/jobs/jobs.service.ts index c3f9f7e0..c2c60ad8 100644 --- a/src/jobs/jobs.service.ts +++ b/src/jobs/jobs.service.ts @@ -21,7 +21,7 @@ export class JobsService implements OnModuleInit, OnModuleDestroy { } public async onModuleDestroy() { - console.log('Jobs Service on module destroy'); + this.logger.log('Jobs Service on module destroy'); try { const intervalUpdateKeys = this.schedulerRegistry.getInterval(this.keysUpdateService.UPDATE_KEYS_JOB_NAME); clearInterval(intervalUpdateKeys); diff --git a/src/mikro-orm.config.ts b/src/mikro-orm.config.ts index a5d8b098..fcd70d5b 100644 --- a/src/mikro-orm.config.ts +++ b/src/mikro-orm.config.ts @@ -11,6 +11,7 @@ import { ConsensusValidatorEntity } from '@lido-nestjs/validators-registry'; import { readFileSync } from 'fs'; import { SrModuleEntity } from './storage/sr-module.entity'; import { ElMetaEntity } from './storage/el-meta.entity'; +import { Logger } from '@nestjs/common'; dotenv.config(); @@ -60,8 +61,9 @@ const findMigrations = (mainFolder: string, npmPackageNames: string[]): Migratio process.exit(1); } - // TODO think about Nest.js logger - console.log(`Found [${migrations.length}] DB migration files.`); + const logger = new Logger(); + logger.log(`Found [${migrations.length}] DB migration files.`); + return migrations; }; From 8422eb832e42fa7de06356073d599931d45f48af Mon Sep 17 00:00:00 2001 From: Alexander Movsunov Date: Thu, 23 Nov 2023 21:17:09 +0400 Subject: [PATCH 33/75] chore: docs + sorting --- rest-api.md | 45 +++++++++++++++++++ .../registry/storage/operator.storage.ts | 4 ++ 2 files changed, 49 insertions(+) diff --git a/rest-api.md b/rest-api.md index ef8eeeda..fcf9340f 100644 --- a/rest-api.md +++ b/rest-api.md @@ -728,6 +728,51 @@ class NotFoundException implements HttpException { } ``` +#### List of modules, operators, keys and meta + +Path: /v2/modules/operators/keys + +Returns modules, operators, keys and meta. + +Request example: + +```bash +curl 'http://localhost:3000/v2/modules/operators/keys +``` + +Response: + +```typescript +type KApiModulesOperatorsKeysResponse = { + key: Key | null; + operator: Operator | null; + meta: { elBlockSnapshot: ELBlockSnapshot } | null; + module: StakingModule | null; +}[]; + +interface HttpException { + statusCode: number; + message: string; +} + +class TooEarlyResponse implements HttpException { + statusCode: number = 425; + message: string = 'Too early response'; +} +``` + +For example: +```json +[ + { "meta": { "elBlockSnapshot": {} }, "module": { "id": 1, "moduleAddress": "0x001" }, "operator": { "id": 1, "moduleAddress": "0x001" }, "key": { "id": 1, "moduleAddress": "0x001" } }, + { "meta": null, "module": null, "operator": { "id": 2, "moduleAddress": "0x001" }, "key": { "id": 2, "moduleAddress": "0x001" } }, + { "meta": null, "module": null, "operator": null, "key": { "id": 3, "moduleAddress": "0x001" } }, + { "meta": null, "module": { "id": 2, "moduleAddress": "0x002" }, "operator": { "id": 1, "moduleAddress": "0x002" }, "key": { "id": 1, "moduleAddress": "0x002" } }, + { "meta": null, "module": null, "operator": { "id": 2, "moduleAddress": "0x002" }, "key": { "id": 2, "moduleAddress": "0x002" } }, + { "meta": null, "module": null, "operator": null, "key": { "id": 3, "moduleAddress": "0x002" } } +] +``` + ### KAPI status #### Return KAPI status diff --git a/src/common/registry/storage/operator.storage.ts b/src/common/registry/storage/operator.storage.ts index a642fd01..af3615ca 100644 --- a/src/common/registry/storage/operator.storage.ts +++ b/src/common/registry/storage/operator.storage.ts @@ -23,6 +23,10 @@ export class RegistryOperatorStorageService { .select(fields || '*') .from('registry_operator') .where(where) + .orderBy([ + { column: 'moduleAddress', order: 'asc' }, + { column: 'index', order: 'asc' }, + ]) .stream(); addTimeoutToStream(stream, 60_000, 'A timeout occurred loading operators from the database'); From 1fdc99a46aa5c1ac2a120e295d3a0f0cedd28af8 Mon Sep 17 00:00:00 2001 From: infloop Date: Thu, 30 Nov 2023 01:00:49 +0500 Subject: [PATCH 34/75] chore: bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 695de50a..42213276 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lido-keys-api", - "version": "0.8.2", + "version": "0.10.1", "description": "Lido Node Operators keys service", "author": "Lido team", "private": true, From ed05fa801f6326081bc8f15f16e3ba3c04375097 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 11 Dec 2023 23:49:28 +0400 Subject: [PATCH 35/75] fix: log --- src/common/registry/main/abstract-registry.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/registry/main/abstract-registry.ts b/src/common/registry/main/abstract-registry.ts index 4eeb5fbc..1c8e14b9 100644 --- a/src/common/registry/main/abstract-registry.ts +++ b/src/common/registry/main/abstract-registry.ts @@ -138,8 +138,6 @@ export abstract class AbstractRegistryService { this.logger.log('Keys saved', { operatorIndex }); } - - console.timeEnd('FETCH_OPERATORS'); } /** storage */ From d5247d523855a5233175dcabdfbff6db635308cb Mon Sep 17 00:00:00 2001 From: Eddort Date: Mon, 18 Dec 2023 15:01:52 +0100 Subject: [PATCH 36/75] feat: add finalizedUsedSigningKeys --- .../fetch/interfaces/operator.interface.ts | 1 + src/common/registry/fetch/operator.fetch.ts | 6 +++ src/common/registry/main/abstract-registry.ts | 39 +++++++++++++++++-- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/common/registry/fetch/interfaces/operator.interface.ts b/src/common/registry/fetch/interfaces/operator.interface.ts index e4d20826..a1392d4a 100644 --- a/src/common/registry/fetch/interfaces/operator.interface.ts +++ b/src/common/registry/fetch/interfaces/operator.interface.ts @@ -8,4 +8,5 @@ export interface RegistryOperator { totalSigningKeys: number; usedSigningKeys: number; moduleAddress: string; + finalizedUsedSigningKeys: number; } diff --git a/src/common/registry/fetch/operator.fetch.ts b/src/common/registry/fetch/operator.fetch.ts index 2e5110f6..673d82f4 100644 --- a/src/common/registry/fetch/operator.fetch.ts +++ b/src/common/registry/fetch/operator.fetch.ts @@ -73,6 +73,9 @@ export class RegistryOperatorFetchService { ): Promise { const fullInfo = true; const operator = await this.getContract(moduleAddress).getNodeOperator(operatorIndex, fullInfo, overrides as any); + const finalizedOperator = await this.getContract(moduleAddress).getNodeOperator(operatorIndex, fullInfo, { + blockTag: 'finalized', + }); const { name, @@ -84,6 +87,8 @@ export class RegistryOperatorFetchService { totalDepositedValidators, } = operator; + const { totalDepositedValidators: finalizedUsedSigningKeys } = finalizedOperator; + return { index: operatorIndex, active, @@ -94,6 +99,7 @@ export class RegistryOperatorFetchService { totalSigningKeys: totalAddedValidators.toNumber(), usedSigningKeys: totalDepositedValidators.toNumber(), moduleAddress, + finalizedUsedSigningKeys: finalizedUsedSigningKeys.toNumber(), }; } diff --git a/src/common/registry/main/abstract-registry.ts b/src/common/registry/main/abstract-registry.ts index 4eeb5fbc..d5abac36 100644 --- a/src/common/registry/main/abstract-registry.ts +++ b/src/common/registry/main/abstract-registry.ts @@ -14,7 +14,7 @@ import { RegistryOperator } from '../storage/operator.entity'; import { compareOperators } from '../utils/operator.utils'; -import { REGISTRY_GLOBAL_OPTIONS_TOKEN } from './constants'; +import { BLOCKS_OVERLAP, REGISTRY_GLOBAL_OPTIONS_TOKEN } from './constants'; import { RegistryOptions } from './interfaces/module.interface'; import { chunk } from '@lido-nestjs/utils'; import { RegistryKeyBatchFetchService } from '../fetch/key-batch.fetch'; @@ -71,10 +71,42 @@ export abstract class AbstractRegistryService { * @returns Check if operators have been changed */ public async operatorsWereChanged( + moduleAddress: string, + { + fromBlockNumber, + toBlockNumber, + }: { + fromBlockNumber?: number; + toBlockNumber: number; + }, + ): Promise { + if (fromBlockNumber === undefined) return true; + + if (fromBlockNumber > toBlockNumber) { + throw new Error(`invalid blocks range: ${fromBlockNumber} (fromBlockNumber) > ${toBlockNumber} (toBlockNumber)`); + } + // check how big the difference between the blocks is, if it exceeds, we should update the state anyway + if (toBlockNumber - fromBlockNumber > BLOCKS_OVERLAP) return true; + + return await this.operatorFetch.operatorsWereChanged(moduleAddress, fromBlockNumber, toBlockNumber); + } + + /** + * + * @param moduleAddress contract address + * @returns Check if operators have been changed + */ + public async keysWereChanged( moduleAddress: string, fromBlockNumber: number, toBlockNumber: number, ): Promise { + if (fromBlockNumber > toBlockNumber) { + throw new Error(`invalid blocks range: ${fromBlockNumber} (fromBlockNumber) > ${toBlockNumber} (toBlockNumber)`); + } + // check how big the difference between the blocks is, if it exceeds, we should update the state anyway + if (toBlockNumber - fromBlockNumber > BLOCKS_OVERLAP) return true; + return await this.operatorFetch.operatorsWereChanged(moduleAddress, fromBlockNumber, toBlockNumber); } @@ -110,7 +142,8 @@ export abstract class AbstractRegistryService { // skip updating keys from 0 to `usedSigningKeys` of previous collected data // since the contract guarantees that these keys cannot be changed - const unchangedKeysMaxIndex = isSameOperator ? prevOperator.usedSigningKeys : 0; + const unchangedKeysMaxIndex = + isSameOperator && prevOperator.finalizedUsedSigningKeys ? prevOperator.finalizedUsedSigningKeys : 0; // get the right border up to which the keys should be updated // it's different for different scenarios const toIndex = this.getToIndex(currOperator); @@ -121,7 +154,7 @@ export abstract class AbstractRegistryService { const operatorIndex = currOperator.index; const overrides = { blockTag: { blockHash } }; - // TODO: use feature flag + const result = await this.keyBatchFetch.fetch(moduleAddress, operatorIndex, fromIndex, toIndex, overrides); const operatorKeys = result.filter((key) => key); From 2471e3f185c3a90db7d6589cc0e004532e72e741 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 19 Dec 2023 13:54:34 +0100 Subject: [PATCH 37/75] feat: new indexing logic --- .../execution-provider.service.ts | 9 ++ src/common/registry/fetch/operator.fetch.ts | 6 +- src/common/registry/main/abstract-registry.ts | 21 +-- src/common/registry/main/constants.ts | 1 + .../registry/storage/operator.entity.ts | 4 + src/jobs/keys-update/keys-update.service.ts | 141 +++++++++++++++--- src/storage/el-meta.entity.ts | 4 + src/storage/el-meta.storage.ts | 8 +- src/storage/sr-module.entity.ts | 7 +- src/storage/sr-module.storage.ts | 4 +- 10 files changed, 158 insertions(+), 47 deletions(-) diff --git a/src/common/execution-provider/execution-provider.service.ts b/src/common/execution-provider/execution-provider.service.ts index 45b1e074..e84eafaa 100644 --- a/src/common/execution-provider/execution-provider.service.ts +++ b/src/common/execution-provider/execution-provider.service.ts @@ -46,4 +46,13 @@ export class ExecutionProviderService { const block = await this.provider.getBlock(blockHashOrBlockTag); return { number: block.number, hash: block.hash, timestamp: block.timestamp }; } + + /** + * + * Returns full block info + */ + public async getFullBlock(blockHashOrBlockTag: number | string) { + const block = await this.provider.getBlock(blockHashOrBlockTag); + return block; + } } diff --git a/src/common/registry/fetch/operator.fetch.ts b/src/common/registry/fetch/operator.fetch.ts index 673d82f4..223e7090 100644 --- a/src/common/registry/fetch/operator.fetch.ts +++ b/src/common/registry/fetch/operator.fetch.ts @@ -72,8 +72,10 @@ export class RegistryOperatorFetchService { overrides: CallOverrides = {}, ): Promise { const fullInfo = true; - const operator = await this.getContract(moduleAddress).getNodeOperator(operatorIndex, fullInfo, overrides as any); - const finalizedOperator = await this.getContract(moduleAddress).getNodeOperator(operatorIndex, fullInfo, { + const contract = this.getContract(moduleAddress); + + const operator = await contract.getNodeOperator(operatorIndex, fullInfo, overrides as any); + const finalizedOperator = await contract.getNodeOperator(operatorIndex, fullInfo, { blockTag: 'finalized', }); diff --git a/src/common/registry/main/abstract-registry.ts b/src/common/registry/main/abstract-registry.ts index d5abac36..e0c3fc2b 100644 --- a/src/common/registry/main/abstract-registry.ts +++ b/src/common/registry/main/abstract-registry.ts @@ -87,26 +87,7 @@ export abstract class AbstractRegistryService { } // check how big the difference between the blocks is, if it exceeds, we should update the state anyway if (toBlockNumber - fromBlockNumber > BLOCKS_OVERLAP) return true; - - return await this.operatorFetch.operatorsWereChanged(moduleAddress, fromBlockNumber, toBlockNumber); - } - - /** - * - * @param moduleAddress contract address - * @returns Check if operators have been changed - */ - public async keysWereChanged( - moduleAddress: string, - fromBlockNumber: number, - toBlockNumber: number, - ): Promise { - if (fromBlockNumber > toBlockNumber) { - throw new Error(`invalid blocks range: ${fromBlockNumber} (fromBlockNumber) > ${toBlockNumber} (toBlockNumber)`); - } - // check how big the difference between the blocks is, if it exceeds, we should update the state anyway - if (toBlockNumber - fromBlockNumber > BLOCKS_OVERLAP) return true; - + // rename return await this.operatorFetch.operatorsWereChanged(moduleAddress, fromBlockNumber, toBlockNumber); } diff --git a/src/common/registry/main/constants.ts b/src/common/registry/main/constants.ts index 17883f7b..8a6fcc14 100644 --- a/src/common/registry/main/constants.ts +++ b/src/common/registry/main/constants.ts @@ -1 +1,2 @@ export const REGISTRY_GLOBAL_OPTIONS_TOKEN = Symbol('registryGlobalOptions'); +export const BLOCKS_OVERLAP = 120; diff --git a/src/common/registry/storage/operator.entity.ts b/src/common/registry/storage/operator.entity.ts index 2538a588..1074a491 100644 --- a/src/common/registry/storage/operator.entity.ts +++ b/src/common/registry/storage/operator.entity.ts @@ -17,6 +17,7 @@ export class RegistryOperator { this.totalSigningKeys = operator.totalSigningKeys; this.usedSigningKeys = operator.usedSigningKeys; this.moduleAddress = operator.moduleAddress; + this.finalizedUsedSigningKeys = operator.finalizedUsedSigningKeys; } @PrimaryKey() @@ -46,4 +47,7 @@ export class RegistryOperator { @PrimaryKey() @Property({ length: ADDRESS_LEN }) moduleAddress!: string; + + @Property() + finalizedUsedSigningKeys!: number; } diff --git a/src/jobs/keys-update/keys-update.service.ts b/src/jobs/keys-update/keys-update.service.ts index 2f5075b4..33388dba 100644 --- a/src/jobs/keys-update/keys-update.service.ts +++ b/src/jobs/keys-update/keys-update.service.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import { range } from '@lido-nestjs/utils'; import { LOGGER_PROVIDER, LoggerService } from 'common/logger'; import { ConfigService } from 'common/config'; import { JobService } from 'common/job'; @@ -15,6 +16,8 @@ import { PrometheusService } from 'common/prometheus'; import { SrModuleEntity } from 'storage/sr-module.entity'; import { StakingModule } from 'staking-router-modules/interfaces/staking-module.interface'; +const MAX_BLOCKS_OVERLAP = 30; + class KeyOutdatedError extends Error { lastBlock: number; @@ -115,10 +118,17 @@ export class KeysUpdateService { // read from database last execution layer data const prevElMeta = await this.elMetaStorage.get(); + // handle the situation when the node has fallen behind the service state if (prevElMeta && prevElMeta?.blockNumber > currElMeta.number) { this.logger.warn('Previous data is newer than current data', prevElMeta); return; } + + if (prevElMeta?.blockHash && prevElMeta.blockHash === currElMeta.hash) { + this.logger.debug?.('same state, skip', { prevElMeta, currElMeta }); + return; + } + // Get modules from storage const storageModules = await this.srModulesStorage.findAll(); // Get staking modules from SR contract @@ -133,44 +143,101 @@ export class KeysUpdateService { await this.entityManager.transactional( async () => { - // Update EL meta in db - await this.elMetaStorage.update(currElMeta); + const prevBlockHash = prevElMeta?.blockHash; + const currentBlockHash = currElMeta.hash; + + let lastChangedBlockHash = prevBlockHash || currentBlockHash; + + let isReorgDetected = false; for (const contractModule of contractModules) { + const { stakingModuleAddress } = contractModule; + // Find implementation for staking module const moduleInstance = this.stakingRouterService.getStakingRouterModuleImpl(contractModule.type); // Read current nonce from contract - const currNonce = await moduleInstance.getCurrentNonce(contractModule.stakingModuleAddress, currElMeta.hash); + const currNonce = await moduleInstance.getCurrentNonce(stakingModuleAddress, currentBlockHash); // Read module in storage const moduleInStorage = await this.srModulesStorage.findOneById(contractModule.moduleId); const prevNonce = moduleInStorage?.nonce; - // update staking module information - await this.srModulesStorage.upsert(contractModule, currNonce); this.logger.log(`Nonce previous value: ${prevNonce}, nonce current value: ${currNonce}`); - if (prevNonce === currNonce) { - this.logger.log("Nonce wasn't changed, no need to update keys"); - // case when prevELMeta is undefined but prevNonce === currNonce looks like invalid - // use here prevElMeta.blockNumber + 1 because operators were updated in database for prevElMeta.blockNumber block - if ( - prevElMeta && - prevElMeta.blockNumber < currElMeta.number && - (await moduleInstance.operatorsWereChanged( - contractModule.stakingModuleAddress, - prevElMeta.blockNumber + 1, - currElMeta.number, - )) - ) { - this.logger.log('Update events happened, need to update operators'); - await moduleInstance.updateOperators(contractModule.stakingModuleAddress, currElMeta.hash); - } + if (!prevElMeta) { + this.logger.log('No past state found, start indexing', { stakingModuleAddress, currentBlockHash }); + + await moduleInstance.update(stakingModuleAddress, currentBlockHash); + await this.srModulesStorage.upsert(contractModule, currNonce, currentBlockHash); + lastChangedBlockHash = currentBlockHash; + continue; + } + + if (prevNonce !== currNonce) { + this.logger.log('Nonce has been changed, start indexing', { + stakingModuleAddress, + currentBlockHash, + prevNonce, + currNonce, + }); + + await moduleInstance.update(stakingModuleAddress, currentBlockHash); + await this.srModulesStorage.upsert(contractModule, currNonce, currentBlockHash); + lastChangedBlockHash = currentBlockHash; + continue; + } + if (this.isTooMuchDiffBetweenBlocks(prevElMeta.blockNumber, currElMeta.number)) { + this.logger.log('Too much difference between the blocks, start indexing', { + stakingModuleAddress, + currentBlockHash, + }); + + await moduleInstance.update(stakingModuleAddress, currentBlockHash); + await this.srModulesStorage.upsert(contractModule, currNonce, currentBlockHash); + lastChangedBlockHash = currentBlockHash; continue; } - await moduleInstance.update(contractModule.stakingModuleAddress, currElMeta.hash); + // calculate once per iteration + // no need to recheck each module separately + isReorgDetected = isReorgDetected ? true : await this.isReorgDetected(prevElMeta.blockHash, currentBlockHash); + + if (isReorgDetected) { + this.logger.log('Reorg detected, start indexing', { stakingModuleAddress, currentBlockHash }); + + await moduleInstance.update(stakingModuleAddress, currentBlockHash); + await this.srModulesStorage.upsert(contractModule, currNonce, currentBlockHash); + lastChangedBlockHash = currentBlockHash; + continue; + } + + if ( + prevElMeta.blockNumber < currElMeta.number && + (await moduleInstance.operatorsWereChanged( + contractModule.stakingModuleAddress, + prevElMeta.blockNumber + 1, + currElMeta.number, + )) + ) { + this.logger.log('Update operator events happened, need to update operators', { + stakingModuleAddress, + currentBlockHash, + }); + + await moduleInstance.updateOperators(stakingModuleAddress, currentBlockHash); + await this.srModulesStorage.upsert(contractModule, currNonce, currentBlockHash); + lastChangedBlockHash = currentBlockHash; + continue; + } + + this.logger.log('No changes have been detected in the module, indexing is not required', { + stakingModuleAddress, + currentBlockHash, + }); } + + // Update EL meta in db + await this.elMetaStorage.update({ ...currElMeta, lastChangedBlockHash }); }, { isolationLevel: IsolationLevel.READ_COMMITTED }, ); @@ -178,6 +245,36 @@ export class KeysUpdateService { return currElMeta; } + public async isReorgDetected(prevBlockHash: string, currentBlockHash: string) { + const currentBlock = await this.executionProvider.getFullBlock(currentBlockHash); + const prevBlock = await this.executionProvider.getFullBlock(prevBlockHash); + + if (currentBlock.parentHash === prevBlock.hash) return false; + // TODO: different hash but same number + if (currentBlock.number === prevBlock.number) return true; + + const blocks = await Promise.all( + range(prevBlock.number, currentBlock.number).map(async (bNumber) => { + return await this.executionProvider.getFullBlock(bNumber); + }), + ); + + for (let i = 1; i < blocks.length; i++) { + const previousBlock = blocks[i - 1]; + const currentBlock = blocks[i]; + + if (currentBlock.parentHash !== previousBlock.hash) { + return false; + } + } + + return true; + } + + public isTooMuchDiffBetweenBlocks(prevBlockNumber: number, currentBlockNumber: number) { + return currentBlockNumber - prevBlockNumber >= MAX_BLOCKS_OVERLAP; + } + /** * Update prometheus metrics of staking modules */ diff --git a/src/storage/el-meta.entity.ts b/src/storage/el-meta.entity.ts index 693e1c5c..49c82f21 100644 --- a/src/storage/el-meta.entity.ts +++ b/src/storage/el-meta.entity.ts @@ -11,6 +11,7 @@ export class ElMetaEntity { this.blockNumber = meta.blockNumber; this.blockHash = meta.blockHash.toLocaleLowerCase(); this.timestamp = meta.timestamp; + this.lastChangedBlockHash = meta.lastChangedBlockHash; } @PrimaryKey() @@ -22,4 +23,7 @@ export class ElMetaEntity { @Property() timestamp: number; + + @Property({ length: BLOCK_HASH_LEN }) + lastChangedBlockHash: string; } diff --git a/src/storage/el-meta.storage.ts b/src/storage/el-meta.storage.ts index 4407ae41..1a022bdf 100644 --- a/src/storage/el-meta.storage.ts +++ b/src/storage/el-meta.storage.ts @@ -12,13 +12,19 @@ export class ElMetaStorageService { return result[0] ?? null; } - async update(currElMeta: { number: number; hash: string; timestamp: number }): Promise { + async update(currElMeta: { + number: number; + hash: string; + timestamp: number; + lastChangedBlockHash: string; + }): Promise { await this.repository.nativeDelete({}); await this.repository.persist( new ElMetaEntity({ blockHash: currElMeta.hash, blockNumber: currElMeta.number, timestamp: currElMeta.timestamp, + lastChangedBlockHash: currElMeta.lastChangedBlockHash, }), ); await this.repository.flush(); diff --git a/src/storage/sr-module.entity.ts b/src/storage/sr-module.entity.ts index 2e23bc24..888bd4b8 100644 --- a/src/storage/sr-module.entity.ts +++ b/src/storage/sr-module.entity.ts @@ -7,7 +7,7 @@ import { SRModuleRepository } from './sr-module.repository'; export class SrModuleEntity implements StakingModule { [EntityRepositoryType]?: SRModuleRepository; - constructor(srModule: StakingModule, nonce: number) { + constructor(srModule: StakingModule, nonce: number, lastChangedBlockHash: string) { this.moduleId = srModule.moduleId; this.stakingModuleAddress = srModule.stakingModuleAddress; this.moduleFee = srModule.moduleFee; @@ -21,6 +21,7 @@ export class SrModuleEntity implements StakingModule { this.type = srModule.type; this.active = srModule.active; this.nonce = nonce; + this.lastChangedBlockHash = lastChangedBlockHash; } @PrimaryKey() @@ -81,4 +82,8 @@ export class SrModuleEntity implements StakingModule { // nonce value @Property() nonce: number; + + // last changed block hash + @Property() + lastChangedBlockHash: string; } diff --git a/src/storage/sr-module.storage.ts b/src/storage/sr-module.storage.ts index 0bbecd61..4a4f9661 100644 --- a/src/storage/sr-module.storage.ts +++ b/src/storage/sr-module.storage.ts @@ -21,7 +21,7 @@ export class SRModuleStorageService { return await this.repository.findAll(); } - async upsert(srModule: StakingModule, nonce: number): Promise { + async upsert(srModule: StakingModule, nonce: number, lastChangedBlockHash: string): Promise { // Try to find an existing entity by moduleId or stakingModuleAddress let existingModule = await this.repository.findOne({ moduleId: srModule.moduleId, @@ -32,6 +32,7 @@ export class SRModuleStorageService { existingModule = new SrModuleEntity( { ...srModule, stakingModuleAddress: srModule.stakingModuleAddress.toLowerCase() }, nonce, + lastChangedBlockHash, ); } else { // If the entity exists, update its properties @@ -45,6 +46,7 @@ export class SRModuleStorageService { existingModule.exitedValidatorsCount = srModule.exitedValidatorsCount; existingModule.active = srModule.active; existingModule.nonce = nonce; + existingModule.lastChangedBlockHash = lastChangedBlockHash; } // Save the entity (either a new one or an updated one) From 92ecf447303ef2d6af0ccd4f1dd432107b48fb41 Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 19 Dec 2023 13:58:51 +0100 Subject: [PATCH 38/75] fix: revert operatorsWereChanged logic --- src/common/registry/main/abstract-registry.ts | 19 +++---------------- src/common/registry/main/constants.ts | 1 - 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/src/common/registry/main/abstract-registry.ts b/src/common/registry/main/abstract-registry.ts index e0c3fc2b..99ebe7d5 100644 --- a/src/common/registry/main/abstract-registry.ts +++ b/src/common/registry/main/abstract-registry.ts @@ -14,7 +14,7 @@ import { RegistryOperator } from '../storage/operator.entity'; import { compareOperators } from '../utils/operator.utils'; -import { BLOCKS_OVERLAP, REGISTRY_GLOBAL_OPTIONS_TOKEN } from './constants'; +import { REGISTRY_GLOBAL_OPTIONS_TOKEN } from './constants'; import { RegistryOptions } from './interfaces/module.interface'; import { chunk } from '@lido-nestjs/utils'; import { RegistryKeyBatchFetchService } from '../fetch/key-batch.fetch'; @@ -72,22 +72,9 @@ export abstract class AbstractRegistryService { */ public async operatorsWereChanged( moduleAddress: string, - { - fromBlockNumber, - toBlockNumber, - }: { - fromBlockNumber?: number; - toBlockNumber: number; - }, + fromBlockNumber: number, + toBlockNumber: number, ): Promise { - if (fromBlockNumber === undefined) return true; - - if (fromBlockNumber > toBlockNumber) { - throw new Error(`invalid blocks range: ${fromBlockNumber} (fromBlockNumber) > ${toBlockNumber} (toBlockNumber)`); - } - // check how big the difference between the blocks is, if it exceeds, we should update the state anyway - if (toBlockNumber - fromBlockNumber > BLOCKS_OVERLAP) return true; - // rename return await this.operatorFetch.operatorsWereChanged(moduleAddress, fromBlockNumber, toBlockNumber); } diff --git a/src/common/registry/main/constants.ts b/src/common/registry/main/constants.ts index 8a6fcc14..17883f7b 100644 --- a/src/common/registry/main/constants.ts +++ b/src/common/registry/main/constants.ts @@ -1,2 +1 @@ export const REGISTRY_GLOBAL_OPTIONS_TOKEN = Symbol('registryGlobalOptions'); -export const BLOCKS_OVERLAP = 120; From 183cbbd7b2cbc412b2ac3ada106ef696951d258a Mon Sep 17 00:00:00 2001 From: Eddort Date: Tue, 19 Dec 2023 14:24:00 +0100 Subject: [PATCH 39/75] fix: tests --- .../registry/test/fetch/key.fetch.spec.ts | 29 ++++++++++++++++++- .../test/fetch/operator.fetch.spec.ts | 4 +-- .../registry/test/fixtures/db.fixture.ts | 4 +++ .../test/fixtures/operator.fixture.ts | 1 + .../test/utils/operator.utils.spec.ts | 2 ++ 5 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/common/registry/test/fetch/key.fetch.spec.ts b/src/common/registry/test/fetch/key.fetch.spec.ts index b23cbb58..6490b325 100644 --- a/src/common/registry/test/fetch/key.fetch.spec.ts +++ b/src/common/registry/test/fetch/key.fetch.spec.ts @@ -76,7 +76,34 @@ describe('Keys', () => { const result = await fetchService.fetch(address, expected.operatorIndex); expect(result).toEqual([expected]); - expect(mockCall).toBeCalledTimes(2); + expect(mockCall).toBeCalledTimes(3); + }); + + test('fetch all operator keys with reorg', async () => { + const expected = { operatorIndex: 1, index: 0, moduleAddress: address, ...key }; + + mockCall + .mockImplementationOnce(async () => { + const iface = new Interface(Registry__factory.abi); + return iface.encodeFunctionResult( + 'getNodeOperator', + operatorFields({ + ...operator, + moduleAddress: address, + totalSigningKeys: 1, + usedSigningKeys: 2, + finalizedUsedSigningKeys: 1, + }), + ); + }) + .mockImplementation(async () => { + const iface = new Interface(Registry__factory.abi); + return iface.encodeFunctionResult('getSigningKey', keyFields); + }); + const result = await fetchService.fetch(address, expected.operatorIndex); + console.log(result); + expect(result).toEqual([expected]); + expect(mockCall).toBeCalledTimes(3); }); test('fetch. fromIndex > toIndex', async () => { diff --git a/src/common/registry/test/fetch/operator.fetch.spec.ts b/src/common/registry/test/fetch/operator.fetch.spec.ts index 5ff8a939..174086c5 100644 --- a/src/common/registry/test/fetch/operator.fetch.spec.ts +++ b/src/common/registry/test/fetch/operator.fetch.spec.ts @@ -48,7 +48,7 @@ describe('Operators', () => { const result = await fetchService.fetchOne(address, expected.index); expect(result).toEqual(expected); - expect(mockCall).toBeCalledTimes(1); + expect(mockCall).toBeCalledTimes(2); }); test('fetch', async () => { @@ -62,7 +62,7 @@ describe('Operators', () => { const result = await fetchService.fetch(address, expectedFirst.index, expectedSecond.index + 1); expect(result).toEqual([expectedFirst, expectedSecond]); - expect(mockCall).toBeCalledTimes(2); + expect(mockCall).toBeCalledTimes(4); }); test('fetch all', async () => { diff --git a/src/common/registry/test/fixtures/db.fixture.ts b/src/common/registry/test/fixtures/db.fixture.ts index 7763870e..4d5f8d99 100644 --- a/src/common/registry/test/fixtures/db.fixture.ts +++ b/src/common/registry/test/fixtures/db.fixture.ts @@ -15,6 +15,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 3, usedSigningKeys: 3, + finalizedUsedSigningKeys: 3, }, { index: 1, @@ -25,6 +26,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 3, usedSigningKeys: 3, + finalizedUsedSigningKeys: 3, }, ]; @@ -99,6 +101,7 @@ export const newOperator = { stoppedValidators: 0, totalSigningKeys: 3, usedSigningKeys: 3, + finalizedUsedSigningKeys: 3, }; export const operatorWithDefaultsRecords = { @@ -110,4 +113,5 @@ export const operatorWithDefaultsRecords = { stoppedValidators: 0, totalSigningKeys: 0, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }; diff --git a/src/common/registry/test/fixtures/operator.fixture.ts b/src/common/registry/test/fixtures/operator.fixture.ts index aa3189a5..c9d88b0d 100644 --- a/src/common/registry/test/fixtures/operator.fixture.ts +++ b/src/common/registry/test/fixtures/operator.fixture.ts @@ -9,6 +9,7 @@ export const operator = { stakingLimit: 1, usedSigningKeys: 2, totalSigningKeys: 3, + finalizedUsedSigningKeys: 2, }; export const operatorFields = (operator: Partial) => [ diff --git a/src/common/registry/test/utils/operator.utils.spec.ts b/src/common/registry/test/utils/operator.utils.spec.ts index 4e26c644..66f1a6b3 100644 --- a/src/common/registry/test/utils/operator.utils.spec.ts +++ b/src/common/registry/test/utils/operator.utils.spec.ts @@ -10,6 +10,7 @@ describe('Compare operators util', () => { stoppedValidators: 0, totalSigningKeys: 1, usedSigningKeys: 1, + finalizedUsedSigningKeys: 1, moduleAddress: '0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320', }; @@ -22,6 +23,7 @@ describe('Compare operators util', () => { stoppedValidators: 0, totalSigningKeys: 2, usedSigningKeys: 2, + finalizedUsedSigningKeys: 2, moduleAddress: '0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320', }; From cca8041d1f7fc5308103e5cf82460ef5e7ffb768 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 10:47:20 +0100 Subject: [PATCH 40/75] feat: add data to endpoints --- src/http/common/entities/el-block-snapshot.ts | 7 +++++++ src/http/common/entities/sr-module.ts | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/http/common/entities/el-block-snapshot.ts b/src/http/common/entities/el-block-snapshot.ts index 6abd34e3..1464b785 100644 --- a/src/http/common/entities/el-block-snapshot.ts +++ b/src/http/common/entities/el-block-snapshot.ts @@ -6,6 +6,7 @@ export class ELBlockSnapshot implements ElMetaEntity { this.blockNumber = meta.blockNumber; this.blockHash = meta.blockHash; this.timestamp = meta.timestamp; + this.lastChangedBlockHash = meta.lastChangedBlockHash; } @ApiProperty({ @@ -25,4 +26,10 @@ export class ELBlockSnapshot implements ElMetaEntity { description: 'Block timestamp', }) timestamp: number; + + @ApiProperty({ + required: true, + description: 'Last changed block hash — used to determine that a change has been made to this block', + }) + lastChangedBlockHash: string; } diff --git a/src/http/common/entities/sr-module.ts b/src/http/common/entities/sr-module.ts index fb313899..f399a250 100644 --- a/src/http/common/entities/sr-module.ts +++ b/src/http/common/entities/sr-module.ts @@ -17,6 +17,7 @@ export class StakingModuleResponse implements Omit Date: Wed, 20 Dec 2023 11:01:51 +0100 Subject: [PATCH 41/75] fix: fixtures and entity types --- src/http/common/entities/operator.ts | 2 +- src/http/db.fixtures.ts | 4 ++++ src/http/module.fixture.ts | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/http/common/entities/operator.ts b/src/http/common/entities/operator.ts index 38acdef0..d8115c40 100644 --- a/src/http/common/entities/operator.ts +++ b/src/http/common/entities/operator.ts @@ -3,7 +3,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { RegistryOperator } from '../../../common/registry'; import { addressToChecksum } from '../utils'; -export class Operator implements RegistryOperator { +export class Operator implements Omit { constructor(operator: RegistryOperator) { this.name = operator.name; this.rewardAddress = operator.rewardAddress; diff --git a/src/http/db.fixtures.ts b/src/http/db.fixtures.ts index 18240eff..0a6c0d41 100644 --- a/src/http/db.fixtures.ts +++ b/src/http/db.fixtures.ts @@ -228,6 +228,7 @@ export const operatorOneCurated: RegistryOperator = { usedSigningKeys: 2, totalSigningKeys: 3, moduleAddress: curatedModule.stakingModuleAddress, + finalizedUsedSigningKeys: 2, }; export const operatorTwoCurated: RegistryOperator = { @@ -240,6 +241,7 @@ export const operatorTwoCurated: RegistryOperator = { usedSigningKeys: 2, totalSigningKeys: 3, moduleAddress: curatedModule.stakingModuleAddress, + finalizedUsedSigningKeys: 2, }; export const operatorOneDvt: RegistryOperator = { @@ -252,6 +254,7 @@ export const operatorOneDvt: RegistryOperator = { usedSigningKeys: 2, totalSigningKeys: 3, moduleAddress: dvtModule.stakingModuleAddress, + finalizedUsedSigningKeys: 2, }; export const operatorTwoDvt: RegistryOperator = { @@ -264,6 +267,7 @@ export const operatorTwoDvt: RegistryOperator = { usedSigningKeys: 2, totalSigningKeys: 3, moduleAddress: dvtModule.stakingModuleAddress, + finalizedUsedSigningKeys: 2, }; export const operators = [operatorOneCurated, operatorTwoCurated, operatorOneDvt, operatorTwoDvt]; diff --git a/src/http/module.fixture.ts b/src/http/module.fixture.ts index be18ec96..1f5e3af1 100644 --- a/src/http/module.fixture.ts +++ b/src/http/module.fixture.ts @@ -17,6 +17,7 @@ export const curatedModuleResp: StakingModuleResponse = { nonce: 1, exitedValidatorsCount: 0, active: true, + lastChangedBlockHash: '', }; export const dvtModuleAddressWithChecksum = '0x0165878A594ca255338adfa4d48449f69242Eb8F'; @@ -35,4 +36,5 @@ export const dvtModuleResp: StakingModuleResponse = { nonce: 1, exitedValidatorsCount: 0, active: true, + lastChangedBlockHash: '', }; From 3fa909b00ab0b0055f7f8bfa528f5db24d4bf4ce Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 11:18:20 +0100 Subject: [PATCH 42/75] fix: operators fetch spec --- src/common/registry/test/fetch/key.fetch.spec.ts | 1 - src/common/registry/test/fetch/operator.fetch.spec.ts | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/common/registry/test/fetch/key.fetch.spec.ts b/src/common/registry/test/fetch/key.fetch.spec.ts index 6490b325..7fee5049 100644 --- a/src/common/registry/test/fetch/key.fetch.spec.ts +++ b/src/common/registry/test/fetch/key.fetch.spec.ts @@ -101,7 +101,6 @@ describe('Keys', () => { return iface.encodeFunctionResult('getSigningKey', keyFields); }); const result = await fetchService.fetch(address, expected.operatorIndex); - console.log(result); expect(result).toEqual([expected]); expect(mockCall).toBeCalledTimes(3); }); diff --git a/src/common/registry/test/fetch/operator.fetch.spec.ts b/src/common/registry/test/fetch/operator.fetch.spec.ts index 174086c5..33948828 100644 --- a/src/common/registry/test/fetch/operator.fetch.spec.ts +++ b/src/common/registry/test/fetch/operator.fetch.spec.ts @@ -73,16 +73,15 @@ describe('Operators', () => { const iface = new Interface(Registry__factory.abi); return iface.encodeFunctionResult('getNodeOperatorsCount', [1]); }) - .mockImplementationOnce(async () => { + .mockImplementation(async () => { const iface = new Interface(Registry__factory.abi); operator['moduleAddress'] = address; - // operatorFields(operator); return iface.encodeFunctionResult('getNodeOperator', operatorFields(operator)); }); const result = await fetchService.fetch(address); expect(result).toEqual([expected]); - expect(mockCall).toBeCalledTimes(2); + expect(mockCall).toBeCalledTimes(3); }); test('fetch. fromIndex > toIndex', async () => { From 138e9c301cbd1686af5ada860c442ac51f2fd67a Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 12:05:02 +0100 Subject: [PATCH 43/75] fix: reorg detection --- src/jobs/keys-update/keys-update.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/jobs/keys-update/keys-update.service.ts b/src/jobs/keys-update/keys-update.service.ts index 33388dba..552c64a4 100644 --- a/src/jobs/keys-update/keys-update.service.ts +++ b/src/jobs/keys-update/keys-update.service.ts @@ -251,7 +251,7 @@ export class KeysUpdateService { if (currentBlock.parentHash === prevBlock.hash) return false; // TODO: different hash but same number - if (currentBlock.number === prevBlock.number) return true; + // if (currentBlock.number === prevBlock.number) return true; const blocks = await Promise.all( range(prevBlock.number, currentBlock.number).map(async (bNumber) => { @@ -264,11 +264,11 @@ export class KeysUpdateService { const currentBlock = blocks[i]; if (currentBlock.parentHash !== previousBlock.hash) { - return false; + return true; } } - return true; + return false; } public isTooMuchDiffBetweenBlocks(prevBlockNumber: number, currentBlockNumber: number) { From 39a92e90240f869ed052052b1a1a9750c8acbdc5 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 12:05:12 +0100 Subject: [PATCH 44/75] feat: add migration --- src/migrations/Migration20231220104403.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/migrations/Migration20231220104403.ts diff --git a/src/migrations/Migration20231220104403.ts b/src/migrations/Migration20231220104403.ts new file mode 100644 index 00000000..00376807 --- /dev/null +++ b/src/migrations/Migration20231220104403.ts @@ -0,0 +1,19 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20231220104403 extends Migration { + async up(): Promise { + this.addSql('alter table "el_meta_entity" add column "last_changed_block_hash" varchar(66) not null;'); + + this.addSql('alter table "registry_operator" add column "finalized_used_signing_keys" int not null;'); + + this.addSql('alter table "sr_module_entity" add column "last_changed_block_hash" varchar(255) not null;'); + } + + async down(): Promise { + this.addSql('alter table "el_meta_entity" drop column "last_changed_block_hash";'); + + this.addSql('alter table "registry_operator" drop column "finalized_used_signing_keys";'); + + this.addSql('alter table "sr_module_entity" drop column "last_changed_block_hash";'); + } +} From 7b018f3ab2206806bb802bd84e90005d64616e32 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 14:50:22 +0100 Subject: [PATCH 45/75] feat: moduleAddresses filter --- src/http/common/entities/key-query.ts | 6 ++++++ src/http/keys/keys.service.ts | 4 +++- src/staking-router-modules/staking-router.service.ts | 10 ++++++---- src/storage/sr-module.storage.ts | 5 +++++ 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/http/common/entities/key-query.ts b/src/http/common/entities/key-query.ts index f9c748ce..5bbb9f9d 100644 --- a/src/http/common/entities/key-query.ts +++ b/src/http/common/entities/key-query.ts @@ -37,4 +37,10 @@ export class KeyQuery { @Type(() => Number) @IsOptional() operatorIndex?: number; + + @ApiProperty({ isArray: true, type: String, required: false, description: 'Module address list' }) + @Transform(({ value }) => (Array.isArray(value) ? value : Array(value))) + @Transform(({ value }) => value.map((v) => v.toLowerCase())) + @IsOptional() + moduleAddresses!: string[]; } diff --git a/src/http/keys/keys.service.ts b/src/http/keys/keys.service.ts index 704a0ca9..2e27b3c3 100644 --- a/src/http/keys/keys.service.ts +++ b/src/http/keys/keys.service.ts @@ -18,7 +18,9 @@ export class KeysService { async get( filters: KeyQuery, ): Promise<{ keysGenerators: AsyncGenerator[]; meta: { elBlockSnapshot: ELBlockSnapshot } }> { - const { stakingModules, elBlockSnapshot } = await this.stakingRouterService.getStakingModulesAndMeta(); + const { stakingModules, elBlockSnapshot } = await this.stakingRouterService.getStakingModulesAndMeta( + filters.moduleAddresses, + ); const keysGenerators: AsyncGenerator[] = []; for (const module of stakingModules) { diff --git a/src/staking-router-modules/staking-router.service.ts b/src/staking-router-modules/staking-router.service.ts index af28e02f..89659d43 100644 --- a/src/staking-router-modules/staking-router.service.ts +++ b/src/staking-router-modules/staking-router.service.ts @@ -26,8 +26,10 @@ export class StakingRouterService { * Method for reading staking modules from database * @returns Staking module list from database */ - public async getStakingModules(): Promise { - const srModules = await this.srModulesStorage.findAll(); + public async getStakingModules(stakingModuleAddresses?: string[]): Promise { + const srModules = stakingModuleAddresses + ? await this.srModulesStorage.findByAddresses(stakingModuleAddresses) + : await this.srModulesStorage.findAll(); return srModules; } @@ -61,13 +63,13 @@ export class StakingRouterService { * Helper method for getting staking module list and execution layer meta * @returns Staking modules list and execution layer meta */ - public async getStakingModulesAndMeta(): Promise<{ + public async getStakingModulesAndMeta(stakingModuleAddresses?: string[]): Promise<{ stakingModules: SrModuleEntity[]; elBlockSnapshot: ELBlockSnapshot; }> { const { stakingModules, elBlockSnapshot } = await this.entityManager.transactional( async () => { - const stakingModules = await this.getStakingModules(); + const stakingModules = await this.getStakingModules(stakingModuleAddresses); if (stakingModules.length === 0) { this.logger.warn("No staking modules in list. Maybe didn't fetched from SR yet"); diff --git a/src/storage/sr-module.storage.ts b/src/storage/sr-module.storage.ts index 4a4f9661..85bc1a87 100644 --- a/src/storage/sr-module.storage.ts +++ b/src/storage/sr-module.storage.ts @@ -21,6 +21,11 @@ export class SRModuleStorageService { return await this.repository.findAll(); } + /** find all keys */ + async findByAddresses(stakingModuleAddresses: string[]): Promise { + return await this.repository.find({ stakingModuleAddress: { $in: stakingModuleAddresses } }); + } + async upsert(srModule: StakingModule, nonce: number, lastChangedBlockHash: string): Promise { // Try to find an existing entity by moduleId or stakingModuleAddress let existingModule = await this.repository.findOne({ From edf5c222bd2a238a4ab7cd5dd2dc87f7c2ae8764 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 19:06:42 +0100 Subject: [PATCH 46/75] refactor: split update function into two --- src/jobs/keys-update/keys-update.service.ts | 264 ++++++++++++-------- 1 file changed, 165 insertions(+), 99 deletions(-) diff --git a/src/jobs/keys-update/keys-update.service.ts b/src/jobs/keys-update/keys-update.service.ts index 552c64a4..a2d9aa91 100644 --- a/src/jobs/keys-update/keys-update.service.ts +++ b/src/jobs/keys-update/keys-update.service.ts @@ -14,7 +14,8 @@ import { SRModuleStorageService } from 'storage/sr-module.storage'; import { IsolationLevel } from '@mikro-orm/core'; import { PrometheusService } from 'common/prometheus'; import { SrModuleEntity } from 'storage/sr-module.entity'; -import { StakingModule } from 'staking-router-modules/interfaces/staking-module.interface'; +import { StakingModule, StakingModuleInterface } from 'staking-router-modules/interfaces/staking-module.interface'; +import { ElMetaEntity } from 'storage/el-meta.entity'; const MAX_BLOCKS_OVERLAP = 30; @@ -27,6 +28,21 @@ class KeyOutdatedError extends Error { } } +interface UpdaterPayload { + currElMeta: { + number: number; + hash: string; + timestamp: number; + }; + prevElMeta: ElMetaEntity | null; + contractModules: StakingModule[]; +} + +interface UpdaterState { + lastChangedBlockHash: string; + isReorgDetected: boolean; +} + @Injectable() export class KeysUpdateService { constructor( @@ -143,101 +159,7 @@ export class KeysUpdateService { await this.entityManager.transactional( async () => { - const prevBlockHash = prevElMeta?.blockHash; - const currentBlockHash = currElMeta.hash; - - let lastChangedBlockHash = prevBlockHash || currentBlockHash; - - let isReorgDetected = false; - - for (const contractModule of contractModules) { - const { stakingModuleAddress } = contractModule; - - // Find implementation for staking module - const moduleInstance = this.stakingRouterService.getStakingRouterModuleImpl(contractModule.type); - // Read current nonce from contract - const currNonce = await moduleInstance.getCurrentNonce(stakingModuleAddress, currentBlockHash); - // Read module in storage - const moduleInStorage = await this.srModulesStorage.findOneById(contractModule.moduleId); - const prevNonce = moduleInStorage?.nonce; - - this.logger.log(`Nonce previous value: ${prevNonce}, nonce current value: ${currNonce}`); - - if (!prevElMeta) { - this.logger.log('No past state found, start indexing', { stakingModuleAddress, currentBlockHash }); - - await moduleInstance.update(stakingModuleAddress, currentBlockHash); - await this.srModulesStorage.upsert(contractModule, currNonce, currentBlockHash); - lastChangedBlockHash = currentBlockHash; - continue; - } - - if (prevNonce !== currNonce) { - this.logger.log('Nonce has been changed, start indexing', { - stakingModuleAddress, - currentBlockHash, - prevNonce, - currNonce, - }); - - await moduleInstance.update(stakingModuleAddress, currentBlockHash); - await this.srModulesStorage.upsert(contractModule, currNonce, currentBlockHash); - lastChangedBlockHash = currentBlockHash; - continue; - } - - if (this.isTooMuchDiffBetweenBlocks(prevElMeta.blockNumber, currElMeta.number)) { - this.logger.log('Too much difference between the blocks, start indexing', { - stakingModuleAddress, - currentBlockHash, - }); - - await moduleInstance.update(stakingModuleAddress, currentBlockHash); - await this.srModulesStorage.upsert(contractModule, currNonce, currentBlockHash); - lastChangedBlockHash = currentBlockHash; - continue; - } - - // calculate once per iteration - // no need to recheck each module separately - isReorgDetected = isReorgDetected ? true : await this.isReorgDetected(prevElMeta.blockHash, currentBlockHash); - - if (isReorgDetected) { - this.logger.log('Reorg detected, start indexing', { stakingModuleAddress, currentBlockHash }); - - await moduleInstance.update(stakingModuleAddress, currentBlockHash); - await this.srModulesStorage.upsert(contractModule, currNonce, currentBlockHash); - lastChangedBlockHash = currentBlockHash; - continue; - } - - if ( - prevElMeta.blockNumber < currElMeta.number && - (await moduleInstance.operatorsWereChanged( - contractModule.stakingModuleAddress, - prevElMeta.blockNumber + 1, - currElMeta.number, - )) - ) { - this.logger.log('Update operator events happened, need to update operators', { - stakingModuleAddress, - currentBlockHash, - }); - - await moduleInstance.updateOperators(stakingModuleAddress, currentBlockHash); - await this.srModulesStorage.upsert(contractModule, currNonce, currentBlockHash); - lastChangedBlockHash = currentBlockHash; - continue; - } - - this.logger.log('No changes have been detected in the module, indexing is not required', { - stakingModuleAddress, - currentBlockHash, - }); - } - - // Update EL meta in db - await this.elMetaStorage.update({ ...currElMeta, lastChangedBlockHash }); + await this.updateStakingModules({ currElMeta, prevElMeta, contractModules }); }, { isolationLevel: IsolationLevel.READ_COMMITTED }, ); @@ -245,13 +167,156 @@ export class KeysUpdateService { return currElMeta; } - public async isReorgDetected(prevBlockHash: string, currentBlockHash: string) { + public async updateStakingModules(updaterPayload: UpdaterPayload): Promise { + const { prevElMeta, currElMeta, contractModules } = updaterPayload; + const prevBlockHash = prevElMeta?.blockHash; + const currentBlockHash = currElMeta.hash; + + const updaterState: UpdaterState = { + lastChangedBlockHash: prevBlockHash || currentBlockHash, + isReorgDetected: false, + }; + + for (const contractModule of contractModules) { + const { stakingModuleAddress } = contractModule; + + // Find implementation for staking module + const moduleInstance = this.stakingRouterService.getStakingRouterModuleImpl(contractModule.type); + // Read current nonce from contract + const currNonce = await moduleInstance.getCurrentNonce(stakingModuleAddress, currentBlockHash); + // Read module in storage + const moduleInStorage = await this.srModulesStorage.findOneById(contractModule.moduleId); + const prevNonce = moduleInStorage?.nonce; + + this.logger.log(`Nonce previous value: ${prevNonce}, nonce current value: ${currNonce}`); + + if (!prevBlockHash) { + this.logger.log('No past state found, start indexing', { stakingModuleAddress, currentBlockHash }); + + await this.updateStakingModule( + updaterState, + moduleInstance, + contractModule, + stakingModuleAddress, + currNonce, + currentBlockHash, + ); + continue; + } + + if (prevNonce !== currNonce) { + this.logger.log('Nonce has been changed, start indexing', { + stakingModuleAddress, + currentBlockHash, + prevNonce, + currNonce, + }); + + await this.updateStakingModule( + updaterState, + moduleInstance, + contractModule, + stakingModuleAddress, + currNonce, + currentBlockHash, + ); + continue; + } + + if (this.isTooMuchDiffBetweenBlocks(prevElMeta.blockNumber, currElMeta.number)) { + this.logger.log('Too much difference between the blocks, start indexing', { + stakingModuleAddress, + currentBlockHash, + }); + + await this.updateStakingModule( + updaterState, + moduleInstance, + contractModule, + stakingModuleAddress, + currNonce, + currentBlockHash, + ); + continue; + } + + if (await this.isReorgDetected(updaterState, prevBlockHash, currentBlockHash)) { + this.logger.log('Reorg detected, start indexing', { stakingModuleAddress, currentBlockHash }); + + await this.updateStakingModule( + updaterState, + moduleInstance, + contractModule, + stakingModuleAddress, + currNonce, + currentBlockHash, + ); + continue; + } + + if ( + prevElMeta.blockNumber < currElMeta.number && + (await moduleInstance.operatorsWereChanged( + contractModule.stakingModuleAddress, + prevElMeta.blockNumber + 1, + currElMeta.number, + )) + ) { + this.logger.log('Update operator events happened, need to update operators', { + stakingModuleAddress, + currentBlockHash, + }); + + await this.updateStakingModule( + updaterState, + moduleInstance, + contractModule, + stakingModuleAddress, + currNonce, + currentBlockHash, + ); + continue; + } + + this.logger.log('No changes have been detected in the module, indexing is not required', { + stakingModuleAddress, + currentBlockHash, + }); + } + + // Update EL meta in db + await this.elMetaStorage.update({ ...currElMeta, lastChangedBlockHash: updaterState.lastChangedBlockHash }); + } + + public async updateStakingModule( + updaterState: UpdaterState, + moduleInstance: StakingModuleInterface, + contractModule: StakingModule, + stakingModuleAddress: string, + currNonce: number, + currentBlockHash: string, + ) { + await moduleInstance.update(stakingModuleAddress, currentBlockHash); + await this.srModulesStorage.upsert(contractModule, currNonce, currentBlockHash); + updaterState.lastChangedBlockHash = currentBlockHash; + } + + public async isReorgDetected(updaterState: UpdaterState, prevBlockHash: string, currentBlockHash: string) { + // calculate once per iteration + // no need to recheck each module separately + if (updaterState.isReorgDetected) { + return true; + } + const currentBlock = await this.executionProvider.getFullBlock(currentBlockHash); const prevBlock = await this.executionProvider.getFullBlock(prevBlockHash); if (currentBlock.parentHash === prevBlock.hash) return false; - // TODO: different hash but same number - // if (currentBlock.number === prevBlock.number) return true; + // different hash but same number + if (currentBlock.number === prevBlock.number) { + updaterState.isReorgDetected = true; + return true; + } const blocks = await Promise.all( range(prevBlock.number, currentBlock.number).map(async (bNumber) => { @@ -264,6 +329,7 @@ export class KeysUpdateService { const currentBlock = blocks[i]; if (currentBlock.parentHash !== previousBlock.hash) { + updaterState.isReorgDetected = true; return true; } } From c7bb4e3c1966cb63b204400741d7f8fb898709d6 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 19:08:19 +0100 Subject: [PATCH 47/75] refactor: move interfaces to a separate file --- src/jobs/keys-update/keys-update.interfaces.ts | 17 +++++++++++++++++ src/jobs/keys-update/keys-update.service.ts | 17 +---------------- 2 files changed, 18 insertions(+), 16 deletions(-) create mode 100644 src/jobs/keys-update/keys-update.interfaces.ts diff --git a/src/jobs/keys-update/keys-update.interfaces.ts b/src/jobs/keys-update/keys-update.interfaces.ts new file mode 100644 index 00000000..d185a8f2 --- /dev/null +++ b/src/jobs/keys-update/keys-update.interfaces.ts @@ -0,0 +1,17 @@ +import { StakingModule } from 'staking-router-modules/interfaces/staking-module.interface'; +import { ElMetaEntity } from 'storage/el-meta.entity'; + +export interface UpdaterPayload { + currElMeta: { + number: number; + hash: string; + timestamp: number; + }; + prevElMeta: ElMetaEntity | null; + contractModules: StakingModule[]; +} + +export interface UpdaterState { + lastChangedBlockHash: string; + isReorgDetected: boolean; +} diff --git a/src/jobs/keys-update/keys-update.service.ts b/src/jobs/keys-update/keys-update.service.ts index a2d9aa91..d5d0b8ea 100644 --- a/src/jobs/keys-update/keys-update.service.ts +++ b/src/jobs/keys-update/keys-update.service.ts @@ -15,7 +15,7 @@ import { IsolationLevel } from '@mikro-orm/core'; import { PrometheusService } from 'common/prometheus'; import { SrModuleEntity } from 'storage/sr-module.entity'; import { StakingModule, StakingModuleInterface } from 'staking-router-modules/interfaces/staking-module.interface'; -import { ElMetaEntity } from 'storage/el-meta.entity'; +import { UpdaterPayload, UpdaterState } from './keys-update.interfaces'; const MAX_BLOCKS_OVERLAP = 30; @@ -28,21 +28,6 @@ class KeyOutdatedError extends Error { } } -interface UpdaterPayload { - currElMeta: { - number: number; - hash: string; - timestamp: number; - }; - prevElMeta: ElMetaEntity | null; - contractModules: StakingModule[]; -} - -interface UpdaterState { - lastChangedBlockHash: string; - isReorgDetected: boolean; -} - @Injectable() export class KeysUpdateService { constructor( From 7e52c4c72a0475473f822ca2aa2babe99083c4f2 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 22:09:10 +0100 Subject: [PATCH 48/75] test: processing of all indexing cases --- src/jobs/keys-update/keys-update.constants.ts | 1 + src/jobs/keys-update/keys-update.fixtures.ts | 34 +++ .../keys-update/keys-update.interfaces.ts | 7 +- .../keys-update/test/update-cases.spec.ts | 165 +++++++++++++++ src/jobs/keys-update/updater.service.ts | 194 ++++++++++++++++++ 5 files changed, 399 insertions(+), 2 deletions(-) create mode 100644 src/jobs/keys-update/keys-update.constants.ts create mode 100644 src/jobs/keys-update/keys-update.fixtures.ts create mode 100644 src/jobs/keys-update/test/update-cases.spec.ts create mode 100644 src/jobs/keys-update/updater.service.ts diff --git a/src/jobs/keys-update/keys-update.constants.ts b/src/jobs/keys-update/keys-update.constants.ts new file mode 100644 index 00000000..4ce41d26 --- /dev/null +++ b/src/jobs/keys-update/keys-update.constants.ts @@ -0,0 +1 @@ +export const MAX_BLOCKS_OVERLAP = 30; diff --git a/src/jobs/keys-update/keys-update.fixtures.ts b/src/jobs/keys-update/keys-update.fixtures.ts new file mode 100644 index 00000000..f5ef37d3 --- /dev/null +++ b/src/jobs/keys-update/keys-update.fixtures.ts @@ -0,0 +1,34 @@ +import { StakingModule } from 'staking-router-modules/interfaces/staking-module.interface'; + +export const stakingModuleFixture: StakingModule = { + moduleId: 1, + stakingModuleAddress: '0x123456789abcdef', + moduleFee: 0.02, + treasuryFee: 0.01, + targetShare: 500, + status: 1, + name: 'Staking Module 1', + lastDepositAt: Date.now() - 86400000, // 24 hours ago + lastDepositBlock: 12345, + exitedValidatorsCount: 10, + type: 'curated', + active: true, +}; + +export const stakingModuleFixtures: StakingModule[] = [ + stakingModuleFixture, + { + moduleId: 2, + stakingModuleAddress: '0x987654321fedcba', + moduleFee: 0.01, + treasuryFee: 0.005, + targetShare: 750, + status: 0, + name: 'Staking Module 2', + lastDepositAt: Date.now() - 172800000, // 48 hours ago + lastDepositBlock: 23456, + exitedValidatorsCount: 5, + type: 'dvt', + active: false, + }, +]; diff --git a/src/jobs/keys-update/keys-update.interfaces.ts b/src/jobs/keys-update/keys-update.interfaces.ts index d185a8f2..63444c06 100644 --- a/src/jobs/keys-update/keys-update.interfaces.ts +++ b/src/jobs/keys-update/keys-update.interfaces.ts @@ -1,5 +1,4 @@ import { StakingModule } from 'staking-router-modules/interfaces/staking-module.interface'; -import { ElMetaEntity } from 'storage/el-meta.entity'; export interface UpdaterPayload { currElMeta: { @@ -7,7 +6,11 @@ export interface UpdaterPayload { hash: string; timestamp: number; }; - prevElMeta: ElMetaEntity | null; + prevElMeta: { + blockNumber: number; + blockHash: string; + timestamp: number; + } | null; contractModules: StakingModule[]; } diff --git a/src/jobs/keys-update/test/update-cases.spec.ts b/src/jobs/keys-update/test/update-cases.spec.ts new file mode 100644 index 00000000..27305007 --- /dev/null +++ b/src/jobs/keys-update/test/update-cases.spec.ts @@ -0,0 +1,165 @@ +import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ExecutionProviderService } from 'common/execution-provider'; +import { StakingRouterService } from 'staking-router-modules/staking-router.service'; +import { ElMetaStorageService } from 'storage/el-meta.storage'; +import { SRModuleStorageService } from 'storage/sr-module.storage'; +import { stakingModuleFixture, stakingModuleFixtures } from '../keys-update.fixtures'; +import { KeysUpdaterService } from '../updater.service'; + +describe('YourService', () => { + let updaterService: KeysUpdaterService; + let stakingRouterService: StakingRouterService; + let sRModuleStorageService: SRModuleStorageService; + let loggerService: { log: jest.Mock }; + let executionProviderService: ExecutionProviderService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: LOGGER_PROVIDER, + useValue: { + log: jest.fn(), + }, + }, + { + provide: StakingRouterService, + useValue: { + getStakingRouterModuleImpl: () => ({ + getCurrentNonce() { + return 1; + }, + }), + }, + }, + { + provide: ElMetaStorageService, + useValue: { + update: jest.fn(), + }, + }, + { + provide: ExecutionProviderService, + useValue: { + getFullBlock: jest.fn(), + }, + }, + { + provide: SRModuleStorageService, + useValue: { + findOneById: jest.fn(), + upsert: jest.fn(), + }, + }, + KeysUpdaterService, + ], + }).compile(); + + updaterService = module.get(KeysUpdaterService); + stakingRouterService = module.get(StakingRouterService); + sRModuleStorageService = module.get(SRModuleStorageService); + executionProviderService = module.get(ExecutionProviderService); + loggerService = module.get(LOGGER_PROVIDER); + }); + + it('should be defined', () => { + expect(updaterService).toBeDefined(); + }); + + it('No past state found', async () => { + const mockUpdate = jest.spyOn(updaterService, 'updateStakingModule').mockImplementation(); + await updaterService.updateStakingModules({ + currElMeta: { number: 1, hash: '0x1', timestamp: 1 }, + prevElMeta: null, + contractModules: [stakingModuleFixture], + }); + + expect(mockUpdate).toBeCalledTimes(1); + expect(loggerService.log.mock.calls[1][0]).toBe('No past state found, start indexing'); + }); + + it('More than 1 module processed', async () => { + const mockUpdate = jest.spyOn(updaterService, 'updateStakingModule').mockImplementation(); + await updaterService.updateStakingModules({ + currElMeta: { number: 1, hash: '0x1', timestamp: 1 }, + prevElMeta: null, + contractModules: stakingModuleFixtures, + }); + + expect(mockUpdate).toBeCalledTimes(2); + }); + + it('Nonce has been changed', async () => { + const mockUpdate = jest.spyOn(updaterService, 'updateStakingModule').mockImplementation(); + + jest.spyOn(stakingRouterService, 'getStakingRouterModuleImpl').mockImplementation( + () => + ({ + getCurrentNonce() { + return 1; + }, + } as any), + ); + + jest.spyOn(sRModuleStorageService, 'findOneById').mockImplementation( + () => + ({ + nonce: 0, + } as any), + ); + + await updaterService.updateStakingModules({ + currElMeta: { number: 2, hash: '0x1', timestamp: 1 }, + prevElMeta: { blockNumber: 1, blockHash: '0x2', timestamp: 1 }, + contractModules: stakingModuleFixtures, + }); + + expect(mockUpdate).toBeCalledTimes(2); + expect(loggerService.log.mock.calls[1][0]).toBe('Nonce has been changed, start indexing'); + }); + + it('Too much difference between the blocks', async () => { + const mockUpdate = jest.spyOn(updaterService, 'updateStakingModule').mockImplementation(); + + jest.spyOn(sRModuleStorageService, 'findOneById').mockImplementation( + () => + ({ + nonce: 1, + } as any), + ); + + await updaterService.updateStakingModules({ + currElMeta: { number: 100, hash: '0x1', timestamp: 1 }, + prevElMeta: { blockNumber: 2, blockHash: '0x2', timestamp: 1 }, + contractModules: stakingModuleFixtures, + }); + + expect(mockUpdate).toBeCalledTimes(2); + expect(loggerService.log.mock.calls[1][0]).toBe('Too much difference between the blocks, start indexing'); + }); + + it('Reorg detected', async () => { + const mockUpdate = jest.spyOn(updaterService, 'updateStakingModule').mockImplementation(); + + jest.spyOn(sRModuleStorageService, 'findOneById').mockImplementation( + () => + ({ + nonce: 1, + } as any), + ); + + jest + .spyOn(executionProviderService, 'getFullBlock') + .mockImplementation(async () => ({ number: 2, hash: '0x2', timestamp: 1, parentHash: '0x1' } as any)); + + await updaterService.updateStakingModules({ + currElMeta: { number: 2, hash: '0x2', timestamp: 1 }, + prevElMeta: { blockNumber: 2, blockHash: '0x1', timestamp: 1 }, + contractModules: stakingModuleFixtures, + }); + + expect(mockUpdate).toBeCalledTimes(2); + expect(loggerService.log.mock.calls[1][0]).toBe('Reorg detected, start indexing'); + }); +}); diff --git a/src/jobs/keys-update/updater.service.ts b/src/jobs/keys-update/updater.service.ts new file mode 100644 index 00000000..4cd847f4 --- /dev/null +++ b/src/jobs/keys-update/updater.service.ts @@ -0,0 +1,194 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { range } from '@lido-nestjs/utils'; +import { LOGGER_PROVIDER, LoggerService } from 'common/logger'; +import { StakingRouterService } from 'staking-router-modules/staking-router.service'; +import { ElMetaStorageService } from 'storage/el-meta.storage'; +import { ExecutionProviderService } from 'common/execution-provider'; +import { SRModuleStorageService } from 'storage/sr-module.storage'; +import { StakingModule, StakingModuleInterface } from 'staking-router-modules/interfaces/staking-module.interface'; +import { UpdaterPayload, UpdaterState } from './keys-update.interfaces'; +import { MAX_BLOCKS_OVERLAP } from './keys-update.constants'; + +@Injectable() +export class KeysUpdaterService { + constructor( + @Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService, + protected readonly stakingRouterService: StakingRouterService, + protected readonly elMetaStorage: ElMetaStorageService, + protected readonly executionProvider: ExecutionProviderService, + protected readonly srModulesStorage: SRModuleStorageService, + ) {} + public async updateStakingModules(updaterPayload: UpdaterPayload): Promise { + const { prevElMeta, currElMeta, contractModules } = updaterPayload; + const prevBlockHash = prevElMeta?.blockHash; + const currentBlockHash = currElMeta.hash; + + const updaterState: UpdaterState = { + lastChangedBlockHash: prevBlockHash || currentBlockHash, + isReorgDetected: false, + }; + + for (const contractModule of contractModules) { + const { stakingModuleAddress } = contractModule; + + // Find implementation for staking module + const moduleInstance = this.stakingRouterService.getStakingRouterModuleImpl(contractModule.type); + // Read current nonce from contract + const currNonce = await moduleInstance.getCurrentNonce(stakingModuleAddress, currentBlockHash); + // Read module in storage + const moduleInStorage = await this.srModulesStorage.findOneById(contractModule.moduleId); + const prevNonce = moduleInStorage?.nonce; + + this.logger.log(`Nonce previous value: ${prevNonce}, nonce current value: ${currNonce}`); + + if (!prevBlockHash) { + this.logger.log('No past state found, start indexing', { stakingModuleAddress, currentBlockHash }); + + await this.updateStakingModule( + updaterState, + moduleInstance, + contractModule, + stakingModuleAddress, + currNonce, + currentBlockHash, + ); + continue; + } + + if (prevNonce !== currNonce) { + this.logger.log('Nonce has been changed, start indexing', { + stakingModuleAddress, + currentBlockHash, + prevNonce, + currNonce, + }); + + await this.updateStakingModule( + updaterState, + moduleInstance, + contractModule, + stakingModuleAddress, + currNonce, + currentBlockHash, + ); + continue; + } + + if (this.isTooMuchDiffBetweenBlocks(prevElMeta.blockNumber, currElMeta.number)) { + this.logger.log('Too much difference between the blocks, start indexing', { + stakingModuleAddress, + currentBlockHash, + }); + + await this.updateStakingModule( + updaterState, + moduleInstance, + contractModule, + stakingModuleAddress, + currNonce, + currentBlockHash, + ); + continue; + } + + if (await this.isReorgDetected(updaterState, prevBlockHash, currentBlockHash)) { + this.logger.log('Reorg detected, start indexing', { stakingModuleAddress, currentBlockHash }); + + await this.updateStakingModule( + updaterState, + moduleInstance, + contractModule, + stakingModuleAddress, + currNonce, + currentBlockHash, + ); + continue; + } + + if ( + prevElMeta.blockNumber < currElMeta.number && + (await moduleInstance.operatorsWereChanged( + contractModule.stakingModuleAddress, + prevElMeta.blockNumber + 1, + currElMeta.number, + )) + ) { + this.logger.log('Update operator events happened, need to update operators', { + stakingModuleAddress, + currentBlockHash, + }); + + await this.updateStakingModule( + updaterState, + moduleInstance, + contractModule, + stakingModuleAddress, + currNonce, + currentBlockHash, + ); + continue; + } + + this.logger.log('No changes have been detected in the module, indexing is not required', { + stakingModuleAddress, + currentBlockHash, + }); + } + + // Update EL meta in db + await this.elMetaStorage.update({ ...currElMeta, lastChangedBlockHash: updaterState.lastChangedBlockHash }); + } + + public async updateStakingModule( + updaterState: UpdaterState, + moduleInstance: StakingModuleInterface, + contractModule: StakingModule, + stakingModuleAddress: string, + currNonce: number, + currentBlockHash: string, + ) { + await moduleInstance.update(stakingModuleAddress, currentBlockHash); + await this.srModulesStorage.upsert(contractModule, currNonce, currentBlockHash); + updaterState.lastChangedBlockHash = currentBlockHash; + } + + public async isReorgDetected(updaterState: UpdaterState, prevBlockHash: string, currentBlockHash: string) { + // calculate once per iteration + // no need to recheck each module separately + if (updaterState.isReorgDetected) { + return true; + } + + const currentBlock = await this.executionProvider.getFullBlock(currentBlockHash); + const prevBlock = await this.executionProvider.getFullBlock(prevBlockHash); + + if (currentBlock.parentHash === prevBlock.hash) return false; + // different hash but same number + if (currentBlock.number === prevBlock.number) { + updaterState.isReorgDetected = true; + return true; + } + + const blocks = await Promise.all( + range(prevBlock.number, currentBlock.number).map(async (bNumber) => { + return await this.executionProvider.getFullBlock(bNumber); + }), + ); + + for (let i = 1; i < blocks.length; i++) { + const previousBlock = blocks[i - 1]; + const currentBlock = blocks[i]; + + if (currentBlock.parentHash !== previousBlock.hash) { + updaterState.isReorgDetected = true; + return true; + } + } + + return false; + } + + public isTooMuchDiffBetweenBlocks(prevBlockNumber: number, currentBlockNumber: number) { + return currentBlockNumber - prevBlockNumber >= MAX_BLOCKS_OVERLAP; + } +} From 2796b6ff7bbd2b6b2e792d5f3fc97eb3ab983872 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 22:16:31 +0100 Subject: [PATCH 49/75] refactor: rename updater --- ...dater.service.ts => staking-module-updater.service.ts} | 2 +- src/jobs/keys-update/test/update-cases.spec.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) rename src/jobs/keys-update/{updater.service.ts => staking-module-updater.service.ts} (99%) diff --git a/src/jobs/keys-update/updater.service.ts b/src/jobs/keys-update/staking-module-updater.service.ts similarity index 99% rename from src/jobs/keys-update/updater.service.ts rename to src/jobs/keys-update/staking-module-updater.service.ts index 4cd847f4..9a4b93db 100644 --- a/src/jobs/keys-update/updater.service.ts +++ b/src/jobs/keys-update/staking-module-updater.service.ts @@ -10,7 +10,7 @@ import { UpdaterPayload, UpdaterState } from './keys-update.interfaces'; import { MAX_BLOCKS_OVERLAP } from './keys-update.constants'; @Injectable() -export class KeysUpdaterService { +export class StakingModuleUpdaterService { constructor( @Inject(LOGGER_PROVIDER) protected readonly logger: LoggerService, protected readonly stakingRouterService: StakingRouterService, diff --git a/src/jobs/keys-update/test/update-cases.spec.ts b/src/jobs/keys-update/test/update-cases.spec.ts index 27305007..a6a287dd 100644 --- a/src/jobs/keys-update/test/update-cases.spec.ts +++ b/src/jobs/keys-update/test/update-cases.spec.ts @@ -5,10 +5,10 @@ import { StakingRouterService } from 'staking-router-modules/staking-router.serv import { ElMetaStorageService } from 'storage/el-meta.storage'; import { SRModuleStorageService } from 'storage/sr-module.storage'; import { stakingModuleFixture, stakingModuleFixtures } from '../keys-update.fixtures'; -import { KeysUpdaterService } from '../updater.service'; +import { StakingModuleUpdaterService } from '../staking-module-updater.service'; describe('YourService', () => { - let updaterService: KeysUpdaterService; + let updaterService: StakingModuleUpdaterService; let stakingRouterService: StakingRouterService; let sRModuleStorageService: SRModuleStorageService; let loggerService: { log: jest.Mock }; @@ -52,11 +52,11 @@ describe('YourService', () => { upsert: jest.fn(), }, }, - KeysUpdaterService, + StakingModuleUpdaterService, ], }).compile(); - updaterService = module.get(KeysUpdaterService); + updaterService = module.get(StakingModuleUpdaterService); stakingRouterService = module.get(StakingRouterService); sRModuleStorageService = module.get(SRModuleStorageService); executionProviderService = module.get(ExecutionProviderService); From 153345401b4605fd4d1deb25d9a72ca615b0944d Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 22:25:34 +0100 Subject: [PATCH 50/75] fix: different hash but same number case --- src/jobs/keys-update/staking-module-updater.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jobs/keys-update/staking-module-updater.service.ts b/src/jobs/keys-update/staking-module-updater.service.ts index 9a4b93db..91bdc165 100644 --- a/src/jobs/keys-update/staking-module-updater.service.ts +++ b/src/jobs/keys-update/staking-module-updater.service.ts @@ -164,7 +164,7 @@ export class StakingModuleUpdaterService { if (currentBlock.parentHash === prevBlock.hash) return false; // different hash but same number - if (currentBlock.number === prevBlock.number) { + if (currentBlock.hash !== prevBlock.hash && currentBlock.number === prevBlock.number) { updaterState.isReorgDetected = true; return true; } From ba06c2c03d70d6eadb24fd347bb398aabd9e7343 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 22:25:44 +0100 Subject: [PATCH 51/75] test: reorg spec --- .../keys-update/test/detect-reorg.spec.ts | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/jobs/keys-update/test/detect-reorg.spec.ts diff --git a/src/jobs/keys-update/test/detect-reorg.spec.ts b/src/jobs/keys-update/test/detect-reorg.spec.ts new file mode 100644 index 00000000..230172ca --- /dev/null +++ b/src/jobs/keys-update/test/detect-reorg.spec.ts @@ -0,0 +1,114 @@ +import { LOGGER_PROVIDER } from '@lido-nestjs/logger'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ExecutionProviderService } from 'common/execution-provider'; +import { StakingRouterService } from 'staking-router-modules/staking-router.service'; +import { ElMetaStorageService } from 'storage/el-meta.storage'; +import { SRModuleStorageService } from 'storage/sr-module.storage'; +import { UpdaterState } from '../keys-update.interfaces'; +import { StakingModuleUpdaterService } from '../staking-module-updater.service'; + +describe('YourService', () => { + let updaterService: StakingModuleUpdaterService; + let executionProviderService: ExecutionProviderService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: LOGGER_PROVIDER, + useValue: { + log: jest.fn(), + }, + }, + { + provide: StakingRouterService, + useValue: { + getStakingRouterModuleImpl: () => ({ + getCurrentNonce() { + return 1; + }, + }), + }, + }, + { + provide: ElMetaStorageService, + useValue: { + update: jest.fn(), + }, + }, + { + provide: ExecutionProviderService, + useValue: { + getFullBlock: jest.fn(), + }, + }, + { + provide: SRModuleStorageService, + useValue: { + findOneById: jest.fn(), + upsert: jest.fn(), + }, + }, + StakingModuleUpdaterService, + ], + }).compile(); + + updaterService = module.get(StakingModuleUpdaterService); + executionProviderService = module.get(ExecutionProviderService); + }); + + it('should be defined', () => { + expect(updaterService).toBeDefined(); + }); + + it('parent hash of the currentBlock matches the hash of the prevBlock', async () => { + const updaterState: UpdaterState = { + lastChangedBlockHash: '0x1', + isReorgDetected: false, + }; + + jest + .spyOn(executionProviderService, 'getFullBlock') + .mockImplementationOnce(async () => ({ number: 2, hash: '0x2', timestamp: 1, parentHash: '0x1' } as any)); + + jest + .spyOn(executionProviderService, 'getFullBlock') + .mockImplementationOnce(async () => ({ number: 1, hash: '0x1', timestamp: 1, parentHash: '0x0' } as any)); + + expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x1')).toBeFalsy(); + }); + + it('parent hash of the currentBlock matches the hash of the prevBlock', async () => { + const updaterState: UpdaterState = { + lastChangedBlockHash: '0x1', + isReorgDetected: false, + }; + + jest + .spyOn(executionProviderService, 'getFullBlock') + .mockImplementationOnce(async () => ({ number: 2, hash: '0x2', timestamp: 1, parentHash: '0x1' } as any)); + + jest + .spyOn(executionProviderService, 'getFullBlock') + .mockImplementationOnce(async () => ({ number: 1, hash: '0x1', timestamp: 1, parentHash: '0x0' } as any)); + + expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x1')).toBeFalsy(); + }); + + it('same block number but different hashes', async () => { + const updaterState: UpdaterState = { + lastChangedBlockHash: '0x1', + isReorgDetected: false, + }; + + jest + .spyOn(executionProviderService, 'getFullBlock') + .mockImplementationOnce(async () => ({ number: 2, hash: '0x2', timestamp: 1, parentHash: '0x2' } as any)); + + jest + .spyOn(executionProviderService, 'getFullBlock') + .mockImplementationOnce(async () => ({ number: 2, hash: '0x1', timestamp: 1, parentHash: '0x0' } as any)); + + expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x1')).toBeTruthy(); + }); +}); From a8a1bc024056ef297a9315e005bf327ed0292b81 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 22:38:50 +0100 Subject: [PATCH 52/75] test: reorg cases --- .../keys-update/test/detect-reorg.spec.ts | 61 ++++++++++++++++++- .../keys-update/test/update-cases.spec.ts | 2 +- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/jobs/keys-update/test/detect-reorg.spec.ts b/src/jobs/keys-update/test/detect-reorg.spec.ts index 230172ca..1230d512 100644 --- a/src/jobs/keys-update/test/detect-reorg.spec.ts +++ b/src/jobs/keys-update/test/detect-reorg.spec.ts @@ -7,7 +7,7 @@ import { SRModuleStorageService } from 'storage/sr-module.storage'; import { UpdaterState } from '../keys-update.interfaces'; import { StakingModuleUpdaterService } from '../staking-module-updater.service'; -describe('YourService', () => { +describe('detect reorg', () => { let updaterService: StakingModuleUpdaterService; let executionProviderService: ExecutionProviderService; @@ -76,6 +76,7 @@ describe('YourService', () => { .mockImplementationOnce(async () => ({ number: 1, hash: '0x1', timestamp: 1, parentHash: '0x0' } as any)); expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x1')).toBeFalsy(); + expect(updaterState.isReorgDetected).toBeFalsy(); }); it('parent hash of the currentBlock matches the hash of the prevBlock', async () => { @@ -93,6 +94,7 @@ describe('YourService', () => { .mockImplementationOnce(async () => ({ number: 1, hash: '0x1', timestamp: 1, parentHash: '0x0' } as any)); expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x1')).toBeFalsy(); + expect(updaterState.isReorgDetected).toBeFalsy(); }); it('same block number but different hashes', async () => { @@ -110,5 +112,62 @@ describe('YourService', () => { .mockImplementationOnce(async () => ({ number: 2, hash: '0x1', timestamp: 1, parentHash: '0x0' } as any)); expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x1')).toBeTruthy(); + expect(updaterState.isReorgDetected).toBeTruthy(); + }); + + it('check blockchain (happy pass)', async () => { + const updaterState: UpdaterState = { + lastChangedBlockHash: '0x1', + isReorgDetected: false, + }; + + jest + .spyOn(executionProviderService, 'getFullBlock') + .mockImplementationOnce(async () => ({ number: 100, hash: '0x100', timestamp: 1, parentHash: '0x99' } as any)); + + jest + .spyOn(executionProviderService, 'getFullBlock') + .mockImplementationOnce(async () => ({ number: 1, hash: '0x1', timestamp: 1, parentHash: '0x0' } as any)); + + jest.spyOn(executionProviderService, 'getFullBlock').mockImplementation( + async (blockHashOrBlockTag: string | number) => + ({ + number: Number(blockHashOrBlockTag), + hash: `0x${blockHashOrBlockTag}`, + timestamp: 1, + parentHash: `0x${Number(blockHashOrBlockTag) - 1}`, + } as any), + ); + + expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x1')).toBeFalsy(); + expect(updaterState.isReorgDetected).toBeFalsy(); + }); + + it('check blockchain (parent hash does not match)', async () => { + const updaterState: UpdaterState = { + lastChangedBlockHash: '0x1', + isReorgDetected: false, + }; + + jest + .spyOn(executionProviderService, 'getFullBlock') + .mockImplementationOnce(async () => ({ number: 100, hash: '0x100', timestamp: 1, parentHash: '0x99' } as any)); + + jest + .spyOn(executionProviderService, 'getFullBlock') + .mockImplementationOnce(async () => ({ number: 1, hash: '0x1', timestamp: 1, parentHash: '0x0' } as any)); + + jest.spyOn(executionProviderService, 'getFullBlock').mockImplementation( + async (blockHashOrBlockTag: string | number) => + ({ + number: Number(blockHashOrBlockTag), + hash: `0x${blockHashOrBlockTag}`, + timestamp: 1, + parentHash: `0xSORRY`, + } as any), + ); + + expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x1')).toBeTruthy(); + expect(updaterState.isReorgDetected).toBeTruthy(); }); }); diff --git a/src/jobs/keys-update/test/update-cases.spec.ts b/src/jobs/keys-update/test/update-cases.spec.ts index a6a287dd..dc990a65 100644 --- a/src/jobs/keys-update/test/update-cases.spec.ts +++ b/src/jobs/keys-update/test/update-cases.spec.ts @@ -7,7 +7,7 @@ import { SRModuleStorageService } from 'storage/sr-module.storage'; import { stakingModuleFixture, stakingModuleFixtures } from '../keys-update.fixtures'; import { StakingModuleUpdaterService } from '../staking-module-updater.service'; -describe('YourService', () => { +describe('update cases', () => { let updaterService: StakingModuleUpdaterService; let stakingRouterService: StakingRouterService; let sRModuleStorageService: SRModuleStorageService; From 403a91a7ed14da062f887600ae71f1da5806a551 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 22:39:14 +0100 Subject: [PATCH 53/75] fix: block range in isReorgDetected method --- src/jobs/keys-update/staking-module-updater.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jobs/keys-update/staking-module-updater.service.ts b/src/jobs/keys-update/staking-module-updater.service.ts index 91bdc165..544632d5 100644 --- a/src/jobs/keys-update/staking-module-updater.service.ts +++ b/src/jobs/keys-update/staking-module-updater.service.ts @@ -170,7 +170,7 @@ export class StakingModuleUpdaterService { } const blocks = await Promise.all( - range(prevBlock.number, currentBlock.number).map(async (bNumber) => { + range(prevBlock.number, currentBlock.number + 1).map(async (bNumber) => { return await this.executionProvider.getFullBlock(bNumber); }), ); From 9cef0d3408946ca223b4b92a2835023e82039fea Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 22:43:36 +0100 Subject: [PATCH 54/75] refactor: connect updater to keys-update service --- src/jobs/keys-update/keys-update.module.ts | 5 +- src/jobs/keys-update/keys-update.service.ts | 184 +------------------- 2 files changed, 7 insertions(+), 182 deletions(-) diff --git a/src/jobs/keys-update/keys-update.module.ts b/src/jobs/keys-update/keys-update.module.ts index 5019bc72..7a03cd27 100644 --- a/src/jobs/keys-update/keys-update.module.ts +++ b/src/jobs/keys-update/keys-update.module.ts @@ -6,6 +6,7 @@ import { StakingRouterFetchModule } from 'staking-router-modules/contracts'; import { ExecutionProviderModule } from 'common/execution-provider'; import { StorageModule } from 'storage/storage.module'; import { PrometheusModule } from 'common/prometheus'; +import { StakingModuleUpdaterService } from './staking-module-updater.service'; @Module({ imports: [ @@ -16,7 +17,7 @@ import { PrometheusModule } from 'common/prometheus'; StorageModule, PrometheusModule, ], - providers: [KeysUpdateService], - exports: [KeysUpdateService], + providers: [KeysUpdateService, StakingModuleUpdaterService], + exports: [KeysUpdateService, StakingModuleUpdaterService], }) export class KeysUpdateModule {} diff --git a/src/jobs/keys-update/keys-update.service.ts b/src/jobs/keys-update/keys-update.service.ts index d5d0b8ea..f5794111 100644 --- a/src/jobs/keys-update/keys-update.service.ts +++ b/src/jobs/keys-update/keys-update.service.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { range } from '@lido-nestjs/utils'; import { LOGGER_PROVIDER, LoggerService } from 'common/logger'; import { ConfigService } from 'common/config'; import { JobService } from 'common/job'; @@ -14,10 +13,8 @@ import { SRModuleStorageService } from 'storage/sr-module.storage'; import { IsolationLevel } from '@mikro-orm/core'; import { PrometheusService } from 'common/prometheus'; import { SrModuleEntity } from 'storage/sr-module.entity'; -import { StakingModule, StakingModuleInterface } from 'staking-router-modules/interfaces/staking-module.interface'; -import { UpdaterPayload, UpdaterState } from './keys-update.interfaces'; - -const MAX_BLOCKS_OVERLAP = 30; +import { StakingModule } from 'staking-router-modules/interfaces/staking-module.interface'; +import { StakingModuleUpdaterService } from './staking-module-updater.service'; class KeyOutdatedError extends Error { lastBlock: number; @@ -42,6 +39,7 @@ export class KeysUpdateService { protected readonly executionProvider: ExecutionProviderService, protected readonly srModulesStorage: SRModuleStorageService, protected readonly prometheusService: PrometheusService, + protected readonly stakingModuleUpdaterService: StakingModuleUpdaterService, ) {} protected lastTimestampSec: number | undefined = undefined; @@ -144,7 +142,7 @@ export class KeysUpdateService { await this.entityManager.transactional( async () => { - await this.updateStakingModules({ currElMeta, prevElMeta, contractModules }); + await this.stakingModuleUpdaterService.updateStakingModules({ currElMeta, prevElMeta, contractModules }); }, { isolationLevel: IsolationLevel.READ_COMMITTED }, ); @@ -152,180 +150,6 @@ export class KeysUpdateService { return currElMeta; } - public async updateStakingModules(updaterPayload: UpdaterPayload): Promise { - const { prevElMeta, currElMeta, contractModules } = updaterPayload; - const prevBlockHash = prevElMeta?.blockHash; - const currentBlockHash = currElMeta.hash; - - const updaterState: UpdaterState = { - lastChangedBlockHash: prevBlockHash || currentBlockHash, - isReorgDetected: false, - }; - - for (const contractModule of contractModules) { - const { stakingModuleAddress } = contractModule; - - // Find implementation for staking module - const moduleInstance = this.stakingRouterService.getStakingRouterModuleImpl(contractModule.type); - // Read current nonce from contract - const currNonce = await moduleInstance.getCurrentNonce(stakingModuleAddress, currentBlockHash); - // Read module in storage - const moduleInStorage = await this.srModulesStorage.findOneById(contractModule.moduleId); - const prevNonce = moduleInStorage?.nonce; - - this.logger.log(`Nonce previous value: ${prevNonce}, nonce current value: ${currNonce}`); - - if (!prevBlockHash) { - this.logger.log('No past state found, start indexing', { stakingModuleAddress, currentBlockHash }); - - await this.updateStakingModule( - updaterState, - moduleInstance, - contractModule, - stakingModuleAddress, - currNonce, - currentBlockHash, - ); - continue; - } - - if (prevNonce !== currNonce) { - this.logger.log('Nonce has been changed, start indexing', { - stakingModuleAddress, - currentBlockHash, - prevNonce, - currNonce, - }); - - await this.updateStakingModule( - updaterState, - moduleInstance, - contractModule, - stakingModuleAddress, - currNonce, - currentBlockHash, - ); - continue; - } - - if (this.isTooMuchDiffBetweenBlocks(prevElMeta.blockNumber, currElMeta.number)) { - this.logger.log('Too much difference between the blocks, start indexing', { - stakingModuleAddress, - currentBlockHash, - }); - - await this.updateStakingModule( - updaterState, - moduleInstance, - contractModule, - stakingModuleAddress, - currNonce, - currentBlockHash, - ); - continue; - } - - if (await this.isReorgDetected(updaterState, prevBlockHash, currentBlockHash)) { - this.logger.log('Reorg detected, start indexing', { stakingModuleAddress, currentBlockHash }); - - await this.updateStakingModule( - updaterState, - moduleInstance, - contractModule, - stakingModuleAddress, - currNonce, - currentBlockHash, - ); - continue; - } - - if ( - prevElMeta.blockNumber < currElMeta.number && - (await moduleInstance.operatorsWereChanged( - contractModule.stakingModuleAddress, - prevElMeta.blockNumber + 1, - currElMeta.number, - )) - ) { - this.logger.log('Update operator events happened, need to update operators', { - stakingModuleAddress, - currentBlockHash, - }); - - await this.updateStakingModule( - updaterState, - moduleInstance, - contractModule, - stakingModuleAddress, - currNonce, - currentBlockHash, - ); - continue; - } - - this.logger.log('No changes have been detected in the module, indexing is not required', { - stakingModuleAddress, - currentBlockHash, - }); - } - - // Update EL meta in db - await this.elMetaStorage.update({ ...currElMeta, lastChangedBlockHash: updaterState.lastChangedBlockHash }); - } - - public async updateStakingModule( - updaterState: UpdaterState, - moduleInstance: StakingModuleInterface, - contractModule: StakingModule, - stakingModuleAddress: string, - currNonce: number, - currentBlockHash: string, - ) { - await moduleInstance.update(stakingModuleAddress, currentBlockHash); - await this.srModulesStorage.upsert(contractModule, currNonce, currentBlockHash); - updaterState.lastChangedBlockHash = currentBlockHash; - } - - public async isReorgDetected(updaterState: UpdaterState, prevBlockHash: string, currentBlockHash: string) { - // calculate once per iteration - // no need to recheck each module separately - if (updaterState.isReorgDetected) { - return true; - } - - const currentBlock = await this.executionProvider.getFullBlock(currentBlockHash); - const prevBlock = await this.executionProvider.getFullBlock(prevBlockHash); - - if (currentBlock.parentHash === prevBlock.hash) return false; - // different hash but same number - if (currentBlock.number === prevBlock.number) { - updaterState.isReorgDetected = true; - return true; - } - - const blocks = await Promise.all( - range(prevBlock.number, currentBlock.number).map(async (bNumber) => { - return await this.executionProvider.getFullBlock(bNumber); - }), - ); - - for (let i = 1; i < blocks.length; i++) { - const previousBlock = blocks[i - 1]; - const currentBlock = blocks[i]; - - if (currentBlock.parentHash !== previousBlock.hash) { - updaterState.isReorgDetected = true; - return true; - } - } - - return false; - } - - public isTooMuchDiffBetweenBlocks(prevBlockNumber: number, currentBlockNumber: number) { - return currentBlockNumber - prevBlockNumber >= MAX_BLOCKS_OVERLAP; - } - /** * Update prometheus metrics of staking modules */ From e9c83f7ff5369d48224b7f4bbf2ea434f2397e4f Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 23:25:05 +0100 Subject: [PATCH 55/75] fix: e2e tests --- .../registry/test/fixtures/connect.fixture.ts | 80 +++++++++++++++++++ src/http/db.fixtures.ts | 3 + src/http/el-meta.fixture.ts | 1 + src/http/keys/keys.e2e-spec.ts | 28 ++++--- src/http/operator.fixtures.ts | 2 + .../sr-modules-keys.e2e-spec.ts | 28 ++++--- .../sr-modules-operators-keys.e2e-spec.ts | 11 ++- .../sr-modules-operators.e2e-spec.ts | 22 ++--- .../sr-modules-validators.e2e-spec.ts | 12 +-- src/http/sr-modules/sr-modules.e2e-spec.ts | 16 ++-- src/jobs/keys-update/keys-update.fixtures.ts | 2 + .../staking-router-fetch.service.ts | 1 + .../interfaces/staking-module.interface.ts | 2 + src/storage/sr-module.storage.e2e-spec.ts | 13 +-- 14 files changed, 173 insertions(+), 48 deletions(-) diff --git a/src/common/registry/test/fixtures/connect.fixture.ts b/src/common/registry/test/fixtures/connect.fixture.ts index aa1c644f..c4877347 100644 --- a/src/common/registry/test/fixtures/connect.fixture.ts +++ b/src/common/registry/test/fixtures/connect.fixture.ts @@ -16,6 +16,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 0, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 1, @@ -27,6 +28,7 @@ export const operators = [ stoppedValidators: 1, totalSigningKeys: 123, usedSigningKeys: 51, + finalizedUsedSigningKeys: 51, }, { index: 2, @@ -38,6 +40,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 0, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 3, @@ -49,6 +52,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 0, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 4, @@ -60,6 +64,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 10, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 5, @@ -71,6 +76,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 0, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 6, @@ -82,6 +88,7 @@ export const operators = [ stoppedValidators: 338, totalSigningKeys: 2511, usedSigningKeys: 2109, + finalizedUsedSigningKeys: 2109, }, { index: 7, @@ -93,6 +100,7 @@ export const operators = [ stoppedValidators: 1, totalSigningKeys: 20, usedSigningKeys: 20, + finalizedUsedSigningKeys: 20, }, { index: 8, @@ -104,6 +112,7 @@ export const operators = [ stoppedValidators: 6, totalSigningKeys: 109, usedSigningKeys: 109, + finalizedUsedSigningKeys: 109, }, { index: 9, @@ -115,6 +124,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 10, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 10, @@ -126,6 +136,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 310, usedSigningKeys: 17, + finalizedUsedSigningKeys: 17, }, { index: 11, @@ -137,6 +148,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 110, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 12, @@ -148,6 +160,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 41, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 13, @@ -159,6 +172,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 0, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 14, @@ -170,6 +184,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 0, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 15, @@ -181,6 +196,7 @@ export const operators = [ stoppedValidators: 4, totalSigningKeys: 20, usedSigningKeys: 20, + finalizedUsedSigningKeys: 20, }, { index: 16, @@ -192,6 +208,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 10, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 17, @@ -203,6 +220,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 10, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 18, @@ -214,6 +232,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 10, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 19, @@ -225,6 +244,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 0, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 20, @@ -236,6 +256,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 0, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 21, @@ -247,6 +268,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 0, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 22, @@ -258,6 +280,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 0, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 23, @@ -269,6 +292,7 @@ export const operators = [ stoppedValidators: 22, totalSigningKeys: 62, usedSigningKeys: 48, + finalizedUsedSigningKeys: 48, }, { index: 24, @@ -280,6 +304,7 @@ export const operators = [ stoppedValidators: 1, totalSigningKeys: 20, usedSigningKeys: 20, + finalizedUsedSigningKeys: 20, }, { index: 25, @@ -291,6 +316,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 10, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 26, @@ -302,6 +328,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 10, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 27, @@ -313,6 +340,7 @@ export const operators = [ stoppedValidators: 7, totalSigningKeys: 22, usedSigningKeys: 22, + finalizedUsedSigningKeys: 22, }, { index: 28, @@ -324,6 +352,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 20, usedSigningKeys: 12, + finalizedUsedSigningKeys: 12, }, { index: 29, @@ -335,6 +364,7 @@ export const operators = [ stoppedValidators: 1, totalSigningKeys: 20, usedSigningKeys: 20, + finalizedUsedSigningKeys: 20, }, { index: 30, @@ -346,6 +376,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 10, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 31, @@ -357,6 +388,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 0, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 32, @@ -368,6 +400,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 2, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 33, @@ -379,6 +412,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 0, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 34, @@ -390,6 +424,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 10, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 35, @@ -401,6 +436,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 250, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 36, @@ -412,6 +448,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 10, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 37, @@ -423,6 +460,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 10, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 38, @@ -434,6 +472,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 10, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 39, @@ -445,6 +484,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 10, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 40, @@ -456,6 +496,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 12, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 41, @@ -467,6 +508,7 @@ export const operators = [ stoppedValidators: 930, totalSigningKeys: 2000, usedSigningKeys: 2000, + finalizedUsedSigningKeys: 2000, }, { index: 42, @@ -478,6 +520,7 @@ export const operators = [ stoppedValidators: 18, totalSigningKeys: 1010, usedSigningKeys: 1010, + finalizedUsedSigningKeys: 1010, }, { index: 43, @@ -489,6 +532,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 10, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 44, @@ -500,6 +544,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 20, usedSigningKeys: 20, + finalizedUsedSigningKeys: 20, }, { index: 45, @@ -511,6 +556,7 @@ export const operators = [ stoppedValidators: 4, totalSigningKeys: 10, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 46, @@ -522,6 +568,7 @@ export const operators = [ stoppedValidators: 3, totalSigningKeys: 500, usedSigningKeys: 10, + finalizedUsedSigningKeys: 10, }, { index: 47, @@ -533,6 +580,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 1, usedSigningKeys: 1, + finalizedUsedSigningKeys: 1, }, { index: 48, @@ -544,6 +592,7 @@ export const operators = [ stoppedValidators: 1, totalSigningKeys: 1, usedSigningKeys: 1, + finalizedUsedSigningKeys: 1, }, { index: 49, @@ -555,6 +604,7 @@ export const operators = [ stoppedValidators: 1, totalSigningKeys: 1, usedSigningKeys: 1, + finalizedUsedSigningKeys: 1, }, { index: 50, @@ -566,6 +616,7 @@ export const operators = [ stoppedValidators: 1, totalSigningKeys: 1, usedSigningKeys: 1, + finalizedUsedSigningKeys: 1, }, { index: 51, @@ -577,6 +628,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 470, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 52, @@ -588,6 +640,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 53, @@ -599,6 +652,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 54, @@ -610,6 +664,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 55, @@ -621,6 +676,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 56, @@ -632,6 +688,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 57, @@ -643,6 +700,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 58, @@ -654,6 +712,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 59, @@ -665,6 +724,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 60, @@ -676,6 +736,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 61, @@ -687,6 +748,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 62, @@ -698,6 +760,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 0, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 63, @@ -709,6 +772,7 @@ export const operators = [ stoppedValidators: 14, totalSigningKeys: 2000, usedSigningKeys: 1786, + finalizedUsedSigningKeys: 1786, }, { index: 64, @@ -720,6 +784,7 @@ export const operators = [ stoppedValidators: 13, totalSigningKeys: 3000, usedSigningKeys: 1785, + finalizedUsedSigningKeys: 1785, }, { index: 65, @@ -731,6 +796,7 @@ export const operators = [ stoppedValidators: 12, totalSigningKeys: 3000, usedSigningKeys: 1783, + finalizedUsedSigningKeys: 1783, }, { index: 66, @@ -742,6 +808,7 @@ export const operators = [ stoppedValidators: 10, totalSigningKeys: 2000, usedSigningKeys: 1781, + finalizedUsedSigningKeys: 1781, }, { index: 67, @@ -753,6 +820,7 @@ export const operators = [ stoppedValidators: 10, totalSigningKeys: 2000, usedSigningKeys: 1781, + finalizedUsedSigningKeys: 1781, }, { index: 68, @@ -764,6 +832,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 1, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 69, @@ -775,6 +844,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 70, @@ -786,6 +856,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 71, @@ -797,6 +868,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 10, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 72, @@ -808,6 +880,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 73, @@ -819,6 +892,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 74, @@ -830,6 +904,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 75, @@ -841,6 +916,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 76, @@ -852,6 +928,7 @@ export const operators = [ stoppedValidators: 0, totalSigningKeys: 0, usedSigningKeys: 0, + finalizedUsedSigningKeys: 0, }, { index: 77, @@ -863,6 +940,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 5, usedSigningKeys: 5, + finalizedUsedSigningKeys: 5, }, { index: 78, @@ -874,6 +952,7 @@ export const operators = [ stoppedValidators: 5, totalSigningKeys: 125, usedSigningKeys: 125, + finalizedUsedSigningKeys: 125, }, { index: 79, @@ -885,6 +964,7 @@ export const operators = [ stoppedValidators: 2, totalSigningKeys: 500, usedSigningKeys: 500, + finalizedUsedSigningKeys: 500, }, ]; diff --git a/src/http/db.fixtures.ts b/src/http/db.fixtures.ts index 0a6c0d41..b59c5c2f 100644 --- a/src/http/db.fixtures.ts +++ b/src/http/db.fixtures.ts @@ -17,6 +17,7 @@ export const curatedModule: StakingModule = { lastDepositBlock: 9, exitedValidatorsCount: 0, active: true, + lastChangedBlockHash: '', }; export const dvtModule: StakingModule = { @@ -32,6 +33,7 @@ export const dvtModule: StakingModule = { lastDepositBlock: 10, exitedValidatorsCount: 0, active: true, + lastChangedBlockHash: '', }; export const updatedCuratedModule: StakingModule = { @@ -47,6 +49,7 @@ export const updatedCuratedModule: StakingModule = { lastDepositBlock: 10, exitedValidatorsCount: 1, active: false, + lastChangedBlockHash: '', }; export const srModules = [curatedModule, dvtModule]; diff --git a/src/http/el-meta.fixture.ts b/src/http/el-meta.fixture.ts index 8d8109e7..92643eaa 100644 --- a/src/http/el-meta.fixture.ts +++ b/src/http/el-meta.fixture.ts @@ -2,4 +2,5 @@ export const elMeta = { number: 74, hash: '0x662e3e713207240b25d01324b6eccdc91493249a5048881544254994694530a5', timestamp: 1691500803, + lastChangedBlockHash: '0x662e3e713207240b25d01324b6eccdc91493249a5048881544254994694530a5', }; diff --git a/src/http/keys/keys.e2e-spec.ts b/src/http/keys/keys.e2e-spec.ts index 3001b5b4..04b7cc75 100644 --- a/src/http/keys/keys.e2e-spec.ts +++ b/src/http/keys/keys.e2e-spec.ts @@ -97,8 +97,8 @@ describe('KeyController (e2e)', () => { await keysStorageService.save(keys); // lets save modules - await moduleStorageService.upsert(dvtModule, 1); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); + await moduleStorageService.upsert(curatedModule, 1, ''); }); afterAll(async () => { @@ -119,6 +119,7 @@ describe('KeyController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -136,6 +137,7 @@ describe('KeyController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -152,6 +154,7 @@ describe('KeyController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -170,6 +173,7 @@ describe('KeyController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -188,6 +192,7 @@ describe('KeyController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -202,6 +207,7 @@ describe('KeyController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -242,6 +248,7 @@ describe('KeyController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -271,7 +278,7 @@ describe('KeyController (e2e)', () => { // lets save keys await keysStorageService.save(keys); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(curatedModule, 1, ''); const resp = await request(app.getHttpServer()).get('/v1/keys'); expect(resp.status).toEqual(425); @@ -307,7 +314,7 @@ describe('KeyController (e2e)', () => { it('should return too early response if there are no meta', async () => { // lets save keys await keysStorageService.save(keys); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(curatedModule, 1, ''); const pubkeys = [keys[0].key, keys[1].key]; const resp = await request(app.getHttpServer()) .post(`/v1/keys/find`) @@ -327,8 +334,8 @@ describe('KeyController (e2e)', () => { await keysStorageService.save(keys); // lets save modules - await moduleStorageService.upsert(dvtModule, 1); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); + await moduleStorageService.upsert(curatedModule, 1, ''); }); afterAll(async () => { @@ -355,6 +362,7 @@ describe('KeyController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -375,6 +383,7 @@ describe('KeyController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -418,7 +427,7 @@ describe('KeyController (e2e)', () => { it('should return too early response if there are no meta', async () => { // lets save keys await keysStorageService.save(keys); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(curatedModule, 1, ''); const resp = await request(app.getHttpServer()).get(`/v1/keys/wrongkey`); expect(resp.status).toEqual(425); expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); @@ -432,8 +441,8 @@ describe('KeyController (e2e)', () => { // lets save keys await keysStorageService.save(keys); // lets save modules - await moduleStorageService.upsert(dvtModule, 1); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); + await moduleStorageService.upsert(curatedModule, 1, ''); }); afterAll(async () => { @@ -455,6 +464,7 @@ describe('KeyController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); diff --git a/src/http/operator.fixtures.ts b/src/http/operator.fixtures.ts index 8efd5515..605214a8 100644 --- a/src/http/operator.fixtures.ts +++ b/src/http/operator.fixtures.ts @@ -10,10 +10,12 @@ import { curatedModuleAddressWithCheckSum, dvtModuleAddressWithChecksum } from ' export const dvtOperatorsResp: Operator[] = [operatorOneDvt, operatorTwoDvt].map((op) => ({ ...op, + finalizedUsedSigningKeys: undefined, moduleAddress: dvtModuleAddressWithChecksum, })); export const curatedOperatorsResp: Operator[] = [operatorOneCurated, operatorTwoCurated].map((op) => ({ ...op, + finalizedUsedSigningKeys: undefined, moduleAddress: curatedModuleAddressWithCheckSum, })); diff --git a/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts b/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts index 5b505373..ecc5d802 100644 --- a/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts +++ b/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts @@ -108,8 +108,8 @@ describe('SRModulesKeysController (e2e)', () => { // lets save keys await keysStorageService.save(keys); // lets save modules - await moduleStorageService.upsert(dvtModule, 1); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); + await moduleStorageService.upsert(curatedModule, 1, ''); }); afterAll(async () => { @@ -127,6 +127,7 @@ describe('SRModulesKeysController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -166,6 +167,7 @@ describe('SRModulesKeysController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -185,6 +187,7 @@ describe('SRModulesKeysController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -202,6 +205,7 @@ describe('SRModulesKeysController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -236,7 +240,7 @@ describe('SRModulesKeysController (e2e)', () => { }); it('Should return too early response if there are no meta', async () => { - await moduleStorageService.upsert(dvtModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); const resp = await request(app.getHttpServer()).get(`/v1/modules/${dvtModule.moduleId}/keys`); expect(resp.status).toEqual(425); expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); @@ -252,8 +256,8 @@ describe('SRModulesKeysController (e2e)', () => { // lets save keys await keysStorageService.save(keys); // lets save modules - await moduleStorageService.upsert(dvtModule, 1); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); + await moduleStorageService.upsert(curatedModule, 1, ''); }); afterAll(async () => { @@ -278,6 +282,7 @@ describe('SRModulesKeysController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -298,6 +303,7 @@ describe('SRModulesKeysController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -326,7 +332,7 @@ describe('SRModulesKeysController (e2e)', () => { }); it('Should return too early response if there are no meta', async () => { - await moduleStorageService.upsert(dvtModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); const resp = await request(app.getHttpServer()) .post(`/v1/modules/${dvtModule.moduleId}/keys/find`) .set('Content-Type', 'application/json') @@ -345,8 +351,8 @@ describe('SRModulesKeysController (e2e)', () => { // lets save keys await keysStorageService.save(keys); // lets save modules - await moduleStorageService.upsert(dvtModule, 1); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); + await moduleStorageService.upsert(curatedModule, 1, ''); }); afterAll(async () => { @@ -363,6 +369,7 @@ describe('SRModulesKeysController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -403,6 +410,7 @@ describe('SRModulesKeysController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -427,6 +435,7 @@ describe('SRModulesKeysController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -444,6 +453,7 @@ describe('SRModulesKeysController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -458,7 +468,7 @@ describe('SRModulesKeysController (e2e)', () => { }); it('Should return too early response if there are no meta', async () => { - await moduleStorageService.upsert(dvtModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); const resp = await request(app.getHttpServer()).get(`/v1/modules/keys`); expect(resp.status).toEqual(425); expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); diff --git a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts index 4aa1ddb3..b933448c 100644 --- a/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts +++ b/src/http/sr-modules-operators-keys/sr-modules-operators-keys.e2e-spec.ts @@ -106,8 +106,8 @@ describe('SRModulesOperatorsKeysController (e2e)', () => { // lets save operators await operatorsStorageService.save(operators); // lets save modules - await moduleStorageService.upsert(dvtModule, 1); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); + await moduleStorageService.upsert(curatedModule, 1, ''); }); afterAll(async () => { @@ -129,6 +129,7 @@ describe('SRModulesOperatorsKeysController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }, operator: dvtOperatorsResp[0], @@ -184,6 +185,7 @@ describe('SRModulesOperatorsKeysController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -225,6 +227,7 @@ describe('SRModulesOperatorsKeysController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -246,6 +249,7 @@ describe('SRModulesOperatorsKeysController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -264,6 +268,7 @@ describe('SRModulesOperatorsKeysController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -298,7 +303,7 @@ describe('SRModulesOperatorsKeysController (e2e)', () => { }); it('should return too early response if there are no meta', async () => { - await moduleStorageService.upsert(dvtModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); const resp = await request(app.getHttpServer()).get(`/v1/modules/${dvtModule.moduleId}/operators/keys`); expect(resp.status).toEqual(425); expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); diff --git a/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts b/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts index 11a3c76a..7ec2ea94 100644 --- a/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts +++ b/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts @@ -110,8 +110,8 @@ describe('SRModuleOperatorsController (e2e)', () => { await operatorsStorageService.save(operators); // lets save modules - await moduleStorageService.upsert(dvtModule, 1); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); + await moduleStorageService.upsert(curatedModule, 1, ''); }); afterAll(async () => { @@ -131,6 +131,7 @@ describe('SRModuleOperatorsController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -159,7 +160,7 @@ describe('SRModuleOperatorsController (e2e)', () => { it('should return too early response if there are no meta', async () => { // lets save operators await operatorsStorageService.save(operators); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(curatedModule, 1, ''); const resp = await request(app.getHttpServer()).get('/v1/operators'); expect(resp.status).toEqual(425); @@ -177,8 +178,8 @@ describe('SRModuleOperatorsController (e2e)', () => { await operatorsStorageService.save(operators); // lets save modules - await moduleStorageService.upsert(dvtModule, 1); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); + await moduleStorageService.upsert(curatedModule, 1, ''); }); afterAll(async () => { @@ -204,6 +205,7 @@ describe('SRModuleOperatorsController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); @@ -217,6 +219,7 @@ describe('SRModuleOperatorsController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -256,7 +259,7 @@ describe('SRModuleOperatorsController (e2e)', () => { it('should return too early response if there are no meta', async () => { // lets save operators await operatorsStorageService.save(operators); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(curatedModule, 1, ''); const resp = await request(app.getHttpServer()).get(`/v1/modules/${curatedModule.moduleId}/operators`); expect(resp.status).toEqual(425); @@ -274,8 +277,8 @@ describe('SRModuleOperatorsController (e2e)', () => { await operatorsStorageService.save(operators); // lets save modules - await moduleStorageService.upsert(dvtModule, 1); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); + await moduleStorageService.upsert(curatedModule, 1, ''); }); afterAll(async () => { @@ -295,6 +298,7 @@ describe('SRModuleOperatorsController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -352,7 +356,7 @@ describe('SRModuleOperatorsController (e2e)', () => { it('should return too early response if there are no meta', async () => { // lets save operators await operatorsStorageService.save(operators); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(curatedModule, 1, ''); const resp = await request(app.getHttpServer()).get(`/v1/modules/${curatedModule.moduleId}/operators/1`); expect(resp.status).toEqual(425); diff --git a/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts b/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts index fc743328..13813b80 100644 --- a/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts +++ b/src/http/sr-modules-validators/sr-modules-validators.e2e-spec.ts @@ -146,8 +146,8 @@ describe('SRModulesValidatorsController (e2e)', () => { // lets save keys await keysStorageService.save(keys); // lets save modules - await moduleStorageService.upsert(dvtModule, 1); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); + await moduleStorageService.upsert(curatedModule, 1, ''); await validatorsRegistry.update(slot); }); @@ -347,7 +347,7 @@ describe('SRModulesValidatorsController (e2e)', () => { }); it('Should return too early response if there are no meta', async () => { - await moduleStorageService.upsert(dvtModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); const resp = await request(app.getHttpServer()).get( `/v1/modules/${dvtModule.moduleId}/validators/validator-exits-to-prepare/1`, ); @@ -363,8 +363,8 @@ describe('SRModulesValidatorsController (e2e)', () => { // lets save keys await keysStorageService.save(keys); // lets save modules - await moduleStorageService.upsert(dvtModule, 1); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); + await moduleStorageService.upsert(curatedModule, 1, ''); await validatorsRegistry.update(slot); }); @@ -565,7 +565,7 @@ describe('SRModulesValidatorsController (e2e)', () => { }); it('Should return too early response if there are no meta', async () => { - await moduleStorageService.upsert(dvtModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); const resp = await request(app.getHttpServer()).get( `/v1/modules/${dvtModule.moduleId}/validators/generate-unsigned-exit-messages/1`, ); diff --git a/src/http/sr-modules/sr-modules.e2e-spec.ts b/src/http/sr-modules/sr-modules.e2e-spec.ts index 47b3de5c..410de1e7 100644 --- a/src/http/sr-modules/sr-modules.e2e-spec.ts +++ b/src/http/sr-modules/sr-modules.e2e-spec.ts @@ -91,8 +91,8 @@ describe('SRModulesController (e2e)', () => { await elMetaStorageService.update(elMeta); // lets save modules - await moduleStorageService.upsert(dvtModule, 1); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); + await moduleStorageService.upsert(curatedModule, 1, ''); }); afterAll(async () => { @@ -110,6 +110,7 @@ describe('SRModulesController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }); }); }); @@ -131,7 +132,7 @@ describe('SRModulesController (e2e)', () => { }); it('Should return too early response if there are no meta', async () => { - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(curatedModule, 1, ''); const resp = await request(app.getHttpServer()).get('/v1/modules'); expect(resp.status).toEqual(425); expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); @@ -145,8 +146,8 @@ describe('SRModulesController (e2e)', () => { // lets save meta await elMetaStorageService.update(elMeta); // lets save modules - await moduleStorageService.upsert(dvtModule, 1); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(dvtModule, 1, ''); + await moduleStorageService.upsert(curatedModule, 1, ''); }); afterAll(async () => { await cleanDB(); @@ -160,6 +161,7 @@ describe('SRModulesController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }); }); @@ -171,6 +173,7 @@ describe('SRModulesController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }); }); @@ -182,6 +185,7 @@ describe('SRModulesController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }); }); @@ -215,7 +219,7 @@ describe('SRModulesController (e2e)', () => { }); it('Should return too early response if there are no meta', async () => { - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(curatedModule, 1, ''); const resp = await request(app.getHttpServer()).get(`/v1/modules/${curatedModule.moduleId}`); expect(resp.status).toEqual(425); expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); diff --git a/src/jobs/keys-update/keys-update.fixtures.ts b/src/jobs/keys-update/keys-update.fixtures.ts index f5ef37d3..5b70abfa 100644 --- a/src/jobs/keys-update/keys-update.fixtures.ts +++ b/src/jobs/keys-update/keys-update.fixtures.ts @@ -13,6 +13,7 @@ export const stakingModuleFixture: StakingModule = { exitedValidatorsCount: 10, type: 'curated', active: true, + lastChangedBlockHash: '', }; export const stakingModuleFixtures: StakingModule[] = [ @@ -30,5 +31,6 @@ export const stakingModuleFixtures: StakingModule[] = [ exitedValidatorsCount: 5, type: 'dvt', active: false, + lastChangedBlockHash: '', }, ]; diff --git a/src/staking-router-modules/contracts/staking-router/staking-router-fetch.service.ts b/src/staking-router-modules/contracts/staking-router/staking-router-fetch.service.ts index 4e167654..92b86825 100644 --- a/src/staking-router-modules/contracts/staking-router/staking-router-fetch.service.ts +++ b/src/staking-router-modules/contracts/staking-router/staking-router-fetch.service.ts @@ -64,6 +64,7 @@ export class StakingRouterFetchService { lastDepositBlock: stakingModule.lastDepositBlock.toNumber(), exitedValidatorsCount: stakingModule.exitedValidatorsCount.toNumber(), active: isActive, + lastChangedBlockHash: '', }; }), ); diff --git a/src/staking-router-modules/interfaces/staking-module.interface.ts b/src/staking-router-modules/interfaces/staking-module.interface.ts index ac77a3ea..83a8ceb5 100644 --- a/src/staking-router-modules/interfaces/staking-module.interface.ts +++ b/src/staking-router-modules/interfaces/staking-module.interface.ts @@ -27,6 +27,8 @@ export interface StakingModule { type: string; //STAKING_MODULE_TYPE; // is module active active: boolean; + // last changed block hash + lastChangedBlockHash: string; } export interface StakingModuleInterface { diff --git a/src/storage/sr-module.storage.e2e-spec.ts b/src/storage/sr-module.storage.e2e-spec.ts index 20e79403..7dd0d392 100644 --- a/src/storage/sr-module.storage.e2e-spec.ts +++ b/src/storage/sr-module.storage.e2e-spec.ts @@ -35,7 +35,7 @@ describe('Staking Module Storage', () => { test('add new module in empty database', async () => { const nonce = 1; - await srModuleStorageService.upsert(curatedModule, nonce); + await srModuleStorageService.upsert(curatedModule, nonce, ''); const updatesStakingModules0 = await srModuleStorageService.findAll(); const stakingModule0 = updatesStakingModules0[0]; @@ -56,7 +56,7 @@ describe('Staking Module Storage', () => { expect(stakingModule0.active).toEqual(curatedModule.active); const dvtNonce = 2; - await srModuleStorageService.upsert(dvtModule, dvtNonce); + await srModuleStorageService.upsert(dvtModule, dvtNonce, ''); const updatesStakingModules1 = await srModuleStorageService.findAll(); expect(updatesStakingModules1.length).toEqual(2); const stakingModule1 = updatesStakingModules1[1]; @@ -78,7 +78,7 @@ describe('Staking Module Storage', () => { test('update existing module', async () => { const nonce = 1; - await srModuleStorageService.upsert(curatedModule, nonce); + await srModuleStorageService.upsert(curatedModule, nonce, ''); const initialStakingModules = await srModuleStorageService.findAll(); const initialStakingModulesAmount = initialStakingModules.length; const stakingModule0 = initialStakingModules[0]; @@ -99,7 +99,7 @@ describe('Staking Module Storage', () => { expect(stakingModule0.active).toEqual(curatedModule.active); const updatedNonce = 12; - await srModuleStorageService.upsert(updatedCuratedModule, updatedNonce); + await srModuleStorageService.upsert(updatedCuratedModule, updatedNonce, ''); const updatedStakingModules = await srModuleStorageService.findAll(); const updatedStakingModulesAmount = updatedStakingModules.length; @@ -129,8 +129,8 @@ describe('Staking Module Storage', () => { test('check search by contract address', async () => { const nonce = 1; - await srModuleStorageService.upsert(curatedModule, nonce); - await srModuleStorageService.upsert(dvtModule, nonce); + await srModuleStorageService.upsert(curatedModule, nonce, ''); + await srModuleStorageService.upsert(dvtModule, nonce, ''); const curatedModuleDb = await srModuleStorageService.findOneByContractAddress(curatedModule.stakingModuleAddress); const dvtModuleDb = await srModuleStorageService.findOneByContractAddress(dvtModule.stakingModuleAddress); @@ -143,6 +143,7 @@ describe('Staking Module Storage', () => { await srModuleStorageService.upsert( { ...curatedModule, stakingModuleAddress: '0xDC64A140AA3E981100A9BECA4E685F962F0CF6C9' }, nonce, + '', ); const stakingModuleNull = await srModuleStorageService.findOneByContractAddress( '0xDC64A140AA3E981100A9BECA4E685F962F0CF6C9', From 60efe6326053a5eb9c50d13d1f0cf17a46ac52b5 Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 23:39:55 +0100 Subject: [PATCH 56/75] fix: reorg spec --- src/jobs/keys-update/test/update-cases.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/jobs/keys-update/test/update-cases.spec.ts b/src/jobs/keys-update/test/update-cases.spec.ts index dc990a65..1e6a7aad 100644 --- a/src/jobs/keys-update/test/update-cases.spec.ts +++ b/src/jobs/keys-update/test/update-cases.spec.ts @@ -151,8 +151,11 @@ describe('update cases', () => { jest .spyOn(executionProviderService, 'getFullBlock') - .mockImplementation(async () => ({ number: 2, hash: '0x2', timestamp: 1, parentHash: '0x1' } as any)); + .mockImplementationOnce(async () => ({ number: 2, hash: '0x2', timestamp: 1, parentHash: '0x111' } as any)); + jest + .spyOn(executionProviderService, 'getFullBlock') + .mockImplementationOnce(async () => ({ number: 2, hash: '0x1', timestamp: 1, parentHash: '0x111' } as any)); await updaterService.updateStakingModules({ currElMeta: { number: 2, hash: '0x2', timestamp: 1 }, prevElMeta: { blockNumber: 2, blockHash: '0x1', timestamp: 1 }, From 8f8bb45d17279a7802769547cff8f39a1935894b Mon Sep 17 00:00:00 2001 From: Eddort Date: Wed, 20 Dec 2023 23:47:35 +0100 Subject: [PATCH 57/75] fix: sql e2e spec --- .../test/validator-registry/connect.e2e-spec.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/common/registry/test/validator-registry/connect.e2e-spec.ts b/src/common/registry/test/validator-registry/connect.e2e-spec.ts index c4ae0b7d..36f294e3 100644 --- a/src/common/registry/test/validator-registry/connect.e2e-spec.ts +++ b/src/common/registry/test/validator-registry/connect.e2e-spec.ts @@ -5,8 +5,7 @@ import { BatchProviderModule, ExtendedJsonRpcBatchProvider } from '@lido-nestjs/ import { ValidatorRegistryModule, ValidatorRegistryService, RegistryStorageService } from '../../'; -import { clearDb, compareTestOperators } from '../testing.utils'; - +import { clearDb, compareTestOperators, mikroORMConfig } from '../testing.utils'; import { operators } from '../fixtures/connect.fixture'; import { MikroORM } from '@mikro-orm/core'; import { REGISTRY_CONTRACT_ADDRESSES } from '@lido-nestjs/contracts'; @@ -31,12 +30,7 @@ describe('Registry', () => { beforeEach(async () => { const imports = [ - MikroOrmModule.forRoot({ - dbName: ':memory:', - type: 'sqlite', - allowGlobalContext: true, - entities: ['./**/*.entity.ts'], - }), + MikroOrmModule.forRoot(mikroORMConfig), BatchProviderModule.forRoot({ url: process.env.PROVIDERS_URLS as string, requestPolicy: { From ac084c59cab2e3c0f2e64903ea0a04481bcc67e6 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 21 Dec 2023 12:19:50 +0100 Subject: [PATCH 58/75] test: debug e2e --- src/common/registry/test/key-registry/connect.e2e-spec.ts | 2 +- src/common/registry/test/validator-registry/connect.e2e-spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/registry/test/key-registry/connect.e2e-spec.ts b/src/common/registry/test/key-registry/connect.e2e-spec.ts index 84cef1a2..63e3c898 100644 --- a/src/common/registry/test/key-registry/connect.e2e-spec.ts +++ b/src/common/registry/test/key-registry/connect.e2e-spec.ts @@ -56,7 +56,7 @@ describe('Registry', () => { await storageService.onModuleDestroy(); }); - test('Update', async () => { + test.skip('Update', async () => { const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; await registryService.update(address, blockHash); diff --git a/src/common/registry/test/validator-registry/connect.e2e-spec.ts b/src/common/registry/test/validator-registry/connect.e2e-spec.ts index 36f294e3..8bad0ade 100644 --- a/src/common/registry/test/validator-registry/connect.e2e-spec.ts +++ b/src/common/registry/test/validator-registry/connect.e2e-spec.ts @@ -62,7 +62,7 @@ describe('Registry', () => { await storageService.onModuleDestroy(); }); - test('Update', async () => { + test.skip('Update', async () => { const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; await registryService.update(address, blockHash); From 91ef826a05b2a3228b66e3673a26b74626907e30 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 21 Dec 2023 12:30:39 +0100 Subject: [PATCH 59/75] fix: chronix tests --- src/app/simple-dvt-deploy.e2e-chain.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/simple-dvt-deploy.e2e-chain.ts b/src/app/simple-dvt-deploy.e2e-chain.ts index 37a2c817..a47f75b4 100644 --- a/src/app/simple-dvt-deploy.e2e-chain.ts +++ b/src/app/simple-dvt-deploy.e2e-chain.ts @@ -9,7 +9,7 @@ import { RegistryKeyStorageService } from '../common/registry'; import { ElMetaStorageService } from '../storage/el-meta.storage'; import { SRModuleStorageService } from '../storage/sr-module.storage'; import { KeysUpdateService } from '../jobs/keys-update'; -import { ExecutionProvider, ExecutionProviderService } from '../common/execution-provider'; +import { ExecutionProvider } from '../common/execution-provider'; import { ConfigService } from '../common/config'; import { PrometheusService } from '../common/prometheus'; import { StakingRouterService } from '../staking-router-modules/staking-router.service'; @@ -64,8 +64,6 @@ describe('Simple DVT deploy', () => { }); moduleRef = await Test.createTestingModule({ imports: [AppModule] }) - .overrideProvider(ExecutionProviderService) - .useValue(session.provider) .overrideProvider(SimpleFallbackJsonRpcBatchProvider) .useValue(session.provider) .overrideProvider(ExecutionProvider) @@ -89,7 +87,9 @@ describe('Simple DVT deploy', () => { .overrideProvider(ConfigService) .useValue({ get(path) { - const conf = { LIDO_LOCATOR_ADDRESS: '0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb' }; + const conf = { + LIDO_LOCATOR_ADDRESS: '0xC1d0b3DE6792Bf6b4b37EccdcC24e45978Cfd2Eb', + }; return conf[path]; }, }) From 05d25535359b01f02d315fabe5fc4a8369628f73 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 21 Dec 2023 18:38:43 +0100 Subject: [PATCH 60/75] fix: e2e connects tests --- src/common/registry/fetch/operator.fetch.ts | 23 +++++++++++++++---- .../test/key-registry/connect.e2e-spec.ts | 13 +++++++---- .../validator-registry/connect.e2e-spec.ts | 19 +++++++++++---- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/common/registry/fetch/operator.fetch.ts b/src/common/registry/fetch/operator.fetch.ts index 223e7090..72fcf4ed 100644 --- a/src/common/registry/fetch/operator.fetch.ts +++ b/src/common/registry/fetch/operator.fetch.ts @@ -59,12 +59,27 @@ export class RegistryOperatorFetchService { return false; } + /** return blockTag for finalized block, it need for testing purposes */ + public getFinalizedBlockTag() { + return 'finalized'; + } + /** fetches number of operators */ public async count(moduleAddress: string, overrides: CallOverrides = {}): Promise { const bigNumber = await this.getContract(moduleAddress).getNodeOperatorsCount(overrides as any); return bigNumber.toNumber(); } + /** fetches finalized operator */ + public async getFinalizedNodeOperator(moduleAddress: string, operatorIndex: number) { + const fullInfo = true; + const contract = this.getContract(moduleAddress); + const finalizedOperator = await contract.getNodeOperator(operatorIndex, fullInfo, { + blockTag: this.getFinalizedBlockTag(), + }); + return finalizedOperator; + } + /** fetches one operator */ public async fetchOne( moduleAddress: string, @@ -75,9 +90,6 @@ export class RegistryOperatorFetchService { const contract = this.getContract(moduleAddress); const operator = await contract.getNodeOperator(operatorIndex, fullInfo, overrides as any); - const finalizedOperator = await contract.getNodeOperator(operatorIndex, fullInfo, { - blockTag: 'finalized', - }); const { name, @@ -89,7 +101,10 @@ export class RegistryOperatorFetchService { totalDepositedValidators, } = operator; - const { totalDepositedValidators: finalizedUsedSigningKeys } = finalizedOperator; + const { totalDepositedValidators: finalizedUsedSigningKeys } = await this.getFinalizedNodeOperator( + moduleAddress, + operatorIndex, + ); return { index: operatorIndex, diff --git a/src/common/registry/test/key-registry/connect.e2e-spec.ts b/src/common/registry/test/key-registry/connect.e2e-spec.ts index 63e3c898..7f407314 100644 --- a/src/common/registry/test/key-registry/connect.e2e-spec.ts +++ b/src/common/registry/test/key-registry/connect.e2e-spec.ts @@ -2,7 +2,7 @@ import { Test } from '@nestjs/testing'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { nullTransport, LoggerModule } from '@lido-nestjs/logger'; import { BatchProviderModule, ExtendedJsonRpcBatchProvider } from '@lido-nestjs/execution'; -import { KeyRegistryModule, KeyRegistryService, RegistryStorageService } from '../../'; +import { KeyRegistryModule, KeyRegistryService, RegistryOperatorFetchService, RegistryStorageService } from '../../'; import { clearDb, compareTestOperators, mikroORMConfig } from '../testing.utils'; import { operators } from '../fixtures/connect.fixture'; import { MikroORM } from '@mikro-orm/core'; @@ -14,6 +14,7 @@ dotenv.config(); describe('Registry', () => { let registryService: KeyRegistryService; let storageService: RegistryStorageService; + let registryOperatorFetchService: RegistryOperatorFetchService; let mikroOrm: MikroORM; if (!process.env.CHAIN_ID) { console.error("CHAIN_ID wasn't provides"); @@ -24,6 +25,8 @@ describe('Registry', () => { return { ...key, moduleAddress: address }; }); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + beforeEach(async () => { const imports = [ MikroOrmModule.forRoot(mikroORMConfig), @@ -46,7 +49,11 @@ describe('Registry', () => { const moduleRef = await Test.createTestingModule({ imports }).compile(); registryService = moduleRef.get(KeyRegistryService); storageService = moduleRef.get(RegistryStorageService); + registryOperatorFetchService = moduleRef.get(RegistryOperatorFetchService); mikroOrm = moduleRef.get(MikroORM); + + jest.spyOn(registryOperatorFetchService, 'getFinalizedBlockTag').mockImplementation(() => ({ blockHash } as any)); + const generator = mikroOrm.getSchemaGenerator(); await generator.updateSchema(); }); @@ -56,9 +63,7 @@ describe('Registry', () => { await storageService.onModuleDestroy(); }); - test.skip('Update', async () => { - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - + test('Update', async () => { await registryService.update(address, blockHash); await compareTestOperators(address, registryService, { diff --git a/src/common/registry/test/validator-registry/connect.e2e-spec.ts b/src/common/registry/test/validator-registry/connect.e2e-spec.ts index 8bad0ade..c25644a6 100644 --- a/src/common/registry/test/validator-registry/connect.e2e-spec.ts +++ b/src/common/registry/test/validator-registry/connect.e2e-spec.ts @@ -3,7 +3,12 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { nullTransport, LoggerModule } from '@lido-nestjs/logger'; import { BatchProviderModule, ExtendedJsonRpcBatchProvider } from '@lido-nestjs/execution'; -import { ValidatorRegistryModule, ValidatorRegistryService, RegistryStorageService } from '../../'; +import { + ValidatorRegistryModule, + ValidatorRegistryService, + RegistryStorageService, + RegistryOperatorFetchService, +} from '../../'; import { clearDb, compareTestOperators, mikroORMConfig } from '../testing.utils'; import { operators } from '../fixtures/connect.fixture'; @@ -15,6 +20,7 @@ dotenv.config(); describe('Registry', () => { let registryService: ValidatorRegistryService; + let registryOperatorFetchService: RegistryOperatorFetchService; let mikroOrm: MikroORM; let storageService: RegistryStorageService; @@ -28,6 +34,8 @@ describe('Registry', () => { return { ...key, moduleAddress: address }; }); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + beforeEach(async () => { const imports = [ MikroOrmModule.forRoot(mikroORMConfig), @@ -50,8 +58,11 @@ describe('Registry', () => { const moduleRef = await Test.createTestingModule({ imports }).compile(); registryService = moduleRef.get(ValidatorRegistryService); storageService = moduleRef.get(RegistryStorageService); - + registryOperatorFetchService = moduleRef.get(RegistryOperatorFetchService); mikroOrm = moduleRef.get(MikroORM); + + jest.spyOn(registryOperatorFetchService, 'getFinalizedBlockTag').mockImplementation(() => ({ blockHash } as any)); + const generator = mikroOrm.getSchemaGenerator(); await generator.updateSchema(); }); @@ -62,9 +73,7 @@ describe('Registry', () => { await storageService.onModuleDestroy(); }); - test.skip('Update', async () => { - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - + test('Update', async () => { await registryService.update(address, blockHash); await compareTestOperators(address, registryService, { From 66ccb6685af1eeea99d8e9ac5f29664375a8ec27 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 21 Dec 2023 18:57:57 +0100 Subject: [PATCH 61/75] fix: 425 error while fetching modules --- src/staking-router-modules/staking-router.service.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/staking-router-modules/staking-router.service.ts b/src/staking-router-modules/staking-router.service.ts index 89659d43..bf4e27d5 100644 --- a/src/staking-router-modules/staking-router.service.ts +++ b/src/staking-router-modules/staking-router.service.ts @@ -70,11 +70,12 @@ export class StakingRouterService { const { stakingModules, elBlockSnapshot } = await this.entityManager.transactional( async () => { const stakingModules = await this.getStakingModules(stakingModuleAddresses); - - if (stakingModules.length === 0) { - this.logger.warn("No staking modules in list. Maybe didn't fetched from SR yet"); - throw httpExceptionTooEarlyResp(); - } + // TODO: Can I just delete this check? + // TODO: approve from Anna + // if (stakingModules.length === 0) { + // this.logger.warn("No staking modules in list. Maybe didn't fetched from SR yet"); + // throw httpExceptionTooEarlyResp(); + // } const elBlockSnapshot = await this.getElBlockSnapshot(); From 959980086af3b67ee044d7a683b1d0eb78077284 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 21 Dec 2023 19:05:10 +0100 Subject: [PATCH 62/75] fix: 425 http error --- src/staking-router-modules/staking-router.service.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/staking-router-modules/staking-router.service.ts b/src/staking-router-modules/staking-router.service.ts index bf4e27d5..6bcb0552 100644 --- a/src/staking-router-modules/staking-router.service.ts +++ b/src/staking-router-modules/staking-router.service.ts @@ -70,12 +70,12 @@ export class StakingRouterService { const { stakingModules, elBlockSnapshot } = await this.entityManager.transactional( async () => { const stakingModules = await this.getStakingModules(stakingModuleAddresses); - // TODO: Can I just delete this check? - // TODO: approve from Anna - // if (stakingModules.length === 0) { - // this.logger.warn("No staking modules in list. Maybe didn't fetched from SR yet"); - // throw httpExceptionTooEarlyResp(); - // } + + // If the target query involves retrieving module-specific data, we do not throw the 425 exception + if (stakingModules.length === 0 && !stakingModuleAddresses) { + this.logger.warn("No staking modules in list. Maybe didn't fetched from SR yet"); + throw httpExceptionTooEarlyResp(); + } const elBlockSnapshot = await this.getElBlockSnapshot(); From e9d22498cce21996a32b69bb4ddf48b040f8693e Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 21 Dec 2023 23:32:43 +0100 Subject: [PATCH 63/75] fix: reorg detection --- src/jobs/keys-update/staking-module-updater.service.ts | 3 +++ src/jobs/keys-update/test/detect-reorg.spec.ts | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/jobs/keys-update/staking-module-updater.service.ts b/src/jobs/keys-update/staking-module-updater.service.ts index 544632d5..aad8ad25 100644 --- a/src/jobs/keys-update/staking-module-updater.service.ts +++ b/src/jobs/keys-update/staking-module-updater.service.ts @@ -175,6 +175,9 @@ export class StakingModuleUpdaterService { }), ); + if (blocks[0].hash !== prevBlockHash) return true; + if (blocks[blocks.length - 1].hash !== currentBlockHash) return true; + for (let i = 1; i < blocks.length; i++) { const previousBlock = blocks[i - 1]; const currentBlock = blocks[i]; diff --git a/src/jobs/keys-update/test/detect-reorg.spec.ts b/src/jobs/keys-update/test/detect-reorg.spec.ts index 1230d512..ab7a7521 100644 --- a/src/jobs/keys-update/test/detect-reorg.spec.ts +++ b/src/jobs/keys-update/test/detect-reorg.spec.ts @@ -117,7 +117,7 @@ describe('detect reorg', () => { it('check blockchain (happy pass)', async () => { const updaterState: UpdaterState = { - lastChangedBlockHash: '0x1', + lastChangedBlockHash: '0x0', isReorgDetected: false, }; @@ -139,7 +139,7 @@ describe('detect reorg', () => { } as any), ); - expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x1')).toBeFalsy(); + expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x100')).toBeFalsy(); expect(updaterState.isReorgDetected).toBeFalsy(); }); @@ -167,7 +167,7 @@ describe('detect reorg', () => { } as any), ); - expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x1')).toBeTruthy(); + expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x100')).toBeTruthy(); expect(updaterState.isReorgDetected).toBeTruthy(); }); }); From d72df8265a090cb89c83b6f5dc515c127f23e821 Mon Sep 17 00:00:00 2001 From: Eddort Date: Thu, 21 Dec 2023 23:42:30 +0100 Subject: [PATCH 64/75] refactor: better logs --- src/jobs/keys-update/keys-update.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jobs/keys-update/keys-update.service.ts b/src/jobs/keys-update/keys-update.service.ts index f5794111..a6cd5ee8 100644 --- a/src/jobs/keys-update/keys-update.service.ts +++ b/src/jobs/keys-update/keys-update.service.ts @@ -124,7 +124,7 @@ export class KeysUpdateService { } if (prevElMeta?.blockHash && prevElMeta.blockHash === currElMeta.hash) { - this.logger.debug?.('same state, skip', { prevElMeta, currElMeta }); + this.logger.log('Same blockHash, indexing is not required', { prevElMeta, currElMeta }); return; } From 754d6a490d026a7a0445eb2046ae9e6f39c8e446 Mon Sep 17 00:00:00 2001 From: Eddort Date: Sat, 23 Dec 2023 16:26:50 +0100 Subject: [PATCH 65/75] test: reorg handling in registry module --- src/common/registry/main/abstract-registry.ts | 4 +- .../test/key-registry/registry-update.spec.ts | 17 +-- src/common/registry/test/mock-utils.ts | 9 +- .../registry-update.spec.ts | 106 +++++++++++++++--- 4 files changed, 100 insertions(+), 36 deletions(-) diff --git a/src/common/registry/main/abstract-registry.ts b/src/common/registry/main/abstract-registry.ts index 99ebe7d5..5564a093 100644 --- a/src/common/registry/main/abstract-registry.ts +++ b/src/common/registry/main/abstract-registry.ts @@ -108,10 +108,10 @@ export abstract class AbstractRegistryService { const prevOperator = previousOperators[currentIndex] ?? null; const isSameOperator = compareOperators(prevOperator, currOperator); + const finalizedUsedSigningKeys = prevOperator ? prevOperator.finalizedUsedSigningKeys : null; // skip updating keys from 0 to `usedSigningKeys` of previous collected data // since the contract guarantees that these keys cannot be changed - const unchangedKeysMaxIndex = - isSameOperator && prevOperator.finalizedUsedSigningKeys ? prevOperator.finalizedUsedSigningKeys : 0; + const unchangedKeysMaxIndex = isSameOperator && finalizedUsedSigningKeys ? finalizedUsedSigningKeys : 0; // get the right border up to which the keys should be updated // it's different for different scenarios const toIndex = this.getToIndex(currOperator); diff --git a/src/common/registry/test/key-registry/registry-update.spec.ts b/src/common/registry/test/key-registry/registry-update.spec.ts index 7661bebf..90f8d47e 100644 --- a/src/common/registry/test/key-registry/registry-update.spec.ts +++ b/src/common/registry/test/key-registry/registry-update.spec.ts @@ -31,6 +31,8 @@ describe('Registry', () => { const CHAIN_ID = process.env.CHAIN_ID || 1; const address = REGISTRY_CONTRACT_ADDRESSES[CHAIN_ID]; + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + const keysWithModuleAddress = keys.map((key) => { return { ...key, moduleAddress: address }; }); @@ -90,8 +92,6 @@ describe('Registry', () => { operators: operatorsWithModuleAddress, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); expect(saveOperatorsRegistryMock).toBeCalledTimes(1); // 2 - number of operators @@ -116,8 +116,6 @@ describe('Registry', () => { operators: operatorsWithModuleAddress, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); expect(saveOperatorsRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); @@ -139,8 +137,6 @@ describe('Registry', () => { operators: newOperators, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); expect(saveOperatorsRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); @@ -163,8 +159,6 @@ describe('Registry', () => { operators: newOperators, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); expect(saveOperatorRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); @@ -189,8 +183,6 @@ describe('Registry', () => { operators: newOperators, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); expect(saveOperatorsRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); @@ -213,8 +205,6 @@ describe('Registry', () => { operators: newOperators, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); expect(saveOperatorsRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); @@ -235,7 +225,6 @@ describe('Registry', () => { keys: keysWithModuleAddress, operators: newOperators, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; await registryService.update(address, blockHash); expect(saveOperatorsRegistryMock).toBeCalledTimes(1); @@ -286,8 +275,6 @@ describe('Registry', () => { operators: newOperators, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); expect(saveOperatorsRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); diff --git a/src/common/registry/test/mock-utils.ts b/src/common/registry/test/mock-utils.ts index 955d1cae..b76912d6 100644 --- a/src/common/registry/test/mock-utils.ts +++ b/src/common/registry/test/mock-utils.ts @@ -19,12 +19,17 @@ export const registryServiceMock = ( { keys, operators }: Payload, ) => { const fetchBatchKey = moduleRef.get(RegistryKeyBatchFetchService); - jest + const fetchSigningKeysInBatchesMock = jest .spyOn(fetchBatchKey, 'fetchSigningKeysInBatches') .mockImplementation(async (moduleAddress, operatorIndex, fromIndex, totalAmount) => { return findKeys(keys, operatorIndex, fromIndex, totalAmount); }); const operatorFetch = moduleRef.get(RegistryOperatorFetchService); - jest.spyOn(operatorFetch, 'fetch').mockImplementation(async () => operators); + const operatorsMock = jest.spyOn(operatorFetch, 'fetch').mockImplementation(async () => operators); + + return () => { + fetchSigningKeysInBatchesMock.mockReset(); + operatorsMock.mockReset(); + }; }; diff --git a/src/common/registry/test/validator-registry/registry-update.spec.ts b/src/common/registry/test/validator-registry/registry-update.spec.ts index 5fe136a4..9b307ff9 100644 --- a/src/common/registry/test/validator-registry/registry-update.spec.ts +++ b/src/common/registry/test/validator-registry/registry-update.spec.ts @@ -23,6 +23,8 @@ import { registryServiceMock } from '../mock-utils'; import { MikroORM } from '@mikro-orm/core'; import { REGISTRY_CONTRACT_ADDRESSES } from '@lido-nestjs/contracts'; +const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + describe('Validator registry', () => { const provider = new JsonRpcBatchProvider(process.env.PROVIDERS_URLS); const CHAIN_ID = process.env.CHAIN_ID || 1; @@ -87,8 +89,6 @@ describe('Validator registry', () => { operators: operatorsWithModuleAddress, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); // update function doesn't make a decision about update no more // so here would happen update if list of keys was changed @@ -110,8 +110,6 @@ describe('Validator registry', () => { operators: operatorsWithModuleAddress, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock).toBeCalledTimes(2); @@ -129,8 +127,6 @@ describe('Validator registry', () => { operators: operatorsWithModuleAddress, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); expect(saveOperatorsRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); @@ -151,8 +147,6 @@ describe('Validator registry', () => { operators: newOperators, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); expect(saveOperatorsRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); @@ -172,8 +166,6 @@ describe('Validator registry', () => { operators: newOperators, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); @@ -197,8 +189,6 @@ describe('Validator registry', () => { operators: newOperators, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); expect(saveOperatorRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); @@ -224,8 +214,6 @@ describe('Validator registry', () => { operators: newOperators, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); @@ -246,8 +234,6 @@ describe('Validator registry', () => { operators: newOperators, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); expect(saveOperatorRegistryMock).toBeCalledTimes(1); expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); @@ -327,7 +313,6 @@ describe('Empty registry', () => { keys: keysWithModuleAddress, operators: operatorsWithModuleAddress, }); - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); @@ -339,3 +324,90 @@ describe('Empty registry', () => { await registryService.update(address, blockHash); }); }); + +describe('Reorg detection', () => { + const provider = new JsonRpcBatchProvider(process.env.PROVIDERS_URLS); + let registryService: ValidatorRegistryService; + let registryStorageService: RegistryStorageService; + let moduleRef: TestingModule; + const mockCall = jest.spyOn(provider, 'call').mockImplementation(async () => ''); + const CHAIN_ID = process.env.CHAIN_ID || 1; + const address = REGISTRY_CONTRACT_ADDRESSES[CHAIN_ID]; + let mikroOrm: MikroORM; + + jest.spyOn(provider, 'detectNetwork').mockImplementation(async () => getNetwork('mainnet')); + + beforeEach(async () => { + const imports = [ + MikroOrmModule.forRoot(mikroORMConfig), + MockLoggerModule.forRoot({ + log: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + }), + ValidatorRegistryModule.forFeature({ provider }), + ]; + moduleRef = await Test.createTestingModule({ + imports, + providers: [{ provide: LOGGER_PROVIDER, useValue: {} }], + }).compile(); + registryService = moduleRef.get(ValidatorRegistryService); + registryStorageService = moduleRef.get(RegistryStorageService); + mikroOrm = moduleRef.get(MikroORM); + const generator = mikroOrm.getSchemaGenerator(); + await generator.updateSchema(); + }); + + afterEach(async () => { + mockCall.mockReset(); + await clearDb(mikroOrm); + await registryStorageService.onModuleDestroy(); + }); + + test('init on update', async () => { + const saveRegistryMock = jest.spyOn(registryService, 'saveOperators'); + const saveKeyRegistryMock = jest.spyOn(registryService, 'saveKeys'); + const finalizedUsedSigningKeys = 1; + + const keysWithModuleAddress = keys.map((key) => { + return { ...key, moduleAddress: address }; + }); + + const operatorsWithModuleAddress = operators.map((key) => { + return { ...key, moduleAddress: address, finalizedUsedSigningKeys }; + }); + + const unrefMock = registryServiceMock(moduleRef, provider, { + keys: keysWithModuleAddress, + operators: operatorsWithModuleAddress, + }); + + await registryService.update(address, blockHash); + + expect(saveRegistryMock).toBeCalledTimes(1); + expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); + + await compareTestKeysAndOperators(address, registryService, { + keys: keysWithModuleAddress, + operators: operatorsWithModuleAddress, + }); + + unrefMock(); + + const keysWithSpoiledLeftEdge = clone(keysWithModuleAddress).map((key) => + key.index >= finalizedUsedSigningKeys ? { ...key } : { ...key, key: '', depositSignature: '' }, + ); + + registryServiceMock(moduleRef, provider, { + keys: keysWithSpoiledLeftEdge, + operators: operatorsWithModuleAddress, + }); + + await registryService.update(address, blockHash); + + await compareTestKeysAndOperators(address, registryService, { + keys: keysWithModuleAddress, + operators: operatorsWithModuleAddress, + }); + }); +}); From 998c17a31214c3a0de287471db8dfaf6689ce8dd Mon Sep 17 00:00:00 2001 From: Eddort Date: Sun, 24 Dec 2023 14:27:04 +0100 Subject: [PATCH 66/75] test: key registry --- .../test/key-registry/registry-update.spec.ts | 95 ++++++++++++++++++- .../registry-update.spec.ts | 4 +- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/src/common/registry/test/key-registry/registry-update.spec.ts b/src/common/registry/test/key-registry/registry-update.spec.ts index 90f8d47e..0ece5c5c 100644 --- a/src/common/registry/test/key-registry/registry-update.spec.ts +++ b/src/common/registry/test/key-registry/registry-update.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { nullTransport, LoggerModule } from '@lido-nestjs/logger'; +import { nullTransport, LoggerModule, MockLoggerModule, LOGGER_PROVIDER } from '@lido-nestjs/logger'; import { getNetwork } from '@ethersproject/networks'; import { JsonRpcBatchProvider } from '@ethersproject/providers'; import { @@ -26,13 +26,13 @@ import * as dotenv from 'dotenv'; dotenv.config(); +const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + describe('Registry', () => { const provider = new JsonRpcBatchProvider(process.env.PROVIDERS_URLS); const CHAIN_ID = process.env.CHAIN_ID || 1; const address = REGISTRY_CONTRACT_ADDRESSES[CHAIN_ID]; - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - const keysWithModuleAddress = keys.map((key) => { return { ...key, moduleAddress: address }; }); @@ -285,3 +285,92 @@ describe('Registry', () => { }); }); }); + +describe('Reorg detection', () => { + const provider = new JsonRpcBatchProvider(process.env.PROVIDERS_URLS); + let registryService: KeyRegistryService; + let registryStorageService: RegistryStorageService; + let moduleRef: TestingModule; + const mockCall = jest.spyOn(provider, 'call').mockImplementation(async () => ''); + const CHAIN_ID = process.env.CHAIN_ID || 1; + const address = REGISTRY_CONTRACT_ADDRESSES[CHAIN_ID]; + let mikroOrm: MikroORM; + + jest.spyOn(provider, 'detectNetwork').mockImplementation(async () => getNetwork('mainnet')); + + beforeEach(async () => { + const imports = [ + MikroOrmModule.forRoot(mikroORMConfig), + MockLoggerModule.forRoot({ + log: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + }), + KeyRegistryModule.forFeature({ provider }), + ]; + moduleRef = await Test.createTestingModule({ + imports, + providers: [{ provide: LOGGER_PROVIDER, useValue: {} }], + }).compile(); + registryService = moduleRef.get(KeyRegistryService); + registryStorageService = moduleRef.get(RegistryStorageService); + mikroOrm = moduleRef.get(MikroORM); + const generator = mikroOrm.getSchemaGenerator(); + await generator.updateSchema(); + }); + + afterEach(async () => { + mockCall.mockReset(); + await clearDb(mikroOrm); + await registryStorageService.onModuleDestroy(); + }); + + test('init on update', async () => { + const saveRegistryMock = jest.spyOn(registryService, 'saveOperators'); + const saveKeyRegistryMock = jest.spyOn(registryService, 'saveKeys'); + const finalizedUsedSigningKeys = 1; + + const keysWithModuleAddress = keys.map((key) => { + return { ...key, moduleAddress: address }; + }); + + const operatorsWithModuleAddress = operators.map((key) => { + return { ...key, moduleAddress: address, finalizedUsedSigningKeys }; + }); + + const unrefMock = registryServiceMock(moduleRef, provider, { + keys: keysWithModuleAddress, + operators: operatorsWithModuleAddress, + }); + + await registryService.update(address, blockHash); + + expect(saveRegistryMock).toBeCalledTimes(1); + expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(2); + + await compareTestKeysAndOperators(address, registryService, { + keys: keysWithModuleAddress, + operators: operatorsWithModuleAddress, + }); + + unrefMock(); + + // Let's corrupt the data below to make sure that + // the update method handles the left boundary correctly + const keysWithSpoiledLeftEdge = clone(keysWithModuleAddress).map((key) => + key.index >= finalizedUsedSigningKeys ? { ...key } : { ...key, key: '', depositSignature: '' }, + ); + + registryServiceMock(moduleRef, provider, { + keys: keysWithSpoiledLeftEdge, + operators: operatorsWithModuleAddress, + }); + + await registryService.update(address, blockHash); + + await compareTestKeysAndOperators(address, registryService, { + keys: keysWithModuleAddress, + operators: operatorsWithModuleAddress, + }); + }); +}); diff --git a/src/common/registry/test/validator-registry/registry-update.spec.ts b/src/common/registry/test/validator-registry/registry-update.spec.ts index 9b307ff9..96ff7f2f 100644 --- a/src/common/registry/test/validator-registry/registry-update.spec.ts +++ b/src/common/registry/test/validator-registry/registry-update.spec.ts @@ -385,7 +385,7 @@ describe('Reorg detection', () => { await registryService.update(address, blockHash); expect(saveRegistryMock).toBeCalledTimes(1); - expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(saveKeyRegistryMock.mock.calls.length).toBeGreaterThanOrEqual(2); await compareTestKeysAndOperators(address, registryService, { keys: keysWithModuleAddress, @@ -394,6 +394,8 @@ describe('Reorg detection', () => { unrefMock(); + // Let's corrupt the data below to make sure that + // the update method handles the left boundary correctly const keysWithSpoiledLeftEdge = clone(keysWithModuleAddress).map((key) => key.index >= finalizedUsedSigningKeys ? { ...key } : { ...key, key: '', depositSignature: '' }, ); From 0d01ef63ee946b9582feec8b3e8122ee36e0a1e6 Mon Sep 17 00:00:00 2001 From: Eddort Date: Sun, 24 Dec 2023 14:40:08 +0100 Subject: [PATCH 67/75] test: fix fixtures --- src/http/operator.fixtures.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/http/operator.fixtures.ts b/src/http/operator.fixtures.ts index 605214a8..8efd5515 100644 --- a/src/http/operator.fixtures.ts +++ b/src/http/operator.fixtures.ts @@ -10,12 +10,10 @@ import { curatedModuleAddressWithCheckSum, dvtModuleAddressWithChecksum } from ' export const dvtOperatorsResp: Operator[] = [operatorOneDvt, operatorTwoDvt].map((op) => ({ ...op, - finalizedUsedSigningKeys: undefined, moduleAddress: dvtModuleAddressWithChecksum, })); export const curatedOperatorsResp: Operator[] = [operatorOneCurated, operatorTwoCurated].map((op) => ({ ...op, - finalizedUsedSigningKeys: undefined, moduleAddress: curatedModuleAddressWithCheckSum, })); From d36ebca9e155e3329276da8f5dd842d5fff06aff Mon Sep 17 00:00:00 2001 From: Eddort Date: Sun, 24 Dec 2023 14:58:15 +0100 Subject: [PATCH 68/75] fix: e2e test --- src/http/operator.fixtures.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/http/operator.fixtures.ts b/src/http/operator.fixtures.ts index 8efd5515..605214a8 100644 --- a/src/http/operator.fixtures.ts +++ b/src/http/operator.fixtures.ts @@ -10,10 +10,12 @@ import { curatedModuleAddressWithCheckSum, dvtModuleAddressWithChecksum } from ' export const dvtOperatorsResp: Operator[] = [operatorOneDvt, operatorTwoDvt].map((op) => ({ ...op, + finalizedUsedSigningKeys: undefined, moduleAddress: dvtModuleAddressWithChecksum, })); export const curatedOperatorsResp: Operator[] = [operatorOneCurated, operatorTwoCurated].map((op) => ({ ...op, + finalizedUsedSigningKeys: undefined, moduleAddress: curatedModuleAddressWithCheckSum, })); From 39a42a1bb79341507607d4847697c807726c4957 Mon Sep 17 00:00:00 2001 From: Eddort Date: Sun, 24 Dec 2023 15:00:22 +0100 Subject: [PATCH 69/75] fix: httpExceptionTooEarlyResp --- src/staking-router-modules/staking-router.service.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/staking-router-modules/staking-router.service.ts b/src/staking-router-modules/staking-router.service.ts index 6bcb0552..02eb93f0 100644 --- a/src/staking-router-modules/staking-router.service.ts +++ b/src/staking-router-modules/staking-router.service.ts @@ -71,12 +71,6 @@ export class StakingRouterService { async () => { const stakingModules = await this.getStakingModules(stakingModuleAddresses); - // If the target query involves retrieving module-specific data, we do not throw the 425 exception - if (stakingModules.length === 0 && !stakingModuleAddresses) { - this.logger.warn("No staking modules in list. Maybe didn't fetched from SR yet"); - throw httpExceptionTooEarlyResp(); - } - const elBlockSnapshot = await this.getElBlockSnapshot(); if (!elBlockSnapshot) { From c60467ee1b10c471cd8d4efca8defd0bb467fa75 Mon Sep 17 00:00:00 2001 From: Eddort Date: Sun, 24 Dec 2023 15:21:42 +0100 Subject: [PATCH 70/75] fix: update log message --- src/jobs/keys-update/keys-update.service.ts | 2 +- src/jobs/keys-update/staking-module-updater.service.ts | 10 +++++----- src/jobs/keys-update/test/update-cases.spec.ts | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/jobs/keys-update/keys-update.service.ts b/src/jobs/keys-update/keys-update.service.ts index a6cd5ee8..aeaebb06 100644 --- a/src/jobs/keys-update/keys-update.service.ts +++ b/src/jobs/keys-update/keys-update.service.ts @@ -124,7 +124,7 @@ export class KeysUpdateService { } if (prevElMeta?.blockHash && prevElMeta.blockHash === currElMeta.hash) { - this.logger.log('Same blockHash, indexing is not required', { prevElMeta, currElMeta }); + this.logger.log('Same blockHash, updating is not required', { prevElMeta, currElMeta }); return; } diff --git a/src/jobs/keys-update/staking-module-updater.service.ts b/src/jobs/keys-update/staking-module-updater.service.ts index aad8ad25..8fa14a87 100644 --- a/src/jobs/keys-update/staking-module-updater.service.ts +++ b/src/jobs/keys-update/staking-module-updater.service.ts @@ -42,7 +42,7 @@ export class StakingModuleUpdaterService { this.logger.log(`Nonce previous value: ${prevNonce}, nonce current value: ${currNonce}`); if (!prevBlockHash) { - this.logger.log('No past state found, start indexing', { stakingModuleAddress, currentBlockHash }); + this.logger.log('No past state found, start updating', { stakingModuleAddress, currentBlockHash }); await this.updateStakingModule( updaterState, @@ -56,7 +56,7 @@ export class StakingModuleUpdaterService { } if (prevNonce !== currNonce) { - this.logger.log('Nonce has been changed, start indexing', { + this.logger.log('Nonce has been changed, start updating', { stakingModuleAddress, currentBlockHash, prevNonce, @@ -75,7 +75,7 @@ export class StakingModuleUpdaterService { } if (this.isTooMuchDiffBetweenBlocks(prevElMeta.blockNumber, currElMeta.number)) { - this.logger.log('Too much difference between the blocks, start indexing', { + this.logger.log('Too much difference between the blocks, start updating', { stakingModuleAddress, currentBlockHash, }); @@ -92,7 +92,7 @@ export class StakingModuleUpdaterService { } if (await this.isReorgDetected(updaterState, prevBlockHash, currentBlockHash)) { - this.logger.log('Reorg detected, start indexing', { stakingModuleAddress, currentBlockHash }); + this.logger.log('Reorg detected, start updating', { stakingModuleAddress, currentBlockHash }); await this.updateStakingModule( updaterState, @@ -129,7 +129,7 @@ export class StakingModuleUpdaterService { continue; } - this.logger.log('No changes have been detected in the module, indexing is not required', { + this.logger.log('No changes have been detected in the module, updating is not required', { stakingModuleAddress, currentBlockHash, }); diff --git a/src/jobs/keys-update/test/update-cases.spec.ts b/src/jobs/keys-update/test/update-cases.spec.ts index 1e6a7aad..11cbefa5 100644 --- a/src/jobs/keys-update/test/update-cases.spec.ts +++ b/src/jobs/keys-update/test/update-cases.spec.ts @@ -76,7 +76,7 @@ describe('update cases', () => { }); expect(mockUpdate).toBeCalledTimes(1); - expect(loggerService.log.mock.calls[1][0]).toBe('No past state found, start indexing'); + expect(loggerService.log.mock.calls[1][0]).toBe('No past state found, start updating'); }); it('More than 1 module processed', async () => { @@ -116,7 +116,7 @@ describe('update cases', () => { }); expect(mockUpdate).toBeCalledTimes(2); - expect(loggerService.log.mock.calls[1][0]).toBe('Nonce has been changed, start indexing'); + expect(loggerService.log.mock.calls[1][0]).toBe('Nonce has been changed, start updating'); }); it('Too much difference between the blocks', async () => { @@ -136,7 +136,7 @@ describe('update cases', () => { }); expect(mockUpdate).toBeCalledTimes(2); - expect(loggerService.log.mock.calls[1][0]).toBe('Too much difference between the blocks, start indexing'); + expect(loggerService.log.mock.calls[1][0]).toBe('Too much difference between the blocks, start updating'); }); it('Reorg detected', async () => { @@ -163,6 +163,6 @@ describe('update cases', () => { }); expect(mockUpdate).toBeCalledTimes(2); - expect(loggerService.log.mock.calls[1][0]).toBe('Reorg detected, start indexing'); + expect(loggerService.log.mock.calls[1][0]).toBe('Reorg detected, start updating'); }); }); From aa921b23f67830c3bd64aa6b51a46dcf7d113204 Mon Sep 17 00:00:00 2001 From: Eddort Date: Sun, 24 Dec 2023 16:11:08 +0100 Subject: [PATCH 71/75] fix: remove old e2e tests --- src/http/keys/keys.e2e-spec.ts | 35 ------------------- .../sr-modules-keys.e2e-spec.ts | 7 ---- .../sr-modules-operators.e2e-spec.ts | 11 ------ src/http/sr-modules/sr-modules.e2e-spec.ts | 8 ----- 4 files changed, 61 deletions(-) diff --git a/src/http/keys/keys.e2e-spec.ts b/src/http/keys/keys.e2e-spec.ts index 04b7cc75..785bf9dd 100644 --- a/src/http/keys/keys.e2e-spec.ts +++ b/src/http/keys/keys.e2e-spec.ts @@ -263,17 +263,6 @@ describe('KeyController (e2e)', () => { await cleanDB(); }); - it('should return too early response if there are no modules in database', async () => { - // lets save meta - await elMetaStorageService.update(elMeta); - // lets save keys - await keysStorageService.save(keys); - - const resp = await request(app.getHttpServer()).get('/v1/keys'); - expect(resp.status).toEqual(425); - expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); - }); - it('should return too early response if there are no meta', async () => { // lets save keys await keysStorageService.save(keys); @@ -297,20 +286,6 @@ describe('KeyController (e2e)', () => { await cleanDB(); }); - it('should return too early response if there are no modules in database', async () => { - // lets save meta - await elMetaStorageService.update(elMeta); - // lets save keys - await keysStorageService.save(keys); - const pubkeys = [keys[0].key, keys[1].key]; - const resp = await request(app.getHttpServer()) - .post(`/v1/keys/find`) - .set('Content-Type', 'application/json') - .send({ pubkeys }); - expect(resp.status).toEqual(425); - expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); - }); - it('should return too early response if there are no meta', async () => { // lets save keys await keysStorageService.save(keys); @@ -414,16 +389,6 @@ describe('KeyController (e2e)', () => { await cleanDB(); }); - it('should return too early response if there are no modules in database', async () => { - // lets save meta - await elMetaStorageService.update(elMeta); - // lets save keys - await keysStorageService.save(keys); - const resp = await request(app.getHttpServer()).get(`/v1/keys/wrongkey`); - expect(resp.status).toEqual(425); - expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); - }); - it('should return too early response if there are no meta', async () => { // lets save keys await keysStorageService.save(keys); diff --git a/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts b/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts index ecc5d802..67513de0 100644 --- a/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts +++ b/src/http/sr-modules-keys/sr-modules-keys.e2e-spec.ts @@ -473,13 +473,6 @@ describe('SRModulesKeysController (e2e)', () => { expect(resp.status).toEqual(425); expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); }); - - it('Should return too early response if there are no modules', async () => { - await elMetaStorageService.update(elMeta); - const resp = await request(app.getHttpServer()).get(`/v1/modules/keys`); - expect(resp.status).toEqual(425); - expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); - }); }); }); }); diff --git a/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts b/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts index 7ec2ea94..fc006576 100644 --- a/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts +++ b/src/http/sr-modules-operators/sr-modules-operators.e2e-spec.ts @@ -146,17 +146,6 @@ describe('SRModuleOperatorsController (e2e)', () => { await cleanDB(); }); - it('should return too early response if there are no modules in database', async () => { - // lets save meta - await elMetaStorageService.update(elMeta); - // lets save operators - await operatorsStorageService.save(operators); - - const resp = await request(app.getHttpServer()).get('/v1/operators'); - expect(resp.status).toEqual(425); - expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); - }); - it('should return too early response if there are no meta', async () => { // lets save operators await operatorsStorageService.save(operators); diff --git a/src/http/sr-modules/sr-modules.e2e-spec.ts b/src/http/sr-modules/sr-modules.e2e-spec.ts index 410de1e7..a1ae4df2 100644 --- a/src/http/sr-modules/sr-modules.e2e-spec.ts +++ b/src/http/sr-modules/sr-modules.e2e-spec.ts @@ -123,14 +123,6 @@ describe('SRModulesController (e2e)', () => { await cleanDB(); }); - it('Should return too early response if there are no modules in database', async () => { - // lets save meta - await elMetaStorageService.update(elMeta); - const resp = await request(app.getHttpServer()).get('/v1/modules'); - expect(resp.status).toEqual(425); - expect(resp.body).toEqual({ message: 'Too early response', statusCode: 425 }); - }); - it('Should return too early response if there are no meta', async () => { await moduleStorageService.upsert(curatedModule, 1, ''); const resp = await request(app.getHttpServer()).get('/v1/modules'); From 506cdcdfa88ccf1522f23259bc850a63d47f0c87 Mon Sep 17 00:00:00 2001 From: Eddort Date: Sun, 24 Dec 2023 20:43:28 +0100 Subject: [PATCH 72/75] refactor: reorg test --- .../keys-update/test/detect-reorg.spec.ts | 20 +------------------ .../{ => test}/keys-update.fixtures.ts | 0 .../keys-update/test/update-cases.spec.ts | 2 +- 3 files changed, 2 insertions(+), 20 deletions(-) rename src/jobs/keys-update/{ => test}/keys-update.fixtures.ts (100%) diff --git a/src/jobs/keys-update/test/detect-reorg.spec.ts b/src/jobs/keys-update/test/detect-reorg.spec.ts index ab7a7521..61a07dde 100644 --- a/src/jobs/keys-update/test/detect-reorg.spec.ts +++ b/src/jobs/keys-update/test/detect-reorg.spec.ts @@ -75,25 +75,7 @@ describe('detect reorg', () => { .spyOn(executionProviderService, 'getFullBlock') .mockImplementationOnce(async () => ({ number: 1, hash: '0x1', timestamp: 1, parentHash: '0x0' } as any)); - expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x1')).toBeFalsy(); - expect(updaterState.isReorgDetected).toBeFalsy(); - }); - - it('parent hash of the currentBlock matches the hash of the prevBlock', async () => { - const updaterState: UpdaterState = { - lastChangedBlockHash: '0x1', - isReorgDetected: false, - }; - - jest - .spyOn(executionProviderService, 'getFullBlock') - .mockImplementationOnce(async () => ({ number: 2, hash: '0x2', timestamp: 1, parentHash: '0x1' } as any)); - - jest - .spyOn(executionProviderService, 'getFullBlock') - .mockImplementationOnce(async () => ({ number: 1, hash: '0x1', timestamp: 1, parentHash: '0x0' } as any)); - - expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x1')).toBeFalsy(); + expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x2')).toBeFalsy(); expect(updaterState.isReorgDetected).toBeFalsy(); }); diff --git a/src/jobs/keys-update/keys-update.fixtures.ts b/src/jobs/keys-update/test/keys-update.fixtures.ts similarity index 100% rename from src/jobs/keys-update/keys-update.fixtures.ts rename to src/jobs/keys-update/test/keys-update.fixtures.ts diff --git a/src/jobs/keys-update/test/update-cases.spec.ts b/src/jobs/keys-update/test/update-cases.spec.ts index 11cbefa5..b95f0180 100644 --- a/src/jobs/keys-update/test/update-cases.spec.ts +++ b/src/jobs/keys-update/test/update-cases.spec.ts @@ -4,7 +4,7 @@ import { ExecutionProviderService } from 'common/execution-provider'; import { StakingRouterService } from 'staking-router-modules/staking-router.service'; import { ElMetaStorageService } from 'storage/el-meta.storage'; import { SRModuleStorageService } from 'storage/sr-module.storage'; -import { stakingModuleFixture, stakingModuleFixtures } from '../keys-update.fixtures'; +import { stakingModuleFixture, stakingModuleFixtures } from './keys-update.fixtures'; import { StakingModuleUpdaterService } from '../staking-module-updater.service'; describe('update cases', () => { From 079d15caea4ed2514b8eb0967ba376aa5c3561d2 Mon Sep 17 00:00:00 2001 From: Eddort Date: Sun, 24 Dec 2023 20:52:43 +0100 Subject: [PATCH 73/75] refactor: add comments to reorg check method --- .../keys-update/staking-module-updater.service.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/jobs/keys-update/staking-module-updater.service.ts b/src/jobs/keys-update/staking-module-updater.service.ts index 8fa14a87..f55c0464 100644 --- a/src/jobs/keys-update/staking-module-updater.service.ts +++ b/src/jobs/keys-update/staking-module-updater.service.ts @@ -158,26 +158,30 @@ export class StakingModuleUpdaterService { if (updaterState.isReorgDetected) { return true; } - + // get full block data by hashes const currentBlock = await this.executionProvider.getFullBlock(currentBlockHash); const prevBlock = await this.executionProvider.getFullBlock(prevBlockHash); - + // prevBlock is a direct child of currentBlock + // there's no need to check deeper as we get the currentBlock by tag if (currentBlock.parentHash === prevBlock.hash) return false; // different hash but same number + // is a direct indication of reorganization, there's no need to look any deeper. if (currentBlock.hash !== prevBlock.hash && currentBlock.number === prevBlock.number) { updaterState.isReorgDetected = true; return true; } - + // get all blocks by block number + // block numbers are the interval between the current and previous blocks const blocks = await Promise.all( range(prevBlock.number, currentBlock.number + 1).map(async (bNumber) => { return await this.executionProvider.getFullBlock(bNumber); }), ); - + // compare hash from the first block if (blocks[0].hash !== prevBlockHash) return true; + // compare hash from the last block if (blocks[blocks.length - 1].hash !== currentBlockHash) return true; - + // check the integrity of the blockchain for (let i = 1; i < blocks.length; i++) { const previousBlock = blocks[i - 1]; const currentBlock = blocks[i]; From 453ac728301f7de91e78b81b23d27e3c6f04ce96 Mon Sep 17 00:00:00 2001 From: Eddort Date: Sun, 24 Dec 2023 22:26:10 +0100 Subject: [PATCH 74/75] fix: reorg spec arg --- src/jobs/keys-update/test/detect-reorg.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jobs/keys-update/test/detect-reorg.spec.ts b/src/jobs/keys-update/test/detect-reorg.spec.ts index 61a07dde..1fcc0abb 100644 --- a/src/jobs/keys-update/test/detect-reorg.spec.ts +++ b/src/jobs/keys-update/test/detect-reorg.spec.ts @@ -93,7 +93,7 @@ describe('detect reorg', () => { .spyOn(executionProviderService, 'getFullBlock') .mockImplementationOnce(async () => ({ number: 2, hash: '0x1', timestamp: 1, parentHash: '0x0' } as any)); - expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x1')).toBeTruthy(); + expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x2')).toBeTruthy(); expect(updaterState.isReorgDetected).toBeTruthy(); }); From 1dc39ecb2723c8d3e7bf5266837cb6eae7fc1cb0 Mon Sep 17 00:00:00 2001 From: Eddort Date: Mon, 25 Dec 2023 13:55:37 +0100 Subject: [PATCH 75/75] fix: wrong migration --- ...n20231220104403.ts => Migration20231225124800.ts} | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) rename src/migrations/{Migration20231220104403.ts => Migration20231225124800.ts} (63%) diff --git a/src/migrations/Migration20231220104403.ts b/src/migrations/Migration20231225124800.ts similarity index 63% rename from src/migrations/Migration20231220104403.ts rename to src/migrations/Migration20231225124800.ts index 00376807..83f26b0d 100644 --- a/src/migrations/Migration20231220104403.ts +++ b/src/migrations/Migration20231225124800.ts @@ -1,7 +1,12 @@ import { Migration } from '@mikro-orm/migrations'; -export class Migration20231220104403 extends Migration { +export class Migration20231225124800 extends Migration { async up(): Promise { + this.addSql('TRUNCATE registry_key'); + this.addSql('TRUNCATE registry_operator'); + this.addSql('TRUNCATE el_meta_entity'); + this.addSql('TRUNCATE sr_module_entity'); + this.addSql('alter table "el_meta_entity" add column "last_changed_block_hash" varchar(66) not null;'); this.addSql('alter table "registry_operator" add column "finalized_used_signing_keys" int not null;'); @@ -10,6 +15,11 @@ export class Migration20231220104403 extends Migration { } async down(): Promise { + this.addSql('TRUNCATE registry_key'); + this.addSql('TRUNCATE registry_operator'); + this.addSql('TRUNCATE el_meta_entity'); + this.addSql('TRUNCATE sr_module_entity'); + this.addSql('alter table "el_meta_entity" drop column "last_changed_block_hash";'); this.addSql('alter table "registry_operator" drop column "finalized_used_signing_keys";');