From bbdc745f739c4608b111849c20ef01bd7a288d83 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 27 Dec 2023 13:11:23 +0400 Subject: [PATCH] fix: tests, invalid keys improvemnt --- src/guardian/guardian.service.spec.ts | 109 ++------- src/guardian/guardian.service.ts | 16 +- src/guardian/interfaces/state.interface.ts | 1 + .../staking-module-guard.service.ts | 97 ++++++-- .../staking-module-guard.spec.ts | 215 ++++++++++++++++-- src/keys-api/interfaces/SRModule.ts | 15 ++ src/staking-router/staking-router.service.ts | 41 ++-- src/staking-router/staking-router.spec.ts | 121 +++++----- test/helpers/mockKeysApi.ts | 9 +- test/manifest.e2e-spec.ts | 84 +++++-- 10 files changed, 467 insertions(+), 241 deletions(-) diff --git a/src/guardian/guardian.service.spec.ts b/src/guardian/guardian.service.spec.ts index adfca110..c5d0db9b 100644 --- a/src/guardian/guardian.service.spec.ts +++ b/src/guardian/guardian.service.spec.ts @@ -24,96 +24,6 @@ import { mockRepository } from 'contracts/repository/repository.mock'; jest.mock('../transport/stomp/stomp.client'); -const vettedKeys = [ - { - key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', - depositSignature: - '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 100, - }, - { - key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', - depositSignature: - '0x898ac7072aa26d983f9ece384c4037966dde614b75dddf982f6a415f3107cb2569b96f6d1c44e608a250ac4bbe908df51473f0de2cf732d283b07d88f3786893124967b8697a8b93d31976e7ac49ab1e568f98db0bbb13384477e8357b6d7e9b', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 101, - }, - { - key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 5, - }, -]; - -const vettedKeysResponse = { - blockHash: 'some_hash', - blockNumber: 1, - vettedKeys, - stakingModulesData: [ - { - blockHash: 'some_hash', - lastChangedBlockHash: 'some_hash', - unusedKeys: [ - '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', - '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', - ], - vettedUnusedKeys: [ - { - key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', - depositSignature: - '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 100, - }, - { - key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', - depositSignature: - '0x898ac7072aa26d983f9ece384c4037966dde614b75dddf982f6a415f3107cb2569b96f6d1c44e608a250ac4bbe908df51473f0de2cf732d283b07d88f3786893124967b8697a8b93d31976e7ac49ab1e568f98db0bbb13384477e8357b6d7e9b', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 101, - }, - ], - nonce: 0, - stakingModuleId: 2, - stakingModuleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - }, - { - blockHash: 'some_hash', - lastChangedBlockHash: 'some_hash', - unusedKeys: [ - '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', - ], - vettedUnusedKeys: [ - { - key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 5, - }, - ], - nonce: 0, - stakingModuleId: 3, - stakingModuleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - }, - ], -}; - describe('GuardianService', () => { let stakingRouterService: StakingRouterService; let blockGuardService: BlockGuardService; @@ -165,19 +75,32 @@ describe('GuardianService', () => { }); it('should exit if the previous call is not completed', async () => { - jest - .spyOn(stakingRouterService, 'getStakingModulesData') - .mockImplementation(async () => vettedKeysResponse); + // OneAtTime test + const getOperatorsAndModulesMock = jest + .spyOn(stakingRouterService, 'getOperatorsAndModules') + .mockImplementation(async () => ({ + data: [], + meta: { + elBlockSnapshot: { + blockNumber: 0, + blockHash: 'string', + timestamp: 0, + lastChangedBlockHash: '', + }, + }, + })); const getBlockGuardServiceMock = jest .spyOn(blockGuardService, 'isNeedToProcessNewState') .mockImplementation(() => false); + // run concurrently and check that second attempt await Promise.all([ guardianService.handleNewBlock(), guardianService.handleNewBlock(), ]); expect(getBlockGuardServiceMock).toBeCalledTimes(1); + expect(getOperatorsAndModulesMock).toBeCalledTimes(1); }); }); diff --git a/src/guardian/guardian.service.ts b/src/guardian/guardian.service.ts index 5eb301a1..2b0165a0 100644 --- a/src/guardian/guardian.service.ts +++ b/src/guardian/guardian.service.ts @@ -94,8 +94,12 @@ export class GuardianService implements OnModuleInit { this.logger.log('New staking router state cycle start'); try { - const { blockHash, blockNumber, stakingModulesData } = - await this.stakingRouterService.getStakingModulesData(); + const { data: operatorsByModules, meta } = + await this.stakingRouterService.getOperatorsAndModules(); + + const { + elBlockSnapshot: { blockHash, blockNumber }, + } = meta; await this.repositoryService.initCachedContracts({ blockHash }); @@ -115,7 +119,7 @@ export class GuardianService implements OnModuleInit { return; } - const stakingModulesCount = stakingModulesData.length; + const stakingModulesCount = operatorsByModules.length; this.logger.log('Staking modules loaded', { modulesCount: stakingModulesCount, @@ -135,6 +139,12 @@ export class GuardianService implements OnModuleInit { blockHash: blockData.blockHash, }); + const stakingModulesData = + await this.stakingRouterService.getStakingModulesData({ + data: operatorsByModules, + meta, + }); + const modulesIdWithDuplicateKeys: number[] = this.stakingModuleGuardService.getModulesIdsWithDuplicatedVettedUnusedKeys( stakingModulesData, diff --git a/src/guardian/interfaces/state.interface.ts b/src/guardian/interfaces/state.interface.ts index b04f98e1..b9e0a858 100644 --- a/src/guardian/interfaces/state.interface.ts +++ b/src/guardian/interfaces/state.interface.ts @@ -3,4 +3,5 @@ export interface ContractsState { nonce: number; depositRoot: string; lastChangedBlockHash: string; + invalidKeysFound: boolean; } diff --git a/src/guardian/staking-module-guard/staking-module-guard.service.ts b/src/guardian/staking-module-guard/staking-module-guard.service.ts index 11c04aa9..ca09ffc0 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -165,6 +165,15 @@ export class StakingModuleGuardService { return; } + const isValidKeys = await this.isVettedUnusedKeysValid( + stakingModuleData, + blockData, + ); + + if (!isValidKeys) { + return; + } + await this.handleCorrectKeys(stakingModuleData, blockData); } } @@ -318,11 +327,14 @@ export class StakingModuleGuardService { const { nonce, stakingModuleId, lastChangedBlockHash } = stakingModuleData; + // if we are here we didn't find invalid keys const currentContractState = { nonce, depositRoot, blockNumber, lastChangedBlockHash, + // if we are here we didn't find invalid keys + invalidKeysFound: false, }; const lastContractsState = @@ -335,29 +347,12 @@ export class StakingModuleGuardService { this.lastContractsStateByModuleId[stakingModuleId] = currentContractState; + // need to check invalidKeysFound if (isSameContractsState) { this.logger.log("Contract states didn't change"); return; } - if ( - !lastContractsState || - currentContractState.lastChangedBlockHash !== - lastContractsState.lastChangedBlockHash - ) { - const invalidKeys = await this.getInvalidKeys( - stakingModuleData, - blockData, - ); - if (invalidKeys.length) { - this.logger.error( - 'Found invalid keys, will skip deposits until solving problem', - ); - this.guardianMetricsService.incrInvalidKeysEventCounter(); - return; - } - } - const signature = await this.securityService.signDepositData( depositRoot, nonce, @@ -386,6 +381,72 @@ export class StakingModuleGuardService { await this.guardianMessageService.sendDepositMessage(depositMessage); } + public async isVettedUnusedKeysValid( + stakingModuleData: StakingModuleData, + blockData: BlockData, + ): Promise { + const { blockNumber, depositRoot } = blockData; + const { nonce, stakingModuleId, lastChangedBlockHash } = stakingModuleData; + const lastContractsState = + this.lastContractsStateByModuleId[stakingModuleId]; + + if ( + lastContractsState && + lastChangedBlockHash === lastContractsState.lastChangedBlockHash && + lastContractsState.invalidKeysFound + ) { + // if found invalid keys on previous iteration and lastChangedBlockHash returned by kapi was not changed + // we dont need to validate again, but we still need to skip deposits until problem will not be solved + this.logger.error( + 'LastChangedBlockHash was not changed and on previous iteration we found invalid keys, skip until solving problem ', + ); + + this.lastContractsStateByModuleId[stakingModuleId] = { + nonce, + depositRoot, + blockNumber, + lastChangedBlockHash, + invalidKeysFound: true, + }; + + return false; + } + + if ( + !lastContractsState || + lastChangedBlockHash !== lastContractsState.lastChangedBlockHash + ) { + // keys was changed or it is a first attempt, need to validate again + const invalidKeys = await this.getInvalidKeys( + stakingModuleData, + blockData, + ); + + // if found invalid keys, update state and exit + if (invalidKeys.length) { + this.logger.error( + 'Found invalid keys, will skip deposits until solving problem', + ); + this.guardianMetricsService.incrInvalidKeysEventCounter(); + // save info about invalid keys in cache + this.lastContractsStateByModuleId[stakingModuleId] = { + nonce, + depositRoot, + blockNumber, + lastChangedBlockHash, + invalidKeysFound: true, + }; + + return false; + } + + // keys are valid, state will be updated later + return true; + } + + return true; + } + public async getInvalidKeys( stakingModuleData: StakingModuleData, blockData: BlockData, diff --git a/src/guardian/staking-module-guard/staking-module-guard.spec.ts b/src/guardian/staking-module-guard/staking-module-guard.spec.ts index 5670add9..256d5769 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -236,7 +236,7 @@ describe('StakingModuleGuardService', () => { expect(mockSecurityContractIsDepositsPaused).toBeCalledTimes(1); }); - it('should call handleCorrectKeys if Lido unused keys are not found in the deposit contract', async () => { + it('should call handleCorrectKeys if Lido unused keys are not found in the deposit contract and vetted unused keys are valid', async () => { const notDepositedKey = '0x2345'; const unusedKeys = [notDepositedKey]; const blockData = { ...currentBlockData, unusedKeys, lidoWC }; @@ -253,6 +253,9 @@ describe('StakingModuleGuardService', () => { .spyOn(securityService, 'isDepositsPaused') .mockImplementation(async () => false); + // not found invalid keys + findInvalidKeys.mockImplementation(async () => []); + await stakingModuleGuardService.checkKeysIntersections( { ...stakingModuleData, @@ -263,6 +266,7 @@ describe('StakingModuleGuardService', () => { blockData, ); + expect(findInvalidKeys).toBeCalledTimes(1); expect(mockHandleKeysIntersections).not.toBeCalled(); expect(mockHandleCorrectKeys).toBeCalledTimes(1); expect(mockHandleCorrectKeys).toBeCalledWith( @@ -276,6 +280,89 @@ describe('StakingModuleGuardService', () => { ); expect(mockSecurityContractIsDepositsPaused).toBeCalledTimes(1); }); + + it('should not call handleCorrectKeys if vetted unused keys are invalid', async () => { + const notDepositedKey = '0x2345'; + const unusedKeys = [notDepositedKey]; + const blockData = { ...currentBlockData, unusedKeys, lidoWC }; + + const mockHandleCorrectKeys = jest + .spyOn(stakingModuleGuardService, 'handleCorrectKeys') + .mockImplementation(async () => undefined); + + const mockHandleKeysIntersections = jest + .spyOn(stakingModuleGuardService, 'handleKeysIntersections') + .mockImplementation(async () => undefined); + + const mockSecurityContractIsDepositsPaused = jest + .spyOn(securityService, 'isDepositsPaused') + .mockImplementation(async () => false); + + // found invalid keys + findInvalidKeys.mockImplementation(async () => ['something']); + + await stakingModuleGuardService.checkKeysIntersections( + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, + blockData, + ); + + expect(findInvalidKeys).toBeCalledTimes(1); + expect(mockHandleKeysIntersections).not.toBeCalled(); + expect(mockHandleCorrectKeys).not.toBeCalled(); + expect(mockSecurityContractIsDepositsPaused).toBeCalledTimes(1); + + // check that if lastChangedBlockHash the same but keys prev was invalid , handleCorrect will not be called + // but we also will not validate keys again + findInvalidKeys.mockClear(); + + await stakingModuleGuardService.checkKeysIntersections( + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, + blockData, + ); + + expect(findInvalidKeys).not.toBeCalled(); + expect(mockHandleKeysIntersections).not.toBeCalled(); + expect(mockHandleCorrectKeys).not.toBeCalled(); + // second execution + expect(mockSecurityContractIsDepositsPaused).toBeCalledTimes(2); + + // now we fixed keys (lastChangedBlockHash was changed) and we will run validation again + findInvalidKeys.mockImplementation(async () => []); + + await stakingModuleGuardService.checkKeysIntersections( + { + ...stakingModuleData, + lastChangedBlockHash: '0x1', + unusedKeys, + vettedUnusedKeys: [], + }, + blockData, + ); + + expect(findInvalidKeys).toBeCalledTimes(1); + expect(mockHandleKeysIntersections).not.toBeCalled(); + expect(mockHandleCorrectKeys).toBeCalledTimes(1); + expect(mockHandleCorrectKeys).toBeCalledWith( + { + ...stakingModuleData, + lastChangedBlockHash: '0x1', + unusedKeys, + vettedUnusedKeys: [], + }, + blockData, + ); + expect(mockSecurityContractIsDepositsPaused).toBeCalledTimes(3); + }); }); describe('handleCorrectKeys', () => { @@ -351,25 +438,51 @@ describe('StakingModuleGuardService', () => { expect(mockSendMessageFromGuardian).toBeCalledTimes(1); expect(mockSignDepositData).toBeCalledTimes(1); }); + }); - it('should call invalid keys check if lastChangedBlockHash was changed', async () => { - const mockSendMessageFromGuardian = jest - .spyOn(guardianMessageService, 'sendMessageFromGuardian') - .mockImplementation(async () => undefined); + describe('isVettedUnusedKeysValid', () => { + const blockData = {} as any; - const mockIsSameContractsStates = jest.spyOn( - stakingModuleGuardService, - 'isSameContractsStates', + it('should return false if last state was undefined and found invalid key', async () => { + findInvalidKeys.mockImplementation(() => ['something']); + + const result = await stakingModuleGuardService.isVettedUnusedKeysValid( + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + }, + blockData, ); - const mockSignDepositData = jest - .spyOn(securityService, 'signDepositData') - .mockImplementation(async () => signature); + expect(findInvalidKeys).toBeCalledTimes(1); + expect(result).toBeFalsy(); + }); - await stakingModuleGuardService.handleCorrectKeys( + it('should return true if last state was undefined and keys are valid', async () => { + findInvalidKeys.mockImplementation(() => []); + const result = await stakingModuleGuardService.isVettedUnusedKeysValid( { ...stakingModuleData, - lastChangedBlockHash: '0x1', + lastChangedBlockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + }, + blockData, + ); + + expect(findInvalidKeys).toBeCalledTimes(1); + expect(result).toBeTruthy(); + }); + + it('should return false if prev found invalid key and lastChangedBlockHash was not changed', async () => { + findInvalidKeys.mockImplementation(() => ['something']); + + const result = await stakingModuleGuardService.isVettedUnusedKeysValid( + { + ...stakingModuleData, + lastChangedBlockHash: '', unusedKeys: [], vettedUnusedKeys: [], }, @@ -377,13 +490,14 @@ describe('StakingModuleGuardService', () => { ); expect(findInvalidKeys).toBeCalledTimes(1); + expect(result).toBeFalsy(); findInvalidKeys.mockClear(); - await stakingModuleGuardService.handleCorrectKeys( + const newResult = await stakingModuleGuardService.isVettedUnusedKeysValid( { ...stakingModuleData, - lastChangedBlockHash: '0x1', + lastChangedBlockHash: '', unusedKeys: [], vettedUnusedKeys: [], }, @@ -391,12 +505,16 @@ describe('StakingModuleGuardService', () => { ); expect(findInvalidKeys).toBeCalledTimes(0); - findInvalidKeys.mockClear(); + expect(newResult).toBeFalsy(); + }); - await stakingModuleGuardService.handleCorrectKeys( + it('should return true if prev found invalid key and problem was solved', async () => { + findInvalidKeys.mockImplementation(() => ['something']); + + const result = await stakingModuleGuardService.isVettedUnusedKeysValid( { ...stakingModuleData, - lastChangedBlockHash: '0x2', + lastChangedBlockHash: '', unusedKeys: [], vettedUnusedKeys: [], }, @@ -404,15 +522,55 @@ describe('StakingModuleGuardService', () => { ); expect(findInvalidKeys).toBeCalledTimes(1); + expect(result).toBeFalsy(); - expect(mockIsSameContractsStates).toBeCalledTimes(3); - const { results } = mockIsSameContractsStates.mock; - expect(results[0].value).toBeFalsy(); - expect(results[1].value).toBeTruthy(); - expect(results[2].value).toBeFalsy(); + findInvalidKeys.mockImplementation(() => []); + + const newResult = await stakingModuleGuardService.isVettedUnusedKeysValid( + { + ...stakingModuleData, + lastChangedBlockHash: '0x1', + unusedKeys: [], + vettedUnusedKeys: [], + }, + blockData, + ); + + expect(findInvalidKeys).toBeCalledTimes(2); + expect(newResult).toBeTruthy(); + }); + + it('should run validation if prev didnt find invalid key and lastChangedBlockHash was not changed', async () => { + // TODO: maybe delete this test + // isVettedUnusedKeysValid didn't change state in positive case + // what is why lastState in this case is undefined + findInvalidKeys.mockImplementation(() => []); + + const result = await stakingModuleGuardService.isVettedUnusedKeysValid( + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + }, + blockData, + ); + + expect(findInvalidKeys).toBeCalledTimes(1); + expect(result).toBeTruthy(); + + const newResult = await stakingModuleGuardService.isVettedUnusedKeysValid( + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + }, + blockData, + ); - expect(mockSendMessageFromGuardian).toBeCalledTimes(2); - expect(mockSignDepositData).toBeCalledTimes(2); + expect(findInvalidKeys).toBeCalledTimes(2); + expect(newResult).toBeTruthy(); }); }); @@ -423,6 +581,8 @@ describe('StakingModuleGuardService', () => { const blockData = { blockHash: '0x1234', lidoWC } as any; it('should exclude invalid intersections', async () => { + // here should be in real test valid deposit + // but function ignore it const intersections = [{ valid: false, pubkey, wc: lidoWC } as any]; const filteredIntersections = @@ -531,6 +691,7 @@ describe('StakingModuleGuardService', () => { nonce: 1, blockNumber: 100, lastChangedBlockHash: 'hash', + invalidKeysFound: false, }; const result = stakingModuleGuardService.isSameContractsStates( { ...state }, @@ -545,6 +706,7 @@ describe('StakingModuleGuardService', () => { nonce: 1, blockNumber: 100, lastChangedBlockHash: 'hash', + invalidKeysFound: false, }; const result = stakingModuleGuardService.isSameContractsStates(state, { ...state, @@ -559,6 +721,7 @@ describe('StakingModuleGuardService', () => { nonce: 1, blockNumber: 100, lastChangedBlockHash: 'hash', + invalidKeysFound: false, }; const result = stakingModuleGuardService.isSameContractsStates(state, { ...state, @@ -573,6 +736,7 @@ describe('StakingModuleGuardService', () => { nonce: 1, blockNumber: 100, lastChangedBlockHash: 'hash', + invalidKeysFound: false, }; const result = stakingModuleGuardService.isSameContractsStates(state, { ...state, @@ -589,6 +753,7 @@ describe('StakingModuleGuardService', () => { nonce: 1, blockNumber: 100, lastChangedBlockHash: 'hash', + invalidKeysFound: false, }; const result = stakingModuleGuardService.isSameContractsStates(state, { ...state, diff --git a/src/keys-api/interfaces/SRModule.ts b/src/keys-api/interfaces/SRModule.ts index cad163f9..d64ccc42 100644 --- a/src/keys-api/interfaces/SRModule.ts +++ b/src/keys-api/interfaces/SRModule.ts @@ -43,4 +43,19 @@ export type SRModule = { * block.number of the last deposit of the module */ lastDepositBlock: number; + + /** + * Exited validators count + */ + exitedValidatorsCount: number; + + /** + * Module activation status + */ + active: boolean; + + /** + * Blockhash from the most recent data update + */ + lastChangedBlockHash: string; }; diff --git a/src/staking-router/staking-router.service.ts b/src/staking-router/staking-router.service.ts index d9476e6a..0bd24d53 100644 --- a/src/staking-router/staking-router.service.ts +++ b/src/staking-router/staking-router.service.ts @@ -7,8 +7,8 @@ import { getVettedUnusedKeys } from './vetted-keys'; import { RegistryOperator } from 'keys-api/interfaces/RegistryOperator'; import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; import { SRModule } from 'keys-api/interfaces'; -import { Meta } from 'keys-api/interfaces/Meta'; import { InconsistentLastChangedBlockHash } from 'common/custom-errors'; +import { GroupedByModuleOperatorListResponse } from 'keys-api/interfaces/GroupedByModuleOperatorListResponse'; @Injectable() export class StakingRouterService { @@ -18,25 +18,30 @@ export class StakingRouterService { protected readonly keysApiService: KeysApiService, ) {} + async getOperatorsAndModules() { + const { data: operatorsByModules, meta: operatorsMeta } = + await this.keysApiService.getOperatorListWithModule(); + + return { data: operatorsByModules, meta: operatorsMeta }; + } + /** * Return staking module data and block information */ - public async getStakingModulesData(): Promise<{ - stakingModulesData: StakingModuleData[]; - blockHash: string; - blockNumber: number; - }> { - const { data: operatorsByModules, meta: operatorsMeta } = - await this.keysApiService.getOperatorListWithModule(); + public async getStakingModulesData( + data: GroupedByModuleOperatorListResponse, + ): Promise { + const { data: operatorsByModules, meta: operatorsMeta } = data; const { data: unusedKeys, meta: unusedKeysMeta } = await this.keysApiService.getUnusedKeys(); const blockHash = operatorsMeta.elBlockSnapshot.blockHash; - const blockNumber = operatorsMeta.elBlockSnapshot.blockNumber; + const lastChangedBlockHash = + operatorsMeta.elBlockSnapshot.lastChangedBlockHash; this.isEqualLastChangedBlockHash( - operatorsMeta.elBlockSnapshot.lastChangedBlockHash, + lastChangedBlockHash, unusedKeysMeta.elBlockSnapshot.lastChangedBlockHash, ); @@ -46,11 +51,13 @@ export class StakingRouterService { operators, stakingModule, unusedKeys, - meta: operatorsMeta, + blockHash, + // Will set the lastChangedBlockHash for the module in KAPI, not the personal module's lastChangedBlockHash + lastChangedBlockHash, }), ); - return { stakingModulesData, blockHash, blockNumber }; + return stakingModulesData; } public isEqualLastChangedBlockHash( @@ -71,12 +78,14 @@ export class StakingRouterService { operators, stakingModule, unusedKeys, - meta, + blockHash, + lastChangedBlockHash, }: { operators: RegistryOperator[]; stakingModule: SRModule; unusedKeys: RegistryKey[]; - meta: Meta; + blockHash: string; + lastChangedBlockHash: string; }): StakingModuleData { const moduleUnusedKeys = unusedKeys.filter( (key) => key.moduleAddress === stakingModule.stakingModuleAddress, @@ -91,8 +100,8 @@ export class StakingRouterService { unusedKeys: moduleUnusedKeys.map((srKey) => srKey.key), nonce: stakingModule.nonce, stakingModuleId: stakingModule.id, - blockHash: meta.elBlockSnapshot.blockHash, - lastChangedBlockHash: meta.elBlockSnapshot.lastChangedBlockHash, + blockHash, + lastChangedBlockHash, vettedUnusedKeys: moduleVettedUnusedKeys, }; } diff --git a/src/staking-router/staking-router.spec.ts b/src/staking-router/staking-router.spec.ts index 4026fa49..66122f3e 100644 --- a/src/staking-router/staking-router.spec.ts +++ b/src/staking-router/staking-router.spec.ts @@ -32,77 +32,68 @@ describe('StakingRouter', () => { }); it("should return correct data when 'lastChangedBlockHash' values of two requests are identical", async () => { - (keysApiService.getOperatorListWithModule as jest.Mock).mockResolvedValue( - groupedByModulesOperators, - ); (keysApiService.getUnusedKeys as jest.Mock).mockResolvedValue( keysAllStakingModules, ); - const result = await stakingRouterService.getStakingModulesData(); + const result = await stakingRouterService.getStakingModulesData( + groupedByModulesOperators, + ); // Assertions - expect(result).toEqual({ - blockNumber: 400153, - blockHash: - '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', - stakingModulesData: [ - { - unusedKeys: [ - '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', - '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', - '0x8d12ec44816f108df84ef9b03e423a6d8fb0f0a1823c871b123ff41f893a7b372eb038a1ed1ff15083e07a777a5cba50', - ], - vettedUnusedKeys: [ - { - key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', - depositSignature: - '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 100, - }, - ], - blockHash: - '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', - lastChangedBlockHash: - '0x194ac4fd960ed44cb3db53fe1f5a53e983280fd438aeba607ae04f1bb416b4a1', - stakingModuleId: 1, - nonce: 364, - }, - { - unusedKeys: [ - '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', - '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', - '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', - ], - vettedUnusedKeys: [ - { - key: '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', - depositSignature: - '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 4, - }, - ], - nonce: 69, - blockHash: - '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', - lastChangedBlockHash: - '0x194ac4fd960ed44cb3db53fe1f5a53e983280fd438aeba607ae04f1bb416b4a1', - stakingModuleId: 2, - }, - ], - }); + expect(result).toEqual([ + { + unusedKeys: [ + '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + '0x8d12ec44816f108df84ef9b03e423a6d8fb0f0a1823c871b123ff41f893a7b372eb038a1ed1ff15083e07a777a5cba50', + ], + vettedUnusedKeys: [ + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 100, + }, + ], + blockHash: + '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + lastChangedBlockHash: + '0x194ac4fd960ed44cb3db53fe1f5a53e983280fd438aeba607ae04f1bb416b4a1', + stakingModuleId: 1, + nonce: 364, + }, + { + unusedKeys: [ + '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', + '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + ], + vettedUnusedKeys: [ + { + key: '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', + depositSignature: + '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 4, + }, + ], + nonce: 69, + blockHash: + '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + lastChangedBlockHash: + '0x194ac4fd960ed44cb3db53fe1f5a53e983280fd438aeba607ae04f1bb416b4a1', + stakingModuleId: 2, + }, + ]); }); it("should throw error when 'lastChangedBlockHash' values of two requests are different", async () => { - (keysApiService.getOperatorListWithModule as jest.Mock).mockResolvedValue( - groupedByModulesOperators, - ); (keysApiService.getUnusedKeys as jest.Mock).mockResolvedValue({ ...keysAllStakingModules, ...{ @@ -115,8 +106,8 @@ describe('StakingRouter', () => { }, }); - expect(stakingRouterService.getStakingModulesData()).rejects.toThrowError( - new InconsistentLastChangedBlockHash(), - ); + expect( + stakingRouterService.getStakingModulesData(groupedByModulesOperators), + ).rejects.toThrowError(new InconsistentLastChangedBlockHash()); }); }); diff --git a/test/helpers/mockKeysApi.ts b/test/helpers/mockKeysApi.ts index bab76105..47e1eaa6 100644 --- a/test/helpers/mockKeysApi.ts +++ b/test/helpers/mockKeysApi.ts @@ -7,7 +7,11 @@ import { SRModule } from 'keys-api/interfaces'; import { ELBlockSnapshot } from 'keys-api/interfaces/ELBlockSnapshot'; import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; -export const mockedModule = (block: ethers.providers.Block, nonce = 6046) => ({ +export const mockedModule = ( + block: ethers.providers.Block, + lastChangedBlockHash: string, + nonce = 6046, +): SRModule => ({ nonce, type: 'grouped-onchain-v1', id: 1, @@ -19,6 +23,9 @@ export const mockedModule = (block: ethers.providers.Block, nonce = 6046) => ({ name: 'NodeOperatorRegistry', lastDepositAt: block.timestamp, lastDepositBlock: block.number, + lastChangedBlockHash, + exitedValidatorsCount: 0, + active: true, }); export const mockedMeta = ( diff --git a/test/manifest.e2e-spec.ts b/test/manifest.e2e-spec.ts index 470d2053..a8eeeb99 100644 --- a/test/manifest.e2e-spec.ts +++ b/test/manifest.e2e-spec.ts @@ -239,7 +239,7 @@ describe('ganache e2e tests', () => { ]; const meta = mockedMeta(currentBlock, currentBlock.hash); - const stakingModule = mockedModule(currentBlock); + const stakingModule = mockedModule(currentBlock, currentBlock.hash); mockedKeysApiOperators( keysApiService, @@ -307,13 +307,13 @@ describe('ganache e2e tests', () => { // Mock Keys API again on new block const newBlock = await providerService.provider.getBlock('latest'); - const newMeta = mockedMeta(newBlock, newBlock.hash); + const updatedStakingModule = mockedModule(currentBlock, newBlock.hash); mockedKeysApiOperators( keysApiService, mockedOperators, - stakingModule, + updatedStakingModule, newMeta, ); @@ -374,7 +374,7 @@ describe('ganache e2e tests', () => { ]; const meta = mockedMeta(currentBlock, currentBlock.hash); - const stakingModule = mockedModule(currentBlock); + const stakingModule = mockedModule(currentBlock, currentBlock.hash); mockedKeysApiOperators( keysApiService, @@ -431,11 +431,12 @@ describe('ganache e2e tests', () => { // Mock Keys API again on new block const newBlock = await providerService.provider.getBlock('latest'); const newMeta = mockedMeta(newBlock, newBlock.hash); + const newStakingModule = mockedModule(currentBlock, currentBlock.hash); mockedKeysApiOperators( keysApiService, mockedOperators, - stakingModule, + newStakingModule, newMeta, ); mockedKeysApiUnusedKeys(keysApiService, unusedKeys, newMeta); @@ -487,7 +488,7 @@ describe('ganache e2e tests', () => { ]; const meta = mockedMeta(currentBlock, currentBlock.hash); - const stakingModule = mockedModule(currentBlock); + const stakingModule = mockedModule(currentBlock, currentBlock.hash); mockedKeysApiOperators( keysApiService, @@ -547,11 +548,12 @@ describe('ganache e2e tests', () => { // Mock Keys API again on new block const newBlock = await providerService.provider.getBlock('latest'); const newMeta = mockedMeta(newBlock, newBlock.hash); + const newStakingModule = mockedModule(currentBlock, newBlock.hash); mockedKeysApiOperators( keysApiService, mockedOperators, - stakingModule, + newStakingModule, newMeta, ); @@ -601,7 +603,7 @@ describe('ganache e2e tests', () => { ]; const meta = mockedMeta(currentBlock, currentBlock.hash); - const stakingModule = mockedModule(currentBlock); + const stakingModule = mockedModule(currentBlock, currentBlock.hash); mockedKeysApiOperators( keysApiService, @@ -649,13 +651,13 @@ describe('ganache e2e tests', () => { // Mock Keys API again on new block const newBlock = await providerService.provider.getBlock('latest'); - const newMeta = mockedMeta(newBlock, newBlock.hash); + const newStakingModule = mockedModule(currentBlock, newBlock.hash); mockedKeysApiOperators( keysApiService, mockedOperators, - stakingModule, + newStakingModule, newMeta, ); @@ -718,7 +720,7 @@ describe('ganache e2e tests', () => { ]; const meta = mockedMeta(currentBlock, currentBlock.hash); - const stakingModule = mockedModule(currentBlock); + const stakingModule = mockedModule(currentBlock, currentBlock.hash); mockedKeysApiOperators( keysApiService, @@ -777,13 +779,13 @@ describe('ganache e2e tests', () => { // Mock Keys API again on new block, but now mark as used const newBlock = await providerService.provider.getBlock('latest'); - const newMeta = mockedMeta(newBlock, newBlock.hash); + const newStakingModule = mockedModule(currentBlock, newBlock.hash); mockedKeysApiOperators( keysApiService, mockedOperators, - stakingModule, + newStakingModule, newMeta, ); @@ -863,7 +865,7 @@ describe('ganache e2e tests', () => { }); // mocked curated module - const stakingModule = mockedModule(currentBlock); + const stakingModule = mockedModule(currentBlock, currentBlock.hash); const meta = mockedMeta(currentBlock, currentBlock.hash); mockedKeysApiOperators( @@ -929,11 +931,12 @@ describe('ganache e2e tests', () => { const newBlock = await tempProvider.getBlock('latest'); const newMeta = mockedMeta(newBlock, newBlock.hash); + const newStakingModule = mockedModule(currentBlock, newBlock.hash); mockedKeysApiOperators( keysApiService, mockedOperators, - stakingModule, + newStakingModule, newMeta, ); @@ -967,8 +970,48 @@ describe('ganache e2e tests', () => { ); const currentBlock = await tempProvider.getBlock('latest'); + const goodDepositMessage = { + pubkey: pk, + withdrawalCredentials: fromHexString(GOOD_WC), + amount: 32000000000, // gwei! + }; + const goodSigningRoot = computeRoot(goodDepositMessage); + const goodSig = sk.sign(goodSigningRoot).toBytes(); + + const goodDepositData = { + ...goodDepositMessage, + signature: goodSig, + }; + const goodDepositDataRoot = DepositData.hashTreeRoot(goodDepositData); + + if (!process.env.WALLET_PRIVATE_KEY) throw new Error(NO_PRIVKEY_MESSAGE); + const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY); + + // Make a deposit + const signer = wallet.connect(providerService.provider); + const depositContract = DepositAbi__factory.connect( + DEPOSIT_CONTRACT, + signer, + ); + await depositContract.deposit( + goodDepositData.pubkey, + goodDepositData.withdrawalCredentials, + goodDepositData.signature, + goodDepositDataRoot, + { value: ethers.constants.WeiPerEther.mul(32) }, + ); + + await depositService.setCachedEvents({ + data: [], + headers: { + startBlock: currentBlock.number, + endBlock: currentBlock.number, + version: '1', + }, + }); + // mocked curated module - const stakingModule = mockedModule(currentBlock); + const stakingModule = mockedModule(currentBlock, currentBlock.hash); const meta = mockedMeta(currentBlock, currentBlock.hash); mockedKeysApiOperators( @@ -1054,7 +1097,7 @@ describe('ganache e2e tests', () => { }); // mocked curated module - const stakingModule = mockedModule(currentBlock); + const stakingModule = mockedModule(currentBlock, currentBlock.hash); const meta = mockedMeta(currentBlock, currentBlock.hash); mockedKeysApiOperators( @@ -1101,11 +1144,12 @@ describe('ganache e2e tests', () => { const newBlock = await tempProvider.getBlock('latest'); const newMeta = mockedMeta(newBlock, newBlock.hash); + const newStakingModule = mockedModule(currentBlock, newBlock.hash); mockedKeysApiOperators( keysApiService, mockedOperators, - stakingModule, + newStakingModule, newMeta, ); @@ -1177,7 +1221,7 @@ describe('ganache e2e tests', () => { }); // mocked curated module - const stakingModule = mockedModule(currentBlock); + const stakingModule = mockedModule(currentBlock, currentBlock.hash); const meta = mockedMeta(currentBlock, currentBlock.hash); mockedKeysApiOperators( @@ -1233,7 +1277,7 @@ describe('ganache e2e tests', () => { // mocked curated module const newMeta = mockedMeta(newBlock, newBlock.hash); - const newStakingModule = mockedModule(newBlock, 6047); + const newStakingModule = mockedModule(newBlock, newBlock.hash, 6047); mockedKeysApiOperators( keysApiService,