diff --git a/package.json b/package.json index 42213276..3c9151b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lido-keys-api", - "version": "0.10.1", + "version": "0.10.2", "description": "Lido Node Operators keys service", "author": "Lido team", "private": true, @@ -51,6 +51,7 @@ "@lido-nestjs/execution": "^1.11.0", "@lido-nestjs/fetch": "^1.4.0", "@lido-nestjs/logger": "^1.3.2", + "@lido-nestjs/middleware": "^1.2.0", "@lido-nestjs/validators-registry": "^1.3.0", "@mikro-orm/cli": "^5.5.3", "@mikro-orm/core": "^5.5.3", 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]; }, }) 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/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..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, @@ -72,7 +87,9 @@ export class RegistryOperatorFetchService { overrides: CallOverrides = {}, ): Promise { const fullInfo = true; - const operator = await this.getContract(moduleAddress).getNodeOperator(operatorIndex, fullInfo, overrides as any); + const contract = this.getContract(moduleAddress); + + const operator = await contract.getNodeOperator(operatorIndex, fullInfo, overrides as any); const { name, @@ -84,6 +101,11 @@ export class RegistryOperatorFetchService { totalDepositedValidators, } = operator; + const { totalDepositedValidators: finalizedUsedSigningKeys } = await this.getFinalizedNodeOperator( + moduleAddress, + operatorIndex, + ); + return { index: operatorIndex, active, @@ -94,6 +116,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..8e8de7d1 100644 --- a/src/common/registry/main/abstract-registry.ts +++ b/src/common/registry/main/abstract-registry.ts @@ -108,9 +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.usedSigningKeys : 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); @@ -121,7 +122,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); @@ -138,8 +139,6 @@ export abstract class AbstractRegistryService { this.logger.log('Keys saved', { operatorIndex }); } - - console.timeEnd('FETCH_OPERATORS'); } /** storage */ 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/common/registry/test/fetch/key.fetch.spec.ts b/src/common/registry/test/fetch/key.fetch.spec.ts index b23cbb58..7fee5049 100644 --- a/src/common/registry/test/fetch/key.fetch.spec.ts +++ b/src/common/registry/test/fetch/key.fetch.spec.ts @@ -76,7 +76,33 @@ 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); + 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..33948828 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 () => { @@ -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 () => { 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/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/key-registry/connect.e2e-spec.ts b/src/common/registry/test/key-registry/connect.e2e-spec.ts index 84cef1a2..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(); }); @@ -57,8 +64,6 @@ describe('Registry', () => { }); test('Update', async () => { - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); await compareTestOperators(address, registryService, { 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..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,6 +26,8 @@ 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; @@ -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); @@ -298,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/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/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', }; 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..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,10 +3,14 @@ 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 { clearDb, compareTestOperators } from '../testing.utils'; +import { + ValidatorRegistryModule, + ValidatorRegistryService, + RegistryStorageService, + RegistryOperatorFetchService, +} from '../../'; +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'; @@ -16,6 +20,7 @@ dotenv.config(); describe('Registry', () => { let registryService: ValidatorRegistryService; + let registryOperatorFetchService: RegistryOperatorFetchService; let mikroOrm: MikroORM; let storageService: RegistryStorageService; @@ -29,14 +34,11 @@ describe('Registry', () => { return { ...key, moduleAddress: address }; }); + const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; + 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: { @@ -56,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(); }); @@ -69,8 +74,6 @@ describe('Registry', () => { }); test('Update', async () => { - const blockHash = '0x4ef0f15a8a04a97f60a9f76ba83d27bcf98dac9635685cd05fe1d78bd6e93418'; - await registryService.update(address, blockHash); await compareTestOperators(address, registryService, { 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..96ff7f2f 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,92 @@ 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(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/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/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/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/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 { 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, }, }); }); @@ -256,22 +263,11 @@ 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); - await moduleStorageService.upsert(curatedModule, 1); + await moduleStorageService.upsert(curatedModule, 1, ''); const resp = await request(app.getHttpServer()).get('/v1/keys'); expect(resp.status).toEqual(425); @@ -290,24 +286,10 @@ 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); - 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 +309,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 +337,7 @@ describe('KeyController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -375,6 +358,7 @@ describe('KeyController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -405,20 +389,10 @@ 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); - 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 +406,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 +429,7 @@ describe('KeyController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); 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/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: '', }; 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..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 @@ -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,14 +468,7 @@ describe('SRModulesKeysController (e2e)', () => { }); it('Should return too early response if there are no meta', async () => { - 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 }); - }); - - it('Should return too early response if there are no modules', async () => { - await elMetaStorageService.update(elMeta); + 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..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 @@ -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, }, }); }); @@ -145,21 +146,10 @@ 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); - 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 +167,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 +194,7 @@ describe('SRModuleOperatorsController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); @@ -217,6 +208,7 @@ describe('SRModuleOperatorsController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -256,7 +248,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 +266,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 +287,7 @@ describe('SRModuleOperatorsController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }, }); }); @@ -352,7 +345,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..a1ae4df2 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, }); }); }); @@ -122,16 +123,8 @@ 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); + 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 +138,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 +153,7 @@ describe('SRModulesController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }); }); @@ -171,6 +165,7 @@ describe('SRModulesController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }); }); @@ -182,6 +177,7 @@ describe('SRModulesController (e2e)', () => { blockNumber: elMeta.number, blockHash: elMeta.hash, timestamp: elMeta.timestamp, + lastChangedBlockHash: elMeta.lastChangedBlockHash, }); }); @@ -215,7 +211,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.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.interfaces.ts b/src/jobs/keys-update/keys-update.interfaces.ts new file mode 100644 index 00000000..63444c06 --- /dev/null +++ b/src/jobs/keys-update/keys-update.interfaces.ts @@ -0,0 +1,20 @@ +import { StakingModule } from 'staking-router-modules/interfaces/staking-module.interface'; + +export interface UpdaterPayload { + currElMeta: { + number: number; + hash: string; + timestamp: number; + }; + prevElMeta: { + blockNumber: number; + blockHash: string; + timestamp: number; + } | null; + contractModules: StakingModule[]; +} + +export interface UpdaterState { + lastChangedBlockHash: string; + isReorgDetected: boolean; +} 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 2f5075b4..ab0d0a44 100644 --- a/src/jobs/keys-update/keys-update.service.ts +++ b/src/jobs/keys-update/keys-update.service.ts @@ -14,6 +14,7 @@ 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 { StakingModuleUpdaterService } from './staking-module-updater.service'; class KeyOutdatedError extends Error { lastBlock: number; @@ -38,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; @@ -115,10 +117,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.log('Same blockHash, updating is not required', { prevElMeta, currElMeta }); + return; + } + // Get modules from storage const storageModules = await this.srModulesStorage.findAll(); // Get staking modules from SR contract @@ -133,44 +142,7 @@ export class KeysUpdateService { await this.entityManager.transactional( async () => { - // Update EL meta in db - await this.elMetaStorage.update(currElMeta); - - for (const contractModule of contractModules) { - // 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); - // 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); - } - - continue; - } - - await moduleInstance.update(contractModule.stakingModuleAddress, currElMeta.hash); - } + await this.stakingModuleUpdaterService.updateStakingModules({ currElMeta, prevElMeta, contractModules }); }, { isolationLevel: IsolationLevel.READ_COMMITTED }, ); @@ -196,21 +168,22 @@ export class KeysUpdateService { this.prometheusService.registryLastUpdate.set(elMeta.timestamp); this.prometheusService.registryBlockNumber.set(elMeta.blockNumber); + this.prometheusService.registryNumberOfKeysBySRModuleAndOperator.reset(); + for (const module of stakingModules) { const moduleInstance = this.stakingRouterService.getStakingRouterModuleImpl(module.type); // update nonce metric - this.prometheusService.registryNonce.set({ srModuleId: module.id }, module.nonce); + this.prometheusService.registryNonce.set({ srModuleId: module.moduleId }, module.nonce); // get operators const operators = await moduleInstance.getOperators(module.stakingModuleAddress); - this.prometheusService.registryNumberOfKeysBySRModuleAndOperator.reset(); operators.forEach((operator) => { this.prometheusService.registryNumberOfKeysBySRModuleAndOperator.set( { operator: operator.index, - srModuleId: module.id, + srModuleId: module.moduleId, used: 'true', }, operator.usedSigningKeys, @@ -219,7 +192,7 @@ export class KeysUpdateService { this.prometheusService.registryNumberOfKeysBySRModuleAndOperator.set( { operator: operator.index, - srModuleId: module.id, + srModuleId: module.moduleId, used: 'false', }, operator.totalSigningKeys - operator.usedSigningKeys, diff --git a/src/jobs/keys-update/staking-module-updater.service.ts b/src/jobs/keys-update/staking-module-updater.service.ts new file mode 100644 index 00000000..f63ad67c --- /dev/null +++ b/src/jobs/keys-update/staking-module-updater.service.ts @@ -0,0 +1,207 @@ +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 StakingModuleUpdaterService { + 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 updating', { stakingModuleAddress, currentBlockHash }); + + await this.updateStakingModule( + updaterState, + moduleInstance, + contractModule, + stakingModuleAddress, + currNonce, + currentBlockHash, + ); + continue; + } + + if (prevNonce !== currNonce) { + this.logger.log('Nonce has been changed, start updating', { + 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 updating', { + 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 updating', { 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, updating 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; + } + // get full block data by hashes + const currentBlock = await this.executionProvider.getFullBlock(currentBlockHash); + const prevBlock = await this.executionProvider.getFullBlock(prevBlockHash); + // prevBlock is a direct parent 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) { + updaterState.isReorgDetected = true; + return true; + } + // compare hash from the last block + if (blocks[blocks.length - 1].hash !== currentBlockHash) { + updaterState.isReorgDetected = true; + 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]; + + if (currentBlock.parentHash !== previousBlock.hash) { + updaterState.isReorgDetected = true; + return true; + } + } + + return false; + } + + public isTooMuchDiffBetweenBlocks(prevBlockNumber: number, currentBlockNumber: number) { + return currentBlockNumber - prevBlockNumber >= MAX_BLOCKS_OVERLAP; + } +} 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..1260713c --- /dev/null +++ b/src/jobs/keys-update/test/detect-reorg.spec.ts @@ -0,0 +1,211 @@ +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('detect reorg', () => { + 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', '0x2')).toBeFalsy(); + expect(updaterState.isReorgDetected).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', '0x2')).toBeTruthy(); + expect(updaterState.isReorgDetected).toBeTruthy(); + }); + + it('check blockchain (happy pass)', async () => { + const updaterState: UpdaterState = { + lastChangedBlockHash: '0x0', + 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', '0x100')).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: blockHashOrBlockTag === 100 ? `0xSORRY` : `0x${Number(blockHashOrBlockTag) - 1}`, + } as any), + ); + + expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x100')).toBeTruthy(); + expect(updaterState.isReorgDetected).toBeTruthy(); + }); + + it("first block from range response doesn't match with first block", 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: blockHashOrBlockTag === 1 ? `0xSORRY` : `0x${Number(blockHashOrBlockTag) - 1}`, + timestamp: 1, + parentHash: `0x${Number(blockHashOrBlockTag) - 1}`, + } as any), + ); + + expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x100')).toBeTruthy(); + expect(updaterState.isReorgDetected).toBeTruthy(); + }); + + it("last block from range response doesn't match with last block", 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: blockHashOrBlockTag === 100 ? `0xSORRY` : `0x${Number(blockHashOrBlockTag) - 1}`, + timestamp: 1, + parentHash: `0x${Number(blockHashOrBlockTag) - 1}`, + } as any), + ); + + expect(await updaterService.isReorgDetected(updaterState, '0x1', '0x100')).toBeTruthy(); + expect(updaterState.isReorgDetected).toBeTruthy(); + }); +}); diff --git a/src/jobs/keys-update/test/keys-update.fixtures.ts b/src/jobs/keys-update/test/keys-update.fixtures.ts new file mode 100644 index 00000000..f5ef37d3 --- /dev/null +++ b/src/jobs/keys-update/test/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/test/update-cases.spec.ts b/src/jobs/keys-update/test/update-cases.spec.ts new file mode 100644 index 00000000..b95f0180 --- /dev/null +++ b/src/jobs/keys-update/test/update-cases.spec.ts @@ -0,0 +1,168 @@ +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 { StakingModuleUpdaterService } from '../staking-module-updater.service'; + +describe('update cases', () => { + let updaterService: StakingModuleUpdaterService; + 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(), + }, + }, + StakingModuleUpdaterService, + ], + }).compile(); + + updaterService = module.get(StakingModuleUpdaterService); + 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 updating'); + }); + + 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 updating'); + }); + + 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 updating'); + }); + + it('Reorg detected', async () => { + const mockUpdate = jest.spyOn(updaterService, 'updateStakingModule').mockImplementation(); + + jest.spyOn(sRModuleStorageService, 'findOneById').mockImplementation( + () => + ({ + nonce: 1, + } as any), + ); + + jest + .spyOn(executionProviderService, 'getFullBlock') + .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 }, + contractModules: stakingModuleFixtures, + }); + + expect(mockUpdate).toBeCalledTimes(2); + expect(loggerService.log.mock.calls[1][0]).toBe('Reorg detected, start updating'); + }); +}); diff --git a/src/migrations/Migration20231225124800.ts b/src/migrations/Migration20231225124800.ts new file mode 100644 index 00000000..83f26b0d --- /dev/null +++ b/src/migrations/Migration20231225124800.ts @@ -0,0 +1,29 @@ +import { Migration } from '@mikro-orm/migrations'; + +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;'); + + this.addSql('alter table "sr_module_entity" add column "last_changed_block_hash" varchar(255) not null;'); + } + + 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";'); + + this.addSql('alter table "sr_module_entity" drop column "last_changed_block_hash";'); + } +} diff --git a/src/staking-router-modules/staking-router.service.ts b/src/staking-router-modules/staking-router.service.ts index af28e02f..02eb93f0 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,18 +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(); - - if (stakingModules.length === 0) { - this.logger.warn("No staking modules in list. Maybe didn't fetched from SR yet"); - throw httpExceptionTooEarlyResp(); - } + const stakingModules = await this.getStakingModules(stakingModuleAddresses); const elBlockSnapshot = await this.getElBlockSnapshot(); 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.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', diff --git a/src/storage/sr-module.storage.ts b/src/storage/sr-module.storage.ts index 0bbecd61..85bc1a87 100644 --- a/src/storage/sr-module.storage.ts +++ b/src/storage/sr-module.storage.ts @@ -21,7 +21,12 @@ export class SRModuleStorageService { return await this.repository.findAll(); } - async upsert(srModule: StakingModule, nonce: number): Promise { + /** 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({ moduleId: srModule.moduleId, @@ -32,6 +37,7 @@ export class SRModuleStorageService { existingModule = new SrModuleEntity( { ...srModule, stakingModuleAddress: srModule.stakingModuleAddress.toLowerCase() }, nonce, + lastChangedBlockHash, ); } else { // If the entity exists, update its properties @@ -45,6 +51,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) diff --git a/yarn.lock b/yarn.lock index d44b1ee1..e0c68a6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1319,7 +1319,7 @@ traverse "^0.6.7" winston "^3.4.0" -"@lido-nestjs/middleware@1.2.0": +"@lido-nestjs/middleware@1.2.0", "@lido-nestjs/middleware@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@lido-nestjs/middleware/-/middleware-1.2.0.tgz#0e64b69149ef1f7e4594896fd08d6e0bd9ef5382" integrity sha512-0rkXWKXEKJPRbq1W6cN+yelhc+CruqhUqmdkNKwl4vTk631fpG9yfSfxxQFDAs3LhsTORqqgKUBpdK+epRYjIQ==