From 849d33af9ebbe15bc69ecd0adf4c77536378f810 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 28 Nov 2023 13:51:21 +0400 Subject: [PATCH 01/37] feat: duplicated vetted keys checks --- src/contracts/deposit/deposit.service.ts | 2 + src/guardian/guardian.service.ts | 27 ++-- .../interfaces/staking-module.interface.ts | 4 +- .../staking-module-guard.service.ts | 115 +++++++++++++---- .../GroupedByModuleOperatorListResponse.ts | 10 ++ src/keys-api/interfaces/KeyListResponse.ts | 7 ++ src/keys-api/interfaces/RegistryKey.ts | 5 + src/keys-api/interfaces/RegistryOperator.ts | 38 ++++++ .../interfaces/SROperatorListWithModule.ts | 13 ++ src/keys-api/interfaces/index.ts | 1 + src/keys-api/keys-api.service.ts | 41 +++++- src/staking-router/keys.fixtures.ts | 66 ++++++++++ src/staking-router/operators.fixtures.ts | 83 +++++++++++++ src/staking-router/staking-router.service.ts | 73 +++++++++++ src/staking-router/staking-router.spec.ts | 117 ++++++++++++++++++ 15 files changed, 559 insertions(+), 43 deletions(-) create mode 100644 src/keys-api/interfaces/GroupedByModuleOperatorListResponse.ts create mode 100644 src/keys-api/interfaces/KeyListResponse.ts create mode 100644 src/keys-api/interfaces/RegistryOperator.ts create mode 100644 src/keys-api/interfaces/SROperatorListWithModule.ts create mode 100644 src/staking-router/keys.fixtures.ts create mode 100644 src/staking-router/operators.fixtures.ts create mode 100644 src/staking-router/staking-router.spec.ts diff --git a/src/contracts/deposit/deposit.service.ts b/src/contracts/deposit/deposit.service.ts index d6402b52..caf51273 100644 --- a/src/contracts/deposit/deposit.service.ts +++ b/src/contracts/deposit/deposit.service.ts @@ -36,6 +36,7 @@ export class DepositService { private blsService: BlsService, ) {} + // Why OneAtTime() ? this function is executed in guardian.service inside another function with OneAtTime() in cron job @OneAtTime() public async handleNewBlock(blockNumber: number): Promise { if (blockNumber % DEPOSIT_EVENTS_CACHE_UPDATE_BLOCK_RATE !== 0) return; @@ -336,6 +337,7 @@ export class DepositService { const mergedEvents = cachedEvents.data.concat(freshEvents); + // TODO: Why we don't cache freshEvents? return { events: mergedEvents, startBlock: cachedEvents.headers.startBlock, diff --git a/src/guardian/guardian.service.ts b/src/guardian/guardian.service.ts index 34d13b6e..fccafc7b 100644 --- a/src/guardian/guardian.service.ts +++ b/src/guardian/guardian.service.ts @@ -96,10 +96,9 @@ export class GuardianService implements OnModuleInit { this.logger.log('New staking router state cycle start'); try { - const { - elBlockSnapshot: { blockHash, blockNumber }, - data: stakingModules, - } = await this.stakingRouterService.getStakingModules(); + // TODO: rename + const { blockHash, blockNumber, vettedKeys, stakingModulesData } = + await this.stakingRouterService.getVettedAndUnusedKeys(); await this.repositoryService.initCachedContracts({ blockHash }); @@ -119,8 +118,10 @@ export class GuardianService implements OnModuleInit { return; } + const stakingModulesNumber = stakingModulesData.length; + this.logger.log('Staking modules loaded', { - modulesCount: stakingModules.length, + modulesCount: stakingModulesNumber, }); await this.depositService.handleNewBlock(blockNumber); @@ -136,14 +137,14 @@ export class GuardianService implements OnModuleInit { blockHash: blockData.blockHash, }); - await Promise.all( - stakingModules.map(async (stakingRouterModule) => { - const stakingModuleData = - await this.stakingModuleGuardService.getStakingRouterModuleData( - stakingRouterModule, - blockHash, - ); + // TODO: check only if one of nonce changed + await this.stakingModuleGuardService.checkVettedKeysDuplicates( + vettedKeys, + blockData, + ); + await Promise.all( + stakingModulesData.map(async (stakingModuleData) => { await this.stakingModuleGuardService.checkKeysIntersections( stakingModuleData, blockData, @@ -157,7 +158,7 @@ export class GuardianService implements OnModuleInit { ); await this.guardianMessageService.pingMessageBroker( - stakingModules.map(({ id }) => id), + stakingModulesData.map(({ stakingModuleId }) => stakingModuleId), blockData, ); diff --git a/src/guardian/interfaces/staking-module.interface.ts b/src/guardian/interfaces/staking-module.interface.ts index fe08e7bd..593a2c83 100644 --- a/src/guardian/interfaces/staking-module.interface.ts +++ b/src/guardian/interfaces/staking-module.interface.ts @@ -1,7 +1,9 @@ +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; + export interface StakingModuleData { blockHash: string; - isDepositsPaused: boolean; unusedKeys: string[]; + vettedKeys: RegistryKey[]; nonce: number; stakingModuleId: number; } 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 6590df66..5333fb4e 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -12,6 +12,7 @@ import { GuardianMessageService } from '../guardian-message'; import { StakingRouterService } from 'staking-router'; import { SRModule } from 'keys-api/interfaces'; +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; @Injectable() export class StakingModuleGuardService { @@ -30,34 +31,55 @@ export class StakingModuleGuardService { private lastContractsStateByModuleId: Record = {}; - public async getStakingRouterModuleData( - stakingRouterModule: SRModule, - blockHash: string, - ): Promise { - const { - data: { - keys, - module: { nonce }, - }, - } = await this.stakingRouterService.getStakingModuleUnusedKeys( - blockHash, - stakingRouterModule, - ); + // public async getStakingRouterModuleData( + // stakingRouterModule: SRModule, + // blockHash: string, + // ): Promise { + // const { + // data: { + // keys, + // module: { nonce }, + // }, + // } = await this.stakingRouterService.getStakingModuleUnusedKeys( + // blockHash, + // stakingRouterModule, + // ); + + // const isDepositsPaused = await this.securityService.isDepositsPaused( + // stakingRouterModule.id, + // { + // blockHash, + // }, + // ); + + // return { + // nonce, + // unusedKeys: keys.map((srKey) => srKey.key), + // stakingModuleId: stakingRouterModule.id, + // blockHash, + // }; + // } - const isDepositsPaused = await this.securityService.isDepositsPaused( - stakingRouterModule.id, - { - blockHash, - }, + /** + * Check vetted among staking modules + */ + public async checkVettedKeysDuplicates( + vettedKeys: RegistryKey[], + blockData: BlockData, + ): Promise { + const uniqueKeys = new Set(); + const duplicatedKeys = vettedKeys.filter( + (vettedKey) => uniqueKeys.size === uniqueKeys.add(vettedKey.key).size, ); - return { - nonce, - unusedKeys: keys.map((srKey) => srKey.key), - isDepositsPaused, - stakingModuleId: stakingRouterModule.id, - blockHash, - }; + if (duplicatedKeys.length) { + this.logger.warn('Found duplicated vetted key', { + blockHash: blockData.blockHash, + duplicatedKeys, + }); + //TODO: set metric + throw Error('Found duplicated vetted key'); + } } /** @@ -88,7 +110,14 @@ export class StakingModuleGuardService { filteredIntersections, ); - if (stakingModuleData.isDepositsPaused) { + const isDepositsPaused = await this.securityService.isDepositsPaused( + stakingModuleData.stakingModuleId, + { + blockHash: stakingModuleData.blockHash, + }, + ); + + if (isDepositsPaused) { this.logger.warn('Deposits are paused', { blockHash, stakingModuleId }); return; } @@ -96,6 +125,17 @@ export class StakingModuleGuardService { if (isFilteredIntersectionsFound) { await this.handleKeysIntersections(stakingModuleData, blockData); } else { + const usedKeys = await this.handleIntersectionBetweenUsedAndUnusedKeys( + keysIntersections, + ); + + // if found used keys, Lido already made deposit on this keys + if (usedKeys.length) { + this.logger.log('Found that we already deposited on these keys'); + // set metric ccouncil_daemon_used_duplicate + return; + } + await this.handleCorrectKeys(stakingModuleData, blockData); } } @@ -156,6 +196,29 @@ export class StakingModuleGuardService { return attackIntersections; } + public async handleIntersectionBetweenUsedAndUnusedKeys( + intersectionsWithLidoWC: VerifiedDepositEvent[], + ) { + const depositedPubkeys = intersectionsWithLidoWC.map( + (deposit) => deposit.pubkey, + ); + + if (depositedPubkeys.length) { + console.log('deposited pubkeys', depositedPubkeys); + this.logger.log( + 'Found intersections with lido credentials, need to check duplicated keys', + ); + + const keys = await this.stakingRouterService.getKeysWithDuplicates( + depositedPubkeys, + ); + const usedKeys = keys.data.filter((key) => key.used); + return usedKeys; + } + + return []; + } + /** * Handles the situation when keys have previously deposited copies * @param blockData - collected data from the current block diff --git a/src/keys-api/interfaces/GroupedByModuleOperatorListResponse.ts b/src/keys-api/interfaces/GroupedByModuleOperatorListResponse.ts new file mode 100644 index 00000000..f2ae79ed --- /dev/null +++ b/src/keys-api/interfaces/GroupedByModuleOperatorListResponse.ts @@ -0,0 +1,10 @@ +import { Meta } from './Meta'; +import { SROperatorListWithModule } from './SROperatorListWithModule'; + +export type GroupedByModuleOperatorListResponse = { + /** + * Staking router module operators with module + */ + data: SROperatorListWithModule[]; + meta: Meta; +}; diff --git a/src/keys-api/interfaces/KeyListResponse.ts b/src/keys-api/interfaces/KeyListResponse.ts new file mode 100644 index 00000000..33db0f39 --- /dev/null +++ b/src/keys-api/interfaces/KeyListResponse.ts @@ -0,0 +1,7 @@ +import type { Meta } from './Meta'; +import { RegistryKey } from './RegistryKey'; + +export type KeyListResponse = { + data: Array; + meta: Meta; +}; diff --git a/src/keys-api/interfaces/RegistryKey.ts b/src/keys-api/interfaces/RegistryKey.ts index e7c331cc..4b133b3f 100644 --- a/src/keys-api/interfaces/RegistryKey.ts +++ b/src/keys-api/interfaces/RegistryKey.ts @@ -19,4 +19,9 @@ export type RegistryKey = { * Key index in contract */ index: number; + + /** + * Staking module address + */ + moduleAddress: string; }; diff --git a/src/keys-api/interfaces/RegistryOperator.ts b/src/keys-api/interfaces/RegistryOperator.ts new file mode 100644 index 00000000..f20d4cb6 --- /dev/null +++ b/src/keys-api/interfaces/RegistryOperator.ts @@ -0,0 +1,38 @@ +export type RegistryOperator = { + /** + * Index of Operator + */ + index: number; + /** + * This value shows if node operator active + */ + active: boolean; + /** + * Operator name + */ + name: string; + /** + * Ethereum 1 address which receives stETH rewards for this operator + */ + rewardAddress: string; + /** + * The number of keys vetted by the DAO and that can be used for the deposit + */ + stakingLimit: number; + /** + * Amount of stopped validators + */ + stoppedValidators: number; + /** + * Total signing keys amount + */ + totalSigningKeys: number; + /** + * Amount of used signing keys + */ + usedSigningKeys: number; + /** + * Staking module address + */ + moduleAddress: string; +}; diff --git a/src/keys-api/interfaces/SROperatorListWithModule.ts b/src/keys-api/interfaces/SROperatorListWithModule.ts new file mode 100644 index 00000000..b3cdfec0 --- /dev/null +++ b/src/keys-api/interfaces/SROperatorListWithModule.ts @@ -0,0 +1,13 @@ +import type { RegistryOperator } from './RegistryOperator'; +import type { SRModule } from './SRModule'; + +export type SROperatorListWithModule = { + /** + * Operators of staking router module + */ + operators: Array; + /** + * Detailed Staking Router information + */ + module: SRModule; +}; diff --git a/src/keys-api/interfaces/index.ts b/src/keys-api/interfaces/index.ts index 3eaafa78..da3cc9df 100644 --- a/src/keys-api/interfaces/index.ts +++ b/src/keys-api/interfaces/index.ts @@ -1,3 +1,4 @@ export type { SRModuleKeysResponse } from './SRModuleKeysResponse'; export type { SRModuleListResponse } from './SRModuleListResponse'; export type { SRModule } from './SRModule'; +export type { KeyListResponse } from './KeyListResponse'; diff --git a/src/keys-api/keys-api.service.ts b/src/keys-api/keys-api.service.ts index aaf84677..6cf28664 100644 --- a/src/keys-api/keys-api.service.ts +++ b/src/keys-api/keys-api.service.ts @@ -1,10 +1,15 @@ import { Injectable, LoggerService, Inject } from '@nestjs/common'; -import { FetchService } from '@lido-nestjs/fetch'; +import { FetchService, RequestInit } from '@lido-nestjs/fetch'; import { AbortController } from 'node-abort-controller'; import { FETCH_REQUEST_TIMEOUT } from './keys-api.constants'; -import { SRModuleKeysResponse, SRModuleListResponse } from './interfaces'; +import { + SRModuleKeysResponse, + SRModuleListResponse, + KeyListResponse, +} from './interfaces'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { Configuration } from 'common/config'; +import { GroupedByModuleOperatorListResponse } from './interfaces/GroupedByModuleOperatorListResponse'; @Injectable() export class KeysApiService { @@ -14,7 +19,7 @@ export class KeysApiService { protected readonly fetchService: FetchService, ) {} - protected async fetch(url: string) { + protected async fetch(url: string, requestInit?: RequestInit) { const controller = new AbortController(); const { signal } = controller; @@ -29,6 +34,7 @@ export class KeysApiService { `${baseUrl}${url}`, { signal, + ...requestInit, }, ); clearTimeout(timer); @@ -54,4 +60,33 @@ export class KeysApiService { throw Error('Keys API not synced, please wait'); return result; } + + /** + * + * @param The /v1/keys/find KAPI endpoint returns a key along with its duplicates + * @returns + */ + public async getKeysWithDuplicates(pubkeys: string[]) { + const result = await this.fetch(`/v1/keys/find`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ pubkeys }), + }); + + return result; + } + + public async getUnusedKeys() { + const result = await this.fetch(`/v1/keys?used=false`); + return result; + } + + public async getOperatorListWithModule() { + const result = await this.fetch( + `/v1/operators`, + ); + return result; + } } diff --git a/src/staking-router/keys.fixtures.ts b/src/staking-router/keys.fixtures.ts new file mode 100644 index 00000000..d6d63188 --- /dev/null +++ b/src/staking-router/keys.fixtures.ts @@ -0,0 +1,66 @@ +export const keysAllStakingModules = { + data: [ + { + 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: '0x8d12ec44816f108df84ef9b03e423a6d8fb0f0a1823c871b123ff41f893a7b372eb038a1ed1ff15083e07a777a5cba50', + depositSignature: + '0xb3a683ec2a71f4b24039ccd10905aee7c08bc542203f68208215853fcf300fde5c10aee40f060da2a35d57050116668511a4b9b1db97e1da33b7c1fcfc192588c7989b00ae3fb7fe697dab18656403fc3d196e6d3bec51bd877c6033653ff5be', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 102, + }, + { + key: '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', + depositSignature: + '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 4, + }, + { + key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 5, + }, + { + key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + depositSignature: + '0xb3967b288b566c72316bc9de9a208d56248df4d29b4666fbe0986f66b1ff64eb1d04dfc484af591084d9992b8dc1c1370f96cf974425b47b1f4315dd3b236005b90c8a88ceef78057bf7c54e84fdc0c3f6ed464257f0823111bac2b2ea1818e0', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 6, + }, + ], + meta: { + elBlockSnapshot: { + blockNumber: 400153, + blockHash: + '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + timestamp: 1701027516, + }, + }, +}; diff --git a/src/staking-router/operators.fixtures.ts b/src/staking-router/operators.fixtures.ts new file mode 100644 index 00000000..fcefbba9 --- /dev/null +++ b/src/staking-router/operators.fixtures.ts @@ -0,0 +1,83 @@ +export const groupedByModulesOperators = { + data: [ + { + operators: [ + { + name: 'Dev team', + rewardAddress: '0x6D725DAe055287f913661ee0b79dE6B21F12A459', + stakingLimit: 101, + stoppedValidators: 0, + totalSigningKeys: 103, + usedSigningKeys: 100, + index: 0, + active: true, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + }, + { + name: 'DSRV', + rewardAddress: '0x39ceC2b3ba293CC15f15a3876dB8D356a1670789', + stakingLimit: 2, + stoppedValidators: 0, + totalSigningKeys: 2, + usedSigningKeys: 2, + index: 1, + active: true, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + }, + ], + module: { + nonce: 364, + type: 'curated-onchain-v1', + id: 1, + stakingModuleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + moduleFee: 500, + treasuryFee: 500, + targetShare: 10000, + status: 0, + name: 'curated-onchain-v1', + lastDepositAt: 1700841084, + lastDepositBlock: 385525, + exitedValidatorsCount: 2, + active: true, + }, + }, + { + operators: [ + { + name: 'Lido x Obol: Delightful Dragonfly', + rewardAddress: '0x142E4542865a638208c17fF288cdA8cC82ecD27a', + stakingLimit: 5, + stoppedValidators: 0, + totalSigningKeys: 7, + usedSigningKeys: 4, + index: 28, + active: true, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + }, + ], + module: { + nonce: 69, + type: 'curated-onchain-v1', + id: 2, + stakingModuleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + moduleFee: 800, + treasuryFee: 200, + targetShare: 500, + status: 0, + name: 'SimpleDVT', + lastDepositAt: 1700764452, + lastDepositBlock: 379465, + exitedValidatorsCount: 0, + active: true, + }, + }, + ], + meta: { + elBlockSnapshot: { + blockNumber: 400153, + blockHash: + '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + timestamp: 1701027516, + }, + }, +}; diff --git a/src/staking-router/staking-router.service.ts b/src/staking-router/staking-router.service.ts index b769ded2..40614346 100644 --- a/src/staking-router/staking-router.service.ts +++ b/src/staking-router/staking-router.service.ts @@ -3,6 +3,8 @@ import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { Configuration } from 'common/config'; import { KeysApiService } from 'keys-api/keys-api.service'; import { SRModuleKeysResponse, SRModule } from 'keys-api/interfaces'; +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; +import { StakingModuleData } from 'guardian'; @Injectable() export class StakingRouterService { @@ -13,6 +15,77 @@ export class StakingRouterService { protected readonly keysApiService: KeysApiService, ) {} + public async getVettedAndUnusedKeys() { + // TODO: add cache by modules nonce + const operatorsByModules = + await this.keysApiService.getOperatorListWithModule(); + const operatorsBlockHash = + operatorsByModules.meta.elBlockSnapshot.blockHash; + const operatorsBlockNumber = + operatorsByModules.meta.elBlockSnapshot.blockNumber; + + const unusedKeys = await this.keysApiService.getUnusedKeys(); + const keysBlockHash = unusedKeys.meta.elBlockSnapshot.blockHash; + if (keysBlockHash != operatorsBlockHash) { + this.logger.log('Blockhash of the received keys and operators', { + keysBlockHash, + operatorsBlockHash, + }); + + throw Error( + 'Blockhash of the received keys does not match the blockhash of operators', + ); + } + + // found vetted keys + const vettedKeys: RegistryKey[] = []; + const stakingModulesData: StakingModuleData[] = []; + + operatorsByModules.data.forEach(({ operators, module: stakingModule }) => { + const moduleKeys: RegistryKey[] = []; + const moduleVettedKeys: RegistryKey[] = []; + operators.forEach((operator) => { + const operatorKeys = unusedKeys.data.filter( + (key) => + key.moduleAddress === operator.moduleAddress && + key.operatorIndex === operator.index, + ); + // Sort the filtered keys by index + operatorKeys.sort((a, b) => a.index - b.index); + + moduleKeys.push(...operatorKeys); + + const numberOfVettedUnusedKeys = + operator.stakingLimit - operator.usedSigningKeys; + const operatorVettedKeys = operatorKeys.slice( + 0, + numberOfVettedUnusedKeys, + ); + moduleVettedKeys.push(...operatorVettedKeys); + vettedKeys.push(...operatorVettedKeys); + }); + + stakingModulesData.push({ + unusedKeys: moduleKeys.map((srKey) => srKey.key), + nonce: stakingModule.nonce, + stakingModuleId: stakingModule.id, + blockHash: operatorsBlockHash, + vettedKeys: moduleVettedKeys, + }); + }); + + return { + stakingModulesData, + vettedKeys, + blockHash: operatorsBlockHash, + blockNumber: operatorsBlockNumber, + }; + } + + public async getKeysWithDuplicates(pubkeys: string[]) { + return await this.keysApiService.getKeysWithDuplicates(pubkeys); + } + public async getStakingModules() { return await this.keysApiService.getModulesList(); } diff --git a/src/staking-router/staking-router.spec.ts b/src/staking-router/staking-router.spec.ts new file mode 100644 index 00000000..4229e2e9 --- /dev/null +++ b/src/staking-router/staking-router.spec.ts @@ -0,0 +1,117 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { KeysApiService } from '../keys-api/keys-api.service'; +import { StakingRouterService } from './staking-router.service'; +import { groupedByModulesOperators } from './operators.fixtures'; +import { keysAllStakingModules } from './keys.fixtures'; +import { ConfigModule } from 'common/config'; +import { LoggerModule } from 'common/logger'; + +describe('YourService', () => { + let stakingRouterService: StakingRouterService; + let keysApiService: KeysApiService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ConfigModule.forRoot(), LoggerModule], + providers: [ + StakingRouterService, + { + provide: KeysApiService, + useValue: { + getOperatorListWithModule: jest.fn(), + getUnusedKeys: jest.fn(), + }, + }, + ], + }).compile(); + + stakingRouterService = + module.get(StakingRouterService); + keysApiService = module.get(KeysApiService); + // keysApiService = module.get(KeysApiService) as jest.Mocked; + }); + + it('should return correct data when block hashes match', async () => { + (keysApiService.getOperatorListWithModule as jest.Mock).mockResolvedValue( + groupedByModulesOperators, + ); + (keysApiService.getUnusedKeys as jest.Mock).mockResolvedValue( + keysAllStakingModules, + ); + + const result = await stakingRouterService.getVettedAndUnusedKeys(); + + // Assertions + expect(result).toEqual({ + blockNumber: 400153, + blockHash: + '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + stakingModulesData: [ + { + unusedKeys: [ + '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + '0x8d12ec44816f108df84ef9b03e423a6d8fb0f0a1823c871b123ff41f893a7b372eb038a1ed1ff15083e07a777a5cba50', + ], + vettedKeys: [ + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 100, + }, + ], + blockHash: + '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + stakingModuleId: 1, + nonce: 364, + }, + { + unusedKeys: [ + '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', + '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + ], + vettedKeys: [ + { + key: '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', + depositSignature: + '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 4, + }, + ], + nonce: 69, + blockHash: + '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + stakingModuleId: 2, + }, + ], + vettedKeys: [ + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 100, + }, + { + key: '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', + depositSignature: + '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 4, + }, + ], + }); + }); +}); From c8b3d25450a9efdeadaa2899be09599b595c4559 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 5 Dec 2023 20:42:22 +0400 Subject: [PATCH 02/37] fix: stop deposits only for modules with duplicates; fix tests --- src/contracts/deposit/deposit.service.ts | 2 - src/guardian/guardian.service.spec.ts | 107 ++++++-- src/guardian/guardian.service.ts | 22 +- .../interfaces/staking-module.interface.ts | 1 + .../staking-module-guard/keys.fixtures.ts | 193 +++++++++++++ .../staking-module-guard.service.ts | 120 ++++---- .../staking-module-guard.spec.ts | 256 +++++++++++++++++- src/staking-router/staking-router.service.ts | 5 +- src/staking-router/staking-router.spec.ts | 4 +- 9 files changed, 620 insertions(+), 90 deletions(-) create mode 100644 src/guardian/staking-module-guard/keys.fixtures.ts diff --git a/src/contracts/deposit/deposit.service.ts b/src/contracts/deposit/deposit.service.ts index caf51273..3f00e87a 100644 --- a/src/contracts/deposit/deposit.service.ts +++ b/src/contracts/deposit/deposit.service.ts @@ -36,8 +36,6 @@ export class DepositService { private blsService: BlsService, ) {} - // Why OneAtTime() ? this function is executed in guardian.service inside another function with OneAtTime() in cron job - @OneAtTime() public async handleNewBlock(blockNumber: number): Promise { if (blockNumber % DEPOSIT_EVENTS_CACHE_UPDATE_BLOCK_RATE !== 0) return; diff --git a/src/guardian/guardian.service.spec.ts b/src/guardian/guardian.service.spec.ts index 16038281..61b481d7 100644 --- a/src/guardian/guardian.service.spec.ts +++ b/src/guardian/guardian.service.spec.ts @@ -26,27 +26,92 @@ jest.mock('../transport/stomp/stomp.client'); const TEST_MODULE_ID = 1; -const stakingModuleResponse = { - data: [ +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', + unusedKeys: [ + '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + ], + 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, + }, + ], + nonce: 0, + stakingModuleId: 2, + stakingModuleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + }, { + blockHash: 'some_hash', + unusedKeys: [ + '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + ], + vettedKeys: [ + { + key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 5, + }, + ], nonce: 0, - type: 'string', - id: TEST_MODULE_ID, - stakingModuleAddress: 'string', - moduleFee: 0, - treasuryFee: 0, - targetShare: 0, - status: 0, - name: 'string', - lastDepositAt: 0, - lastDepositBlock: 0, + stakingModuleId: 3, + stakingModuleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', }, ], - elBlockSnapshot: { - blockNumber: 0, - blockHash: 'string', - timestamp: 0, - }, }; describe('GuardianService', () => { @@ -101,9 +166,9 @@ describe('GuardianService', () => { }); it('should exit if the previous call is not completed', async () => { - const getStakingModulesMock = jest - .spyOn(stakingRouterService, 'getStakingModules') - .mockImplementation(async () => stakingModuleResponse); + const getVettedKeysMock = jest + .spyOn(stakingRouterService, 'getVettedAndUnusedKeys') + .mockImplementation(async () => vettedKeysResponse); const getBlockGuardServiceMock = jest .spyOn(blockGuardService, 'isNeedToProcessNewState') @@ -114,7 +179,7 @@ describe('GuardianService', () => { guardianService.handleNewBlock(), ]); - expect(getStakingModulesMock).toBeCalledTimes(1); + // expect(getStakingModulesMock).toBeCalledTimes(1); expect(getBlockGuardServiceMock).toBeCalledTimes(1); }); }); diff --git a/src/guardian/guardian.service.ts b/src/guardian/guardian.service.ts index fccafc7b..3b88f7a9 100644 --- a/src/guardian/guardian.service.ts +++ b/src/guardian/guardian.service.ts @@ -22,6 +22,7 @@ import { BlockGuardService } from './block-guard'; import { StakingModuleGuardService } from './staking-module-guard'; import { GuardianMessageService } from './guardian-message'; import { GuardianMetricsService } from './guardian-metrics'; +import { StakingModuleData } from './interfaces'; @Injectable() export class GuardianService implements OnModuleInit { @@ -138,13 +139,24 @@ export class GuardianService implements OnModuleInit { }); // TODO: check only if one of nonce changed - await this.stakingModuleGuardService.checkVettedKeysDuplicates( - vettedKeys, - blockData, - ); + const addressesOfModulesWithDuplicateKeys: string[] = + this.stakingModuleGuardService.checkVettedKeysDuplicates( + vettedKeys, + blockData, + ); + + const stakingModulesWithoutDuplicates: StakingModuleData[] = + this.stakingModuleGuardService.excludeModulesWithDuplicatedKeys( + stakingModulesData, + addressesOfModulesWithDuplicateKeys, + ); + + this.logger.log('Staking modules without duplicates', { + modulesCount: stakingModulesWithoutDuplicates.length, + }); await Promise.all( - stakingModulesData.map(async (stakingModuleData) => { + stakingModulesWithoutDuplicates.map(async (stakingModuleData) => { await this.stakingModuleGuardService.checkKeysIntersections( stakingModuleData, blockData, diff --git a/src/guardian/interfaces/staking-module.interface.ts b/src/guardian/interfaces/staking-module.interface.ts index 593a2c83..c5a48f99 100644 --- a/src/guardian/interfaces/staking-module.interface.ts +++ b/src/guardian/interfaces/staking-module.interface.ts @@ -6,4 +6,5 @@ export interface StakingModuleData { vettedKeys: RegistryKey[]; nonce: number; stakingModuleId: number; + stakingModuleAddress: string; } diff --git a/src/guardian/staking-module-guard/keys.fixtures.ts b/src/guardian/staking-module-guard/keys.fixtures.ts new file mode 100644 index 00000000..b2e8e2bf --- /dev/null +++ b/src/guardian/staking-module-guard/keys.fixtures.ts @@ -0,0 +1,193 @@ +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; + +export const vettedKeysDuplicatesAcrossModules: RegistryKey[] = [ + { + 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: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + depositSignature: + '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 4, + }, + { + key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 5, + }, + { + key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: 'another_module', + index: 5, + }, +]; + +export const vettedKeysDuplicatesAcrossOneModule: RegistryKey[] = [ + { + 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: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 102, + }, + { + key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 5, + }, + { + key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: 'another_module', + index: 5, + }, +]; + +export const vettedKeysDuplicatesAcrossOneModuleAndFew: RegistryKey[] = [ + { + 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: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + depositSignature: + '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 4, + }, + { + key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 5, + }, + { + key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + depositSignature: + '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 6, + }, + { + key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: 'another_module', + index: 5, + }, +]; + +export const vettedKeysWithoutDuplicates: RegistryKey[] = [ + { + 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, + }, + { + key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: 'another_module', + index: 5, + }, +]; 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 5333fb4e..f3d3d14a 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -11,7 +11,6 @@ import { GuardianMetricsService } from '../guardian-metrics'; import { GuardianMessageService } from '../guardian-message'; import { StakingRouterService } from 'staking-router'; -import { SRModule } from 'keys-api/interfaces'; import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; @Injectable() @@ -31,55 +30,83 @@ export class StakingModuleGuardService { private lastContractsStateByModuleId: Record = {}; - // public async getStakingRouterModuleData( - // stakingRouterModule: SRModule, - // blockHash: string, - // ): Promise { - // const { - // data: { - // keys, - // module: { nonce }, - // }, - // } = await this.stakingRouterService.getStakingModuleUnusedKeys( - // blockHash, - // stakingRouterModule, - // ); - - // const isDepositsPaused = await this.securityService.isDepositsPaused( - // stakingRouterModule.id, - // { - // blockHash, - // }, - // ); - - // return { - // nonce, - // unusedKeys: keys.map((srKey) => srKey.key), - // stakingModuleId: stakingRouterModule.id, - // blockHash, - // }; - // } - /** - * Check vetted among staking modules + * + * @param vettedKeys vetted keys of all staking modules + * @returns List of staking modules with duplicates */ - public async checkVettedKeysDuplicates( + public checkVettedKeysDuplicates( vettedKeys: RegistryKey[], blockData: BlockData, - ): Promise { - const uniqueKeys = new Set(); - const duplicatedKeys = vettedKeys.filter( - (vettedKey) => uniqueKeys.size === uniqueKeys.add(vettedKey.key).size, - ); + ): string[] { + const keyMap = new Map>(); + + // Populate the map + vettedKeys.forEach((vettedKey) => { + if (!keyMap.has(vettedKey.key)) { + // keyMap doesn't have vettedKey.key key + keyMap.set(vettedKey.key, new Map([[vettedKey.moduleAddress, 1]])); + } else { + // keyMap has vettedKey.key key + // get moduleAddress set + const modules = keyMap.get(vettedKey.key); + const moduleCount = modules?.get(vettedKey.moduleAddress) || 0; + modules?.set(vettedKey.moduleAddress, moduleCount + 1); + } + }); + + const duplicatedKeysWithModules: { key: string; modules: string[] }[] = []; + const modulesWithDuplicatedKeysSet = new Set(); - if (duplicatedKeys.length) { - this.logger.warn('Found duplicated vetted key', { + keyMap.forEach((modules, key) => { + if (modules.size > 1 || Array.from(modules)[0][1] > 1) { + duplicatedKeysWithModules.push({ + key, + modules: Array.from(modules.keys()), + }); + + Array.from(modules.keys()).forEach((stakingModule) => + modulesWithDuplicatedKeysSet.add(stakingModule), + ); + } + }); + + // TODO: consider add of contract module_id + if (duplicatedKeysWithModules.length) { + const moduleAddressesWithDuplicatesList: string[] = Array.from( + modulesWithDuplicatedKeysSet, + ); + this.logger.warn('Found duplicated vetted keys', { blockHash: blockData.blockHash, - duplicatedKeys, + duplicatedKeys: duplicatedKeysWithModules, + moduleAddressesWithDuplicates: moduleAddressesWithDuplicatesList, }); - //TODO: set metric - throw Error('Found duplicated vetted key'); + + //TODO: set prometheus metric council_daemon_vetted_unused_duplicate + return moduleAddressesWithDuplicatesList; } + + return []; + } + + public excludeModulesWithDuplicatedKeys( + stakingModulesData: StakingModuleData[], + addressesOfModulesWithDuplicateKeys: string[], + ): StakingModuleData[] { + // exclude from stakingModulesData stakingModulesWithDuplicates + let stakingModulesWithoutDuplicates: StakingModuleData[] = + stakingModulesData; + + if (addressesOfModulesWithDuplicateKeys.length) { + // need to filter stakingModulesWithoutDuplicates + + stakingModulesWithoutDuplicates = stakingModulesWithoutDuplicates.filter( + ({ stakingModuleAddress }) => + !addressesOfModulesWithDuplicateKeys.includes(stakingModuleAddress), + ); + } + + return stakingModulesWithoutDuplicates; } /** @@ -125,14 +152,14 @@ export class StakingModuleGuardService { if (isFilteredIntersectionsFound) { await this.handleKeysIntersections(stakingModuleData, blockData); } else { - const usedKeys = await this.handleIntersectionBetweenUsedAndUnusedKeys( + const usedKeys = await this.getIntersectionBetweenUsedAndUnusedKeys( keysIntersections, ); // if found used keys, Lido already made deposit on this keys if (usedKeys.length) { this.logger.log('Found that we already deposited on these keys'); - // set metric ccouncil_daemon_used_duplicate + // set metric council_daemon_used_duplicate return; } @@ -196,7 +223,7 @@ export class StakingModuleGuardService { return attackIntersections; } - public async handleIntersectionBetweenUsedAndUnusedKeys( + public async getIntersectionBetweenUsedAndUnusedKeys( intersectionsWithLidoWC: VerifiedDepositEvent[], ) { const depositedPubkeys = intersectionsWithLidoWC.map( @@ -204,7 +231,6 @@ export class StakingModuleGuardService { ); if (depositedPubkeys.length) { - console.log('deposited pubkeys', depositedPubkeys); this.logger.log( 'Found intersections with lido credentials, need to check duplicated keys', ); @@ -212,6 +238,8 @@ export class StakingModuleGuardService { const keys = await this.stakingRouterService.getKeysWithDuplicates( depositedPubkeys, ); + + // TODO: add block number check. keys blockNumber should be newer than we have in blockData because of used keys is not deleted const usedKeys = keys.data.filter((key) => key.used); return usedKeys; } 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 4f45e7f1..90691306 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -17,10 +17,19 @@ import { GuardianMessageService, } from '../guardian-message'; import { StakingModuleGuardService } from './staking-module-guard.service'; +import { StakingModuleData } from 'guardian/interfaces'; +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; +import { + vettedKeysDuplicatesAcrossModules, + vettedKeysDuplicatesAcrossOneModule, + vettedKeysDuplicatesAcrossOneModuleAndFew, + vettedKeysWithoutDuplicates, +} from './keys.fixtures'; jest.mock('../../transport/stomp/stomp.client'); const TEST_MODULE_ID = 1; + const stakingModuleData = { nonce: 0, type: 'string', @@ -37,7 +46,7 @@ const stakingModuleData = { isDepositsPaused: false, }; -describe('GuardianService', () => { +describe('StakingModuleGuardService', () => { let loggerService: LoggerService; let lidoService: LidoService; let securityService: SecurityService; @@ -81,7 +90,7 @@ describe('GuardianService', () => { }; const blockData = { unusedKeys, depositedEvents } as any; const matched = stakingModuleGuardService.getKeysIntersections( - { ...stakingModuleData, unusedKeys }, + { ...stakingModuleData, unusedKeys, vettedKeys: [] }, blockData, ); @@ -98,7 +107,7 @@ describe('GuardianService', () => { }; const blockData = { unusedKeys, depositedEvents } as any; const matched = stakingModuleGuardService.getKeysIntersections( - { ...stakingModuleData, unusedKeys }, + { ...stakingModuleData, unusedKeys, vettedKeys: [] }, blockData, ); @@ -114,7 +123,7 @@ describe('GuardianService', () => { }; const blockData = { unusedKeys, depositedEvents } as any; const matched = stakingModuleGuardService.getKeysIntersections( - { ...stakingModuleData, unusedKeys }, + { ...stakingModuleData, unusedKeys, vettedKeys: [] }, blockData, ); @@ -180,18 +189,23 @@ describe('GuardianService', () => { .spyOn(lidoService, 'getWithdrawalCredentials') .mockImplementation(async () => lidoWC); + const mockSecurityContractIsDepositsPaused = jest + .spyOn(securityService, 'isDepositsPaused') + .mockImplementation(async () => false); + await stakingModuleGuardService.checkKeysIntersections( - { ...stakingModuleData, unusedKeys }, + { ...stakingModuleData, unusedKeys, vettedKeys: [] }, blockData, ); expect(mockHandleCorrectKeys).not.toBeCalled(); expect(mockHandleKeysIntersections).toBeCalledTimes(1); expect(mockHandleKeysIntersections).toBeCalledWith( - { ...stakingModuleData, unusedKeys }, + { ...stakingModuleData, unusedKeys, vettedKeys: [] }, blockData, ); expect(mockGetWithdrawalCredentials).toBeCalledTimes(1); + expect(mockSecurityContractIsDepositsPaused).toBeCalledTimes(1); }); it('should call handleCorrectKeys if Lido unused keys are not found in the deposit contract', async () => { @@ -207,17 +221,22 @@ describe('GuardianService', () => { .spyOn(stakingModuleGuardService, 'handleKeysIntersections') .mockImplementation(async () => undefined); + const mockSecurityContractIsDepositsPaused = jest + .spyOn(securityService, 'isDepositsPaused') + .mockImplementation(async () => false); + await stakingModuleGuardService.checkKeysIntersections( - { ...stakingModuleData, unusedKeys }, + { ...stakingModuleData, unusedKeys, vettedKeys: [] }, blockData, ); expect(mockHandleKeysIntersections).not.toBeCalled(); expect(mockHandleCorrectKeys).toBeCalledTimes(1); expect(mockHandleCorrectKeys).toBeCalledWith( - { ...stakingModuleData, unusedKeys }, + { ...stakingModuleData, unusedKeys, vettedKeys: [] }, blockData, ); + expect(mockSecurityContractIsDepositsPaused).toBeCalledTimes(1); }); }); @@ -245,11 +264,11 @@ describe('GuardianService', () => { .mockImplementation(async () => signature); await stakingModuleGuardService.handleCorrectKeys( - { ...stakingModuleData, unusedKeys: [] }, + { ...stakingModuleData, unusedKeys: [], vettedKeys: [] }, blockData, ); await stakingModuleGuardService.handleCorrectKeys( - { ...stakingModuleData, unusedKeys: [] }, + { ...stakingModuleData, unusedKeys: [], vettedKeys: [] }, blockData, ); @@ -272,7 +291,7 @@ describe('GuardianService', () => { .mockImplementation(async () => signature); await stakingModuleGuardService.handleCorrectKeys( - { ...stakingModuleData, unusedKeys: [] }, + { ...stakingModuleData, unusedKeys: [], vettedKeys: [] }, blockData, ); @@ -352,7 +371,7 @@ describe('GuardianService', () => { .mockImplementation(async () => undefined); await stakingModuleGuardService.handleKeysIntersections( - { ...stakingModuleData, unusedKeys: [] }, + { ...stakingModuleData, unusedKeys: [], vettedKeys: [] }, blockData, ); @@ -374,7 +393,7 @@ describe('GuardianService', () => { .mockImplementation(async () => undefined); await stakingModuleGuardService.handleKeysIntersections( - { ...stakingModuleData, unusedKeys: [] }, + { ...stakingModuleData, unusedKeys: [], vettedKeys: [] }, blockData, ); @@ -431,4 +450,215 @@ describe('GuardianService', () => { expect(result).toBeFalsy(); }); }); + + describe('excludeModulesWithDuplicatedKeys', () => { + const stakingModules: StakingModuleData[] = [ + { + blockHash: '', + unusedKeys: [], + vettedKeys: [], + nonce: 0, + stakingModuleId: 1, + stakingModuleAddress: 'first_address', + }, + { + blockHash: '', + unusedKeys: [], + vettedKeys: [], + nonce: 0, + stakingModuleId: 2, + stakingModuleAddress: 'second_address', + }, + { + blockHash: '', + unusedKeys: [], + vettedKeys: [], + nonce: 0, + stakingModuleId: 3, + stakingModuleAddress: 'third_address', + }, + ]; + + it('should exclude modules', () => { + const addressesOfModulesWithDuplicateKeys = ['second_address']; + const expectedStakingModules: StakingModuleData[] = [ + { + blockHash: '', + unusedKeys: [], + vettedKeys: [], + nonce: 0, + stakingModuleId: 1, + stakingModuleAddress: 'first_address', + }, + { + blockHash: '', + unusedKeys: [], + vettedKeys: [], + nonce: 0, + stakingModuleId: 3, + stakingModuleAddress: 'third_address', + }, + ]; + + const result = stakingModuleGuardService.excludeModulesWithDuplicatedKeys( + stakingModules, + addressesOfModulesWithDuplicateKeys, + ); + + expect(result.length).toEqual(2); + expect(result).toEqual(expect.arrayContaining(expectedStakingModules)); + }); + + it('should return list without changes', () => { + const addressesOfModulesWithDuplicateKeys = ['fourth_address']; + const expectedStakingModules = [ + { + blockHash: '', + unusedKeys: [], + vettedKeys: [], + nonce: 0, + stakingModuleId: 1, + stakingModuleAddress: 'first_address', + }, + { + blockHash: '', + unusedKeys: [], + vettedKeys: [], + nonce: 0, + stakingModuleId: 2, + stakingModuleAddress: 'second_address', + }, + { + blockHash: '', + unusedKeys: [], + vettedKeys: [], + nonce: 0, + stakingModuleId: 3, + stakingModuleAddress: 'third_address', + }, + ]; + + const result = stakingModuleGuardService.excludeModulesWithDuplicatedKeys( + stakingModules, + addressesOfModulesWithDuplicateKeys, + ); + + expect(result.length).toEqual(3); + expect(result).toEqual(expect.arrayContaining(expectedStakingModules)); + }); + + it('should return list without changes if duplicated keys were not found', () => { + const addressesOfModulesWithDuplicateKeys = []; + const expectedStakingModules = [ + { + blockHash: '', + unusedKeys: [], + vettedKeys: [], + nonce: 0, + stakingModuleId: 1, + stakingModuleAddress: 'first_address', + }, + { + blockHash: '', + unusedKeys: [], + vettedKeys: [], + nonce: 0, + stakingModuleId: 2, + stakingModuleAddress: 'second_address', + }, + { + blockHash: '', + unusedKeys: [], + vettedKeys: [], + nonce: 0, + stakingModuleId: 3, + stakingModuleAddress: 'third_address', + }, + ]; + + const result = stakingModuleGuardService.excludeModulesWithDuplicatedKeys( + stakingModules, + addressesOfModulesWithDuplicateKeys, + ); + + expect(result.length).toEqual(3); + expect(result).toEqual(expect.arrayContaining(expectedStakingModules)); + }); + }); + + describe('checkVettedKeysDuplicates', () => { + const blockData = { blockHash: 'some_hash' } as any; + + it('should found duplicated keys across two module', () => { + const result = stakingModuleGuardService.checkVettedKeysDuplicates( + vettedKeysDuplicatesAcrossModules, + blockData, + ); + + const addressesOfModulesWithDuplicateKeys = [ + '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + ]; + + // result has all addressesOfModulesWithDuplicateKeys elements + // but it also could contain more elements, that is why we check length too + expect(result).toEqual( + expect.arrayContaining(addressesOfModulesWithDuplicateKeys), + ); + expect(result.length).toEqual(2); + }); + + it('should found duplicated keys across one module', () => { + const result = stakingModuleGuardService.checkVettedKeysDuplicates( + vettedKeysDuplicatesAcrossOneModule, + blockData, + ); + + const addressesOfModulesWithDuplicateKeys = [ + '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + ]; + + // result has all addressesOfModulesWithDuplicateKeys elements + // but it also could contain more elements, that is why we check length too + expect(result).toEqual( + expect.arrayContaining(addressesOfModulesWithDuplicateKeys), + ); + expect(result.length).toEqual(1); + }); + + it('should found duplicated keys across one module and few', () => { + const result = stakingModuleGuardService.checkVettedKeysDuplicates( + vettedKeysDuplicatesAcrossOneModuleAndFew, + blockData, + ); + + const addressesOfModulesWithDuplicateKeys = [ + '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + ]; + + // result has all addressesOfModulesWithDuplicateKeys elements + // but it also could contain more elements, that is why we check length too + expect(result).toEqual( + expect.arrayContaining(addressesOfModulesWithDuplicateKeys), + ); + expect(result.length).toEqual(2); + }); + + it('should return empty list if duplicated keys were not found', () => { + const result = stakingModuleGuardService.checkVettedKeysDuplicates( + vettedKeysWithoutDuplicates, + blockData, + ); + + const addressesOfModulesWithDuplicateKeys = []; + + // result has all addressesOfModulesWithDuplicateKeys elements + // but it also could contain more elements, that is why we check length too + expect(result).toEqual( + expect.arrayContaining(addressesOfModulesWithDuplicateKeys), + ); + expect(result.length).toEqual(0); + }); + }); }); diff --git a/src/staking-router/staking-router.service.ts b/src/staking-router/staking-router.service.ts index 40614346..95c8b050 100644 --- a/src/staking-router/staking-router.service.ts +++ b/src/staking-router/staking-router.service.ts @@ -26,7 +26,7 @@ export class StakingRouterService { const unusedKeys = await this.keysApiService.getUnusedKeys(); const keysBlockHash = unusedKeys.meta.elBlockSnapshot.blockHash; - if (keysBlockHash != operatorsBlockHash) { + if (keysBlockHash !== operatorsBlockHash) { this.logger.log('Blockhash of the received keys and operators', { keysBlockHash, operatorsBlockHash, @@ -69,6 +69,7 @@ export class StakingRouterService { unusedKeys: moduleKeys.map((srKey) => srKey.key), nonce: stakingModule.nonce, stakingModuleId: stakingModule.id, + stakingModuleAddress: stakingModule.stakingModuleAddress, blockHash: operatorsBlockHash, vettedKeys: moduleVettedKeys, }); @@ -107,7 +108,7 @@ export class StakingRouterService { }); throw Error( - 'Blockhash of the received keys does not match the current blockhash', + 'Block hash of the received keys does not match the current block hash', ); } diff --git a/src/staking-router/staking-router.spec.ts b/src/staking-router/staking-router.spec.ts index 4229e2e9..94bacb66 100644 --- a/src/staking-router/staking-router.spec.ts +++ b/src/staking-router/staking-router.spec.ts @@ -6,7 +6,7 @@ import { keysAllStakingModules } from './keys.fixtures'; import { ConfigModule } from 'common/config'; import { LoggerModule } from 'common/logger'; -describe('YourService', () => { +describe('StakingRouter', () => { let stakingRouterService: StakingRouterService; let keysApiService: KeysApiService; @@ -67,6 +67,7 @@ describe('YourService', () => { blockHash: '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', stakingModuleId: 1, + stakingModuleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', nonce: 364, }, { @@ -90,6 +91,7 @@ describe('YourService', () => { blockHash: '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', stakingModuleId: 2, + stakingModuleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', }, ], vettedKeys: [ From c14bc1e56d54368dd7e42150ece63cb8dd8f0fcd Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 6 Dec 2023 16:22:41 +0400 Subject: [PATCH 03/37] fix: added more tests, fixed e2e --- src/guardian/guardian.service.ts | 1 - .../staking-module-guard.service.ts | 25 +- .../staking-module-guard.spec.ts | 253 +++++++++++++++++- 3 files changed, 272 insertions(+), 7 deletions(-) diff --git a/src/guardian/guardian.service.ts b/src/guardian/guardian.service.ts index 3b88f7a9..4a75c3ec 100644 --- a/src/guardian/guardian.service.ts +++ b/src/guardian/guardian.service.ts @@ -97,7 +97,6 @@ export class GuardianService implements OnModuleInit { this.logger.log('New staking router state cycle start'); try { - // TODO: rename const { blockHash, blockNumber, vettedKeys, stakingModulesData } = await this.stakingRouterService.getVettedAndUnusedKeys(); 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 f3d3d14a..ef35ddeb 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -152,8 +152,10 @@ export class StakingModuleGuardService { if (isFilteredIntersectionsFound) { await this.handleKeysIntersections(stakingModuleData, blockData); } else { + // it could throw error if kapi returned old data const usedKeys = await this.getIntersectionBetweenUsedAndUnusedKeys( keysIntersections, + blockData, ); // if found used keys, Lido already made deposit on this keys @@ -225,6 +227,7 @@ export class StakingModuleGuardService { public async getIntersectionBetweenUsedAndUnusedKeys( intersectionsWithLidoWC: VerifiedDepositEvent[], + blockData: BlockData, ) { const depositedPubkeys = intersectionsWithLidoWC.map( (deposit) => deposit.pubkey, @@ -235,12 +238,24 @@ export class StakingModuleGuardService { 'Found intersections with lido credentials, need to check duplicated keys', ); - const keys = await this.stakingRouterService.getKeysWithDuplicates( - depositedPubkeys, - ); + const { data, meta } = + await this.stakingRouterService.getKeysWithDuplicates(depositedPubkeys); + + if (meta.elBlockSnapshot.blockNumber < blockData.blockNumber) { + // blockData.blockNumber we also read from kapi, so smth is wrong in kapi + this.logger.error( + 'BlockNumber of the response older than previous response from KAPI', + { + previous: blockData.blockNumber, + current: meta.elBlockSnapshot.blockNumber, + }, + ); + throw Error( + 'BlockNumber of the response older than previous response from KAPI', + ); + } - // TODO: add block number check. keys blockNumber should be newer than we have in blockData because of used keys is not deleted - const usedKeys = keys.data.filter((key) => key.used); + const usedKeys = data.filter((key) => key.used); return usedKeys; } 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 90691306..690cf026 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -10,7 +10,7 @@ import { RepositoryModule } from 'contracts/repository'; import { LidoModule, LidoService } from 'contracts/lido'; import { MessageType } from 'messages'; import { StakingModuleGuardModule } from './staking-module-guard.module'; -import { StakingRouterModule } from 'staking-router'; +import { StakingRouterModule, StakingRouterService } from 'staking-router'; import { GuardianMetricsModule } from '../guardian-metrics'; import { GuardianMessageModule, @@ -52,6 +52,7 @@ describe('StakingModuleGuardService', () => { let securityService: SecurityService; let stakingModuleGuardService: StakingModuleGuardService; let guardianMessageService: GuardianMessageService; + let stakingRouterService: StakingRouterService; beforeEach(async () => { const moduleRef = await Test.createTestingModule({ @@ -75,6 +76,7 @@ describe('StakingModuleGuardService', () => { loggerService = moduleRef.get(WINSTON_MODULE_NEST_PROVIDER); stakingModuleGuardService = moduleRef.get(StakingModuleGuardService); guardianMessageService = moduleRef.get(GuardianMessageService); + stakingRouterService = moduleRef.get(StakingRouterService); jest.spyOn(loggerService, 'log').mockImplementation(() => undefined); jest.spyOn(loggerService, 'warn').mockImplementation(() => undefined); @@ -661,4 +663,253 @@ describe('StakingModuleGuardService', () => { expect(result.length).toEqual(0); }); }); + + describe('getIntersectionBetweenUsedAndUnusedKeys', () => { + // function that return list from kapi that match keys in parameter + it('intersection is empty', async () => { + const intersectionsWithLidoWC = []; + // function that return list from kapi that match keys in parameter + const mockSendMessageFromGuardian = jest.spyOn( + stakingRouterService, + 'getKeysWithDuplicates', + ); + + const result = + await stakingModuleGuardService.getIntersectionBetweenUsedAndUnusedKeys( + intersectionsWithLidoWC, + { + blockNumber: 1, + blockHash: '0x1234', + } as any, + ); + + expect(result).toEqual([]); + expect(mockSendMessageFromGuardian).toBeCalledTimes(0); + }); + + it('should return keys list if deposits with lido wx were made by lido', async () => { + const pubkeyWithUsedKey1 = '0x1234'; + const pubkeyWithoutUsedKey = '0x56789'; + const pubkeyWithUsedKey2 = '0x3478'; + const lidoWC = '0x12'; + const intersectionsWithLidoWC = [ + { pubkey: pubkeyWithUsedKey1, wc: lidoWC } as any, + { pubkey: pubkeyWithoutUsedKey, wc: lidoWC } as any, + { pubkey: pubkeyWithUsedKey2, wc: lidoWC } as any, + ]; + // function that return list from kapi that match keys in parameter + const mockSendMessageFromGuardian = jest + .spyOn(stakingRouterService, 'getKeysWithDuplicates') + .mockImplementation(async () => ({ + data: [ + { + key: pubkeyWithUsedKey1, + depositSignature: 'signature', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: '0x0000', + }, + { + key: pubkeyWithUsedKey1, + depositSignature: 'signature', + operatorIndex: 0, + used: true, + index: 0, + moduleAddress: '0x0000', + }, + { + key: pubkeyWithUsedKey2, + depositSignature: 'signature', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: '0x0000', + }, + { + key: pubkeyWithUsedKey2, + depositSignature: 'signature', + operatorIndex: 0, + used: true, + index: 0, + moduleAddress: '0x0000', + }, + { + key: pubkeyWithoutUsedKey, + depositSignature: 'signature', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: '0x0000', + }, + ], + meta: { + elBlockSnapshot: { + blockNumber: 0, + blockHash: 'hash', + timestamp: 12345, + }, + }, + })); + + const result = + await stakingModuleGuardService.getIntersectionBetweenUsedAndUnusedKeys( + intersectionsWithLidoWC, + { + blockNumber: 0, + blockHash: '0x1234', + } as any, + ); + + expect(result.length).toEqual(2); + expect(result).toEqual( + expect.arrayContaining([ + { + key: pubkeyWithUsedKey1, + depositSignature: 'signature', + operatorIndex: 0, + used: true, + index: 0, + moduleAddress: '0x0000', + }, + { + key: pubkeyWithUsedKey2, + depositSignature: 'signature', + operatorIndex: 0, + used: true, + index: 0, + moduleAddress: '0x0000', + }, + ]), + ); + expect(mockSendMessageFromGuardian).toBeCalledTimes(1); + }); + + it('should return empty list if deposits with lido wc were made by someone else ', async () => { + const pubkey1 = '0x1234'; + const pubkey2 = '0x56789'; + const pubkey3 = '0x3478'; + const lidoWC = '0x12'; + const intersectionsWithLidoWC = [ + { pubkey: pubkey1, wc: lidoWC } as any, + { pubkey: pubkey2, wc: lidoWC } as any, + { pubkey: pubkey3, wc: lidoWC } as any, + ]; + // function that return list from kapi that match keys in parameter + const mockSendMessageFromGuardian = jest + .spyOn(stakingRouterService, 'getKeysWithDuplicates') + .mockImplementation(async () => ({ + data: [ + { + key: pubkey1, + depositSignature: 'signature', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: '0x0000', + }, + { + key: pubkey2, + depositSignature: 'signature', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: '0x0000', + }, + { + key: pubkey3, + depositSignature: 'signature', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: '0x0000', + }, + ], + meta: { + elBlockSnapshot: { + blockNumber: 0, + blockHash: 'hash', + timestamp: 12345, + }, + }, + })); + + const result = + await stakingModuleGuardService.getIntersectionBetweenUsedAndUnusedKeys( + intersectionsWithLidoWC, + { + blockNumber: 0, + blockHash: '0x1234', + } as any, + ); + + expect(result).toEqual([]); + expect(mockSendMessageFromGuardian).toBeCalledTimes(1); + }); + + it('should skip if blockNumber that kapi returned is smaller than in blockData ', async () => { + const pubkey1 = '0x1234'; + const pubkey2 = '0x56789'; + const pubkey3 = '0x3478'; + const lidoWC = '0x12'; + const intersectionsWithLidoWC = [ + { pubkey: pubkey1, wc: lidoWC } as any, + { pubkey: pubkey2, wc: lidoWC } as any, + { pubkey: pubkey3, wc: lidoWC } as any, + ]; + // function that return list from kapi that match keys in parameter + const mockSendMessageFromGuardian = jest + .spyOn(stakingRouterService, 'getKeysWithDuplicates') + .mockImplementation(async () => ({ + data: [ + { + key: pubkey1, + depositSignature: 'signature', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: '0x0000', + }, + { + key: pubkey2, + depositSignature: 'signature', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: '0x0000', + }, + { + key: pubkey3, + depositSignature: 'signature', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: '0x0000', + }, + ], + meta: { + elBlockSnapshot: { + blockNumber: 0, + blockHash: 'hash', + timestamp: 12345, + }, + }, + })); + + expect( + async () => + await stakingModuleGuardService.getIntersectionBetweenUsedAndUnusedKeys( + intersectionsWithLidoWC, + { + blockNumber: 1, + blockHash: '0x1234', + } as any, + ), + ).rejects.toThrowError( + 'BlockNumber of the response older than previous response from KAPI', + ); + + expect(mockSendMessageFromGuardian).toBeCalledTimes(1); + }); + }); }); From 4e73aad1026ecd3bd15aff593b035e9f27159110 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 6 Dec 2023 16:46:34 +0400 Subject: [PATCH 04/37] fix: mocks --- src/contracts/deposit/deposit.service.ts | 2 -- src/guardian/guardian.service.spec.ts | 1 - test/helpers/mockKeysApi.ts | 31 ++++++++++++++++++------ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/contracts/deposit/deposit.service.ts b/src/contracts/deposit/deposit.service.ts index 3f00e87a..b4103089 100644 --- a/src/contracts/deposit/deposit.service.ts +++ b/src/contracts/deposit/deposit.service.ts @@ -16,7 +16,6 @@ import { VerifiedDepositEventsCacheHeaders, VerifiedDepositEventGroup, } from './interfaces'; -import { OneAtTime } from 'common/decorators'; import { RepositoryService } from 'contracts/repository'; import { CacheService } from 'cache'; import { BlockTag } from 'provider'; @@ -335,7 +334,6 @@ export class DepositService { const mergedEvents = cachedEvents.data.concat(freshEvents); - // TODO: Why we don't cache freshEvents? return { events: mergedEvents, startBlock: cachedEvents.headers.startBlock, diff --git a/src/guardian/guardian.service.spec.ts b/src/guardian/guardian.service.spec.ts index 61b481d7..29115fa5 100644 --- a/src/guardian/guardian.service.spec.ts +++ b/src/guardian/guardian.service.spec.ts @@ -179,7 +179,6 @@ describe('GuardianService', () => { guardianService.handleNewBlock(), ]); - // expect(getStakingModulesMock).toBeCalledTimes(1); expect(getBlockGuardServiceMock).toBeCalledTimes(1); }); }); diff --git a/test/helpers/mockKeysApi.ts b/test/helpers/mockKeysApi.ts index 196efb3e..80243769 100644 --- a/test/helpers/mockKeysApi.ts +++ b/test/helpers/mockKeysApi.ts @@ -3,6 +3,7 @@ import { toHexString } from '@chainsafe/ssz'; import { KeysApiService } from '../../src/keys-api/keys-api.service'; import { NOP_REGISTRY, pk } from './../constants'; +import { RegistryOperator } from 'keys-api/interfaces/RegistryOperator'; export const mockKeysApi = ( sig: Uint8Array[], @@ -36,20 +37,34 @@ export const mockKeysApi = ( operatorIndex: 0, used, index: 0, + moduleAddress: NOP_REGISTRY, })); - jest.spyOn(keysApiService, 'getModulesList').mockImplementation(async () => ({ - data: [mockedModule], - elBlockSnapshot: mockedMeta, + const mockedOperators: RegistryOperator[] = [ + { + name: 'Dev team', + rewardAddress: '0x6D725DAe055287f913661ee0b79dE6B21F12A459', + stakingLimit: 11, + stoppedValidators: 0, + totalSigningKeys: 10, + usedSigningKeys: 10, + index: 0, + active: true, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + }, + ]; + + jest.spyOn(keysApiService, 'getUnusedKeys').mockImplementation(async () => ({ + data: mockedKeys, + meta: { + elBlockSnapshot: mockedMeta, + }, })); jest - .spyOn(keysApiService, 'getUnusedModuleKeys') + .spyOn(keysApiService, 'getOperatorListWithModule') .mockImplementation(async () => ({ - data: { - keys: mockedKeys, - module: mockedModule, - }, + data: [{ operators: mockedOperators, module: mockedModule }], meta: { elBlockSnapshot: mockedMeta, }, From 8822aff30375ba4c96e8df2f58fb6e280c4e3719 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 6 Dec 2023 17:16:24 +0400 Subject: [PATCH 05/37] fix: added logs --- .../staking-module-guard/staking-module-guard.service.ts | 3 +++ test/helpers/mockKeysApi.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) 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 ef35ddeb..e361dd4c 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -117,6 +117,7 @@ export class StakingModuleGuardService { stakingModuleData: StakingModuleData, blockData: BlockData, ): Promise { + this.logger.log('Keys intersection check'); const { blockHash } = blockData; const { stakingModuleId } = stakingModuleData; @@ -129,6 +130,8 @@ export class StakingModuleGuardService { blockData, keysIntersections, ); + + this.logger.log(filteredIntersections); const isFilteredIntersectionsFound = filteredIntersections.length > 0; this.guardianMetricsService.collectIntersectionsMetrics( diff --git a/test/helpers/mockKeysApi.ts b/test/helpers/mockKeysApi.ts index 80243769..c26a44cc 100644 --- a/test/helpers/mockKeysApi.ts +++ b/test/helpers/mockKeysApi.ts @@ -46,7 +46,7 @@ export const mockKeysApi = ( rewardAddress: '0x6D725DAe055287f913661ee0b79dE6B21F12A459', stakingLimit: 11, stoppedValidators: 0, - totalSigningKeys: 10, + totalSigningKeys: 11, usedSigningKeys: 10, index: 0, active: true, From fe634ba6f4f17b0c32ecce255429272913140d0f Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 6 Dec 2023 17:32:35 +0400 Subject: [PATCH 06/37] fix: added logs --- .../staking-module-guard/staking-module-guard.service.ts | 3 +++ 1 file changed, 3 insertions(+) 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 e361dd4c..a2b6e875 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -131,6 +131,9 @@ export class StakingModuleGuardService { keysIntersections, ); + this.logger.log(blockData); + this.logger.log(keysIntersections); + this.logger.log(filteredIntersections); const isFilteredIntersectionsFound = filteredIntersections.length > 0; From 3ed5b4d93ed2a8cf2027a5747ac0640ca202ca68 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 6 Dec 2023 17:36:52 +0400 Subject: [PATCH 07/37] fix: added logs --- .../staking-module-guard/staking-module-guard.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a2b6e875..08c7ccfa 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -132,7 +132,7 @@ export class StakingModuleGuardService { ); this.logger.log(blockData); - this.logger.log(keysIntersections); + this.logger.log(stakingModuleData.unusedKeys); this.logger.log(filteredIntersections); const isFilteredIntersectionsFound = filteredIntersections.length > 0; From 1476592385f87e2fe46f1135727695ca3cacd39f Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 6 Dec 2023 18:01:27 +0400 Subject: [PATCH 08/37] fix: operators e2e mock --- src/guardian/guardian.service.spec.ts | 4 +--- .../staking-module-guard/staking-module-guard.spec.ts | 1 - test/helpers/mockKeysApi.ts | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/guardian/guardian.service.spec.ts b/src/guardian/guardian.service.spec.ts index 29115fa5..5643928b 100644 --- a/src/guardian/guardian.service.spec.ts +++ b/src/guardian/guardian.service.spec.ts @@ -24,8 +24,6 @@ import { mockRepository } from 'contracts/repository/repository.mock'; jest.mock('../transport/stomp/stomp.client'); -const TEST_MODULE_ID = 1; - const vettedKeys = [ { key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', @@ -166,7 +164,7 @@ describe('GuardianService', () => { }); it('should exit if the previous call is not completed', async () => { - const getVettedKeysMock = jest + jest .spyOn(stakingRouterService, 'getVettedAndUnusedKeys') .mockImplementation(async () => vettedKeysResponse); 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 690cf026..a484ecfd 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -18,7 +18,6 @@ import { } from '../guardian-message'; import { StakingModuleGuardService } from './staking-module-guard.service'; import { StakingModuleData } from 'guardian/interfaces'; -import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; import { vettedKeysDuplicatesAcrossModules, vettedKeysDuplicatesAcrossOneModule, diff --git a/test/helpers/mockKeysApi.ts b/test/helpers/mockKeysApi.ts index c26a44cc..ba40fad6 100644 --- a/test/helpers/mockKeysApi.ts +++ b/test/helpers/mockKeysApi.ts @@ -50,7 +50,7 @@ export const mockKeysApi = ( usedSigningKeys: 10, index: 0, active: true, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + moduleAddress: NOP_REGISTRY, }, ]; From 63be41d7ce5bd66488633b52acc3d64a15023f5d Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 6 Dec 2023 18:15:10 +0400 Subject: [PATCH 09/37] fix: remove extra logs --- .../staking-module-guard/staking-module-guard.service.ts | 5 ----- 1 file changed, 5 deletions(-) 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 08c7ccfa..117623cf 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -117,7 +117,6 @@ export class StakingModuleGuardService { stakingModuleData: StakingModuleData, blockData: BlockData, ): Promise { - this.logger.log('Keys intersection check'); const { blockHash } = blockData; const { stakingModuleId } = stakingModuleData; @@ -131,10 +130,6 @@ export class StakingModuleGuardService { keysIntersections, ); - this.logger.log(blockData); - this.logger.log(stakingModuleData.unusedKeys); - - this.logger.log(filteredIntersections); const isFilteredIntersectionsFound = filteredIntersections.length > 0; this.guardianMetricsService.collectIntersectionsMetrics( From cab9f0098c382e4894ca98092eb350cdae0d5585 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 6 Dec 2023 18:25:32 +0400 Subject: [PATCH 10/37] fix: mock --- test/helpers/mockKeysApi.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/helpers/mockKeysApi.ts b/test/helpers/mockKeysApi.ts index ba40fad6..7eba73b9 100644 --- a/test/helpers/mockKeysApi.ts +++ b/test/helpers/mockKeysApi.ts @@ -69,4 +69,13 @@ export const mockKeysApi = ( elBlockSnapshot: mockedMeta, }, })); + + jest + .spyOn(keysApiService, 'getKeysWithDuplicates') + .mockImplementation(async () => ({ + data: mockedKeys, + meta: { + elBlockSnapshot: mockedMeta, + }, + })); }; From 63c374a5626061b719902becf667648b98daf24a Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 7 Dec 2023 21:55:13 +0400 Subject: [PATCH 11/37] fix: simplify duplicates search --- src/guardian/guardian.service.ts | 8 +- .../interfaces/staking-module.interface.ts | 1 - .../staking-module-guard/keys.fixtures.ts | 413 ++++++++++-------- .../staking-module-guard.service.ts | 89 ++-- .../staking-module-guard.spec.ts | 45 +- src/staking-router/staking-router.service.ts | 162 ++++--- src/staking-router/staking-router.spec.ts | 22 - 7 files changed, 408 insertions(+), 332 deletions(-) diff --git a/src/guardian/guardian.service.ts b/src/guardian/guardian.service.ts index 4a75c3ec..1f69979a 100644 --- a/src/guardian/guardian.service.ts +++ b/src/guardian/guardian.service.ts @@ -97,7 +97,7 @@ export class GuardianService implements OnModuleInit { this.logger.log('New staking router state cycle start'); try { - const { blockHash, blockNumber, vettedKeys, stakingModulesData } = + const { blockHash, blockNumber, stakingModulesData } = await this.stakingRouterService.getVettedAndUnusedKeys(); await this.repositoryService.initCachedContracts({ blockHash }); @@ -138,16 +138,16 @@ export class GuardianService implements OnModuleInit { }); // TODO: check only if one of nonce changed - const addressesOfModulesWithDuplicateKeys: string[] = + const modulesIdWithDuplicateKeys: number[] = this.stakingModuleGuardService.checkVettedKeysDuplicates( - vettedKeys, + stakingModulesData, blockData, ); const stakingModulesWithoutDuplicates: StakingModuleData[] = this.stakingModuleGuardService.excludeModulesWithDuplicatedKeys( stakingModulesData, - addressesOfModulesWithDuplicateKeys, + modulesIdWithDuplicateKeys, ); this.logger.log('Staking modules without duplicates', { diff --git a/src/guardian/interfaces/staking-module.interface.ts b/src/guardian/interfaces/staking-module.interface.ts index c5a48f99..593a2c83 100644 --- a/src/guardian/interfaces/staking-module.interface.ts +++ b/src/guardian/interfaces/staking-module.interface.ts @@ -6,5 +6,4 @@ export interface StakingModuleData { vettedKeys: RegistryKey[]; nonce: number; stakingModuleId: number; - stakingModuleAddress: string; } diff --git a/src/guardian/staking-module-guard/keys.fixtures.ts b/src/guardian/staking-module-guard/keys.fixtures.ts index b2e8e2bf..b1756895 100644 --- a/src/guardian/staking-module-guard/keys.fixtures.ts +++ b/src/guardian/staking-module-guard/keys.fixtures.ts @@ -1,193 +1,254 @@ -import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; - -export const vettedKeysDuplicatesAcrossModules: RegistryKey[] = [ - { - 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: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', - depositSignature: - '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 4, - }, - { - key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 5, - }, - { - key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: 'another_module', - index: 5, +export const vettedKeysDuplicatesAcrossModules: any = [ + { + stakingModuleId: 100, + 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, + }, + ], + }, + { + stakingModuleId: 102, + vettedKeys: [ + { + key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + depositSignature: + '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 4, + }, + { + key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 5, + }, + ], + }, + { + stakingModuleId: 103, + vettedKeys: [ + { + key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: 'another_module', + index: 5, + }, + ], }, ]; -export const vettedKeysDuplicatesAcrossOneModule: RegistryKey[] = [ - { - 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: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', - depositSignature: - '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 102, - }, - { - key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 5, +export const vettedKeysDuplicatesAcrossOneModule: any = [ + { + stakingModuleId: 100, + 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: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 102, + }, + ], + }, + { + stakingModuleId: 102, + vettedKeys: [ + { + key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 5, + }, + ], }, + { - key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: 'another_module', - index: 5, + stakingModuleId: 103, + vettedKeys: [ + { + key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: 'another_module', + index: 5, + }, + ], }, ]; -export const vettedKeysDuplicatesAcrossOneModuleAndFew: RegistryKey[] = [ - { - 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: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', - depositSignature: - '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 4, - }, - { - key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 5, - }, - { - key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', - depositSignature: - '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 6, - }, - { - key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: 'another_module', - index: 5, +export const vettedKeysDuplicatesAcrossOneModuleAndFew: any = [ + { + stakingModuleId: 100, + 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, + }, + ], + }, + { + stakingModuleId: 102, + vettedKeys: [ + { + key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + depositSignature: + '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 4, + }, + { + key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 5, + }, + { + key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + depositSignature: + '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 6, + }, + ], + }, + { + stakingModuleId: 103, + vettedKeys: [ + { + key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: 'another_module', + index: 5, + }, + ], }, ]; -export const vettedKeysWithoutDuplicates: RegistryKey[] = [ - { - key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', - depositSignature: - '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 100, - }, - { - key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', - depositSignature: - '0x898ac7072aa26d983f9ece384c4037966dde614b75dddf982f6a415f3107cb2569b96f6d1c44e608a250ac4bbe908df51473f0de2cf732d283b07d88f3786893124967b8697a8b93d31976e7ac49ab1e568f98db0bbb13384477e8357b6d7e9b', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 101, +export const vettedKeysWithoutDuplicates: any = [ + { + stakingModuleId: 100, + 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, + stakingModuleId: 102, + vettedKeys: [ + { + key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 5, + }, + ], }, + { - key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', - depositSignature: - '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', - operatorIndex: 28, - used: false, - moduleAddress: 'another_module', - index: 5, + stakingModuleId: 103, + vettedKeys: [ + { + key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: 'another_module', + index: 5, + }, + ], }, ]; 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 117623cf..bc770847 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -11,7 +11,6 @@ import { GuardianMetricsService } from '../guardian-metrics'; import { GuardianMessageService } from '../guardian-message'; import { StakingRouterService } from 'staking-router'; -import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; @Injectable() export class StakingModuleGuardService { @@ -31,54 +30,50 @@ export class StakingModuleGuardService { {}; /** - * - * @param vettedKeys vetted keys of all staking modules - * @returns List of staking modules with duplicates + * @returns List of staking modules id with duplicates */ public checkVettedKeysDuplicates( - vettedKeys: RegistryKey[], + stakingModulesData: StakingModuleData[], blockData: BlockData, - ): string[] { - const keyMap = new Map>(); - - // Populate the map - vettedKeys.forEach((vettedKey) => { - if (!keyMap.has(vettedKey.key)) { - // keyMap doesn't have vettedKey.key key - keyMap.set(vettedKey.key, new Map([[vettedKey.moduleAddress, 1]])); - } else { - // keyMap has vettedKey.key key - // get moduleAddress set - const modules = keyMap.get(vettedKey.key); - const moduleCount = modules?.get(vettedKey.moduleAddress) || 0; - modules?.set(vettedKey.moduleAddress, moduleCount + 1); - } - }); - - const duplicatedKeysWithModules: { key: string; modules: string[] }[] = []; - const modulesWithDuplicatedKeysSet = new Set(); - - keyMap.forEach((modules, key) => { - if (modules.size > 1 || Array.from(modules)[0][1] > 1) { - duplicatedKeysWithModules.push({ - key, - modules: Array.from(modules.keys()), - }); - - Array.from(modules.keys()).forEach((stakingModule) => - modulesWithDuplicatedKeysSet.add(stakingModule), - ); - } + ): number[] { + // Collects the duplicate count for each unique key across staking modules. + // The outer Map uses the key string as the key and holds an inner Map. + // The inner Map uses module id as keys and stores the duplicate count for each module. + const keyMap = new Map>(); + const modulesWithDuplicatedKeysSet = new Set(); + const duplicatedKeys = new Map>(); + + stakingModulesData.forEach(({ vettedKeys, stakingModuleId }) => { + // check module keys on duplicates across all modules + vettedKeys.forEach((key) => { + const stakingModules = keyMap.get(key.key); + + if (!stakingModules) { + // add new key + keyMap.set(key.key, new Map([[stakingModuleId, 1]])); + } else { + // found duplicate + // Duplicate key found + const moduleCount = stakingModules.get(stakingModuleId) || 0; + stakingModules.set(stakingModuleId, moduleCount + 1); + + if (this.hasDuplicateKeys(stakingModules)) { + stakingModules.forEach((_, id) => { + modulesWithDuplicatedKeysSet.add(id); + }); + duplicatedKeys.set(key.key, stakingModules); + } + } + }); }); - // TODO: consider add of contract module_id - if (duplicatedKeysWithModules.length) { - const moduleAddressesWithDuplicatesList: string[] = Array.from( + if (modulesWithDuplicatedKeysSet.size) { + const moduleAddressesWithDuplicatesList: number[] = Array.from( modulesWithDuplicatedKeysSet, ); this.logger.warn('Found duplicated vetted keys', { blockHash: blockData.blockHash, - duplicatedKeys: duplicatedKeysWithModules, + duplicatedKeys: Array.from(duplicatedKeys), moduleAddressesWithDuplicates: moduleAddressesWithDuplicatesList, }); @@ -89,20 +84,26 @@ export class StakingModuleGuardService { return []; } + private hasDuplicateKeys(stakingModules: Map): boolean { + const moduleCounts = Array.from(stakingModules.values()); + + return stakingModules.size > 1 || moduleCounts[0] > 1; + } + public excludeModulesWithDuplicatedKeys( stakingModulesData: StakingModuleData[], - addressesOfModulesWithDuplicateKeys: string[], + modulesIdWithDuplicateKeys: number[], ): StakingModuleData[] { // exclude from stakingModulesData stakingModulesWithDuplicates let stakingModulesWithoutDuplicates: StakingModuleData[] = stakingModulesData; - if (addressesOfModulesWithDuplicateKeys.length) { + if (modulesIdWithDuplicateKeys.length) { // need to filter stakingModulesWithoutDuplicates stakingModulesWithoutDuplicates = stakingModulesWithoutDuplicates.filter( - ({ stakingModuleAddress }) => - !addressesOfModulesWithDuplicateKeys.includes(stakingModuleAddress), + ({ stakingModuleId }) => + !modulesIdWithDuplicateKeys.includes(stakingModuleId), ); } 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 a484ecfd..e80f97ce 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -460,7 +460,6 @@ describe('StakingModuleGuardService', () => { vettedKeys: [], nonce: 0, stakingModuleId: 1, - stakingModuleAddress: 'first_address', }, { blockHash: '', @@ -468,7 +467,6 @@ describe('StakingModuleGuardService', () => { vettedKeys: [], nonce: 0, stakingModuleId: 2, - stakingModuleAddress: 'second_address', }, { blockHash: '', @@ -476,12 +474,11 @@ describe('StakingModuleGuardService', () => { vettedKeys: [], nonce: 0, stakingModuleId: 3, - stakingModuleAddress: 'third_address', }, ]; it('should exclude modules', () => { - const addressesOfModulesWithDuplicateKeys = ['second_address']; + const moduleIdsWithDuplicateKeys = [2]; const expectedStakingModules: StakingModuleData[] = [ { blockHash: '', @@ -489,7 +486,6 @@ describe('StakingModuleGuardService', () => { vettedKeys: [], nonce: 0, stakingModuleId: 1, - stakingModuleAddress: 'first_address', }, { blockHash: '', @@ -497,13 +493,12 @@ describe('StakingModuleGuardService', () => { vettedKeys: [], nonce: 0, stakingModuleId: 3, - stakingModuleAddress: 'third_address', }, ]; const result = stakingModuleGuardService.excludeModulesWithDuplicatedKeys( stakingModules, - addressesOfModulesWithDuplicateKeys, + moduleIdsWithDuplicateKeys, ); expect(result.length).toEqual(2); @@ -511,7 +506,7 @@ describe('StakingModuleGuardService', () => { }); it('should return list without changes', () => { - const addressesOfModulesWithDuplicateKeys = ['fourth_address']; + const moduleIdsWithDuplicateKeys = [4]; const expectedStakingModules = [ { blockHash: '', @@ -519,7 +514,6 @@ describe('StakingModuleGuardService', () => { vettedKeys: [], nonce: 0, stakingModuleId: 1, - stakingModuleAddress: 'first_address', }, { blockHash: '', @@ -527,7 +521,6 @@ describe('StakingModuleGuardService', () => { vettedKeys: [], nonce: 0, stakingModuleId: 2, - stakingModuleAddress: 'second_address', }, { blockHash: '', @@ -535,13 +528,12 @@ describe('StakingModuleGuardService', () => { vettedKeys: [], nonce: 0, stakingModuleId: 3, - stakingModuleAddress: 'third_address', }, ]; const result = stakingModuleGuardService.excludeModulesWithDuplicatedKeys( stakingModules, - addressesOfModulesWithDuplicateKeys, + moduleIdsWithDuplicateKeys, ); expect(result.length).toEqual(3); @@ -549,7 +541,7 @@ describe('StakingModuleGuardService', () => { }); it('should return list without changes if duplicated keys were not found', () => { - const addressesOfModulesWithDuplicateKeys = []; + const moduleIdsWithDuplicateKeys = []; const expectedStakingModules = [ { blockHash: '', @@ -557,7 +549,6 @@ describe('StakingModuleGuardService', () => { vettedKeys: [], nonce: 0, stakingModuleId: 1, - stakingModuleAddress: 'first_address', }, { blockHash: '', @@ -565,7 +556,6 @@ describe('StakingModuleGuardService', () => { vettedKeys: [], nonce: 0, stakingModuleId: 2, - stakingModuleAddress: 'second_address', }, { blockHash: '', @@ -573,13 +563,12 @@ describe('StakingModuleGuardService', () => { vettedKeys: [], nonce: 0, stakingModuleId: 3, - stakingModuleAddress: 'third_address', }, ]; const result = stakingModuleGuardService.excludeModulesWithDuplicatedKeys( stakingModules, - addressesOfModulesWithDuplicateKeys, + moduleIdsWithDuplicateKeys, ); expect(result.length).toEqual(3); @@ -596,10 +585,7 @@ describe('StakingModuleGuardService', () => { blockData, ); - const addressesOfModulesWithDuplicateKeys = [ - '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - ]; + const addressesOfModulesWithDuplicateKeys = [100, 102]; // result has all addressesOfModulesWithDuplicateKeys elements // but it also could contain more elements, that is why we check length too @@ -615,12 +601,7 @@ describe('StakingModuleGuardService', () => { blockData, ); - const addressesOfModulesWithDuplicateKeys = [ - '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - ]; - - // result has all addressesOfModulesWithDuplicateKeys elements - // but it also could contain more elements, that is why we check length too + const addressesOfModulesWithDuplicateKeys = [100]; expect(result).toEqual( expect.arrayContaining(addressesOfModulesWithDuplicateKeys), ); @@ -633,13 +614,7 @@ describe('StakingModuleGuardService', () => { blockData, ); - const addressesOfModulesWithDuplicateKeys = [ - '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - ]; - - // result has all addressesOfModulesWithDuplicateKeys elements - // but it also could contain more elements, that is why we check length too + const addressesOfModulesWithDuplicateKeys = [100, 102]; expect(result).toEqual( expect.arrayContaining(addressesOfModulesWithDuplicateKeys), ); @@ -654,8 +629,6 @@ describe('StakingModuleGuardService', () => { const addressesOfModulesWithDuplicateKeys = []; - // result has all addressesOfModulesWithDuplicateKeys elements - // but it also could contain more elements, that is why we check length too expect(result).toEqual( expect.arrayContaining(addressesOfModulesWithDuplicateKeys), ); diff --git a/src/staking-router/staking-router.service.ts b/src/staking-router/staking-router.service.ts index 95c8b050..65c6e4c9 100644 --- a/src/staking-router/staking-router.service.ts +++ b/src/staking-router/staking-router.service.ts @@ -5,6 +5,7 @@ import { KeysApiService } from 'keys-api/keys-api.service'; import { SRModuleKeysResponse, SRModule } from 'keys-api/interfaces'; import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; import { StakingModuleData } from 'guardian'; +import { RegistryOperator } from 'keys-api/interfaces/RegistryOperator'; @Injectable() export class StakingRouterService { @@ -15,74 +16,137 @@ export class StakingRouterService { protected readonly keysApiService: KeysApiService, ) {} + /** + * Return staking module data and block information + */ public async getVettedAndUnusedKeys() { // TODO: add cache by modules nonce + const { operatorsByModules, unusedKeys, blockHash, blockNumber } = + await this.getOperatorsAndKeysFromKAPI(); + // all staking modules list + const stakingModulesData: StakingModuleData[] = operatorsByModules.data.map( + ({ operators, module: stakingModule }) => { + const { moduleUnusedKeys, moduleVettedKeys } = + this.getModuleOperatorsVettedKeys(operators, unusedKeys.data); + + return { + unusedKeys: moduleUnusedKeys.map((srKey) => srKey.key), + nonce: stakingModule.nonce, + stakingModuleId: stakingModule.id, + blockHash, + vettedKeys: moduleVettedKeys, + }; + }, + ); + + return { + stakingModulesData, + blockHash, + blockNumber, + }; + } + + /** + * Request grouped by modules operators and all staking modules keys with meta from KAPI + */ + private async getOperatorsAndKeysFromKAPI() { const operatorsByModules = await this.keysApiService.getOperatorListWithModule(); - const operatorsBlockHash = - operatorsByModules.meta.elBlockSnapshot.blockHash; - const operatorsBlockNumber = - operatorsByModules.meta.elBlockSnapshot.blockNumber; + const { blockHash: operatorsBlockHash, blockNumber: operatorsBlockNumber } = + operatorsByModules.meta.elBlockSnapshot; const unusedKeys = await this.keysApiService.getUnusedKeys(); - const keysBlockHash = unusedKeys.meta.elBlockSnapshot.blockHash; + const { blockHash: keysBlockHash } = unusedKeys.meta.elBlockSnapshot; + + this.validateBlockHashMatch(keysBlockHash, operatorsBlockHash); + + return { + operatorsByModules, + unusedKeys, + blockHash: operatorsBlockHash, + blockNumber: operatorsBlockNumber, + }; + } + + private validateBlockHashMatch( + keysBlockHash: string, + operatorsBlockHash: string, + ) { if (keysBlockHash !== operatorsBlockHash) { - this.logger.log('Blockhash of the received keys and operators', { - keysBlockHash, - operatorsBlockHash, - }); + this.logger.error( + 'Blockhash of the received keys and operators dont match', + { + keysBlockHash, + operatorsBlockHash, + }, + ); - throw Error( + throw new Error( 'Blockhash of the received keys does not match the blockhash of operators', ); } + } - // found vetted keys - const vettedKeys: RegistryKey[] = []; - const stakingModulesData: StakingModuleData[] = []; - - operatorsByModules.data.forEach(({ operators, module: stakingModule }) => { - const moduleKeys: RegistryKey[] = []; - const moduleVettedKeys: RegistryKey[] = []; - operators.forEach((operator) => { - const operatorKeys = unusedKeys.data.filter( - (key) => - key.moduleAddress === operator.moduleAddress && - key.operatorIndex === operator.index, - ); - // Sort the filtered keys by index - operatorKeys.sort((a, b) => a.index - b.index); - - moduleKeys.push(...operatorKeys); - - const numberOfVettedUnusedKeys = - operator.stakingLimit - operator.usedSigningKeys; - const operatorVettedKeys = operatorKeys.slice( - 0, - numberOfVettedUnusedKeys, - ); - moduleVettedKeys.push(...operatorVettedKeys); - vettedKeys.push(...operatorVettedKeys); - }); + private getModuleOperatorsVettedKeys( + moduleOperators: RegistryOperator[], + allModulesUnusedKeys: RegistryKey[], + ) { + const moduleUnusedKeys: RegistryKey[] = []; + // all module vetted keys + const moduleVettedKeys: RegistryKey[] = []; + + moduleOperators.forEach((operator) => { + const operatorKeys = this.getSortedOperatorKeys( + allModulesUnusedKeys, + operator, + ); - stakingModulesData.push({ - unusedKeys: moduleKeys.map((srKey) => srKey.key), - nonce: stakingModule.nonce, - stakingModuleId: stakingModule.id, - stakingModuleAddress: stakingModule.stakingModuleAddress, - blockHash: operatorsBlockHash, - vettedKeys: moduleVettedKeys, - }); + moduleUnusedKeys.push(...operatorKeys); + + const operatorVettedKeys = this.getOperatorVettedKeys( + operatorKeys, + operator, + ); + moduleVettedKeys.push(...operatorVettedKeys); }); return { - stakingModulesData, - vettedKeys, - blockHash: operatorsBlockHash, - blockNumber: operatorsBlockNumber, + moduleUnusedKeys, + moduleVettedKeys, }; } + /*** + * @param unusedKeys - keys list of all staking modules + * @param operator - staking module operator + * @returns sorted operator's keys list + */ + private getSortedOperatorKeys( + keys: RegistryKey[], + operator: RegistryOperator, + ): RegistryKey[] { + const operatorSortedKeys = keys + .filter( + (key) => + key.moduleAddress === operator.moduleAddress && + key.operatorIndex === operator.index, + ) + .sort((a, b) => a.index - b.index); + return operatorSortedKeys; + } + + /*** + * Got sorted unused keys and return vetted keys + */ + private getOperatorVettedKeys( + unusedKeys: RegistryKey[], + operator: RegistryOperator, + ): RegistryKey[] { + const numberOfVettedUnusedKeys = + operator.stakingLimit - operator.usedSigningKeys; + return unusedKeys.slice(0, numberOfVettedUnusedKeys); + } + public async getKeysWithDuplicates(pubkeys: string[]) { return await this.keysApiService.getKeysWithDuplicates(pubkeys); } diff --git a/src/staking-router/staking-router.spec.ts b/src/staking-router/staking-router.spec.ts index 94bacb66..03b68e2f 100644 --- a/src/staking-router/staking-router.spec.ts +++ b/src/staking-router/staking-router.spec.ts @@ -67,7 +67,6 @@ describe('StakingRouter', () => { blockHash: '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', stakingModuleId: 1, - stakingModuleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', nonce: 364, }, { @@ -91,27 +90,6 @@ describe('StakingRouter', () => { blockHash: '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', stakingModuleId: 2, - stakingModuleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - }, - ], - vettedKeys: [ - { - key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', - depositSignature: - '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', - operatorIndex: 0, - used: false, - moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', - index: 100, - }, - { - key: '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', - depositSignature: - '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', - operatorIndex: 28, - used: false, - moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', - index: 4, }, ], }); From 4831c7a8ed4befe05888e562309fe50ea43b4c4f Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 8 Dec 2023 12:42:51 +0400 Subject: [PATCH 12/37] fix: invert condition --- .../staking-module-guard.service.ts | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) 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 bc770847..d0030362 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -235,33 +235,33 @@ export class StakingModuleGuardService { (deposit) => deposit.pubkey, ); - if (depositedPubkeys.length) { - this.logger.log( - 'Found intersections with lido credentials, need to check duplicated keys', - ); + if (!depositedPubkeys.length) { + return []; + } - const { data, meta } = - await this.stakingRouterService.getKeysWithDuplicates(depositedPubkeys); - - if (meta.elBlockSnapshot.blockNumber < blockData.blockNumber) { - // blockData.blockNumber we also read from kapi, so smth is wrong in kapi - this.logger.error( - 'BlockNumber of the response older than previous response from KAPI', - { - previous: blockData.blockNumber, - current: meta.elBlockSnapshot.blockNumber, - }, - ); - throw Error( - 'BlockNumber of the response older than previous response from KAPI', - ); - } + this.logger.log( + 'Found intersections with lido credentials, need to check duplicated keys', + ); - const usedKeys = data.filter((key) => key.used); - return usedKeys; + const { data, meta } = + await this.stakingRouterService.getKeysWithDuplicates(depositedPubkeys); + + if (meta.elBlockSnapshot.blockNumber < blockData.blockNumber) { + // blockData.blockNumber we also read from kapi, so smth is wrong in kapi + this.logger.error( + 'BlockNumber of the response older than previous response from KAPI', + { + previous: blockData.blockNumber, + current: meta.elBlockSnapshot.blockNumber, + }, + ); + throw Error( + 'BlockNumber of the response older than previous response from KAPI', + ); } - return []; + const usedKeys = data.filter((key) => key.used); + return usedKeys; } /** From 66c5dc5cedac808dd750780e7eab1f20296d0cba Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 8 Dec 2023 13:39:50 +0400 Subject: [PATCH 13/37] fix: made more readable checks --- .../staking-module-guard.service.ts | 38 +++++++++++++++---- .../staking-module-guard.spec.ts | 2 +- 2 files changed, 32 insertions(+), 8 deletions(-) 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 d0030362..3f70ada7 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -11,6 +11,7 @@ import { GuardianMetricsService } from '../guardian-metrics'; import { GuardianMessageService } from '../guardian-message'; import { StakingRouterService } from 'staking-router'; +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; @Injectable() export class StakingModuleGuardService { @@ -227,6 +228,11 @@ export class StakingModuleGuardService { return attackIntersections; } + /** + * If we find an intersection between the unused keys and the deposited keys in the Ethereum deposit contract + * with Lido withdrawal credentials, we need to determine whether this deposit was made by Lido. + * If it was indeed made by Lido, we set a metric and skip sending deposit messages in the queue for this iteration. + */ public async getIntersectionBetweenUsedAndUnusedKeys( intersectionsWithLidoWC: VerifiedDepositEvent[], blockData: BlockData, @@ -240,27 +246,45 @@ export class StakingModuleGuardService { } this.logger.log( - 'Found intersections with lido credentials, need to check duplicated keys', + 'Found intersections with lido credentials, need to check used duplicated keys', ); + const alreadyDepositedKeys = await this.getDuplicatedLidoUsedKeys( + depositedPubkeys, + blockData.blockNumber, + ); + + return alreadyDepositedKeys; + } + + /** + * Upon identifying the intersection of keys deposited and unused with Lido withdrawal credentials, + * use the KAPI /v1/keys/find endpoint to locate all keys with duplicates. + * Filter out the used keys, and since used keys cannot be deleted, + * it is sufficient to check if the blockNumber in the new result is greater than the current blockNumber. + */ + private async getDuplicatedLidoUsedKeys( + keys: string[], + prevBlockNumber: number, + ): Promise { const { data, meta } = - await this.stakingRouterService.getKeysWithDuplicates(depositedPubkeys); + await this.stakingRouterService.getKeysWithDuplicates(keys); - if (meta.elBlockSnapshot.blockNumber < blockData.blockNumber) { - // blockData.blockNumber we also read from kapi, so smth is wrong in kapi + if (meta.elBlockSnapshot.blockNumber < prevBlockNumber) { this.logger.error( - 'BlockNumber of the response older than previous response from KAPI', + 'BlockNumber of the current response older than previous response from KAPI', { - previous: blockData.blockNumber, + previous: prevBlockNumber, current: meta.elBlockSnapshot.blockNumber, }, ); throw Error( - 'BlockNumber of the response older than previous response from KAPI', + 'BlockNumber of the current response older than previous response from KAPI', ); } const usedKeys = data.filter((key) => key.used); + return usedKeys; } 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 e80f97ce..3110f4ca 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -878,7 +878,7 @@ describe('StakingModuleGuardService', () => { } as any, ), ).rejects.toThrowError( - 'BlockNumber of the response older than previous response from KAPI', + 'BlockNumber of the current response older than previous response from KAPI', ); expect(mockSendMessageFromGuardian).toBeCalledTimes(1); From 1da8b708eee556df44f9a7fceaef1b6d3c43698a Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 13 Dec 2023 01:51:55 +0400 Subject: [PATCH 14/37] feat: e2e test for new checks, fixed existing tests --- README.md | 2 + src/contracts/deposit/deposit.service.ts | 3 + src/guardian/guardian.service.ts | 2 +- .../staking-module-guard.service.ts | 9 +- src/keys-api/keys-api.service.ts | 2 +- test/helpers/mockKeysApi.ts | 111 ++-- test/manifest.e2e-spec.ts | 534 +++++++++++++++++- 7 files changed, 587 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index d3830471..83f4dc10 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,8 @@ $ yarn test:e2e $ yarn test:cov ``` +To run e2e tests, ensure the RPC_URL environment variable is set to the Goerli provider's endpoint, and generate private keys, which should be subsequently set in the WALLET_PRIVATE_KEY variable. + ## Release flow To create a new release: diff --git a/src/contracts/deposit/deposit.service.ts b/src/contracts/deposit/deposit.service.ts index b4103089..686d7215 100644 --- a/src/contracts/deposit/deposit.service.ts +++ b/src/contracts/deposit/deposit.service.ts @@ -243,6 +243,7 @@ export class DepositService { * The last N blocks are not stored, in order to avoid storing reorganized blocks */ public async updateEventsCache(): Promise { + this.logger.log('try to update?'); const fetchTimeStart = performance.now(); const [currentBlock, initialCache] = await Promise.all([ @@ -270,6 +271,8 @@ export class DepositService { chunkToBlock, ); + console.log('chunk?', chunkEventGroup); + updatedCachedEvents.headers.endBlock = chunkEventGroup.endBlock; updatedCachedEvents.data = updatedCachedEvents.data.concat( chunkEventGroup.events, diff --git a/src/guardian/guardian.service.ts b/src/guardian/guardian.service.ts index 1f69979a..6cf67dfc 100644 --- a/src/guardian/guardian.service.ts +++ b/src/guardian/guardian.service.ts @@ -126,6 +126,7 @@ export class GuardianService implements OnModuleInit { await this.depositService.handleNewBlock(blockNumber); + // TODO: e2e test 'node operator deposit frontrun' shows that it is possible to find event and not save in cache const blockData = await this.blockGuardService.getCurrentBlockData({ blockHash, blockNumber, @@ -137,7 +138,6 @@ export class GuardianService implements OnModuleInit { blockHash: blockData.blockHash, }); - // TODO: check only if one of nonce changed const modulesIdWithDuplicateKeys: number[] = this.stakingModuleGuardService.checkVettedKeysDuplicates( stakingModulesData, 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 3f70ada7..82aacba7 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -237,7 +237,14 @@ export class StakingModuleGuardService { intersectionsWithLidoWC: VerifiedDepositEvent[], blockData: BlockData, ) { - const depositedPubkeys = intersectionsWithLidoWC.map( + // should not check invalid + // TODO: fix in prev PR + const validIntersections = intersectionsWithLidoWC.filter( + ({ valid }) => valid, + ); + if (!validIntersections.length) return []; + + const depositedPubkeys = validIntersections.map( (deposit) => deposit.pubkey, ); diff --git a/src/keys-api/keys-api.service.ts b/src/keys-api/keys-api.service.ts index 6cf28664..3cbb990d 100644 --- a/src/keys-api/keys-api.service.ts +++ b/src/keys-api/keys-api.service.ts @@ -63,7 +63,7 @@ export class KeysApiService { /** * - * @param The /v1/keys/find KAPI endpoint returns a key along with its duplicates + * @param The /v1/keys/find API endpoint returns keys along with their duplicates * @returns */ public async getKeysWithDuplicates(pubkeys: string[]) { diff --git a/test/helpers/mockKeysApi.ts b/test/helpers/mockKeysApi.ts index 7eba73b9..056162e0 100644 --- a/test/helpers/mockKeysApi.ts +++ b/test/helpers/mockKeysApi.ts @@ -1,66 +1,52 @@ import ethers from 'ethers'; -import { toHexString } from '@chainsafe/ssz'; import { KeysApiService } from '../../src/keys-api/keys-api.service'; -import { NOP_REGISTRY, pk } from './../constants'; +import { NOP_REGISTRY } from './../constants'; import { RegistryOperator } from 'keys-api/interfaces/RegistryOperator'; +import { SRModule } from 'keys-api/interfaces'; +import { ELBlockSnapshot } from 'keys-api/interfaces/ELBlockSnapshot'; +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; -export const mockKeysApi = ( - sig: Uint8Array[], - block: ethers.providers.Block, - keysApiService: KeysApiService, - used = false, -) => { - const mockedModule = { - nonce: 6046, - type: 'grouped-onchain-v1', - id: 1, - stakingModuleAddress: NOP_REGISTRY, - moduleFee: 10, - treasuryFee: 10, - targetShare: 10, - status: 1, - name: 'NodeOperatorRegistry', - lastDepositAt: block.timestamp, - lastDepositBlock: block.number, - }; +export const mockedModule = (block: ethers.providers.Block) => ({ + nonce: 6046, + type: 'grouped-onchain-v1', + id: 1, + stakingModuleAddress: NOP_REGISTRY, + moduleFee: 10, + treasuryFee: 10, + targetShare: 10, + status: 1, + name: 'NodeOperatorRegistry', + lastDepositAt: block.timestamp, + lastDepositBlock: block.number, +}); - const mockedMeta = { - blockNumber: block.number, - blockHash: block.hash, - timestamp: block.timestamp, - }; +export const mockedMeta = (block: ethers.providers.Block) => ({ + blockNumber: block.number, + blockHash: block.hash, + timestamp: block.timestamp, +}); - const mockedKeys = sig.map((x) => ({ - key: toHexString(pk), - depositSignature: toHexString(x), - operatorIndex: 0, - used, +export const mockedOperators: RegistryOperator[] = [ + { + name: 'Dev team', + rewardAddress: '0x6D725DAe055287f913661ee0b79dE6B21F12A459', + stakingLimit: 12, + stoppedValidators: 0, + totalSigningKeys: 12, + usedSigningKeys: 10, index: 0, + active: true, moduleAddress: NOP_REGISTRY, - })); - - const mockedOperators: RegistryOperator[] = [ - { - name: 'Dev team', - rewardAddress: '0x6D725DAe055287f913661ee0b79dE6B21F12A459', - stakingLimit: 11, - stoppedValidators: 0, - totalSigningKeys: 11, - usedSigningKeys: 10, - index: 0, - active: true, - moduleAddress: NOP_REGISTRY, - }, - ]; - - jest.spyOn(keysApiService, 'getUnusedKeys').mockImplementation(async () => ({ - data: mockedKeys, - meta: { - elBlockSnapshot: mockedMeta, - }, - })); + }, +]; +export const mockedKeysApiOperators = ( + keysApiService: KeysApiService, + mockedOperators: RegistryOperator[], + mockedModule: SRModule, + mockedMeta: ELBlockSnapshot, +) => { jest .spyOn(keysApiService, 'getOperatorListWithModule') .mockImplementation(async () => ({ @@ -69,7 +55,26 @@ export const mockKeysApi = ( elBlockSnapshot: mockedMeta, }, })); +}; +export const mockedKeysApiUnusedKeys = ( + keysApiService: KeysApiService, + mockedKeys: RegistryKey[], + mockedMeta: ELBlockSnapshot, +) => { + jest.spyOn(keysApiService, 'getUnusedKeys').mockImplementation(async () => ({ + data: mockedKeys, + meta: { + elBlockSnapshot: mockedMeta, + }, + })); +}; + +export const mockedKeysWithDuplicates = ( + keysApiService: KeysApiService, + mockedKeys: RegistryKey[], + mockedMeta: ELBlockSnapshot, +) => { jest .spyOn(keysApiService, 'getKeysWithDuplicates') .mockImplementation(async () => ({ diff --git a/test/manifest.e2e-spec.ts b/test/manifest.e2e-spec.ts index b60b2c9d..712185f7 100644 --- a/test/manifest.e2e-spec.ts +++ b/test/manifest.e2e-spec.ts @@ -5,7 +5,15 @@ import { ethers } from 'ethers'; import { fromHexString, toHexString } from '@chainsafe/ssz'; // Helpers -import { computeRoot, mockKeysApi } from './helpers'; +import { + computeRoot, + mockedKeysApiOperators, + mockedKeysApiUnusedKeys, + mockedKeysWithDuplicates, + mockedMeta, + mockedModule, + mockedOperators, +} from './helpers'; // Constants import { WeiPerEther } from '@ethersproject/constants'; @@ -25,6 +33,7 @@ import { NO_PRIVKEY_MESSAGE, sk, pk, + NOP_REGISTRY, } from './constants'; // Ganache @@ -139,7 +148,7 @@ describe('ganache e2e tests', () => { depositService = moduleRef.get(DepositService); guardianMessageService = moduleRef.get(GuardianMessageService); - // Initialising needed service instead of the whole app + // Initializing needed service instead of the whole app blsService = moduleRef.get(BlsService); await blsService.onModuleInit(); @@ -202,6 +211,7 @@ describe('ganache e2e tests', () => { const forkBlock = await tempProvider.getBlock(FORK_BLOCK); const currentBlock = await tempProvider.getBlock('latest'); + // create correct sign for deposit message for pk const goodDepositMessage = { pubkey: pk, withdrawalCredentials: fromHexString(GOOD_WC), @@ -210,7 +220,29 @@ describe('ganache e2e tests', () => { const goodSigningRoot = computeRoot(goodDepositMessage); const goodSig = sk.sign(goodSigningRoot).toBytes(); - mockKeysApi([goodSig], currentBlock, keysApiService); + const unusedKeys = [ + { + key: toHexString(pk), + depositSignature: toHexString(goodSig), + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: NOP_REGISTRY, + }, + ]; + + const meta = mockedMeta(currentBlock); + const stakingModule = mockedModule(currentBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + meta, + ); + + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, meta); + mockedKeysWithDuplicates(keysApiService, unusedKeys, meta); await depositService.setCachedEvents({ data: [ @@ -268,10 +300,21 @@ describe('ganache e2e tests', () => { // Mock Keys API again on new block const newBlock = await providerService.provider.getBlock('latest'); - mockKeysApi([goodSig], newBlock, keysApiService); + + const newMeta = mockedMeta(newBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + newMeta, + ); + + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, newMeta); // Run a cycle and wait for possible changes await guardianService.handleNewBlock(); + expect(sendPauseMessage).toHaveBeenCalledWith( expect.objectContaining({ blockNumber: newBlock.number, @@ -303,6 +346,7 @@ describe('ganache e2e tests', () => { ); const currentBlock = await tempProvider.getBlock('latest'); + // mock kapi response const goodDepositMessage = { pubkey: pk, withdrawalCredentials: fromHexString(GOOD_WC), @@ -311,7 +355,28 @@ describe('ganache e2e tests', () => { const goodSigningRoot = computeRoot(goodDepositMessage); const goodSig = sk.sign(goodSigningRoot).toBytes(); - mockKeysApi([goodSig], currentBlock, keysApiService); + const unusedKeys = [ + { + key: toHexString(pk), + depositSignature: toHexString(goodSig), + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: NOP_REGISTRY, + }, + ]; + + const meta = mockedMeta(currentBlock); + const stakingModule = mockedModule(currentBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + meta, + ); + + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, meta); await depositService.setCachedEvents({ data: [], @@ -358,7 +423,18 @@ describe('ganache e2e tests', () => { // Mock Keys API again on new block const newBlock = await providerService.provider.getBlock('latest'); - mockKeysApi([goodSig], newBlock, keysApiService); + const newMeta = mockedMeta(newBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + newMeta, + ); + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, newMeta); + // we make check that there are no duplicated used keys + // this request return keys along with their duplicates + mockedKeysWithDuplicates(keysApiService, unusedKeys, newMeta); // Run a cycle and wait for possible changes await guardianService.handleNewBlock(); @@ -384,7 +460,6 @@ describe('ganache e2e tests', () => { `http://127.0.0.1:${GANACHE_PORT}`, ); const currentBlock = await tempProvider.getBlock('latest'); - const goodDepositMessage = { pubkey: pk, withdrawalCredentials: fromHexString(GOOD_WC), @@ -393,7 +468,28 @@ describe('ganache e2e tests', () => { const goodSigningRoot = computeRoot(goodDepositMessage); const goodSig = sk.sign(goodSigningRoot).toBytes(); - mockKeysApi([goodSig], currentBlock, keysApiService); + const unusedKeys = [ + { + key: toHexString(pk), + depositSignature: toHexString(goodSig), + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: NOP_REGISTRY, + }, + ]; + + const meta = mockedMeta(currentBlock); + const stakingModule = mockedModule(currentBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + meta, + ); + + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, meta); await depositService.setCachedEvents({ data: [], @@ -421,16 +517,13 @@ describe('ganache e2e tests', () => { }; const weirdSigningRoot = computeRoot(weirdDepositMessage); const weirdSig = sk.sign(weirdSigningRoot).toBytes(); - const badDepositData = { ...badDepositMessage, signature: weirdSig, }; const badDepositDataRoot = DepositData.hashTreeRoot(badDepositData); - if (!process.env.WALLET_PRIVATE_KEY) throw new Error(NO_PRIVKEY_MESSAGE); const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY); - // Make a bad deposit const signer = wallet.connect(providerService.provider); const depositContract = DepositAbi__factory.connect( @@ -444,15 +537,21 @@ describe('ganache e2e tests', () => { badDepositDataRoot, { value: ethers.constants.WeiPerEther.mul(1) }, ); - // Mock Keys API again on new block const newBlock = await providerService.provider.getBlock('latest'); - mockKeysApi([goodSig], newBlock, keysApiService); + const newMeta = mockedMeta(newBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + newMeta, + ); + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, newMeta); // Run a cycle and wait for possible changes await guardianService.handleNewBlock(); await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); - // Check if on pause now const routerContract = StakingRouterAbi__factory.connect( STAKING_ROUTER, @@ -474,6 +573,7 @@ describe('ganache e2e tests', () => { ); const currentBlock = await tempProvider.getBlock('latest'); + // no diff const goodDepositMessage = { pubkey: pk, withdrawalCredentials: fromHexString(GOOD_WC), @@ -482,7 +582,28 @@ describe('ganache e2e tests', () => { const goodSigningRoot = computeRoot(goodDepositMessage); const goodSig = sk.sign(goodSigningRoot).toBytes(); - mockKeysApi([goodSig], currentBlock, keysApiService); + const unusedKeys = [ + { + key: toHexString(pk), + depositSignature: toHexString(goodSig), + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: NOP_REGISTRY, + }, + ]; + + const meta = mockedMeta(currentBlock); + const stakingModule = mockedModule(currentBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + meta, + ); + + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, meta); const goodDepositData = { ...goodDepositMessage, @@ -521,7 +642,18 @@ describe('ganache e2e tests', () => { // Mock Keys API again on new block const newBlock = await providerService.provider.getBlock('latest'); - mockKeysApi([goodSig], newBlock, keysApiService); + + const newMeta = mockedMeta(newBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + newMeta, + ); + + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, newMeta); + mockedKeysWithDuplicates(keysApiService, unusedKeys, newMeta); // Run a cycle and wait for possible changes await guardianService.handleNewBlock(); @@ -553,6 +685,7 @@ describe('ganache e2e tests', () => { test( 'reorganization', async () => { + // TODOL need attention to this test const tempProvider = new ethers.providers.JsonRpcProvider( `http://127.0.0.1:${GANACHE_PORT}`, ); @@ -566,7 +699,28 @@ describe('ganache e2e tests', () => { const goodSigningRoot = computeRoot(goodDepositMessage); const goodSig = sk.sign(goodSigningRoot).toBytes(); - mockKeysApi([goodSig], currentBlock, keysApiService); + const unusedKeys = [ + { + key: toHexString(pk), + depositSignature: toHexString(goodSig), + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: NOP_REGISTRY, + }, + ]; + + const meta = mockedMeta(currentBlock); + const stakingModule = mockedModule(currentBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + meta, + ); + + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, meta); const goodDepositData = { ...goodDepositMessage, @@ -616,7 +770,17 @@ describe('ganache e2e tests', () => { // Mock Keys API again on new block, but now mark as used const newBlock = await providerService.provider.getBlock('latest'); - mockKeysApi([goodSig], newBlock, keysApiService, true); + + const newMeta = mockedMeta(newBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + newMeta, + ); + + mockedKeysApiUnusedKeys(keysApiService, [], newMeta); // Run a cycle and wait for possible changes await guardianService.handleNewBlock(); @@ -631,8 +795,8 @@ describe('ganache e2e tests', () => { server = makeServer(FORK_BLOCK, CHAIN_ID, UNLOCKED_ACCOUNTS); await server.listen(GANACHE_PORT); - // Changing keys api keys to used=false - mockKeysApi([goodSig], newBlock, keysApiService, false); + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, newMeta); + mockedKeysWithDuplicates(keysApiService, unusedKeys, newMeta); // Check if on pause now const isOnPauseAfter = @@ -641,4 +805,334 @@ describe('ganache e2e tests', () => { }, TESTS_TIMEOUT, ); + + test( + 'skip deposit if find duplicated key', + async () => { + const tempProvider = new ethers.providers.JsonRpcProvider( + `http://127.0.0.1:${GANACHE_PORT}`, + ); + const currentBlock = await tempProvider.getBlock('latest'); + + // this key should be used in kapi + 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 meta = mockedMeta(currentBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + meta, + ); + + // list of keys for /keys?used=false mock + const unusedKeys = [ + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: NOP_REGISTRY, + }, + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + index: 1, + moduleAddress: NOP_REGISTRY, + }, + ]; + + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, meta); + + // Check that module was not paused + const routerContract = StakingRouterAbi__factory.connect( + STAKING_ROUTER, + providerService.provider, + ); + const isOnPause = await routerContract.getStakingModuleIsDepositsPaused( + 1, + ); + expect(isOnPause).toBe(false); + + await guardianService.handleNewBlock(); + + // just skip on this iteration deposit for staking module + expect(sendDepositMessage).toBeCalledTimes(0); + expect(sendPauseMessage).toBeCalledTimes(0); + + // after deleting duplicates in staking module, + // council will resume deposits to module + const unusedKeysWithoutDuplicates = [ + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: NOP_REGISTRY, + }, + ]; + + const newBlock = await tempProvider.getBlock('latest'); + const newMeta = mockedMeta(newBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + newMeta, + ); + + mockedKeysApiUnusedKeys( + keysApiService, + unusedKeysWithoutDuplicates, + newMeta, + ); + + await guardianService.handleNewBlock(); + + expect(sendDepositMessage).toBeCalledTimes(1); + + expect(sendDepositMessage).toHaveBeenLastCalledWith( + expect.objectContaining({ + blockNumber: newBlock.number, + guardianAddress: wallet.address, + guardianIndex: 9, + stakingModuleId: 1, + }), + ); + }, + TESTS_TIMEOUT, + ); + + test( + 'added unused keys for that deposit was already made', + async () => { + const tempProvider = new ethers.providers.JsonRpcProvider( + `http://127.0.0.1:${GANACHE_PORT}`, + ); + const currentBlock = await tempProvider.getBlock('latest'); + + // this key should be used in kapi + 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 meta = mockedMeta(currentBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + meta, + ); + + // list of keys for /keys?used=false mock + const unusedKeys = [ + { + key: toHexString(pk), + depositSignature: toHexString(goodSig), + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: NOP_REGISTRY, + }, + ]; + + const keys = [...unusedKeys, { ...unusedKeys[0], used: true }]; + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, meta); + mockedKeysWithDuplicates(keysApiService, keys, meta); + + // Check that module was not paused + const routerContract = StakingRouterAbi__factory.connect( + STAKING_ROUTER, + providerService.provider, + ); + const isOnPause = await routerContract.getStakingModuleIsDepositsPaused( + 1, + ); + expect(isOnPause).toBe(false); + + await guardianService.handleNewBlock(); + + // just skip on this iteration deposit for staking module + expect(sendDepositMessage).toBeCalledTimes(0); + expect(sendPauseMessage).toBeCalledTimes(0); + + // after deleting duplicates in staking module, + // council will resume deposits to module + + const newBlock = await tempProvider.getBlock('latest'); + const newMeta = mockedMeta(newBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + newMeta, + ); + + mockedKeysApiUnusedKeys(keysApiService, [], newMeta); + mockedKeysWithDuplicates(keysApiService, [], meta); + + await guardianService.handleNewBlock(); + + expect(sendDepositMessage).toBeCalledTimes(1); + + expect(sendDepositMessage).toHaveBeenLastCalledWith( + expect.objectContaining({ + blockNumber: newBlock.number, + guardianAddress: wallet.address, + guardianIndex: 9, + stakingModuleId: 1, + }), + ); + }, + TESTS_TIMEOUT, + ); + + test( + 'invalid unused keys', + async () => { + const tempProvider = new ethers.providers.JsonRpcProvider( + `http://127.0.0.1:${GANACHE_PORT}`, + ); + const currentBlock = await tempProvider.getBlock('latest'); + + await depositService.setCachedEvents({ + data: [], + headers: { + startBlock: currentBlock.number, + endBlock: currentBlock.number, + version: '1', + }, + }); + + // mocked curated module + const stakingModule = mockedModule(currentBlock); + const meta = mockedMeta(currentBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + meta, + ); + + // list of keys for /keys?used=false mock + const keyWithWrongSign = { + key: toHexString(pk), + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: NOP_REGISTRY, + }; + mockedKeysApiUnusedKeys(keysApiService, [keyWithWrongSign], meta); + + if (!process.env.WALLET_PRIVATE_KEY) throw new Error(NO_PRIVKEY_MESSAGE); + const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY); + + await guardianService.handleNewBlock(); + + // just skip on this iteration deposit for staking module + expect(sendDepositMessage).toBeCalledTimes(1); + + expect(sendDepositMessage).toHaveBeenLastCalledWith( + expect.objectContaining({ + blockNumber: currentBlock.number, + guardianAddress: wallet.address, + guardianIndex: 9, + stakingModuleId: 1, + }), + ); + }, + TESTS_TIMEOUT, + ); }); From 2bff3122dbad3c252bc548ad0cc9ebd28f6d0841 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 13 Dec 2023 02:05:58 +0400 Subject: [PATCH 15/37] fix: unit tests --- .../staking-module-guard.spec.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) 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 3110f4ca..f046c0cd 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -665,9 +665,9 @@ describe('StakingModuleGuardService', () => { const pubkeyWithUsedKey2 = '0x3478'; const lidoWC = '0x12'; const intersectionsWithLidoWC = [ - { pubkey: pubkeyWithUsedKey1, wc: lidoWC } as any, - { pubkey: pubkeyWithoutUsedKey, wc: lidoWC } as any, - { pubkey: pubkeyWithUsedKey2, wc: lidoWC } as any, + { pubkey: pubkeyWithUsedKey1, wc: lidoWC, valid: true } as any, + { pubkey: pubkeyWithoutUsedKey, wc: lidoWC, valid: true } as any, + { pubkey: pubkeyWithUsedKey2, wc: lidoWC, valid: true } as any, ]; // function that return list from kapi that match keys in parameter const mockSendMessageFromGuardian = jest @@ -763,9 +763,9 @@ describe('StakingModuleGuardService', () => { const pubkey3 = '0x3478'; const lidoWC = '0x12'; const intersectionsWithLidoWC = [ - { pubkey: pubkey1, wc: lidoWC } as any, - { pubkey: pubkey2, wc: lidoWC } as any, - { pubkey: pubkey3, wc: lidoWC } as any, + { pubkey: pubkey1, wc: lidoWC, valid: true } as any, + { pubkey: pubkey2, wc: lidoWC, valid: true } as any, + { pubkey: pubkey3, wc: lidoWC, valid: true } as any, ]; // function that return list from kapi that match keys in parameter const mockSendMessageFromGuardian = jest @@ -825,9 +825,9 @@ describe('StakingModuleGuardService', () => { const pubkey3 = '0x3478'; const lidoWC = '0x12'; const intersectionsWithLidoWC = [ - { pubkey: pubkey1, wc: lidoWC } as any, - { pubkey: pubkey2, wc: lidoWC } as any, - { pubkey: pubkey3, wc: lidoWC } as any, + { pubkey: pubkey1, wc: lidoWC, valid: true } as any, + { pubkey: pubkey2, wc: lidoWC, valid: true } as any, + { pubkey: pubkey3, wc: lidoWC, valid: true } as any, ]; // function that return list from kapi that match keys in parameter const mockSendMessageFromGuardian = jest From 28e299a08522a2b7625c78c91afe580acdd44a68 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Wed, 13 Dec 2023 20:42:08 +0400 Subject: [PATCH 16/37] feat: added keys-validation library and validation method --- package.json | 1 + .../keys-validation/keys-validation.module.ts | 11 + .../keys-validation.service.ts | 68 +++ test/manifest.e2e-spec.ts | 4 +- yarn.lock | 485 +++++++++++++++++- 5 files changed, 564 insertions(+), 5 deletions(-) create mode 100644 src/guardian/keys-validation/keys-validation.module.ts create mode 100644 src/guardian/keys-validation/keys-validation.service.ts diff --git a/package.json b/package.json index 0f16f2f1..9b274d84 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@nestjs/platform-express": "^8.1.1", "@nestjs/schedule": "^2.1.0", "@nestjs/terminus": "^8.0.1", + "@lido-nestjs/key-validation": "^7.4.0", "@willsoto/nestjs-prometheus": "^4.0.1", "app-root-path": "^3.0.0", "cache-manager": "^3.6.3", diff --git a/src/guardian/keys-validation/keys-validation.module.ts b/src/guardian/keys-validation/keys-validation.module.ts new file mode 100644 index 00000000..8a20ff10 --- /dev/null +++ b/src/guardian/keys-validation/keys-validation.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { KeyValidatorModule } from '@lido-nestjs/key-validation'; +import { KeysValidationService } from './keys-validation.service'; +import { LidoModule } from 'contracts/lido'; + +@Module({ + imports: [LidoModule, KeyValidatorModule.forFeature({ multithreaded: true })], + providers: [KeysValidationService], + exports: [KeysValidationService], +}) +export class KeysValidationModule {} diff --git a/src/guardian/keys-validation/keys-validation.service.ts b/src/guardian/keys-validation/keys-validation.service.ts new file mode 100644 index 00000000..c350eee1 --- /dev/null +++ b/src/guardian/keys-validation/keys-validation.service.ts @@ -0,0 +1,68 @@ +import { Inject, Injectable, LoggerService } from '@nestjs/common'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { + KeyValidatorInterface, + bufferFromHexString, +} from '@lido-nestjs/key-validation'; +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; +import { ProviderService } from 'provider'; +import { GENESIS_FORK_VERSION_BY_CHAIN_ID } from 'bls/bls.constants'; +import { LidoService } from 'contracts/lido'; + +@Injectable() +export class KeysValidationService { + constructor( + @Inject(WINSTON_MODULE_NEST_PROVIDER) + protected readonly logger: LoggerService, + private readonly keyValidator: KeyValidatorInterface, + private readonly provider: ProviderService, + private readonly lidoService: LidoService, + ) {} + + /** + * + * Return list of invalid keys + */ + async validateKeys( + unusedKeys: RegistryKey[], + // withdrawalCredentials: string, + ): Promise<{ key: string; depositSignature: string }[]> { + this.logger.log('Start keys validation', { keysCount: unusedKeys.length }); + const forkVersion: Uint8Array = await this.forkVersion(); + const withdrawalCredentials = await this.withdrawalCredentials(); + + const validatedKeys: [ + { + key: string; + depositSignature: string; + used: boolean; + }, + boolean, + ][] = await this.keyValidator.validateKeys( + unusedKeys.map((key) => ({ + key: key.key, + depositSignature: key.depositSignature, + used: false, + withdrawalCredentials: bufferFromHexString(withdrawalCredentials), + genesisForkVersion: Buffer.from(forkVersion.buffer), + })), + ); + + return validatedKeys + .filter(([, result]) => !result) + .map(([key]) => ({ + key: key.key, + depositSignature: key.depositSignature, + })); + } + + async forkVersion(): Promise { + // TODO: check chainId + const chainId = await this.provider.getChainId(); + return GENESIS_FORK_VERSION_BY_CHAIN_ID[chainId]; + } + + async withdrawalCredentials() { + return await this.lidoService.getWithdrawalCredentials(); + } +} diff --git a/test/manifest.e2e-spec.ts b/test/manifest.e2e-spec.ts index 712185f7..62758e63 100644 --- a/test/manifest.e2e-spec.ts +++ b/test/manifest.e2e-spec.ts @@ -1104,9 +1104,9 @@ describe('ganache e2e tests', () => { meta, ); - // list of keys for /keys?used=false mock const keyWithWrongSign = { key: toHexString(pk), + // just some random sign depositSignature: '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', operatorIndex: 0, @@ -1114,6 +1114,7 @@ describe('ganache e2e tests', () => { index: 0, moduleAddress: NOP_REGISTRY, }; + // list of keys for /keys?used=false mock mockedKeysApiUnusedKeys(keysApiService, [keyWithWrongSign], meta); if (!process.env.WALLET_PRIVATE_KEY) throw new Error(NO_PRIVKEY_MESSAGE); @@ -1121,7 +1122,6 @@ describe('ganache e2e tests', () => { await guardianService.handleNewBlock(); - // just skip on this iteration deposit for staking module expect(sendDepositMessage).toBeCalledTimes(1); expect(sendDepositMessage).toHaveBeenLastCalledWith( diff --git a/yarn.lock b/yarn.lock index 396241b0..779f0c47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -58,6 +58,11 @@ ora "5.4.1" rxjs "6.6.7" +"@assemblyscript/loader@^0.10.1": + version "0.10.1" + resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.10.1.tgz#70e45678f06c72fa2e350e8553ec4a4d72b92e06" + integrity sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg== + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -391,7 +396,7 @@ resolved "https://registry.yarnpkg.com/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz#3639df0e1435cab03f4d9870cc3ac079e57a6fc9" integrity sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg== -"@chainsafe/blst@^0.2.4": +"@chainsafe/blst@0.2.4", "@chainsafe/blst@^0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@chainsafe/blst/-/blst-0.2.4.tgz#ed0737dcf52a8775bd163c9a892424fa365d2c8b" integrity sha512-jjhB4dALUvLdTc2flHE6BEI7KCvXVGevIP8si4OdtERu+Ed+cc6zBsrpLvOySX9pgAMAmAuTnB349AlmRfmR2Q== @@ -406,7 +411,7 @@ dependencies: "@chainsafe/as-sha256" "^0.3.1" -"@chainsafe/ssz@^0.9.2": +"@chainsafe/ssz@0.9.2", "@chainsafe/ssz@^0.9.2": version "0.9.2" resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.9.2.tgz#6f2552db312217b911e34bcdef9057f8a3a108f2" integrity sha512-r3bKiGMF7EZlsgXTyyzQbS+GJTj6MvTlY3Ms1byFZLL1H9Maht8muE2LkF3pS1zU9KY4tiJeQd+KABdhyfB9Ag== @@ -471,6 +476,21 @@ "@ethersproject/properties" "^5.5.0" "@ethersproject/strings" "^5.5.0" +"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449" + integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/abstract-provider@5.5.1", "@ethersproject/abstract-provider@^5.5.0": version "5.5.1" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.5.1.tgz#2f1f6e8a3ab7d378d8ad0b5718460f85649710c5" @@ -484,6 +504,19 @@ "@ethersproject/transactions" "^5.5.0" "@ethersproject/web" "^5.5.0" +"@ethersproject/abstract-provider@5.7.0", "@ethersproject/abstract-provider@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz#b0a8550f88b6bf9d51f90e4795d48294630cb9ef" + integrity sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + "@ethersproject/abstract-signer@5.5.0", "@ethersproject/abstract-signer@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.5.0.tgz#590ff6693370c60ae376bf1c7ada59eb2a8dd08d" @@ -495,6 +528,17 @@ "@ethersproject/logger" "^5.5.0" "@ethersproject/properties" "^5.5.0" +"@ethersproject/abstract-signer@5.7.0", "@ethersproject/abstract-signer@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz#13f4f32117868452191a4649723cb086d2b596b2" + integrity sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/address@5.5.0", "@ethersproject/address@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.5.0.tgz#bcc6f576a553f21f3dd7ba17248f81b473c9c78f" @@ -506,6 +550,17 @@ "@ethersproject/logger" "^5.5.0" "@ethersproject/rlp" "^5.5.0" +"@ethersproject/address@5.7.0", "@ethersproject/address@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" + integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/base64@5.5.0", "@ethersproject/base64@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.5.0.tgz#881e8544e47ed976930836986e5eb8fab259c090" @@ -513,6 +568,13 @@ dependencies: "@ethersproject/bytes" "^5.5.0" +"@ethersproject/base64@5.7.0", "@ethersproject/base64@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c" + integrity sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/basex@5.5.0", "@ethersproject/basex@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.5.0.tgz#e40a53ae6d6b09ab4d977bd037010d4bed21b4d3" @@ -521,6 +583,14 @@ "@ethersproject/bytes" "^5.5.0" "@ethersproject/properties" "^5.5.0" +"@ethersproject/basex@5.7.0", "@ethersproject/basex@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.7.0.tgz#97034dc7e8938a8ca943ab20f8a5e492ece4020b" + integrity sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/bignumber@5.5.0", "@ethersproject/bignumber@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.5.0.tgz#875b143f04a216f4f8b96245bde942d42d279527" @@ -530,6 +600,15 @@ "@ethersproject/logger" "^5.5.0" bn.js "^4.11.9" +"@ethersproject/bignumber@5.7.0", "@ethersproject/bignumber@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" + integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + bn.js "^5.2.1" + "@ethersproject/bytes@5.5.0", "@ethersproject/bytes@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.5.0.tgz#cb11c526de657e7b45d2e0f0246fb3b9d29a601c" @@ -537,6 +616,13 @@ dependencies: "@ethersproject/logger" "^5.5.0" +"@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" + integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A== + dependencies: + "@ethersproject/logger" "^5.7.0" + "@ethersproject/constants@5.5.0", "@ethersproject/constants@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.5.0.tgz#d2a2cd7d94bd1d58377d1d66c4f53c9be4d0a45e" @@ -544,6 +630,13 @@ dependencies: "@ethersproject/bignumber" "^5.5.0" +"@ethersproject/constants@5.7.0", "@ethersproject/constants@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e" + integrity sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/contracts@5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.5.0.tgz#b735260d4bd61283a670a82d5275e2a38892c197" @@ -560,6 +653,22 @@ "@ethersproject/properties" "^5.5.0" "@ethersproject/transactions" "^5.5.0" +"@ethersproject/contracts@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.7.0.tgz#c305e775abd07e48aa590e1a877ed5c316f8bd1e" + integrity sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg== + dependencies: + "@ethersproject/abi" "^5.7.0" + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/hash@5.5.0", "@ethersproject/hash@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.5.0.tgz#7cee76d08f88d1873574c849e0207dcb32380cc9" @@ -574,6 +683,21 @@ "@ethersproject/properties" "^5.5.0" "@ethersproject/strings" "^5.5.0" +"@ethersproject/hash@5.7.0", "@ethersproject/hash@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7" + integrity sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/hdnode@5.5.0", "@ethersproject/hdnode@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.5.0.tgz#4a04e28f41c546f7c978528ea1575206a200ddf6" @@ -592,6 +716,24 @@ "@ethersproject/transactions" "^5.5.0" "@ethersproject/wordlists" "^5.5.0" +"@ethersproject/hdnode@5.7.0", "@ethersproject/hdnode@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.7.0.tgz#e627ddc6b466bc77aebf1a6b9e47405ca5aef9cf" + integrity sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/basex" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/pbkdf2" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/wordlists" "^5.7.0" + "@ethersproject/json-wallets@5.5.0", "@ethersproject/json-wallets@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.5.0.tgz#dd522d4297e15bccc8e1427d247ec8376b60e325" @@ -611,6 +753,25 @@ aes-js "3.0.0" scrypt-js "3.0.1" +"@ethersproject/json-wallets@5.7.0", "@ethersproject/json-wallets@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz#5e3355287b548c32b368d91014919ebebddd5360" + integrity sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/hdnode" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/pbkdf2" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + aes-js "3.0.0" + scrypt-js "3.0.1" + "@ethersproject/keccak256@5.5.0", "@ethersproject/keccak256@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.5.0.tgz#e4b1f9d7701da87c564ffe336f86dcee82983492" @@ -619,11 +780,24 @@ "@ethersproject/bytes" "^5.5.0" js-sha3 "0.8.0" +"@ethersproject/keccak256@5.7.0", "@ethersproject/keccak256@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a" + integrity sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + js-sha3 "0.8.0" + "@ethersproject/logger@5.5.0", "@ethersproject/logger@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.5.0.tgz#0c2caebeff98e10aefa5aef27d7441c7fd18cf5d" integrity sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg== +"@ethersproject/logger@5.7.0", "@ethersproject/logger@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892" + integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig== + "@ethersproject/networks@5.5.2", "@ethersproject/networks@^5.5.0": version "5.5.2" resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.5.2.tgz#784c8b1283cd2a931114ab428dae1bd00c07630b" @@ -631,6 +805,13 @@ dependencies: "@ethersproject/logger" "^5.5.0" +"@ethersproject/networks@5.7.1", "@ethersproject/networks@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6" + integrity sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ== + dependencies: + "@ethersproject/logger" "^5.7.0" + "@ethersproject/pbkdf2@5.5.0", "@ethersproject/pbkdf2@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.5.0.tgz#e25032cdf02f31505d47afbf9c3e000d95c4a050" @@ -639,6 +820,14 @@ "@ethersproject/bytes" "^5.5.0" "@ethersproject/sha2" "^5.5.0" +"@ethersproject/pbkdf2@5.7.0", "@ethersproject/pbkdf2@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz#d2267d0a1f6e123f3771007338c47cccd83d3102" + integrity sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/properties@5.5.0", "@ethersproject/properties@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.5.0.tgz#61f00f2bb83376d2071baab02245f92070c59995" @@ -646,6 +835,13 @@ dependencies: "@ethersproject/logger" "^5.5.0" +"@ethersproject/properties@5.7.0", "@ethersproject/properties@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.7.0.tgz#a6e12cb0439b878aaf470f1902a176033067ed30" + integrity sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw== + dependencies: + "@ethersproject/logger" "^5.7.0" + "@ethersproject/providers@5.5.3", "@ethersproject/providers@^5.4.5": version "5.5.3" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.5.3.tgz#56c2b070542ac44eb5de2ed3cf6784acd60a3130" @@ -671,6 +867,32 @@ bech32 "1.1.4" ws "7.4.6" +"@ethersproject/providers@5.7.2", "@ethersproject/providers@^5.5.3": + version "5.7.2" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" + integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/basex" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + bech32 "1.1.4" + ws "7.4.6" + "@ethersproject/random@5.5.1", "@ethersproject/random@^5.5.0": version "5.5.1" resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.5.1.tgz#7cdf38ea93dc0b1ed1d8e480ccdaf3535c555415" @@ -679,6 +901,14 @@ "@ethersproject/bytes" "^5.5.0" "@ethersproject/logger" "^5.5.0" +"@ethersproject/random@5.7.0", "@ethersproject/random@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.7.0.tgz#af19dcbc2484aae078bb03656ec05df66253280c" + integrity sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/rlp@5.5.0", "@ethersproject/rlp@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.5.0.tgz#530f4f608f9ca9d4f89c24ab95db58ab56ab99a0" @@ -687,6 +917,14 @@ "@ethersproject/bytes" "^5.5.0" "@ethersproject/logger" "^5.5.0" +"@ethersproject/rlp@5.7.0", "@ethersproject/rlp@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304" + integrity sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/sha2@5.5.0", "@ethersproject/sha2@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.5.0.tgz#a40a054c61f98fd9eee99af2c3cc6ff57ec24db7" @@ -696,6 +934,15 @@ "@ethersproject/logger" "^5.5.0" hash.js "1.1.7" +"@ethersproject/sha2@5.7.0", "@ethersproject/sha2@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.7.0.tgz#9a5f7a7824ef784f7f7680984e593a800480c9fb" + integrity sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + hash.js "1.1.7" + "@ethersproject/signing-key@5.5.0", "@ethersproject/signing-key@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.5.0.tgz#2aa37169ce7e01e3e80f2c14325f624c29cedbe0" @@ -708,6 +955,18 @@ elliptic "6.5.4" hash.js "1.1.7" +"@ethersproject/signing-key@5.7.0", "@ethersproject/signing-key@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.7.0.tgz#06b2df39411b00bc57c7c09b01d1e41cf1b16ab3" + integrity sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + bn.js "^5.2.1" + elliptic "6.5.4" + hash.js "1.1.7" + "@ethersproject/solidity@5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.5.0.tgz#2662eb3e5da471b85a20531e420054278362f93f" @@ -720,6 +979,18 @@ "@ethersproject/sha2" "^5.5.0" "@ethersproject/strings" "^5.5.0" +"@ethersproject/solidity@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.7.0.tgz#5e9c911d8a2acce2a5ebb48a5e2e0af20b631cb8" + integrity sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/strings@5.5.0", "@ethersproject/strings@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.5.0.tgz#e6784d00ec6c57710755699003bc747e98c5d549" @@ -729,6 +1000,15 @@ "@ethersproject/constants" "^5.5.0" "@ethersproject/logger" "^5.5.0" +"@ethersproject/strings@5.7.0", "@ethersproject/strings@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.7.0.tgz#54c9d2a7c57ae8f1205c88a9d3a56471e14d5ed2" + integrity sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/transactions@5.5.0", "@ethersproject/transactions@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.5.0.tgz#7e9bf72e97bcdf69db34fe0d59e2f4203c7a2908" @@ -744,6 +1024,21 @@ "@ethersproject/rlp" "^5.5.0" "@ethersproject/signing-key" "^5.5.0" +"@ethersproject/transactions@5.7.0", "@ethersproject/transactions@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b" + integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + "@ethersproject/units@5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.5.0.tgz#104d02db5b5dc42cc672cc4587bafb87a95ee45e" @@ -753,6 +1048,15 @@ "@ethersproject/constants" "^5.5.0" "@ethersproject/logger" "^5.5.0" +"@ethersproject/units@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.7.0.tgz#637b563d7e14f42deeee39245275d477aae1d8b1" + integrity sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/wallet@5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.5.0.tgz#322a10527a440ece593980dca6182f17d54eae75" @@ -774,6 +1078,27 @@ "@ethersproject/transactions" "^5.5.0" "@ethersproject/wordlists" "^5.5.0" +"@ethersproject/wallet@5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.7.0.tgz#4e5d0790d96fe21d61d38fb40324e6c7ef350b2d" + integrity sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/hdnode" "^5.7.0" + "@ethersproject/json-wallets" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/wordlists" "^5.7.0" + "@ethersproject/web@5.5.1", "@ethersproject/web@^5.5.0": version "5.5.1" resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.5.1.tgz#cfcc4a074a6936c657878ac58917a61341681316" @@ -785,6 +1110,17 @@ "@ethersproject/properties" "^5.5.0" "@ethersproject/strings" "^5.5.0" +"@ethersproject/web@5.7.1", "@ethersproject/web@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae" + integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w== + dependencies: + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/wordlists@5.5.0", "@ethersproject/wordlists@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.5.0.tgz#aac74963aa43e643638e5172353d931b347d584f" @@ -796,6 +1132,17 @@ "@ethersproject/properties" "^5.5.0" "@ethersproject/strings" "^5.5.0" +"@ethersproject/wordlists@5.7.0", "@ethersproject/wordlists@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.7.0.tgz#8fb2c07185d68c3e09eb3bfd6e779ba2774627f5" + integrity sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@gar/promisify@^1.0.1": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" @@ -1039,6 +1386,26 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@lido-nestjs/constants@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@lido-nestjs/constants/-/constants-5.2.0.tgz#93b3c35ca8e194f12c0d8f2de60d43182fffaab3" + integrity sha512-TM0V2gStu2IdPa5ceGGaxmyKhuM7C1EzIF3sUnAfPxrB2yLgQj91voJ0Qf00NF+Fp94rCLRBoShDyUsLMcBl2g== + +"@lido-nestjs/contracts@9.4.0": + version "9.4.0" + resolved "https://registry.yarnpkg.com/@lido-nestjs/contracts/-/contracts-9.4.0.tgz#5d4c41fd42733c108a538d641b5429a8487712d0" + integrity sha512-pGpQQXD4CPLYGH2MmNqaIIGV2nmwmblg4NlVIFEpOrhZAad8apR9+//zvukcPsjskH/HTwnW4XNUey21e6ZxzA== + dependencies: + "@ethersproject/abi" "^5.5.0" + "@ethersproject/providers" "^5.5.3" + "@lido-nestjs/constants" "5.2.0" + ethers "^5.5.4" + +"@lido-nestjs/di@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@lido-nestjs/di/-/di-1.0.1.tgz#cb1b1f06ca0b8ed93eefa3f9974691658456910e" + integrity sha512-n/oPivizv6PffyLjtxf8wxm1IteG/YvelHFiz4X6jb4WhrCPpK8J46L/LHTLUmFfcCBXquVcUJqG5Nwy3b2pIg== + "@lido-nestjs/fetch@^1.3.1": version "1.3.1" resolved "https://registry.npmjs.org/@lido-nestjs/fetch/-/fetch-1.3.1.tgz#475bb7c04bfcaa970e35b424cc55cf5637280d03" @@ -1048,11 +1415,29 @@ "@types/node-fetch" "^2.5.12" node-fetch "^2.6.7" +"@lido-nestjs/key-validation@^7.4.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@lido-nestjs/key-validation/-/key-validation-7.5.0.tgz#32aee5cdd4bb18f6e1b32b230be7c1d5918d0bb3" + integrity sha512-utzi6pERZXp84Ygnpibx3z8k+hu+8CcgSSdpr6MIIIPlYhjFh7tSBNM6tV35KnC8r5laXnx2cQwUlT7A7Ho7hw== + dependencies: + "@chainsafe/blst" "0.2.4" + "@chainsafe/ssz" "0.9.2" + "@lido-nestjs/constants" "5.2.0" + "@lido-nestjs/contracts" "9.4.0" + "@lido-nestjs/di" "1.0.1" + "@lido-nestjs/utils" "1.3.0" + piscina "3.2.0" + "@lido-nestjs/middleware@1.1.1", "@lido-nestjs/middleware@^1.1.1": version "1.1.1" resolved "https://registry.npmjs.org/@lido-nestjs/middleware/-/middleware-1.1.1.tgz#ffe6cb343f5e81282b70f8746879f3cf506ac3d5" integrity sha512-0y9e7ydM3NkAhoiiv/tSsBu8KLbvPJ19QPRiAYBy33y8Fv5PPjypA3qAG03ImBQ3qFXAp29ZUrSs18vkACf8Ng== +"@lido-nestjs/utils@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@lido-nestjs/utils/-/utils-1.3.0.tgz#8bdc69c3fd18bc57752b1799516096d284f1112b" + integrity sha512-/5I30Dos9TC18YfZq8O9ele28DpUQQCySB9X9GXPeSmY1AV4w2kIGQrws1wu2gtw7x5mZnW8PtWthJmH1f6vJQ== + "@lido-sdk/constants@^3.2.1": version "3.2.1" resolved "https://registry.yarnpkg.com/@lido-sdk/constants/-/constants-3.2.1.tgz#0c4582d7e76e4f8bc42e8f3c0d14dc0fbe481d77" @@ -2068,7 +2453,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: +base64-js@^1.2.0, base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== @@ -2102,6 +2487,11 @@ bn.js@^4.11.9: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== +bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + body-parser@1.19.1: version "1.19.1" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.1.tgz#1499abbaa9274af3ecc9f6f10396c995943e31d4" @@ -3138,6 +3528,42 @@ ethers@^5.4.7: "@ethersproject/web" "5.5.1" "@ethersproject/wordlists" "5.5.0" +ethers@^5.5.4: + version "5.7.2" + resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e" + integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg== + dependencies: + "@ethersproject/abi" "5.7.0" + "@ethersproject/abstract-provider" "5.7.0" + "@ethersproject/abstract-signer" "5.7.0" + "@ethersproject/address" "5.7.0" + "@ethersproject/base64" "5.7.0" + "@ethersproject/basex" "5.7.0" + "@ethersproject/bignumber" "5.7.0" + "@ethersproject/bytes" "5.7.0" + "@ethersproject/constants" "5.7.0" + "@ethersproject/contracts" "5.7.0" + "@ethersproject/hash" "5.7.0" + "@ethersproject/hdnode" "5.7.0" + "@ethersproject/json-wallets" "5.7.0" + "@ethersproject/keccak256" "5.7.0" + "@ethersproject/logger" "5.7.0" + "@ethersproject/networks" "5.7.1" + "@ethersproject/pbkdf2" "5.7.0" + "@ethersproject/properties" "5.7.0" + "@ethersproject/providers" "5.7.2" + "@ethersproject/random" "5.7.0" + "@ethersproject/rlp" "5.7.0" + "@ethersproject/sha2" "5.7.0" + "@ethersproject/signing-key" "5.7.0" + "@ethersproject/solidity" "5.7.0" + "@ethersproject/strings" "5.7.0" + "@ethersproject/transactions" "5.7.0" + "@ethersproject/units" "5.7.0" + "@ethersproject/wallet" "5.7.0" + "@ethersproject/web" "5.7.1" + "@ethersproject/wordlists" "5.7.0" + event-emitter@^0.3.5: version "0.3.5" resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" @@ -3146,6 +3572,11 @@ event-emitter@^0.3.5: d "1" es5-ext "~0.10.14" +eventemitter-asyncresource@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz#734ff2e44bf448e627f7748f905d6bdd57bdb65b" + integrity sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ== + events@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -3646,6 +4077,20 @@ hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hdr-histogram-js@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz#0b860534655722b6e3f3e7dca7b78867cf43dcb5" + integrity sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g== + dependencies: + "@assemblyscript/loader" "^0.10.1" + base64-js "^1.2.0" + pako "^1.0.3" + +hdr-histogram-percentiles-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz#9409f4de0c2dda78e61de2d9d78b1e9f3cba283c" + integrity sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw== + hmac-drbg@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -5015,6 +5460,14 @@ next-tick@~1.0.0: resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= +nice-napi@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nice-napi/-/nice-napi-1.0.2.tgz#dc0ab5a1eac20ce548802fc5686eaa6bc654927b" + integrity sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA== + dependencies: + node-addon-api "^3.0.0" + node-gyp-build "^4.2.2" + node-abort-controller@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e" @@ -5025,6 +5478,11 @@ node-addon-api@^2.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== +node-addon-api@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + node-emoji@1.11.0: version "1.11.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" @@ -5056,6 +5514,11 @@ node-gyp-build@^4.2.0, node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== +node-gyp-build@^4.2.2: + version "4.7.1" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.7.1.tgz#cd7d2eb48e594874053150a9418ac85af83ca8f7" + integrity sha512-wTSrZ+8lsRRa3I3H8Xr65dLWSgCvY2l4AOnaeKdPA9TB/WYMPaTcrzf3rXvFoVvjKNVnu0CcWSx54qq9GKRUYg== + node-gyp@^8.4.0: version "8.4.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" @@ -5269,6 +5732,11 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +pako@^1.0.3: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -5343,6 +5811,17 @@ pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" +piscina@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/piscina/-/piscina-3.2.0.tgz#f5a1dde0c05567775690cccefe59d9223924d154" + integrity sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA== + dependencies: + eventemitter-asyncresource "^1.0.0" + hdr-histogram-js "^2.0.1" + hdr-histogram-percentiles-obj "^3.0.0" + optionalDependencies: + nice-napi "^1.0.2" + pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" From 3bdb364e79de705f916afccfdad668e74c1fd6b2 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 14 Dec 2023 03:29:07 +0400 Subject: [PATCH 17/37] fix: added validation check in guards checks --- src/contracts/deposit/deposit.service.ts | 3 - .../block-guard/block-guard.module.ts | 3 +- .../block-guard/block-guard.service.ts | 15 ++- .../guardian-message.service.ts | 3 - src/guardian/guardian.service.spec.ts | 1 - src/guardian/guardian.service.ts | 3 - src/guardian/interfaces/block.interface.ts | 1 + .../keys-validation/keys-validation.module.ts | 3 +- .../keys-validation.service.ts | 21 ++-- .../staking-module-guard.module.ts | 3 +- .../staking-module-guard.service.ts | 105 ++++++++++++++---- .../staking-module-guard.spec.ts | 25 ++--- test/manifest.e2e-spec.ts | 27 ++--- 13 files changed, 123 insertions(+), 90 deletions(-) diff --git a/src/contracts/deposit/deposit.service.ts b/src/contracts/deposit/deposit.service.ts index 686d7215..b4103089 100644 --- a/src/contracts/deposit/deposit.service.ts +++ b/src/contracts/deposit/deposit.service.ts @@ -243,7 +243,6 @@ export class DepositService { * The last N blocks are not stored, in order to avoid storing reorganized blocks */ public async updateEventsCache(): Promise { - this.logger.log('try to update?'); const fetchTimeStart = performance.now(); const [currentBlock, initialCache] = await Promise.all([ @@ -271,8 +270,6 @@ export class DepositService { chunkToBlock, ); - console.log('chunk?', chunkEventGroup); - updatedCachedEvents.headers.endBlock = chunkEventGroup.endBlock; updatedCachedEvents.data = updatedCachedEvents.data.concat( chunkEventGroup.events, diff --git a/src/guardian/block-guard/block-guard.module.ts b/src/guardian/block-guard/block-guard.module.ts index bd7bb4ec..6290761f 100644 --- a/src/guardian/block-guard/block-guard.module.ts +++ b/src/guardian/block-guard/block-guard.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { DepositModule } from 'contracts/deposit'; import { SecurityModule } from 'contracts/security'; import { BlockGuardService } from './block-guard.service'; +import { LidoModule } from 'contracts/lido'; @Module({ - imports: [DepositModule, SecurityModule], + imports: [LidoModule, DepositModule, SecurityModule], providers: [BlockGuardService], exports: [BlockGuardService], }) diff --git a/src/guardian/block-guard/block-guard.service.ts b/src/guardian/block-guard/block-guard.service.ts index 1779e58a..6dc1bf4e 100644 --- a/src/guardian/block-guard/block-guard.service.ts +++ b/src/guardian/block-guard/block-guard.service.ts @@ -12,6 +12,7 @@ import { METRIC_BLOCK_DATA_REQUEST_ERRORS, } from 'common/prometheus'; import { Counter, Histogram } from 'prom-client'; +import { LidoService } from 'contracts/lido'; @Injectable() export class BlockGuardService { @@ -29,6 +30,7 @@ export class BlockGuardService { private depositService: DepositService, private securityService: SecurityService, + private lidoService: LidoService, ) {} public isNeedToProcessNewState(newMeta: { @@ -67,11 +69,13 @@ export class BlockGuardService { try { const guardianAddress = this.securityService.getGuardianAddress(); - const [depositRoot, depositedEvents, guardianIndex] = await Promise.all([ - this.depositService.getDepositRoot({ blockHash }), - this.depositService.getAllDepositedEvents(blockNumber, blockHash), - this.securityService.getGuardianIndex({ blockHash }), - ]); + const [depositRoot, depositedEvents, guardianIndex, lidoWC] = + await Promise.all([ + this.depositService.getDepositRoot({ blockHash }), + this.depositService.getAllDepositedEvents(blockNumber, blockHash), + this.securityService.getGuardianIndex({ blockHash }), + this.lidoService.getWithdrawalCredentials({ blockHash }), + ]); return { blockNumber, @@ -80,6 +84,7 @@ export class BlockGuardService { depositedEvents, guardianAddress, guardianIndex, + lidoWC, }; } catch (error) { this.blockErrorsCounter.inc(); diff --git a/src/guardian/guardian-message/guardian-message.service.ts b/src/guardian/guardian-message/guardian-message.service.ts index 364db4d5..a76c7ee8 100644 --- a/src/guardian/guardian-message/guardian-message.service.ts +++ b/src/guardian/guardian-message/guardian-message.service.ts @@ -84,12 +84,9 @@ export class GuardianMessageService { this.logger.warn( 'Your address is not in the Guardian List. The message will not be sent', ); - return; } - const messageWithMeta = this.addMessageMetaData(messageData); - this.logger.log('Sending a message to broker', messageData); await this.messagesService.sendMessage(messageWithMeta); } diff --git a/src/guardian/guardian.service.spec.ts b/src/guardian/guardian.service.spec.ts index 5643928b..4d4b2abc 100644 --- a/src/guardian/guardian.service.spec.ts +++ b/src/guardian/guardian.service.spec.ts @@ -129,7 +129,6 @@ describe('GuardianService', () => { MockProviderModule.forRoot(), LoggerModule, PrometheusModule, - GuardianModule, RepositoryModule, DepositModule, diff --git a/src/guardian/guardian.service.ts b/src/guardian/guardian.service.ts index 6cf67dfc..893c3d77 100644 --- a/src/guardian/guardian.service.ts +++ b/src/guardian/guardian.service.ts @@ -10,7 +10,6 @@ import { CronJob } from 'cron'; import { DepositService } from 'contracts/deposit'; import { SecurityService } from 'contracts/security'; import { RepositoryService } from 'contracts/repository'; -import { ProviderService } from 'provider'; import { GUARDIAN_DEPOSIT_JOB_DURATION, GUARDIAN_DEPOSIT_JOB_NAME, @@ -37,8 +36,6 @@ export class GuardianService implements OnModuleInit { private depositService: DepositService, private securityService: SecurityService, - private providerService: ProviderService, - private stakingRouterService: StakingRouterService, private blockGuardService: BlockGuardService, diff --git a/src/guardian/interfaces/block.interface.ts b/src/guardian/interfaces/block.interface.ts index 7e360196..7e5d1d97 100644 --- a/src/guardian/interfaces/block.interface.ts +++ b/src/guardian/interfaces/block.interface.ts @@ -7,4 +7,5 @@ export interface BlockData { depositedEvents: VerifiedDepositEventGroup; guardianAddress: string; guardianIndex: number; + lidoWC: string; } diff --git a/src/guardian/keys-validation/keys-validation.module.ts b/src/guardian/keys-validation/keys-validation.module.ts index 8a20ff10..dc6b9951 100644 --- a/src/guardian/keys-validation/keys-validation.module.ts +++ b/src/guardian/keys-validation/keys-validation.module.ts @@ -1,10 +1,9 @@ import { Module } from '@nestjs/common'; import { KeyValidatorModule } from '@lido-nestjs/key-validation'; import { KeysValidationService } from './keys-validation.service'; -import { LidoModule } from 'contracts/lido'; @Module({ - imports: [LidoModule, KeyValidatorModule.forFeature({ multithreaded: true })], + imports: [KeyValidatorModule.forFeature({ multithreaded: true })], providers: [KeysValidationService], exports: [KeysValidationService], }) diff --git a/src/guardian/keys-validation/keys-validation.service.ts b/src/guardian/keys-validation/keys-validation.service.ts index c350eee1..ebfda35e 100644 --- a/src/guardian/keys-validation/keys-validation.service.ts +++ b/src/guardian/keys-validation/keys-validation.service.ts @@ -7,7 +7,6 @@ import { import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; import { ProviderService } from 'provider'; import { GENESIS_FORK_VERSION_BY_CHAIN_ID } from 'bls/bls.constants'; -import { LidoService } from 'contracts/lido'; @Injectable() export class KeysValidationService { @@ -16,7 +15,6 @@ export class KeysValidationService { protected readonly logger: LoggerService, private readonly keyValidator: KeyValidatorInterface, private readonly provider: ProviderService, - private readonly lidoService: LidoService, ) {} /** @@ -24,12 +22,10 @@ export class KeysValidationService { * Return list of invalid keys */ async validateKeys( - unusedKeys: RegistryKey[], - // withdrawalCredentials: string, + vettedKeys: RegistryKey[], + withdrawalCredentials: string, ): Promise<{ key: string; depositSignature: string }[]> { - this.logger.log('Start keys validation', { keysCount: unusedKeys.length }); const forkVersion: Uint8Array = await this.forkVersion(); - const withdrawalCredentials = await this.withdrawalCredentials(); const validatedKeys: [ { @@ -39,7 +35,7 @@ export class KeysValidationService { }, boolean, ][] = await this.keyValidator.validateKeys( - unusedKeys.map((key) => ({ + vettedKeys.map((key) => ({ key: key.key, depositSignature: key.depositSignature, used: false, @@ -57,12 +53,13 @@ export class KeysValidationService { } async forkVersion(): Promise { - // TODO: check chainId const chainId = await this.provider.getChainId(); - return GENESIS_FORK_VERSION_BY_CHAIN_ID[chainId]; - } + const forkVersion = GENESIS_FORK_VERSION_BY_CHAIN_ID[chainId]; + + if (!forkVersion) { + throw new Error(`Unsupported chain id ${chainId}`); + } - async withdrawalCredentials() { - return await this.lidoService.getWithdrawalCredentials(); + return forkVersion; } } diff --git a/src/guardian/staking-module-guard/staking-module-guard.module.ts b/src/guardian/staking-module-guard/staking-module-guard.module.ts index 55e0cb1b..505a3720 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.module.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.module.ts @@ -8,14 +8,15 @@ import { GuardianMetricsModule } from '../guardian-metrics'; import { GuardianMessageModule } from '../guardian-message'; import { StakingModuleGuardService } from './staking-module-guard.service'; +import { KeysValidationModule } from 'guardian/keys-validation/keys-validation.module'; @Module({ imports: [ SecurityModule, - LidoModule, StakingRouterModule, GuardianMetricsModule, GuardianMessageModule, + KeysValidationModule, ], providers: [StakingModuleGuardService], exports: [StakingModuleGuardService], 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 82aacba7..accf138b 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -3,7 +3,6 @@ import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { VerifiedDepositEvent } from 'contracts/deposit'; import { SecurityService } from 'contracts/security'; -import { LidoService } from 'contracts/lido'; import { ContractsState, BlockData, StakingModuleData } from '../interfaces'; import { GUARDIAN_DEPOSIT_RESIGNING_BLOCKS } from '../guardian.constants'; @@ -12,6 +11,8 @@ import { GuardianMessageService } from '../guardian-message'; import { StakingRouterService } from 'staking-router'; import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; +import { KeysValidationService } from 'guardian/keys-validation/keys-validation.service'; +import { performance } from 'perf_hooks'; @Injectable() export class StakingModuleGuardService { @@ -20,11 +21,10 @@ export class StakingModuleGuardService { private logger: LoggerService, private securityService: SecurityService, - private lidoService: LidoService, - private stakingRouterService: StakingRouterService, private guardianMetricsService: GuardianMetricsService, private guardianMessageService: GuardianMessageService, + private keysValidationService: KeysValidationService, ) {} private lastContractsStateByModuleId: Record = @@ -72,7 +72,7 @@ export class StakingModuleGuardService { const moduleAddressesWithDuplicatesList: number[] = Array.from( modulesWithDuplicatedKeysSet, ); - this.logger.warn('Found duplicated vetted keys', { + this.logger.error('Found duplicated vetted keys', { blockHash: blockData.blockHash, duplicatedKeys: Array.from(duplicatedKeys), moduleAddressesWithDuplicates: moduleAddressesWithDuplicatesList, @@ -163,7 +163,7 @@ export class StakingModuleGuardService { // if found used keys, Lido already made deposit on this keys if (usedKeys.length) { - this.logger.log('Found that we already deposited on these keys'); + this.logger.error('Found that we already deposited on these keys'); // set metric council_daemon_used_duplicate return; } @@ -217,12 +217,8 @@ export class StakingModuleGuardService { if (!validIntersections.length) return []; // Exclude deposits with Lido withdrawal credentials - const { blockHash } = blockData; - const lidoWC = await this.lidoService.getWithdrawalCredentials({ - blockHash, - }); const attackIntersections = validIntersections.filter( - (deposit) => deposit.wc !== lidoWC, + (deposit) => deposit.wc !== blockData.lidoWC, ); return attackIntersections; @@ -278,18 +274,14 @@ export class StakingModuleGuardService { await this.stakingRouterService.getKeysWithDuplicates(keys); if (meta.elBlockSnapshot.blockNumber < prevBlockNumber) { - this.logger.error( - 'BlockNumber of the current response older than previous response from KAPI', - { - previous: prevBlockNumber, - current: meta.elBlockSnapshot.blockNumber, - }, - ); - throw Error( - 'BlockNumber of the current response older than previous response from KAPI', - ); + const errorMsg = + 'BlockNumber of the current response older than previous response from KAPI'; + this.logger.error(errorMsg, { + previous: prevBlockNumber, + current: meta.elBlockSnapshot.blockNumber, + }); + throw Error(errorMsg); } - const usedKeys = data.filter((key) => key.used); return usedKeys; @@ -374,6 +366,26 @@ export class StakingModuleGuardService { if (isSameContractsState) return; + if ( + !lastContractsState || + !this.isSameStakingModuleContractState( + currentContractState.blockNumber, + lastContractsState.blockNumber, + ) + ) { + const invalidKeys = await this.getInvalidKeys( + stakingModuleData, + blockData, + ); + if (invalidKeys.length) { + this.logger.error( + 'Found invalid keys, will skip deposits until solving problem', + ); + // set metric council_daemon_invalid_key + return; + } + } + const signature = await this.securityService.signDepositData( depositRoot, nonce, @@ -402,6 +414,36 @@ export class StakingModuleGuardService { await this.guardianMessageService.sendDepositMessage(depositMessage); } + public async getInvalidKeys( + stakingModuleData: StakingModuleData, + blockData: BlockData, + ): Promise<{ key: string; depositSignature: string }[]> { + this.logger.log('Start keys validation', { + keysCount: stakingModuleData.vettedKeys.length, + }); + const validationTimeStart = performance.now(); + const invalidKeysList = await this.keysValidationService.validateKeys( + [ + ...stakingModuleData.vettedKeys, + { + ...stakingModuleData.vettedKeys[0], + depositSignature: stakingModuleData.vettedKeys[1].depositSignature, + }, + ], + blockData.lidoWC, + ); + const validationTimeEnd = performance.now(); + const validationTime = + Math.ceil(validationTimeEnd - validationTimeStart) / 1000; + + this.logger.log('Keys validated', { + invalidKeysList, + validationTime, + }); + + return invalidKeysList; + } + /** * Compares the states of the contracts to decide if the message needs to be re-signed * @param firstState - contracts state @@ -414,7 +456,14 @@ export class StakingModuleGuardService { ): boolean { if (!firstState || !secondState) return false; if (firstState.depositRoot !== secondState.depositRoot) return false; - if (firstState.nonce !== secondState.nonce) return false; + if ( + !this.isSameStakingModuleContractState( + firstState.nonce, + secondState.nonce, + ) + ) + return false; + if ( Math.floor(firstState.blockNumber / GUARDIAN_DEPOSIT_RESIGNING_BLOCKS) !== Math.floor(secondState.blockNumber / GUARDIAN_DEPOSIT_RESIGNING_BLOCKS) @@ -424,4 +473,16 @@ export class StakingModuleGuardService { return true; } + + /** + * @returns true if nonce is the same + */ + public isSameStakingModuleContractState( + firstNonce: number, + secondNonce: number, + ): boolean { + if (firstNonce !== secondNonce) return false; + + return true; + } } 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 f046c0cd..4eacdf5f 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -7,7 +7,7 @@ import { ConfigModule } from 'common/config'; import { PrometheusModule } from 'common/prometheus'; import { SecurityModule, SecurityService } from 'contracts/security'; import { RepositoryModule } from 'contracts/repository'; -import { LidoModule, LidoService } from 'contracts/lido'; +import { LidoModule } from 'contracts/lido'; import { MessageType } from 'messages'; import { StakingModuleGuardModule } from './staking-module-guard.module'; import { StakingRouterModule, StakingRouterService } from 'staking-router'; @@ -28,6 +28,7 @@ import { jest.mock('../../transport/stomp/stomp.client'); const TEST_MODULE_ID = 1; +const lidoWC = '0x12'; const stakingModuleData = { nonce: 0, @@ -47,7 +48,6 @@ const stakingModuleData = { describe('StakingModuleGuardService', () => { let loggerService: LoggerService; - let lidoService: LidoService; let securityService: SecurityService; let stakingModuleGuardService: StakingModuleGuardService; let guardianMessageService: GuardianMessageService; @@ -70,7 +70,6 @@ describe('StakingModuleGuardService', () => { ], }).compile(); - lidoService = moduleRef.get(LidoService); securityService = moduleRef.get(SecurityService); loggerService = moduleRef.get(WINSTON_MODULE_NEST_PROVIDER); stakingModuleGuardService = moduleRef.get(StakingModuleGuardService); @@ -176,6 +175,7 @@ describe('StakingModuleGuardService', () => { ...currentBlockData, depositedEvents: { ...currentBlockData.depositedEvents, events }, unusedKeys, + lidoWC, }; const mockHandleCorrectKeys = jest @@ -186,10 +186,6 @@ describe('StakingModuleGuardService', () => { .spyOn(stakingModuleGuardService, 'handleKeysIntersections') .mockImplementation(async () => undefined); - const mockGetWithdrawalCredentials = jest - .spyOn(lidoService, 'getWithdrawalCredentials') - .mockImplementation(async () => lidoWC); - const mockSecurityContractIsDepositsPaused = jest .spyOn(securityService, 'isDepositsPaused') .mockImplementation(async () => false); @@ -205,14 +201,13 @@ describe('StakingModuleGuardService', () => { { ...stakingModuleData, unusedKeys, vettedKeys: [] }, blockData, ); - expect(mockGetWithdrawalCredentials).toBeCalledTimes(1); expect(mockSecurityContractIsDepositsPaused).toBeCalledTimes(1); }); it('should call handleCorrectKeys if Lido unused keys are not found in the deposit contract', async () => { const notDepositedKey = '0x2345'; const unusedKeys = [notDepositedKey]; - const blockData = { ...currentBlockData, unusedKeys }; + const blockData = { ...currentBlockData, unusedKeys, lidoWC }; const mockHandleCorrectKeys = jest .spyOn(stakingModuleGuardService, 'handleCorrectKeys') @@ -305,13 +300,7 @@ describe('StakingModuleGuardService', () => { const pubkey = '0x1234'; const lidoWC = '0x12'; const attackerWC = '0x23'; - const blockData = { blockHash: '0x1234' } as any; - - beforeEach(async () => { - jest - .spyOn(lidoService, 'getWithdrawalCredentials') - .mockImplementation(async () => lidoWC); - }); + const blockData = { blockHash: '0x1234', lidoWC } as any; it('should exclude invalid intersections', async () => { const intersections = [{ valid: false, pubkey, wc: lidoWC } as any]; @@ -540,7 +529,7 @@ describe('StakingModuleGuardService', () => { expect(result).toEqual(expect.arrayContaining(expectedStakingModules)); }); - it('should return list without changes if duplicated keys were not found', () => { + it('should return list without changes if duplicated keys were not found', () => { const moduleIdsWithDuplicateKeys = []; const expectedStakingModules = [ { @@ -659,7 +648,7 @@ describe('StakingModuleGuardService', () => { expect(mockSendMessageFromGuardian).toBeCalledTimes(0); }); - it('should return keys list if deposits with lido wx were made by lido', async () => { + it('should return keys list if deposits with lido wc were made by lido', async () => { const pubkeyWithUsedKey1 = '0x1234'; const pubkeyWithoutUsedKey = '0x56789'; const pubkeyWithUsedKey2 = '0x3478'; diff --git a/test/manifest.e2e-spec.ts b/test/manifest.e2e-spec.ts index 62758e63..3c21d07d 100644 --- a/test/manifest.e2e-spec.ts +++ b/test/manifest.e2e-spec.ts @@ -869,18 +869,18 @@ describe('ganache e2e tests', () => { // list of keys for /keys?used=false mock const unusedKeys = [ { - key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + key: '0xa9bfaa8207ee6c78644c079ffc91b6e5abcc5eede1b7a06abb8fb40e490a75ea269c178dd524b65185299d2bbd2eb7b2', depositSignature: - '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + '0xaa5f2a1053ba7d197495df44d4a32b7ae10265cf9e38560a16b782978c0a24271a113c9538453b7e45f35cb64c7adb460d7a9fe8c8ce6b8c80ca42fd5c48e180c73fc08f7d35ba32e39f32c902fd333faf47611827f0b7813f11c4c518dd2e59', operatorIndex: 0, used: false, index: 0, moduleAddress: NOP_REGISTRY, }, { - key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + key: '0xa9bfaa8207ee6c78644c079ffc91b6e5abcc5eede1b7a06abb8fb40e490a75ea269c178dd524b65185299d2bbd2eb7b2', depositSignature: - '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + '0xaa5f2a1053ba7d197495df44d4a32b7ae10265cf9e38560a16b782978c0a24271a113c9538453b7e45f35cb64c7adb460d7a9fe8c8ce6b8c80ca42fd5c48e180c73fc08f7d35ba32e39f32c902fd333faf47611827f0b7813f11c4c518dd2e59', operatorIndex: 0, used: false, index: 1, @@ -910,9 +910,9 @@ describe('ganache e2e tests', () => { // council will resume deposits to module const unusedKeysWithoutDuplicates = [ { - key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + key: '0xa9bfaa8207ee6c78644c079ffc91b6e5abcc5eede1b7a06abb8fb40e490a75ea269c178dd524b65185299d2bbd2eb7b2', depositSignature: - '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + '0xaa5f2a1053ba7d197495df44d4a32b7ae10265cf9e38560a16b782978c0a24271a113c9538453b7e45f35cb64c7adb460d7a9fe8c8ce6b8c80ca42fd5c48e180c73fc08f7d35ba32e39f32c902fd333faf47611827f0b7813f11c4c518dd2e59', operatorIndex: 0, used: false, index: 0, @@ -1117,21 +1117,10 @@ describe('ganache e2e tests', () => { // list of keys for /keys?used=false mock mockedKeysApiUnusedKeys(keysApiService, [keyWithWrongSign], meta); - if (!process.env.WALLET_PRIVATE_KEY) throw new Error(NO_PRIVKEY_MESSAGE); - const wallet = new ethers.Wallet(process.env.WALLET_PRIVATE_KEY); - await guardianService.handleNewBlock(); - expect(sendDepositMessage).toBeCalledTimes(1); - - expect(sendDepositMessage).toHaveBeenLastCalledWith( - expect.objectContaining({ - blockNumber: currentBlock.number, - guardianAddress: wallet.address, - guardianIndex: 9, - stakingModuleId: 1, - }), - ); + expect(sendDepositMessage).toBeCalledTimes(0); + expect(sendPauseMessage).toBeCalledTimes(0); }, TESTS_TIMEOUT, ); From c082cfd654b9b78801f5da13f29cda75c99a84c3 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 14 Dec 2023 03:35:40 +0400 Subject: [PATCH 18/37] fix: unit tests --- .../staking-module-guard/staking-module-guard.service.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) 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 accf138b..921bc0bf 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -115,6 +115,7 @@ export class StakingModuleGuardService { * Checks keys for intersections with previously deposited keys and handles the situation * @param blockData - collected data from the current block */ + // TODO: rename, because this method more than intersections checks public async checkKeysIntersections( stakingModuleData: StakingModuleData, blockData: BlockData, @@ -423,13 +424,7 @@ export class StakingModuleGuardService { }); const validationTimeStart = performance.now(); const invalidKeysList = await this.keysValidationService.validateKeys( - [ - ...stakingModuleData.vettedKeys, - { - ...stakingModuleData.vettedKeys[0], - depositSignature: stakingModuleData.vettedKeys[1].depositSignature, - }, - ], + stakingModuleData.vettedKeys, blockData.lidoWC, ); const validationTimeEnd = performance.now(); From 4b95c7fb3f380b5f6c753480b9600f10df31fe2e Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 14 Dec 2023 04:28:48 +0400 Subject: [PATCH 19/37] fix: added cache --- package.json | 3 +- src/guardian/keys-validation/constants.ts | 3 + .../keys-validation.service.ts | 78 +++++++++++++++---- yarn.lock | 5 ++ 4 files changed, 71 insertions(+), 18 deletions(-) create mode 100644 src/guardian/keys-validation/constants.ts diff --git a/package.json b/package.json index 9b274d84..3f16972d 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,8 @@ "rimraf": "^3.0.2", "rxjs": "^7.2.0", "winston": "^3.3.3", - "ws": "^8.10.0" + "ws": "^8.10.0", + "lru-cache": "^9.1.1" }, "devDependencies": { "@nestjs/cli": "^8.2.5", diff --git a/src/guardian/keys-validation/constants.ts b/src/guardian/keys-validation/constants.ts new file mode 100644 index 00000000..b67e3fea --- /dev/null +++ b/src/guardian/keys-validation/constants.ts @@ -0,0 +1,3 @@ +// TODO: put in config +// need to check metrics and decide about cache size +export const KEYS_LRU_CACHE_SIZE = 40000; diff --git a/src/guardian/keys-validation/keys-validation.service.ts b/src/guardian/keys-validation/keys-validation.service.ts index ebfda35e..697a22f7 100644 --- a/src/guardian/keys-validation/keys-validation.service.ts +++ b/src/guardian/keys-validation/keys-validation.service.ts @@ -3,19 +3,35 @@ import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { KeyValidatorInterface, bufferFromHexString, + Pubkey, + WithdrawalCredentialsBuffer, + Key, } from '@lido-nestjs/key-validation'; import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; import { ProviderService } from 'provider'; import { GENESIS_FORK_VERSION_BY_CHAIN_ID } from 'bls/bls.constants'; +import { LRUCache } from 'lru-cache'; +import { KEYS_LRU_CACHE_SIZE } from './constants'; + +type DepositData = { + key: Pubkey; + depositSignature: string; + withdrawalCredentials: WithdrawalCredentialsBuffer; + genesisForkVersion: Buffer; +}; @Injectable() export class KeysValidationService { + private keysCache: LRUCache; + constructor( @Inject(WINSTON_MODULE_NEST_PROVIDER) protected readonly logger: LoggerService, private readonly keyValidator: KeyValidatorInterface, private readonly provider: ProviderService, - ) {} + ) { + this.keysCache = new LRUCache({ max: KEYS_LRU_CACHE_SIZE }); + } /** * @@ -27,22 +43,16 @@ export class KeysValidationService { ): Promise<{ key: string; depositSignature: string }[]> { const forkVersion: Uint8Array = await this.forkVersion(); - const validatedKeys: [ - { - key: string; - depositSignature: string; - used: boolean; - }, - boolean, - ][] = await this.keyValidator.validateKeys( - vettedKeys.map((key) => ({ - key: key.key, - depositSignature: key.depositSignature, - used: false, - withdrawalCredentials: bufferFromHexString(withdrawalCredentials), - genesisForkVersion: Buffer.from(forkVersion.buffer), - })), - ); + const keysForValidation = vettedKeys + .map((key) => { + return this.getKeyFromCache(key, withdrawalCredentials, forkVersion); + }) + .filter((key): key is DepositData => key !== undefined) as DepositData[]; + + const validatedKeys: [Key & DepositData, boolean][] = + await this.keyValidator.validateKeys(keysForValidation); + + this.updateCache(validatedKeys); return validatedKeys .filter(([, result]) => !result) @@ -52,6 +62,40 @@ export class KeysValidationService { })); } + getKeyFromCache( + key: RegistryKey, + withdrawalCredentials: string, + forkVersion: Uint8Array, + ) { + const sign = this.keysCache.get(key.key); + + if (sign == key.depositSignature) { + // key was not changed + return undefined; + } + return this.depositData(key, withdrawalCredentials, forkVersion); + } + + async updateCache(validatedKeys: [Key & DepositData, boolean][]) { + // keys doesnt exist in cache or that we need to update + validatedKeys.forEach(([key, _]) => + this.keysCache.set(key.key, key.depositSignature), + ); + } + + depositData( + key: RegistryKey, + withdrawalCredentials: string, + forkVersion: Uint8Array, + ): DepositData { + return { + key: key.key, + depositSignature: key.depositSignature, + withdrawalCredentials: bufferFromHexString(withdrawalCredentials), + genesisForkVersion: Buffer.from(forkVersion.buffer), + }; + } + async forkVersion(): Promise { const chainId = await this.provider.getChainId(); const forkVersion = GENESIS_FORK_VERSION_BY_CHAIN_ID[chainId]; diff --git a/yarn.lock b/yarn.lock index 779f0c47..e80121ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5127,6 +5127,11 @@ lru-cache@6.0.0, lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^9.1.1: + version "9.1.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.1.2.tgz#255fdbc14b75589d6d0e73644ca167a8db506835" + integrity sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ== + lru-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" From 409f7ab8bc671ef63435372dfd558c7ed13f5d4a Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Thu, 14 Dec 2023 21:12:23 +0400 Subject: [PATCH 20/37] fix: put valid deposits filter in separate check, added prom metrics --- src/common/prometheus/prometheus.constants.ts | 6 +++ src/common/prometheus/prometheus.module.ts | 6 +++ src/common/prometheus/prometheus.provider.ts | 21 +++++++++++ .../guardian-metrics.service.ts | 37 ++++++++++++++++++- .../staking-module-guard.service.ts | 26 +++++++++---- 5 files changed, 86 insertions(+), 10 deletions(-) diff --git a/src/common/prometheus/prometheus.constants.ts b/src/common/prometheus/prometheus.constants.ts index 84b6e18f..3c49cf95 100644 --- a/src/common/prometheus/prometheus.constants.ts +++ b/src/common/prometheus/prometheus.constants.ts @@ -21,3 +21,9 @@ export const METRIC_DEPOSITED_KEYS_TOTAL = `${METRICS_PREFIX}deposited_keys_tota export const METRIC_OPERATORS_KEYS_TOTAL = `${METRICS_PREFIX}operators_keys_total`; export const METRIC_KEYS_API_REQUEST_DURATION = `${METRICS_PREFIX}keys_api_requests_duration_seconds`; + +export const METRIC_DUPLICATED_VETTED_UNUSED_KEYS_EVENT_COUNTER = `${METRICS_PREFIX}vetted_unused_keys_event_total`; + +export const METRIC_DUPLICATED_USED_KEYS_EVENT_COUNTER = `${METRICS_PREFIX}used_keys_event_total`; + +export const METRIC_INVALID_KEYS_EVENT_COUNTER = `${METRICS_PREFIX}invalid_keys_event_total`; diff --git a/src/common/prometheus/prometheus.module.ts b/src/common/prometheus/prometheus.module.ts index 45d92c7f..f0be4a78 100644 --- a/src/common/prometheus/prometheus.module.ts +++ b/src/common/prometheus/prometheus.module.ts @@ -13,6 +13,9 @@ import { PrometheusDepositedKeysProvider, PrometheusOperatorsKeysProvider, PrometheusKeysApiRequestsProvider, + PrometheusVettedUnusedKeysEventProvider, + PrometheusUsedKeysEventProvider, + PrometheusInvalidKeysEventProvider, } from './prometheus.provider'; import { METRICS_PREFIX, METRICS_URL } from './prometheus.constants'; @@ -38,6 +41,9 @@ const providers = [ PrometheusDepositedKeysProvider, PrometheusOperatorsKeysProvider, PrometheusKeysApiRequestsProvider, + PrometheusVettedUnusedKeysEventProvider, + PrometheusUsedKeysEventProvider, + PrometheusInvalidKeysEventProvider, ]; PrometheusModule.global = true; diff --git a/src/common/prometheus/prometheus.provider.ts b/src/common/prometheus/prometheus.provider.ts index e28dbc7c..32a2e314 100644 --- a/src/common/prometheus/prometheus.provider.ts +++ b/src/common/prometheus/prometheus.provider.ts @@ -17,6 +17,9 @@ import { METRIC_DEPOSITED_KEYS_TOTAL, METRIC_OPERATORS_KEYS_TOTAL, METRIC_KEYS_API_REQUEST_DURATION, + METRIC_DUPLICATED_VETTED_UNUSED_KEYS_EVENT_COUNTER, + METRIC_DUPLICATED_USED_KEYS_EVENT_COUNTER, + METRIC_INVALID_KEYS_EVENT_COUNTER, } from './prometheus.constants'; export const PrometheusTransportMessageCounterProvider = makeCounterProvider({ @@ -93,3 +96,21 @@ export const PrometheusKeysApiRequestsProvider = makeHistogramProvider({ buckets: [0.1, 0.2, 0.3, 0.6, 1, 1.5, 2, 5], labelNames: ['result', 'status'] as const, }); + +export const PrometheusVettedUnusedKeysEventProvider = makeCounterProvider({ + name: METRIC_DUPLICATED_VETTED_UNUSED_KEYS_EVENT_COUNTER, + help: 'Number of duplicated vetted unused keys events', + labelNames: ['stakingModuleId'] as const, +}); + +export const PrometheusUsedKeysEventProvider = makeCounterProvider({ + name: METRIC_DUPLICATED_USED_KEYS_EVENT_COUNTER, + help: 'Number of duplicated used keys events', + labelNames: ['stakingModuleId'] as const, +}); + +export const PrometheusInvalidKeysEventProvider = makeGaugeProvider({ + name: METRIC_INVALID_KEYS_EVENT_COUNTER, + help: 'Number of invalid keys', + labelNames: ['stakingModuleId'] as const, +}); diff --git a/src/guardian/guardian-metrics/guardian-metrics.service.ts b/src/guardian/guardian-metrics/guardian-metrics.service.ts index 54801286..487f902f 100644 --- a/src/guardian/guardian-metrics/guardian-metrics.service.ts +++ b/src/guardian/guardian-metrics/guardian-metrics.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { VerifiedDepositEvent } from 'contracts/deposit'; import { BlockData, StakingModuleData } from '../interfaces'; import { InjectMetric } from '@willsoto/nestjs-prometheus'; @@ -7,8 +7,11 @@ import { METRIC_DEPOSITED_KEYS_TOTAL, METRIC_OPERATORS_KEYS_TOTAL, METRIC_INTERSECTIONS_TOTAL, + METRIC_DUPLICATED_USED_KEYS_EVENT_COUNTER, + METRIC_INVALID_KEYS_EVENT_COUNTER, + METRIC_DUPLICATED_VETTED_UNUSED_KEYS_EVENT_COUNTER, } from 'common/prometheus'; -import { Gauge } from 'prom-client'; +import { Counter, Gauge } from 'prom-client'; @Injectable() export class GuardianMetricsService { @@ -24,6 +27,15 @@ export class GuardianMetricsService { @InjectMetric(METRIC_INTERSECTIONS_TOTAL) private intersectionsCounter: Gauge, + + @InjectMetric(METRIC_DUPLICATED_USED_KEYS_EVENT_COUNTER) + private duplicatedUsedKeysEventCounter: Counter, + + @InjectMetric(METRIC_DUPLICATED_VETTED_UNUSED_KEYS_EVENT_COUNTER) + private duplicatedVettedUnusedKeysEventCounter: Counter, + + @InjectMetric(METRIC_INVALID_KEYS_EVENT_COUNTER) + private invalidKeysEventCounter: Counter, ) {} /** @@ -117,4 +129,25 @@ export class GuardianMetricsService { filtered.length, ); } + + /** + * increment duplicated vetted unused keys event counter + */ + public incrDuplicatedVettedUnusedKeysEventCounter() { + this.duplicatedVettedUnusedKeysEventCounter.inc(); + } + + /** + * increment duplicated used keys event counter + */ + public incrDuplicatedUsedKeysEventCounter() { + this.duplicatedUsedKeysEventCounter.inc(); + } + + /** + * increment invalid keys event counter + */ + public incrInvalidKeysEventCounter() { + this.invalidKeysEventCounter.inc(); + } } 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 3f70ada7..3abcec5c 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -78,7 +78,7 @@ export class StakingModuleGuardService { moduleAddressesWithDuplicates: moduleAddressesWithDuplicatesList, }); - //TODO: set prometheus metric council_daemon_vetted_unused_duplicate + this.guardianMetricsService.incrDuplicatedVettedUnusedKeysEventCounter(); return moduleAddressesWithDuplicatesList; } @@ -127,9 +127,12 @@ export class StakingModuleGuardService { blockData, ); + // exclude invalid deposits as they ignored by cl + const validIntersections = this.excludeInvalidDeposits(keysIntersections); + const filteredIntersections = await this.excludeEligibleIntersections( blockData, - keysIntersections, + validIntersections, ); const isFilteredIntersectionsFound = filteredIntersections.length > 0; @@ -157,14 +160,14 @@ export class StakingModuleGuardService { } else { // it could throw error if kapi returned old data const usedKeys = await this.getIntersectionBetweenUsedAndUnusedKeys( - keysIntersections, + validIntersections, blockData, ); // if found used keys, Lido already made deposit on this keys if (usedKeys.length) { this.logger.log('Found that we already deposited on these keys'); - // set metric council_daemon_used_duplicate + this.guardianMetricsService.incrDuplicatedUsedKeysEventCounter(); return; } @@ -203,6 +206,12 @@ export class StakingModuleGuardService { return intersections; } + public excludeInvalidDeposits(intersections: VerifiedDepositEvent[]) { + // Exclude deposits with invalid signature over the deposit data + const validIntersections = intersections.filter(({ valid }) => valid); + return validIntersections; + } + /** * Excludes invalid deposits and deposits with Lido WC from intersections * @param intersections - list of deposits with keys that were deposited earlier @@ -210,11 +219,12 @@ export class StakingModuleGuardService { */ public async excludeEligibleIntersections( blockData: BlockData, - intersections: VerifiedDepositEvent[], + validIntersections: VerifiedDepositEvent[], ): Promise { - // Exclude deposits with invalid signature over the deposit data - const validIntersections = intersections.filter(({ valid }) => valid); - if (!validIntersections.length) return []; + if (!validIntersections.length) { + this.logger.log('Not found valid intersection'); + return []; + } // Exclude deposits with Lido withdrawal credentials const { blockHash } = blockData; From c09abd1e71af6852cd757d57817ead1b7250b62c Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 15 Dec 2023 01:04:23 +0400 Subject: [PATCH 21/37] fix: added metric --- .../staking-module-guard/staking-module-guard.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8ed9a27a..5682281d 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -392,7 +392,7 @@ export class StakingModuleGuardService { this.logger.error( 'Found invalid keys, will skip deposits until solving problem', ); - // set metric council_daemon_invalid_key + this.guardianMetricsService.incrInvalidKeysEventCounter(); return; } } From fac284a6cd02cd590eebc8af97d898771bd39df2 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 15 Dec 2023 02:59:15 +0400 Subject: [PATCH 22/37] fix: cache --- .../keys-validation.service.ts | 66 +++++++++++++------ 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/src/guardian/keys-validation/keys-validation.service.ts b/src/guardian/keys-validation/keys-validation.service.ts index 697a22f7..f69444d3 100644 --- a/src/guardian/keys-validation/keys-validation.service.ts +++ b/src/guardian/keys-validation/keys-validation.service.ts @@ -22,7 +22,7 @@ type DepositData = { @Injectable() export class KeysValidationService { - private keysCache: LRUCache; + private keysCache: LRUCache; constructor( @Inject(WINSTON_MODULE_NEST_PROVIDER) @@ -44,43 +44,40 @@ export class KeysValidationService { const forkVersion: Uint8Array = await this.forkVersion(); const keysForValidation = vettedKeys - .map((key) => { - return this.getKeyFromCache(key, withdrawalCredentials, forkVersion); - }) - .filter((key): key is DepositData => key !== undefined) as DepositData[]; + .map((key) => + this.prepareKeyForValidation(key, withdrawalCredentials, forkVersion), + ) + .filter((key) => key !== undefined) as DepositData[]; const validatedKeys: [Key & DepositData, boolean][] = await this.keyValidator.validateKeys(keysForValidation); - this.updateCache(validatedKeys); + this.updateCacheWithValidationResults(validatedKeys); - return validatedKeys - .filter(([, result]) => !result) + // this list will not include invalid keys from cache + const invalidKeysFromCurrentValidation = validatedKeys + .filter(([, isValid]) => !isValid) .map(([key]) => ({ key: key.key, depositSignature: key.depositSignature, })); + + return this.mergeInvalidKeys(invalidKeysFromCurrentValidation); } - getKeyFromCache( + prepareKeyForValidation( key: RegistryKey, withdrawalCredentials: string, forkVersion: Uint8Array, ) { - const sign = this.keysCache.get(key.key); + const cachedEntry = this.keysCache.get(key.key); - if (sign == key.depositSignature) { - // key was not changed + // key wasn't in cache or signature was changed + if (cachedEntry && cachedEntry.signature == key.depositSignature) { return undefined; } - return this.depositData(key, withdrawalCredentials, forkVersion); - } - async updateCache(validatedKeys: [Key & DepositData, boolean][]) { - // keys doesnt exist in cache or that we need to update - validatedKeys.forEach(([key, _]) => - this.keysCache.set(key.key, key.depositSignature), - ); + return this.depositData(key, withdrawalCredentials, forkVersion); } depositData( @@ -106,4 +103,35 @@ export class KeysValidationService { return forkVersion; } + + async updateCacheWithValidationResults( + validatedKeys: [Key & DepositData, boolean][], + ) { + validatedKeys.forEach(([key, isValid]) => + this.keysCache.set(key.key, { signature: key.depositSignature, isValid }), + ); + } + + private mergeInvalidKeys( + recentInvalidKeys: { key: string; depositSignature: string }[], + ): { key: string; depositSignature: string }[] { + const allInvalidKeys = new Map< + string, + { key: string; depositSignature: string } + >(); + + // Add invalid keys from the current validation + for (const key of recentInvalidKeys) { + allInvalidKeys.set(key.key, key); + } + + // Merge with invalid keys from the cache + this.keysCache.forEach((value, key) => { + if (!value.isValid && !allInvalidKeys.has(key)) { + allInvalidKeys.set(key, { key, depositSignature: value.signature }); + } + }); + + return Array.from(allInvalidKeys.values()); + } } From 9fce10b45e503a06bd6186e99e3fedf6c6059fe3 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 15 Dec 2023 03:03:06 +0400 Subject: [PATCH 23/37] fix: filter deposits --- .../staking-module-guard/staking-module-guard.service.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) 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 5682281d..a222da98 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -244,14 +244,7 @@ export class StakingModuleGuardService { intersectionsWithLidoWC: VerifiedDepositEvent[], blockData: BlockData, ) { - // should not check invalid - // TODO: fix in prev PR - const validIntersections = intersectionsWithLidoWC.filter( - ({ valid }) => valid, - ); - if (!validIntersections.length) return []; - - const depositedPubkeys = validIntersections.map( + const depositedPubkeys = intersectionsWithLidoWC.map( (deposit) => deposit.pubkey, ); From faa2ec98fcaf32b6b189cb0a985818367841d331 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 15 Dec 2023 13:47:05 +0400 Subject: [PATCH 24/37] fix: cache and tests --- .../keys-validation.service.ts | 27 ++++++-- .../staking-module-guard.service.ts | 13 +++- test/helpers/mockKeysApi.ts | 4 +- test/manifest.e2e-spec.ts | 66 +++++++++++++++++++ 4 files changed, 100 insertions(+), 10 deletions(-) diff --git a/src/guardian/keys-validation/keys-validation.service.ts b/src/guardian/keys-validation/keys-validation.service.ts index f69444d3..39a98f6d 100644 --- a/src/guardian/keys-validation/keys-validation.service.ts +++ b/src/guardian/keys-validation/keys-validation.service.ts @@ -62,7 +62,7 @@ export class KeysValidationService { depositSignature: key.depositSignature, })); - return this.mergeInvalidKeys(invalidKeysFromCurrentValidation); + return this.mergeInvalidKeys(vettedKeys, invalidKeysFromCurrentValidation); } prepareKeyForValidation( @@ -113,6 +113,7 @@ export class KeysValidationService { } private mergeInvalidKeys( + vettedKeys: RegistryKey[], recentInvalidKeys: { key: string; depositSignature: string }[], ): { key: string; depositSignature: string }[] { const allInvalidKeys = new Map< @@ -125,13 +126,29 @@ export class KeysValidationService { allInvalidKeys.set(key.key, key); } - // Merge with invalid keys from the cache - this.keysCache.forEach((value, key) => { - if (!value.isValid && !allInvalidKeys.has(key)) { - allInvalidKeys.set(key, { key, depositSignature: value.signature }); + const newInvalidKeys = allInvalidKeys.size; + this.logger.log('New invalid keys', allInvalidKeys.size); + + // want to add invalid keys from cache that we have in current list of vetted keys + vettedKeys.forEach((vettedKey) => { + const cachedEntry = this.keysCache.get(vettedKey.key); + if ( + cachedEntry && + !cachedEntry.isValid && + !allInvalidKeys.has(vettedKey.key) + ) { + allInvalidKeys.set(vettedKey.key, { + key: vettedKey.key, + depositSignature: cachedEntry.signature, + }); } }); + this.logger.log( + 'Invalid keys from cache', + allInvalidKeys.size - newInvalidKeys, + ); + return Array.from(allInvalidKeys.values()); } } 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 a222da98..2e821b4b 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -72,9 +72,13 @@ export class StakingModuleGuardService { const moduleAddressesWithDuplicatesList: number[] = Array.from( modulesWithDuplicatedKeysSet, ); - this.logger.error('Found duplicated vetted keys', { + this.logger.error('Found duplicated vetted keys'); + this.logger.log('Duplicated keys', { blockHash: blockData.blockHash, - duplicatedKeys: Array.from(duplicatedKeys), + duplicatedKeys: Array.from(duplicatedKeys).map(([key, innerMap]) => ({ + key: key, + stakingModuleIds: Array.from(innerMap.keys()), + })), moduleAddressesWithDuplicates: moduleAddressesWithDuplicatesList, }); @@ -368,7 +372,10 @@ export class StakingModuleGuardService { this.lastContractsStateByModuleId[stakingModuleId] = currentContractState; - if (isSameContractsState) return; + if (isSameContractsState) { + this.logger.log('Contract states didnt change'); + return; + } if ( !lastContractsState || diff --git a/test/helpers/mockKeysApi.ts b/test/helpers/mockKeysApi.ts index 056162e0..84d726cd 100644 --- a/test/helpers/mockKeysApi.ts +++ b/test/helpers/mockKeysApi.ts @@ -7,8 +7,8 @@ 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, nonce = 6046) => ({ + nonce, type: 'grouped-onchain-v1', id: 1, stakingModuleAddress: NOP_REGISTRY, diff --git a/test/manifest.e2e-spec.ts b/test/manifest.e2e-spec.ts index 3c21d07d..e7cbc313 100644 --- a/test/manifest.e2e-spec.ts +++ b/test/manifest.e2e-spec.ts @@ -1084,6 +1084,37 @@ 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: { @@ -1116,9 +1147,44 @@ describe('ganache e2e tests', () => { }; // list of keys for /keys?used=false mock mockedKeysApiUnusedKeys(keysApiService, [keyWithWrongSign], meta); + mockedKeysWithDuplicates(keysApiService, [], meta); + + await guardianService.handleNewBlock(); + + expect(sendDepositMessage).toBeCalledTimes(0); + expect(sendPauseMessage).toBeCalledTimes(0); + + await new Promise((res) => setTimeout(res, SLEEP_FOR_RESULT)); + + const newBlock = await tempProvider.getBlock('latest'); + + await depositService.setCachedEvents({ + data: [], + headers: { + startBlock: newBlock.number, + endBlock: newBlock.number, + version: '1', + }, + }); + + // mocked curated module + const newMeta = mockedMeta(newBlock); + const newStakingModule = mockedModule(newBlock, 6047); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + newStakingModule, + newMeta, + ); + + // list of keys for /keys?used=false mock + mockedKeysApiUnusedKeys(keysApiService, [keyWithWrongSign], newMeta); await guardianService.handleNewBlock(); + // should found invalid key and skip again + // on this iteration cache will be used expect(sendDepositMessage).toBeCalledTimes(0); expect(sendPauseMessage).toBeCalledTimes(0); }, From e57d74edad5c50857c6325d144e768e34bb3db71 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 18 Dec 2023 16:06:32 +0400 Subject: [PATCH 25/37] fix: isSameStakingModuleContractState check --- .../staking-module-guard/staking-module-guard.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 5682281d..798cc0e4 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -380,8 +380,8 @@ export class StakingModuleGuardService { if ( !lastContractsState || !this.isSameStakingModuleContractState( - currentContractState.blockNumber, - lastContractsState.blockNumber, + currentContractState.nonce, + lastContractsState.nonce, ) ) { const invalidKeys = await this.getInvalidKeys( From 8d260fd7c33cdcdcc9ec0876bd7247a9cec5a267 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 18 Dec 2023 19:17:59 +0400 Subject: [PATCH 26/37] fix: small fixes --- src/guardian/keys-validation/constants.ts | 3 +- .../keys-validation.service.ts | 32 ++++++++++++------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/guardian/keys-validation/constants.ts b/src/guardian/keys-validation/constants.ts index b67e3fea..1f71acf3 100644 --- a/src/guardian/keys-validation/constants.ts +++ b/src/guardian/keys-validation/constants.ts @@ -1,3 +1,2 @@ // TODO: put in config -// need to check metrics and decide about cache size -export const KEYS_LRU_CACHE_SIZE = 40000; +export const KEYS_LRU_CACHE_SIZE = 32000; diff --git a/src/guardian/keys-validation/keys-validation.service.ts b/src/guardian/keys-validation/keys-validation.service.ts index 39a98f6d..4e668770 100644 --- a/src/guardian/keys-validation/keys-validation.service.ts +++ b/src/guardian/keys-validation/keys-validation.service.ts @@ -45,8 +45,9 @@ export class KeysValidationService { const keysForValidation = vettedKeys .map((key) => - this.prepareKeyForValidation(key, withdrawalCredentials, forkVersion), + this.filterKeyForValidation(key, withdrawalCredentials, forkVersion), ) + // filter keys that were in cache or signature wasn't changed .filter((key) => key !== undefined) as DepositData[]; const validatedKeys: [Key & DepositData, boolean][] = @@ -62,24 +63,32 @@ export class KeysValidationService { depositSignature: key.depositSignature, })); + // merge just checked invalid keys and invalid keys from cache but only from vettedKeys return this.mergeInvalidKeys(vettedKeys, invalidKeysFromCurrentValidation); } - prepareKeyForValidation( + filterKeyForValidation( key: RegistryKey, withdrawalCredentials: string, forkVersion: Uint8Array, - ) { + ): DepositData | null { const cachedEntry = this.keysCache.get(key.key); - // key wasn't in cache or signature was changed - if (cachedEntry && cachedEntry.signature == key.depositSignature) { - return undefined; + // key was in cache and signature wasn't changed + if (this.keyNeedToBeValidated(cachedEntry, key)) { + return null; } return this.depositData(key, withdrawalCredentials, forkVersion); } + keyNeedToBeValidated( + cachedEntry: { signature: string; isValid: boolean } | undefined, + key: RegistryKey, + ): boolean { + return !!cachedEntry && cachedEntry.signature == key.depositSignature; + } + depositData( key: RegistryKey, withdrawalCredentials: string, @@ -127,7 +136,9 @@ export class KeysValidationService { } const newInvalidKeys = allInvalidKeys.size; - this.logger.log('New invalid keys', allInvalidKeys.size); + this.logger.log('New invalid keys', { + count: allInvalidKeys.size, + }); // want to add invalid keys from cache that we have in current list of vetted keys vettedKeys.forEach((vettedKey) => { @@ -144,10 +155,9 @@ export class KeysValidationService { } }); - this.logger.log( - 'Invalid keys from cache', - allInvalidKeys.size - newInvalidKeys, - ); + this.logger.log('Invalid keys from cache', { + count: allInvalidKeys.size - newInvalidKeys, + }); return Array.from(allInvalidKeys.values()); } From 7c6f9dd1713fa9724a5459144e597938bce5b2d2 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 19 Dec 2023 04:33:16 +0400 Subject: [PATCH 27/37] fix: tests & refactoring --- .../keys-validation.service.ts | 118 +++++------ .../keys-validation/keys-validation.spec.ts | 189 ++++++++++++++++++ src/guardian/keys-validation/keys.fixtures.ts | 42 ++++ .../staking-module-guard.service.ts | 5 +- 4 files changed, 278 insertions(+), 76 deletions(-) create mode 100644 src/guardian/keys-validation/keys-validation.spec.ts create mode 100644 src/guardian/keys-validation/keys.fixtures.ts diff --git a/src/guardian/keys-validation/keys-validation.service.ts b/src/guardian/keys-validation/keys-validation.service.ts index 4e668770..00ba0309 100644 --- a/src/guardian/keys-validation/keys-validation.service.ts +++ b/src/guardian/keys-validation/keys-validation.service.ts @@ -8,10 +8,10 @@ import { Key, } from '@lido-nestjs/key-validation'; import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; -import { ProviderService } from 'provider'; import { GENESIS_FORK_VERSION_BY_CHAIN_ID } from 'bls/bls.constants'; import { LRUCache } from 'lru-cache'; import { KEYS_LRU_CACHE_SIZE } from './constants'; +import { ProviderService } from 'provider'; type DepositData = { key: Pubkey; @@ -37,23 +37,23 @@ export class KeysValidationService { * * Return list of invalid keys */ - async validateKeys( + public async findInvalidKeys( vettedKeys: RegistryKey[], withdrawalCredentials: string, ): Promise<{ key: string; depositSignature: string }[]> { const forkVersion: Uint8Array = await this.forkVersion(); - const keysForValidation = vettedKeys - .map((key) => - this.filterKeyForValidation(key, withdrawalCredentials, forkVersion), - ) - // filter keys that were in cache or signature wasn't changed - .filter((key) => key !== undefined) as DepositData[]; + const { keysNeedingValidation, unchangedAndInvalidKeys } = + this.divideKeys(vettedKeys); + + const keysForValidation = keysNeedingValidation.map((key) => + this.toDepositData(key, withdrawalCredentials, forkVersion), + ); const validatedKeys: [Key & DepositData, boolean][] = await this.keyValidator.validateKeys(keysForValidation); - this.updateCacheWithValidationResults(validatedKeys); + this.updateCache(validatedKeys); // this list will not include invalid keys from cache const invalidKeysFromCurrentValidation = validatedKeys @@ -63,33 +63,48 @@ export class KeysValidationService { depositSignature: key.depositSignature, })); + this.logger.log('Validation keys information', { + vettedKeysCount: vettedKeys.length, + currentCacheSize: this.keysCache.size, + cacheInvalidKeysCount: unchangedAndInvalidKeys.length, + newInvalidKeys: invalidKeysFromCurrentValidation.length, + }); + + const unchangedAndInvalidKeysValues = unchangedAndInvalidKeys.map( + (key) => ({ + key: key.key, + depositSignature: key.depositSignature, + }), + ); + // merge just checked invalid keys and invalid keys from cache but only from vettedKeys - return this.mergeInvalidKeys(vettedKeys, invalidKeysFromCurrentValidation); + return [ + ...invalidKeysFromCurrentValidation, + ...unchangedAndInvalidKeysValues, + ]; } - filterKeyForValidation( - key: RegistryKey, - withdrawalCredentials: string, - forkVersion: Uint8Array, - ): DepositData | null { - const cachedEntry = this.keysCache.get(key.key); + private divideKeys(vettedKeys: RegistryKey[]): { + keysNeedingValidation: RegistryKey[]; + unchangedAndInvalidKeys: RegistryKey[]; + } { + const keysNeedingValidation: RegistryKey[] = []; + const unchangedAndInvalidKeys: RegistryKey[] = []; - // key was in cache and signature wasn't changed - if (this.keyNeedToBeValidated(cachedEntry, key)) { - return null; - } + vettedKeys.forEach((key) => { + const cachedEntry = this.keysCache.get(key.key); - return this.depositData(key, withdrawalCredentials, forkVersion); - } + if (!cachedEntry || cachedEntry.signature !== key.depositSignature) { + keysNeedingValidation.push(key); + } else if (!cachedEntry.isValid) { + unchangedAndInvalidKeys.push(key); + } + }); - keyNeedToBeValidated( - cachedEntry: { signature: string; isValid: boolean } | undefined, - key: RegistryKey, - ): boolean { - return !!cachedEntry && cachedEntry.signature == key.depositSignature; + return { keysNeedingValidation, unchangedAndInvalidKeys }; } - depositData( + private toDepositData( key: RegistryKey, withdrawalCredentials: string, forkVersion: Uint8Array, @@ -102,7 +117,7 @@ export class KeysValidationService { }; } - async forkVersion(): Promise { + private async forkVersion(): Promise { const chainId = await this.provider.getChainId(); const forkVersion = GENESIS_FORK_VERSION_BY_CHAIN_ID[chainId]; @@ -113,52 +128,9 @@ export class KeysValidationService { return forkVersion; } - async updateCacheWithValidationResults( - validatedKeys: [Key & DepositData, boolean][], - ) { + private async updateCache(validatedKeys: [Key & DepositData, boolean][]) { validatedKeys.forEach(([key, isValid]) => this.keysCache.set(key.key, { signature: key.depositSignature, isValid }), ); } - - private mergeInvalidKeys( - vettedKeys: RegistryKey[], - recentInvalidKeys: { key: string; depositSignature: string }[], - ): { key: string; depositSignature: string }[] { - const allInvalidKeys = new Map< - string, - { key: string; depositSignature: string } - >(); - - // Add invalid keys from the current validation - for (const key of recentInvalidKeys) { - allInvalidKeys.set(key.key, key); - } - - const newInvalidKeys = allInvalidKeys.size; - this.logger.log('New invalid keys', { - count: allInvalidKeys.size, - }); - - // want to add invalid keys from cache that we have in current list of vetted keys - vettedKeys.forEach((vettedKey) => { - const cachedEntry = this.keysCache.get(vettedKey.key); - if ( - cachedEntry && - !cachedEntry.isValid && - !allInvalidKeys.has(vettedKey.key) - ) { - allInvalidKeys.set(vettedKey.key, { - key: vettedKey.key, - depositSignature: cachedEntry.signature, - }); - } - }); - - this.logger.log('Invalid keys from cache', { - count: allInvalidKeys.size - newInvalidKeys, - }); - - return Array.from(allInvalidKeys.values()); - } } diff --git a/src/guardian/keys-validation/keys-validation.spec.ts b/src/guardian/keys-validation/keys-validation.spec.ts new file mode 100644 index 00000000..fd22bbad --- /dev/null +++ b/src/guardian/keys-validation/keys-validation.spec.ts @@ -0,0 +1,189 @@ +import { Test } from '@nestjs/testing'; +import { KeysValidationModule } from './keys-validation.module'; +import { + KeyValidatorInterface, + KeyValidatorModule, + bufferFromHexString, +} from '@lido-nestjs/key-validation'; +import { KeysValidationService } from './keys-validation.service'; +import { LoggerModule } from 'common/logger'; +import { ConfigModule } from 'common/config'; +import { MockProviderModule } from 'provider'; +import { + invalidKey, + invalidKey2, + invalidKey2GoodSign, + validKeys, +} from './keys.fixtures'; +import { GENESIS_FORK_VERSION_BY_CHAIN_ID } from 'bls/bls.constants'; + +describe('KeysValidationService', () => { + let keysValidationService: KeysValidationService; + let keysValidator: KeyValidatorInterface; + + let validateKeysFun: jest.SpyInstance; + + const wc = + '0x010000000000000000000000dc62f9e8c34be08501cdef4ebde0a280f576d762'; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot(), + MockProviderModule.forRoot(), + LoggerModule, + KeyValidatorModule.forFeature({ multithreaded: true }), + KeysValidationModule, + ], + }).compile(); + + keysValidationService = moduleRef.get(KeysValidationService); + keysValidator = moduleRef.get(KeyValidatorInterface); + + validateKeysFun = jest.spyOn(keysValidator, 'validateKeys'); + }); + + it('add new key in empty cache', async () => { + // add a new keys + const result = await keysValidationService.findInvalidKeys( + [...validKeys, invalidKey], + wc, + ); + + const expected = [invalidKey].map((key) => ({ + key: key.key, + depositSignature: key.depositSignature, + })); + + const fork = GENESIS_FORK_VERSION_BY_CHAIN_ID[5]; + + const depositData = [...validKeys, invalidKey].map((key) => ({ + key: key.key, + depositSignature: key.depositSignature, + withdrawalCredentials: bufferFromHexString(wc), + genesisForkVersion: Buffer.from(fork.buffer), + })); + + expect(validateKeysFun).toBeCalledTimes(1); + expect(validateKeysFun).toBeCalledWith(depositData); + expect(result).toEqual(expect.arrayContaining(expected)); + expect(result.length).toEqual(1); + + validateKeysFun.mockClear(); + + const newResult = await keysValidationService.findInvalidKeys( + [...validKeys, invalidKey], + wc, + ); + + expect(validateKeysFun).toBeCalledTimes(1); + expect(validateKeysFun).toBeCalledWith([]); + // will be read from cache + expect(newResult).toEqual(expect.arrayContaining(expected)); + expect(result.length).toEqual(1); + }); + + it('add new key in non empty cache', async () => { + // will include in result only invalid keys from actual list + // add a new keys + const result = await keysValidationService.findInvalidKeys( + [...validKeys, invalidKey, invalidKey2], + wc, + ); + + const expected = [invalidKey, invalidKey2].map((key) => ({ + key: key.key, + depositSignature: key.depositSignature, + })); + + const fork = GENESIS_FORK_VERSION_BY_CHAIN_ID[5]; + + const depositData = [...validKeys, invalidKey, invalidKey2].map((key) => ({ + key: key.key, + depositSignature: key.depositSignature, + withdrawalCredentials: bufferFromHexString(wc), + genesisForkVersion: Buffer.from(fork.buffer), + })); + + expect(validateKeysFun).toBeCalledTimes(1); + expect(validateKeysFun).toBeCalledWith(depositData); + expect(result).toEqual(expect.arrayContaining(expected)); + expect(result.length).toEqual(expected.length); + + // one invalid key was deleted + validateKeysFun.mockClear(); + const newResult = await keysValidationService.findInvalidKeys( + [...validKeys, invalidKey], + wc, + ); + + const newExpected = [invalidKey].map((key) => ({ + key: key.key, + depositSignature: key.depositSignature, + })); + + expect(validateKeysFun).toBeCalledTimes(1); + expect(validateKeysFun).toBeCalledWith([]); + expect(newResult).toEqual(expect.arrayContaining(newExpected)); + expect(newResult.length).toEqual(newExpected.length); + }); + + it('validate key again if signature was changed', async () => { + // if signature was changed we need to repeat validation + // invalid key could become valid and visa versa + // add a new keys + const result = await keysValidationService.findInvalidKeys( + [...validKeys, invalidKey, invalidKey2], + wc, + ); + + const expected = [invalidKey, invalidKey2].map((key) => ({ + key: key.key, + depositSignature: key.depositSignature, + })); + + const fork = GENESIS_FORK_VERSION_BY_CHAIN_ID[5]; + + const depositData = [...validKeys, invalidKey, invalidKey2].map((key) => ({ + key: key.key, + depositSignature: key.depositSignature, + withdrawalCredentials: bufferFromHexString(wc), + genesisForkVersion: Buffer.from(fork.buffer), + })); + + expect(validateKeysFun).toBeCalledTimes(1); + expect(validateKeysFun).toBeCalledWith(depositData); + expect(result).toEqual(expect.arrayContaining(expected)); + expect(result.length).toEqual(expected.length); + + // repeat and check that cache will be used + validateKeysFun.mockClear(); + const newResult = await keysValidationService.findInvalidKeys( + [ + ...validKeys, + invalidKey, + { ...invalidKey2, depositSignature: invalidKey2GoodSign }, + ], + wc, + ); + + const newDepositData = [ + { ...invalidKey2, depositSignature: invalidKey2GoodSign }, + ].map((key) => ({ + key: key.key, + depositSignature: key.depositSignature, + withdrawalCredentials: bufferFromHexString(wc), + genesisForkVersion: Buffer.from(fork.buffer), + })); + + const newExpected = [invalidKey].map((key) => ({ + key: key.key, + depositSignature: key.depositSignature, + })); + + expect(validateKeysFun).toBeCalledTimes(1); + expect(validateKeysFun).toBeCalledWith(newDepositData); + expect(newResult).toEqual(expect.arrayContaining(newExpected)); + expect(newResult.length).toEqual(newExpected.length); + }); +}); diff --git a/src/guardian/keys-validation/keys.fixtures.ts b/src/guardian/keys-validation/keys.fixtures.ts new file mode 100644 index 00000000..eafda3bf --- /dev/null +++ b/src/guardian/keys-validation/keys.fixtures.ts @@ -0,0 +1,42 @@ +export const validKeys = [ + { + key: '0xa9bfaa8207ee6c78644c079ffc91b6e5abcc5eede1b7a06abb8fb40e490a75ea269c178dd524b65185299d2bbd2eb7b2', + depositSignature: + '0xaa5f2a1053ba7d197495df44d4a32b7ae10265cf9e38560a16b782978c0a24271a113c9538453b7e45f35cb64c7adb460d7a9fe8c8ce6b8c80ca42fd5c48e180c73fc08f7d35ba32e39f32c902fd333faf47611827f0b7813f11c4c518dd2e59', + operatorIndex: 1, + used: false, + moduleAddress: '0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320', + index: 51, + }, + { + key: '0xb3c90525010a5710d43acbea46047fc37ed55306d032527fa15dd7e8cd8a9a5fa490347cc5fce59936fb8300683cd9f3', + depositSignature: + '0x8a77d9411781360cc107344a99f6660b206d2c708ae7fa35565b76ec661a0b86b6c78f5b5691d2cf469c27d0655dfc6311451a9e0501f3c19c6f7e35a770d1a908bfec7cba2e07339dc633b8b6626216ce76ec0fa48ee56aaaf2f9dc7ccb2fe2', + operatorIndex: 1, + used: false, + moduleAddress: '0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320', + index: 52, + }, +]; +export const invalidKey = { + key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + depositSignature: + '0xb45b15f6e043d91eabbda838eae32f7dcb998578919bd813d8add67de9b14bc268a4fde41d08058a9dc2c40b881f47970c30fd3beee46517e4e5eebd4aba52060425e021302c987d365347d478681b2cabfd31208d0607f71f3766a53ca1ada0', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 5, +}; + +export const invalidKey2 = { + key: '0x9100e67cfb22cb7f1c3924e91bc8f70111f0634fa87d3361f807585e7ab06f84a0f504b7390683ce01567e5de3ad7445', + depositSignature: + '0x8d4ed47875fab45e9cfec65bf67c956be0b00d4d4cde2b6b898b09d07eed10457b4e2a8f496077e4a145e523d5b18749035b87c2412360d4fbbc850051b307f704a758f4ef35ca4af6c5f8f4e4a95603dc688bb3773b5a22c6c21b5440c71e13', + operatorIndex: 1, + used: false, + moduleAddress: '0x9D4AF1Ee19Dad8857db3a45B0374c81c8A1C6320', + index: 54, +}; + +export const invalidKey2GoodSign = + '0x889531faa742982deab20afd3c76e4c0e4af784aed814c15ccb25fe2b77cbaaddda39dc78f364b06990972690958bae7077efa352e51c57283129598612d2ce4f3f4a4df06695d42d804ebc923a1811c80b60503b8c87e19ceee8c0bc1bb9650'; 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 3d793ce8..75fd583b 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -227,7 +227,6 @@ export class StakingModuleGuardService { validIntersections: VerifiedDepositEvent[], ): Promise { if (!validIntersections.length) { - this.logger.log('Not found valid intersection'); return []; } @@ -373,7 +372,7 @@ export class StakingModuleGuardService { this.lastContractsStateByModuleId[stakingModuleId] = currentContractState; if (isSameContractsState) { - this.logger.log('Contract states didnt change'); + this.logger.log("Contract states didn't change"); return; } @@ -433,7 +432,7 @@ export class StakingModuleGuardService { keysCount: stakingModuleData.vettedKeys.length, }); const validationTimeStart = performance.now(); - const invalidKeysList = await this.keysValidationService.validateKeys( + const invalidKeysList = await this.keysValidationService.findInvalidKeys( stakingModuleData.vettedKeys, blockData.lidoWC, ); From 0ba171a0e08fc1dd237fc9951ce3d79fdcc8bd0d Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 19 Dec 2023 04:42:17 +0400 Subject: [PATCH 28/37] fix: refact --- .../staking-module-guard.service.ts | 25 ++----------------- 1 file changed, 2 insertions(+), 23 deletions(-) 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 798cc0e4..e0b54bba 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -379,10 +379,7 @@ export class StakingModuleGuardService { if ( !lastContractsState || - !this.isSameStakingModuleContractState( - currentContractState.nonce, - lastContractsState.nonce, - ) + currentContractState.nonce !== lastContractsState.nonce ) { const invalidKeys = await this.getInvalidKeys( stakingModuleData, @@ -461,13 +458,7 @@ export class StakingModuleGuardService { ): boolean { if (!firstState || !secondState) return false; if (firstState.depositRoot !== secondState.depositRoot) return false; - if ( - !this.isSameStakingModuleContractState( - firstState.nonce, - secondState.nonce, - ) - ) - return false; + if (firstState.nonce !== secondState.nonce) return false; if ( Math.floor(firstState.blockNumber / GUARDIAN_DEPOSIT_RESIGNING_BLOCKS) !== @@ -478,16 +469,4 @@ export class StakingModuleGuardService { return true; } - - /** - * @returns true if nonce is the same - */ - public isSameStakingModuleContractState( - firstNonce: number, - secondNonce: number, - ): boolean { - if (firstNonce !== secondNonce) return false; - - return true; - } } From 8758649e50e42e6e887ac7d77536f005db15e4e1 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 19 Dec 2023 17:12:52 +0400 Subject: [PATCH 29/37] fix: simple refactoring --- src/guardian/guardian.service.spec.ts | 2 +- src/guardian/guardian.service.ts | 9 ++-- .../staking-module-guard.service.ts | 26 ++++------- .../staking-module-guard.spec.ts | 44 ++++++++++--------- src/keys-api/keys-api.service.ts | 2 +- src/staking-router/staking-router.service.ts | 14 +++--- src/staking-router/staking-router.spec.ts | 2 +- test/helpers/mockKeysApi.ts | 2 +- 8 files changed, 50 insertions(+), 51 deletions(-) diff --git a/src/guardian/guardian.service.spec.ts b/src/guardian/guardian.service.spec.ts index 5643928b..b4929147 100644 --- a/src/guardian/guardian.service.spec.ts +++ b/src/guardian/guardian.service.spec.ts @@ -165,7 +165,7 @@ describe('GuardianService', () => { it('should exit if the previous call is not completed', async () => { jest - .spyOn(stakingRouterService, 'getVettedAndUnusedKeys') + .spyOn(stakingRouterService, 'getStakingModulesData') .mockImplementation(async () => vettedKeysResponse); const getBlockGuardServiceMock = jest diff --git a/src/guardian/guardian.service.ts b/src/guardian/guardian.service.ts index 1f69979a..23f9640d 100644 --- a/src/guardian/guardian.service.ts +++ b/src/guardian/guardian.service.ts @@ -98,7 +98,7 @@ export class GuardianService implements OnModuleInit { try { const { blockHash, blockNumber, stakingModulesData } = - await this.stakingRouterService.getVettedAndUnusedKeys(); + await this.stakingRouterService.getStakingModulesData(); await this.repositoryService.initCachedContracts({ blockHash }); @@ -118,10 +118,10 @@ export class GuardianService implements OnModuleInit { return; } - const stakingModulesNumber = stakingModulesData.length; + const stakingModulesCount = stakingModulesData.length; this.logger.log('Staking modules loaded', { - modulesCount: stakingModulesNumber, + modulesCount: stakingModulesCount, }); await this.depositService.handleNewBlock(blockNumber); @@ -137,9 +137,8 @@ export class GuardianService implements OnModuleInit { blockHash: blockData.blockHash, }); - // TODO: check only if one of nonce changed const modulesIdWithDuplicateKeys: number[] = - this.stakingModuleGuardService.checkVettedKeysDuplicates( + this.stakingModuleGuardService.getModulesIdsWithDuplicatedVettedUnusedKeys( stakingModulesData, blockData, ); 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 3abcec5c..7cb29964 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -33,7 +33,7 @@ export class StakingModuleGuardService { /** * @returns List of staking modules id with duplicates */ - public checkVettedKeysDuplicates( + public getModulesIdsWithDuplicatedVettedUnusedKeys( stakingModulesData: StakingModuleData[], blockData: BlockData, ): number[] { @@ -95,20 +95,10 @@ export class StakingModuleGuardService { stakingModulesData: StakingModuleData[], modulesIdWithDuplicateKeys: number[], ): StakingModuleData[] { - // exclude from stakingModulesData stakingModulesWithDuplicates - let stakingModulesWithoutDuplicates: StakingModuleData[] = - stakingModulesData; - - if (modulesIdWithDuplicateKeys.length) { - // need to filter stakingModulesWithoutDuplicates - - stakingModulesWithoutDuplicates = stakingModulesWithoutDuplicates.filter( - ({ stakingModuleId }) => - !modulesIdWithDuplicateKeys.includes(stakingModuleId), - ); - } - - return stakingModulesWithoutDuplicates; + return stakingModulesData.filter( + ({ stakingModuleId }) => + !modulesIdWithDuplicateKeys.includes(stakingModuleId), + ); } /** @@ -273,12 +263,14 @@ export class StakingModuleGuardService { * Filter out the used keys, and since used keys cannot be deleted, * it is sufficient to check if the blockNumber in the new result is greater than the current blockNumber. */ + // getUsedLidoKeysForUnused ? private async getDuplicatedLidoUsedKeys( keys: string[], prevBlockNumber: number, ): Promise { - const { data, meta } = - await this.stakingRouterService.getKeysWithDuplicates(keys); + const { data, meta } = await this.stakingRouterService.findKeysEntires( + keys, + ); if (meta.elBlockSnapshot.blockNumber < prevBlockNumber) { this.logger.error( 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 3110f4ca..0b9a8377 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -580,10 +580,11 @@ describe('StakingModuleGuardService', () => { const blockData = { blockHash: 'some_hash' } as any; it('should found duplicated keys across two module', () => { - const result = stakingModuleGuardService.checkVettedKeysDuplicates( - vettedKeysDuplicatesAcrossModules, - blockData, - ); + const result = + stakingModuleGuardService.getModulesIdsWithDuplicatedVettedUnusedKeys( + vettedKeysDuplicatesAcrossModules, + blockData, + ); const addressesOfModulesWithDuplicateKeys = [100, 102]; @@ -596,10 +597,11 @@ describe('StakingModuleGuardService', () => { }); it('should found duplicated keys across one module', () => { - const result = stakingModuleGuardService.checkVettedKeysDuplicates( - vettedKeysDuplicatesAcrossOneModule, - blockData, - ); + const result = + stakingModuleGuardService.getModulesIdsWithDuplicatedVettedUnusedKeys( + vettedKeysDuplicatesAcrossOneModule, + blockData, + ); const addressesOfModulesWithDuplicateKeys = [100]; expect(result).toEqual( @@ -609,10 +611,11 @@ describe('StakingModuleGuardService', () => { }); it('should found duplicated keys across one module and few', () => { - const result = stakingModuleGuardService.checkVettedKeysDuplicates( - vettedKeysDuplicatesAcrossOneModuleAndFew, - blockData, - ); + const result = + stakingModuleGuardService.getModulesIdsWithDuplicatedVettedUnusedKeys( + vettedKeysDuplicatesAcrossOneModuleAndFew, + blockData, + ); const addressesOfModulesWithDuplicateKeys = [100, 102]; expect(result).toEqual( @@ -622,10 +625,11 @@ describe('StakingModuleGuardService', () => { }); it('should return empty list if duplicated keys were not found', () => { - const result = stakingModuleGuardService.checkVettedKeysDuplicates( - vettedKeysWithoutDuplicates, - blockData, - ); + const result = + stakingModuleGuardService.getModulesIdsWithDuplicatedVettedUnusedKeys( + vettedKeysWithoutDuplicates, + blockData, + ); const addressesOfModulesWithDuplicateKeys = []; @@ -643,7 +647,7 @@ describe('StakingModuleGuardService', () => { // function that return list from kapi that match keys in parameter const mockSendMessageFromGuardian = jest.spyOn( stakingRouterService, - 'getKeysWithDuplicates', + 'findKeysEntires', ); const result = @@ -671,7 +675,7 @@ describe('StakingModuleGuardService', () => { ]; // function that return list from kapi that match keys in parameter const mockSendMessageFromGuardian = jest - .spyOn(stakingRouterService, 'getKeysWithDuplicates') + .spyOn(stakingRouterService, 'findKeysEntires') .mockImplementation(async () => ({ data: [ { @@ -769,7 +773,7 @@ describe('StakingModuleGuardService', () => { ]; // function that return list from kapi that match keys in parameter const mockSendMessageFromGuardian = jest - .spyOn(stakingRouterService, 'getKeysWithDuplicates') + .spyOn(stakingRouterService, 'findKeysEntires') .mockImplementation(async () => ({ data: [ { @@ -831,7 +835,7 @@ describe('StakingModuleGuardService', () => { ]; // function that return list from kapi that match keys in parameter const mockSendMessageFromGuardian = jest - .spyOn(stakingRouterService, 'getKeysWithDuplicates') + .spyOn(stakingRouterService, 'findKeysEntires') .mockImplementation(async () => ({ data: [ { diff --git a/src/keys-api/keys-api.service.ts b/src/keys-api/keys-api.service.ts index 6cf28664..7363882b 100644 --- a/src/keys-api/keys-api.service.ts +++ b/src/keys-api/keys-api.service.ts @@ -66,7 +66,7 @@ export class KeysApiService { * @param The /v1/keys/find KAPI endpoint returns a key along with its duplicates * @returns */ - public async getKeysWithDuplicates(pubkeys: string[]) { + public async findKeysEntires(pubkeys: string[]) { const result = await this.fetch(`/v1/keys/find`, { method: 'POST', headers: { diff --git a/src/staking-router/staking-router.service.ts b/src/staking-router/staking-router.service.ts index 65c6e4c9..ff0bc5e6 100644 --- a/src/staking-router/staking-router.service.ts +++ b/src/staking-router/staking-router.service.ts @@ -19,10 +19,14 @@ export class StakingRouterService { /** * Return staking module data and block information */ - public async getVettedAndUnusedKeys() { + public async getStakingModulesData(): Promise<{ + stakingModulesData: StakingModuleData[]; + blockHash: string; + blockNumber: number; + }> { // TODO: add cache by modules nonce const { operatorsByModules, unusedKeys, blockHash, blockNumber } = - await this.getOperatorsAndKeysFromKAPI(); + await this.getOperatorsAndUnusedKeysFromKAPI(); // all staking modules list const stakingModulesData: StakingModuleData[] = operatorsByModules.data.map( ({ operators, module: stakingModule }) => { @@ -49,7 +53,7 @@ export class StakingRouterService { /** * Request grouped by modules operators and all staking modules keys with meta from KAPI */ - private async getOperatorsAndKeysFromKAPI() { + private async getOperatorsAndUnusedKeysFromKAPI() { const operatorsByModules = await this.keysApiService.getOperatorListWithModule(); const { blockHash: operatorsBlockHash, blockNumber: operatorsBlockNumber } = @@ -147,8 +151,8 @@ export class StakingRouterService { return unusedKeys.slice(0, numberOfVettedUnusedKeys); } - public async getKeysWithDuplicates(pubkeys: string[]) { - return await this.keysApiService.getKeysWithDuplicates(pubkeys); + public async findKeysEntires(pubkeys: string[]) { + return await this.keysApiService.findKeysEntires(pubkeys); } public async getStakingModules() { diff --git a/src/staking-router/staking-router.spec.ts b/src/staking-router/staking-router.spec.ts index 03b68e2f..2635fbc4 100644 --- a/src/staking-router/staking-router.spec.ts +++ b/src/staking-router/staking-router.spec.ts @@ -39,7 +39,7 @@ describe('StakingRouter', () => { keysAllStakingModules, ); - const result = await stakingRouterService.getVettedAndUnusedKeys(); + const result = await stakingRouterService.getStakingModulesData(); // Assertions expect(result).toEqual({ diff --git a/test/helpers/mockKeysApi.ts b/test/helpers/mockKeysApi.ts index 7eba73b9..579d9adf 100644 --- a/test/helpers/mockKeysApi.ts +++ b/test/helpers/mockKeysApi.ts @@ -71,7 +71,7 @@ export const mockKeysApi = ( })); jest - .spyOn(keysApiService, 'getKeysWithDuplicates') + .spyOn(keysApiService, 'findKeysEntires') .mockImplementation(async () => ({ data: mockedKeys, meta: { From 898593f11dc0c91302bee754dfd3c7a58244cc9d Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 19 Dec 2023 21:41:02 +0400 Subject: [PATCH 30/37] fix: refactoring --- src/contracts/deposit/deposit.service.ts | 3 - src/guardian/guardian.service.spec.ts | 4 +- src/guardian/guardian.service.ts | 2 - .../interfaces/staking-module.interface.ts | 2 +- .../staking-module-guard/keys.fixtures.ts | 24 ++--- .../staking-module-guard.service.ts | 6 +- .../staking-module-guard.spec.ts | 48 +++++----- src/staking-router/errors.ts | 10 +++ src/staking-router/staking-router.service.ts | 88 +++++-------------- src/staking-router/staking-router.spec.ts | 4 +- 10 files changed, 73 insertions(+), 118 deletions(-) create mode 100644 src/staking-router/errors.ts diff --git a/src/contracts/deposit/deposit.service.ts b/src/contracts/deposit/deposit.service.ts index 686d7215..b4103089 100644 --- a/src/contracts/deposit/deposit.service.ts +++ b/src/contracts/deposit/deposit.service.ts @@ -243,7 +243,6 @@ export class DepositService { * The last N blocks are not stored, in order to avoid storing reorganized blocks */ public async updateEventsCache(): Promise { - this.logger.log('try to update?'); const fetchTimeStart = performance.now(); const [currentBlock, initialCache] = await Promise.all([ @@ -271,8 +270,6 @@ export class DepositService { chunkToBlock, ); - console.log('chunk?', chunkEventGroup); - updatedCachedEvents.headers.endBlock = chunkEventGroup.endBlock; updatedCachedEvents.data = updatedCachedEvents.data.concat( chunkEventGroup.events, diff --git a/src/guardian/guardian.service.spec.ts b/src/guardian/guardian.service.spec.ts index b4929147..6e7f8fcb 100644 --- a/src/guardian/guardian.service.spec.ts +++ b/src/guardian/guardian.service.spec.ts @@ -65,7 +65,7 @@ const vettedKeysResponse = { '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', ], - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', depositSignature: @@ -94,7 +94,7 @@ const vettedKeysResponse = { unusedKeys: [ '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', ], - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', depositSignature: diff --git a/src/guardian/guardian.service.ts b/src/guardian/guardian.service.ts index abf11d21..6dcc9277 100644 --- a/src/guardian/guardian.service.ts +++ b/src/guardian/guardian.service.ts @@ -10,7 +10,6 @@ import { CronJob } from 'cron'; import { DepositService } from 'contracts/deposit'; import { SecurityService } from 'contracts/security'; import { RepositoryService } from 'contracts/repository'; -import { ProviderService } from 'provider'; import { GUARDIAN_DEPOSIT_JOB_DURATION, GUARDIAN_DEPOSIT_JOB_NAME, @@ -37,7 +36,6 @@ export class GuardianService implements OnModuleInit { private depositService: DepositService, private securityService: SecurityService, - private providerService: ProviderService, private stakingRouterService: StakingRouterService, diff --git a/src/guardian/interfaces/staking-module.interface.ts b/src/guardian/interfaces/staking-module.interface.ts index 593a2c83..714a8b38 100644 --- a/src/guardian/interfaces/staking-module.interface.ts +++ b/src/guardian/interfaces/staking-module.interface.ts @@ -3,7 +3,7 @@ import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; export interface StakingModuleData { blockHash: string; unusedKeys: string[]; - vettedKeys: RegistryKey[]; + vettedUnusedKeys: RegistryKey[]; nonce: number; stakingModuleId: number; } diff --git a/src/guardian/staking-module-guard/keys.fixtures.ts b/src/guardian/staking-module-guard/keys.fixtures.ts index b1756895..e69cd5bc 100644 --- a/src/guardian/staking-module-guard/keys.fixtures.ts +++ b/src/guardian/staking-module-guard/keys.fixtures.ts @@ -1,7 +1,7 @@ export const vettedKeysDuplicatesAcrossModules: any = [ { stakingModuleId: 100, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', depositSignature: @@ -24,7 +24,7 @@ export const vettedKeysDuplicatesAcrossModules: any = [ }, { stakingModuleId: 102, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', depositSignature: @@ -47,7 +47,7 @@ export const vettedKeysDuplicatesAcrossModules: any = [ }, { stakingModuleId: 103, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', depositSignature: @@ -64,7 +64,7 @@ export const vettedKeysDuplicatesAcrossModules: any = [ export const vettedKeysDuplicatesAcrossOneModule: any = [ { stakingModuleId: 100, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', depositSignature: @@ -96,7 +96,7 @@ export const vettedKeysDuplicatesAcrossOneModule: any = [ }, { stakingModuleId: 102, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', depositSignature: @@ -111,7 +111,7 @@ export const vettedKeysDuplicatesAcrossOneModule: any = [ { stakingModuleId: 103, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', depositSignature: @@ -128,7 +128,7 @@ export const vettedKeysDuplicatesAcrossOneModule: any = [ export const vettedKeysDuplicatesAcrossOneModuleAndFew: any = [ { stakingModuleId: 100, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', depositSignature: @@ -151,7 +151,7 @@ export const vettedKeysDuplicatesAcrossOneModuleAndFew: any = [ }, { stakingModuleId: 102, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', depositSignature: @@ -183,7 +183,7 @@ export const vettedKeysDuplicatesAcrossOneModuleAndFew: any = [ }, { stakingModuleId: 103, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', depositSignature: @@ -200,7 +200,7 @@ export const vettedKeysDuplicatesAcrossOneModuleAndFew: any = [ export const vettedKeysWithoutDuplicates: any = [ { stakingModuleId: 100, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', depositSignature: @@ -224,7 +224,7 @@ export const vettedKeysWithoutDuplicates: any = [ { stakingModuleId: 102, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', depositSignature: @@ -239,7 +239,7 @@ export const vettedKeysWithoutDuplicates: any = [ { stakingModuleId: 103, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', depositSignature: 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 09e7f120..80644ff0 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -44,9 +44,9 @@ export class StakingModuleGuardService { const modulesWithDuplicatedKeysSet = new Set(); const duplicatedKeys = new Map>(); - stakingModulesData.forEach(({ vettedKeys, stakingModuleId }) => { + stakingModulesData.forEach(({ vettedUnusedKeys, stakingModuleId }) => { // check module keys on duplicates across all modules - vettedKeys.forEach((key) => { + vettedUnusedKeys.forEach((key) => { const stakingModules = keyMap.get(key.key); if (!stakingModules) { @@ -237,8 +237,6 @@ export class StakingModuleGuardService { intersectionsWithLidoWC: VerifiedDepositEvent[], blockData: BlockData, ) { - // should not check invalid - // TODO: fix in prev PR const validIntersections = intersectionsWithLidoWC.filter( ({ valid }) => valid, ); 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 f926a007..be7e0c8f 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -91,7 +91,7 @@ describe('StakingModuleGuardService', () => { }; const blockData = { unusedKeys, depositedEvents } as any; const matched = stakingModuleGuardService.getKeysIntersections( - { ...stakingModuleData, unusedKeys, vettedKeys: [] }, + { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, blockData, ); @@ -108,7 +108,7 @@ describe('StakingModuleGuardService', () => { }; const blockData = { unusedKeys, depositedEvents } as any; const matched = stakingModuleGuardService.getKeysIntersections( - { ...stakingModuleData, unusedKeys, vettedKeys: [] }, + { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, blockData, ); @@ -124,7 +124,7 @@ describe('StakingModuleGuardService', () => { }; const blockData = { unusedKeys, depositedEvents } as any; const matched = stakingModuleGuardService.getKeysIntersections( - { ...stakingModuleData, unusedKeys, vettedKeys: [] }, + { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, blockData, ); @@ -195,14 +195,14 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => false); await stakingModuleGuardService.checkKeysIntersections( - { ...stakingModuleData, unusedKeys, vettedKeys: [] }, + { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, blockData, ); expect(mockHandleCorrectKeys).not.toBeCalled(); expect(mockHandleKeysIntersections).toBeCalledTimes(1); expect(mockHandleKeysIntersections).toBeCalledWith( - { ...stakingModuleData, unusedKeys, vettedKeys: [] }, + { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, blockData, ); expect(mockGetWithdrawalCredentials).toBeCalledTimes(1); @@ -227,14 +227,14 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => false); await stakingModuleGuardService.checkKeysIntersections( - { ...stakingModuleData, unusedKeys, vettedKeys: [] }, + { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, blockData, ); expect(mockHandleKeysIntersections).not.toBeCalled(); expect(mockHandleCorrectKeys).toBeCalledTimes(1); expect(mockHandleCorrectKeys).toBeCalledWith( - { ...stakingModuleData, unusedKeys, vettedKeys: [] }, + { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, blockData, ); expect(mockSecurityContractIsDepositsPaused).toBeCalledTimes(1); @@ -265,11 +265,11 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => signature); await stakingModuleGuardService.handleCorrectKeys( - { ...stakingModuleData, unusedKeys: [], vettedKeys: [] }, + { ...stakingModuleData, unusedKeys: [], vettedUnusedKeys: [] }, blockData, ); await stakingModuleGuardService.handleCorrectKeys( - { ...stakingModuleData, unusedKeys: [], vettedKeys: [] }, + { ...stakingModuleData, unusedKeys: [], vettedUnusedKeys: [] }, blockData, ); @@ -292,7 +292,7 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => signature); await stakingModuleGuardService.handleCorrectKeys( - { ...stakingModuleData, unusedKeys: [], vettedKeys: [] }, + { ...stakingModuleData, unusedKeys: [], vettedUnusedKeys: [] }, blockData, ); @@ -372,7 +372,7 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => undefined); await stakingModuleGuardService.handleKeysIntersections( - { ...stakingModuleData, unusedKeys: [], vettedKeys: [] }, + { ...stakingModuleData, unusedKeys: [], vettedUnusedKeys: [] }, blockData, ); @@ -394,7 +394,7 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => undefined); await stakingModuleGuardService.handleKeysIntersections( - { ...stakingModuleData, unusedKeys: [], vettedKeys: [] }, + { ...stakingModuleData, unusedKeys: [], vettedUnusedKeys: [] }, blockData, ); @@ -457,21 +457,21 @@ describe('StakingModuleGuardService', () => { { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 1, }, { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 2, }, { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 3, }, @@ -483,14 +483,14 @@ describe('StakingModuleGuardService', () => { { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 1, }, { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 3, }, @@ -511,21 +511,21 @@ describe('StakingModuleGuardService', () => { { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 1, }, { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 2, }, { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 3, }, @@ -546,21 +546,21 @@ describe('StakingModuleGuardService', () => { { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 1, }, { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 2, }, { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 3, }, @@ -576,7 +576,7 @@ describe('StakingModuleGuardService', () => { }); }); - describe('checkVettedKeysDuplicates', () => { + describe('getModulesIdsWithDuplicatedVettedUnusedKeys', () => { const blockData = { blockHash: 'some_hash' } as any; it('should found duplicated keys across two module', () => { diff --git a/src/staking-router/errors.ts b/src/staking-router/errors.ts new file mode 100644 index 00000000..491bab57 --- /dev/null +++ b/src/staking-router/errors.ts @@ -0,0 +1,10 @@ +export class InconsistentBlockhashError extends Error { + constructor( + message = 'Blockhash of the received keys does not match the blockhash of operators', + ) { + super(message); + this.name = 'InconsistentBlockhashError'; + + Object.setPrototypeOf(this, InconsistentBlockhashError.prototype); + } +} diff --git a/src/staking-router/staking-router.service.ts b/src/staking-router/staking-router.service.ts index ff0bc5e6..1c1394b0 100644 --- a/src/staking-router/staking-router.service.ts +++ b/src/staking-router/staking-router.service.ts @@ -6,6 +6,7 @@ import { SRModuleKeysResponse, SRModule } from 'keys-api/interfaces'; import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; import { StakingModuleData } from 'guardian'; import { RegistryOperator } from 'keys-api/interfaces/RegistryOperator'; +import { InconsistentBlockhashError } from './errors'; @Injectable() export class StakingRouterService { @@ -24,21 +25,22 @@ export class StakingRouterService { blockHash: string; blockNumber: number; }> { - // TODO: add cache by modules nonce + // get list of all unused keys and operators const { operatorsByModules, unusedKeys, blockHash, blockNumber } = - await this.getOperatorsAndUnusedKeysFromKAPI(); - // all staking modules list + await this.getOperatorsAndUnusedKeys(); + + // iterate by modules and filter const stakingModulesData: StakingModuleData[] = operatorsByModules.data.map( ({ operators, module: stakingModule }) => { const { moduleUnusedKeys, moduleVettedKeys } = - this.getModuleOperatorsVettedKeys(operators, unusedKeys.data); + this.getModuleUnusedAndVettedUnusedKeys(operators, unusedKeys.data); return { unusedKeys: moduleUnusedKeys.map((srKey) => srKey.key), nonce: stakingModule.nonce, stakingModuleId: stakingModule.id, blockHash, - vettedKeys: moduleVettedKeys, + vettedUnusedKeys: moduleVettedKeys, }; }, ); @@ -53,7 +55,7 @@ export class StakingRouterService { /** * Request grouped by modules operators and all staking modules keys with meta from KAPI */ - private async getOperatorsAndUnusedKeysFromKAPI() { + private async getOperatorsAndUnusedKeys() { const operatorsByModules = await this.keysApiService.getOperatorListWithModule(); const { blockHash: operatorsBlockHash, blockNumber: operatorsBlockNumber } = @@ -85,13 +87,11 @@ export class StakingRouterService { }, ); - throw new Error( - 'Blockhash of the received keys does not match the blockhash of operators', - ); + throw new InconsistentBlockhashError(); } } - private getModuleOperatorsVettedKeys( + private getModuleUnusedAndVettedUnusedKeys( moduleOperators: RegistryOperator[], allModulesUnusedKeys: RegistryKey[], ) { @@ -100,18 +100,19 @@ export class StakingRouterService { const moduleVettedKeys: RegistryKey[] = []; moduleOperators.forEach((operator) => { - const operatorKeys = this.getSortedOperatorKeys( + // all operator unused keys + const operatorKeys = this.filterAndSortOperatorKeys( allModulesUnusedKeys, operator, ); moduleUnusedKeys.push(...operatorKeys); - const operatorVettedKeys = this.getOperatorVettedKeys( + const operatorVettedUnusedKeys = this.getOperatorVettedUnusedKeys( operatorKeys, operator, ); - moduleVettedKeys.push(...operatorVettedKeys); + moduleVettedKeys.push(...operatorVettedUnusedKeys); }); return { @@ -121,28 +122,28 @@ export class StakingRouterService { } /*** - * @param unusedKeys - keys list of all staking modules + * @param keys - keys list of all staking modules * @param operator - staking module operator * @returns sorted operator's keys list */ - private getSortedOperatorKeys( + private filterAndSortOperatorKeys( keys: RegistryKey[], operator: RegistryOperator, ): RegistryKey[] { - const operatorSortedKeys = keys + const operatorKeys = keys .filter( (key) => key.moduleAddress === operator.moduleAddress && key.operatorIndex === operator.index, ) .sort((a, b) => a.index - b.index); - return operatorSortedKeys; + return operatorKeys; } /*** - * Got sorted unused keys and return vetted keys + * Got sorted by index unused keys and return vetted unused keys */ - private getOperatorVettedKeys( + private getOperatorVettedUnusedKeys( unusedKeys: RegistryKey[], operator: RegistryOperator, ): RegistryKey[] { @@ -154,53 +155,4 @@ export class StakingRouterService { public async findKeysEntires(pubkeys: string[]) { return await this.keysApiService.findKeysEntires(pubkeys); } - - public async getStakingModules() { - return await this.keysApiService.getModulesList(); - } - - public async getStakingModuleUnusedKeys( - blockHash: string, - { id, nonce }: SRModule, - ) { - if (!this.isNeedToUpdateState(id, nonce)) - return this.getStakingRouterKeysCache(id); - - const srResponse = await this.keysApiService.getUnusedModuleKeys(id); - const srModuleBlockHash = srResponse.meta.elBlockSnapshot.blockHash; - - if (srModuleBlockHash !== blockHash) { - this.logger.log('Blockhash of the received keys', { - srModuleBlockHash, - blockHash, - }); - - throw Error( - 'Block hash of the received keys does not match the current block hash', - ); - } - - this.setStakingRouterCache(id, srResponse); - - return srResponse; - } - - protected getStakingRouterKeysCache(stakingModuleId: number) { - return this.stakingRouterCache[stakingModuleId]; - } - - protected setStakingRouterCache( - stakingModuleId: number, - srResponse: SRModuleKeysResponse, - ) { - this.stakingRouterCache[stakingModuleId] = srResponse; - } - - protected isNeedToUpdateState(stakingModuleId: number, nextNonce: number) { - const cache = this.getStakingRouterKeysCache(stakingModuleId); - if (!cache) return true; - - const prevNonce = cache.data.module.nonce; - return prevNonce !== nextNonce; - } } diff --git a/src/staking-router/staking-router.spec.ts b/src/staking-router/staking-router.spec.ts index 2635fbc4..a038c804 100644 --- a/src/staking-router/staking-router.spec.ts +++ b/src/staking-router/staking-router.spec.ts @@ -53,7 +53,7 @@ describe('StakingRouter', () => { '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', '0x8d12ec44816f108df84ef9b03e423a6d8fb0f0a1823c871b123ff41f893a7b372eb038a1ed1ff15083e07a777a5cba50', ], - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', depositSignature: @@ -75,7 +75,7 @@ describe('StakingRouter', () => { '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', ], - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', depositSignature: From 64b82e598face2c3b9177a29d7d3afb97e094c31 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 19 Dec 2023 17:12:52 +0400 Subject: [PATCH 31/37] fix: simple refactoring --- src/guardian/guardian.service.spec.ts | 2 +- src/guardian/guardian.service.ts | 8 ++-- .../staking-module-guard.service.ts | 26 ++++------- .../staking-module-guard.spec.ts | 44 ++++++++++--------- src/keys-api/keys-api.service.ts | 2 +- src/staking-router/staking-router.service.ts | 14 +++--- src/staking-router/staking-router.spec.ts | 2 +- test/helpers/mockKeysApi.ts | 2 +- 8 files changed, 50 insertions(+), 50 deletions(-) diff --git a/src/guardian/guardian.service.spec.ts b/src/guardian/guardian.service.spec.ts index 4d4b2abc..94ad7d5f 100644 --- a/src/guardian/guardian.service.spec.ts +++ b/src/guardian/guardian.service.spec.ts @@ -164,7 +164,7 @@ describe('GuardianService', () => { it('should exit if the previous call is not completed', async () => { jest - .spyOn(stakingRouterService, 'getVettedAndUnusedKeys') + .spyOn(stakingRouterService, 'getStakingModulesData') .mockImplementation(async () => vettedKeysResponse); const getBlockGuardServiceMock = jest diff --git a/src/guardian/guardian.service.ts b/src/guardian/guardian.service.ts index 893c3d77..5eb301a1 100644 --- a/src/guardian/guardian.service.ts +++ b/src/guardian/guardian.service.ts @@ -95,7 +95,7 @@ export class GuardianService implements OnModuleInit { try { const { blockHash, blockNumber, stakingModulesData } = - await this.stakingRouterService.getVettedAndUnusedKeys(); + await this.stakingRouterService.getStakingModulesData(); await this.repositoryService.initCachedContracts({ blockHash }); @@ -115,10 +115,10 @@ export class GuardianService implements OnModuleInit { return; } - const stakingModulesNumber = stakingModulesData.length; + const stakingModulesCount = stakingModulesData.length; this.logger.log('Staking modules loaded', { - modulesCount: stakingModulesNumber, + modulesCount: stakingModulesCount, }); await this.depositService.handleNewBlock(blockNumber); @@ -136,7 +136,7 @@ export class GuardianService implements OnModuleInit { }); const modulesIdWithDuplicateKeys: number[] = - this.stakingModuleGuardService.checkVettedKeysDuplicates( + this.stakingModuleGuardService.getModulesIdsWithDuplicatedVettedUnusedKeys( stakingModulesData, blockData, ); 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 131b6532..2784d72a 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -33,7 +33,7 @@ export class StakingModuleGuardService { /** * @returns List of staking modules id with duplicates */ - public checkVettedKeysDuplicates( + public getModulesIdsWithDuplicatedVettedUnusedKeys( stakingModulesData: StakingModuleData[], blockData: BlockData, ): number[] { @@ -99,20 +99,10 @@ export class StakingModuleGuardService { stakingModulesData: StakingModuleData[], modulesIdWithDuplicateKeys: number[], ): StakingModuleData[] { - // exclude from stakingModulesData stakingModulesWithDuplicates - let stakingModulesWithoutDuplicates: StakingModuleData[] = - stakingModulesData; - - if (modulesIdWithDuplicateKeys.length) { - // need to filter stakingModulesWithoutDuplicates - - stakingModulesWithoutDuplicates = stakingModulesWithoutDuplicates.filter( - ({ stakingModuleId }) => - !modulesIdWithDuplicateKeys.includes(stakingModuleId), - ); - } - - return stakingModulesWithoutDuplicates; + return stakingModulesData.filter( + ({ stakingModuleId }) => + !modulesIdWithDuplicateKeys.includes(stakingModuleId), + ); } /** @@ -273,12 +263,14 @@ export class StakingModuleGuardService { * Filter out the used keys, and since used keys cannot be deleted, * it is sufficient to check if the blockNumber in the new result is greater than the current blockNumber. */ + // getUsedLidoKeysForUnused ? private async getDuplicatedLidoUsedKeys( keys: string[], prevBlockNumber: number, ): Promise { - const { data, meta } = - await this.stakingRouterService.getKeysWithDuplicates(keys); + const { data, meta } = await this.stakingRouterService.findKeysEntires( + keys, + ); if (meta.elBlockSnapshot.blockNumber < prevBlockNumber) { const errorMsg = 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 4eacdf5f..53d7f6e8 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -569,10 +569,11 @@ describe('StakingModuleGuardService', () => { const blockData = { blockHash: 'some_hash' } as any; it('should found duplicated keys across two module', () => { - const result = stakingModuleGuardService.checkVettedKeysDuplicates( - vettedKeysDuplicatesAcrossModules, - blockData, - ); + const result = + stakingModuleGuardService.getModulesIdsWithDuplicatedVettedUnusedKeys( + vettedKeysDuplicatesAcrossModules, + blockData, + ); const addressesOfModulesWithDuplicateKeys = [100, 102]; @@ -585,10 +586,11 @@ describe('StakingModuleGuardService', () => { }); it('should found duplicated keys across one module', () => { - const result = stakingModuleGuardService.checkVettedKeysDuplicates( - vettedKeysDuplicatesAcrossOneModule, - blockData, - ); + const result = + stakingModuleGuardService.getModulesIdsWithDuplicatedVettedUnusedKeys( + vettedKeysDuplicatesAcrossOneModule, + blockData, + ); const addressesOfModulesWithDuplicateKeys = [100]; expect(result).toEqual( @@ -598,10 +600,11 @@ describe('StakingModuleGuardService', () => { }); it('should found duplicated keys across one module and few', () => { - const result = stakingModuleGuardService.checkVettedKeysDuplicates( - vettedKeysDuplicatesAcrossOneModuleAndFew, - blockData, - ); + const result = + stakingModuleGuardService.getModulesIdsWithDuplicatedVettedUnusedKeys( + vettedKeysDuplicatesAcrossOneModuleAndFew, + blockData, + ); const addressesOfModulesWithDuplicateKeys = [100, 102]; expect(result).toEqual( @@ -611,10 +614,11 @@ describe('StakingModuleGuardService', () => { }); it('should return empty list if duplicated keys were not found', () => { - const result = stakingModuleGuardService.checkVettedKeysDuplicates( - vettedKeysWithoutDuplicates, - blockData, - ); + const result = + stakingModuleGuardService.getModulesIdsWithDuplicatedVettedUnusedKeys( + vettedKeysWithoutDuplicates, + blockData, + ); const addressesOfModulesWithDuplicateKeys = []; @@ -632,7 +636,7 @@ describe('StakingModuleGuardService', () => { // function that return list from kapi that match keys in parameter const mockSendMessageFromGuardian = jest.spyOn( stakingRouterService, - 'getKeysWithDuplicates', + 'findKeysEntires', ); const result = @@ -660,7 +664,7 @@ describe('StakingModuleGuardService', () => { ]; // function that return list from kapi that match keys in parameter const mockSendMessageFromGuardian = jest - .spyOn(stakingRouterService, 'getKeysWithDuplicates') + .spyOn(stakingRouterService, 'findKeysEntires') .mockImplementation(async () => ({ data: [ { @@ -758,7 +762,7 @@ describe('StakingModuleGuardService', () => { ]; // function that return list from kapi that match keys in parameter const mockSendMessageFromGuardian = jest - .spyOn(stakingRouterService, 'getKeysWithDuplicates') + .spyOn(stakingRouterService, 'findKeysEntires') .mockImplementation(async () => ({ data: [ { @@ -820,7 +824,7 @@ describe('StakingModuleGuardService', () => { ]; // function that return list from kapi that match keys in parameter const mockSendMessageFromGuardian = jest - .spyOn(stakingRouterService, 'getKeysWithDuplicates') + .spyOn(stakingRouterService, 'findKeysEntires') .mockImplementation(async () => ({ data: [ { diff --git a/src/keys-api/keys-api.service.ts b/src/keys-api/keys-api.service.ts index 3cbb990d..e61bc15d 100644 --- a/src/keys-api/keys-api.service.ts +++ b/src/keys-api/keys-api.service.ts @@ -66,7 +66,7 @@ export class KeysApiService { * @param The /v1/keys/find API endpoint returns keys along with their duplicates * @returns */ - public async getKeysWithDuplicates(pubkeys: string[]) { + public async findKeysEntires(pubkeys: string[]) { const result = await this.fetch(`/v1/keys/find`, { method: 'POST', headers: { diff --git a/src/staking-router/staking-router.service.ts b/src/staking-router/staking-router.service.ts index 65c6e4c9..ff0bc5e6 100644 --- a/src/staking-router/staking-router.service.ts +++ b/src/staking-router/staking-router.service.ts @@ -19,10 +19,14 @@ export class StakingRouterService { /** * Return staking module data and block information */ - public async getVettedAndUnusedKeys() { + public async getStakingModulesData(): Promise<{ + stakingModulesData: StakingModuleData[]; + blockHash: string; + blockNumber: number; + }> { // TODO: add cache by modules nonce const { operatorsByModules, unusedKeys, blockHash, blockNumber } = - await this.getOperatorsAndKeysFromKAPI(); + await this.getOperatorsAndUnusedKeysFromKAPI(); // all staking modules list const stakingModulesData: StakingModuleData[] = operatorsByModules.data.map( ({ operators, module: stakingModule }) => { @@ -49,7 +53,7 @@ export class StakingRouterService { /** * Request grouped by modules operators and all staking modules keys with meta from KAPI */ - private async getOperatorsAndKeysFromKAPI() { + private async getOperatorsAndUnusedKeysFromKAPI() { const operatorsByModules = await this.keysApiService.getOperatorListWithModule(); const { blockHash: operatorsBlockHash, blockNumber: operatorsBlockNumber } = @@ -147,8 +151,8 @@ export class StakingRouterService { return unusedKeys.slice(0, numberOfVettedUnusedKeys); } - public async getKeysWithDuplicates(pubkeys: string[]) { - return await this.keysApiService.getKeysWithDuplicates(pubkeys); + public async findKeysEntires(pubkeys: string[]) { + return await this.keysApiService.findKeysEntires(pubkeys); } public async getStakingModules() { diff --git a/src/staking-router/staking-router.spec.ts b/src/staking-router/staking-router.spec.ts index 03b68e2f..2635fbc4 100644 --- a/src/staking-router/staking-router.spec.ts +++ b/src/staking-router/staking-router.spec.ts @@ -39,7 +39,7 @@ describe('StakingRouter', () => { keysAllStakingModules, ); - const result = await stakingRouterService.getVettedAndUnusedKeys(); + const result = await stakingRouterService.getStakingModulesData(); // Assertions expect(result).toEqual({ diff --git a/test/helpers/mockKeysApi.ts b/test/helpers/mockKeysApi.ts index 84d726cd..f4eb0245 100644 --- a/test/helpers/mockKeysApi.ts +++ b/test/helpers/mockKeysApi.ts @@ -76,7 +76,7 @@ export const mockedKeysWithDuplicates = ( mockedMeta: ELBlockSnapshot, ) => { jest - .spyOn(keysApiService, 'getKeysWithDuplicates') + .spyOn(keysApiService, 'findKeysEntires') .mockImplementation(async () => ({ data: mockedKeys, meta: { From b283465e258dd4deb50c7d330fc199447191e12b Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Tue, 19 Dec 2023 21:41:02 +0400 Subject: [PATCH 32/37] fix: refactoring --- src/guardian/guardian.service.spec.ts | 4 +- .../interfaces/staking-module.interface.ts | 2 +- .../staking-module-guard/keys.fixtures.ts | 24 ++--- .../staking-module-guard.service.ts | 58 ++++------- .../staking-module-guard.spec.ts | 97 +++++++++---------- src/staking-router/errors.ts | 10 ++ src/staking-router/staking-router.service.ts | 88 ++++------------- src/staking-router/staking-router.spec.ts | 4 +- 8 files changed, 115 insertions(+), 172 deletions(-) create mode 100644 src/staking-router/errors.ts diff --git a/src/guardian/guardian.service.spec.ts b/src/guardian/guardian.service.spec.ts index 94ad7d5f..41f8defa 100644 --- a/src/guardian/guardian.service.spec.ts +++ b/src/guardian/guardian.service.spec.ts @@ -65,7 +65,7 @@ const vettedKeysResponse = { '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', ], - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', depositSignature: @@ -94,7 +94,7 @@ const vettedKeysResponse = { unusedKeys: [ '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', ], - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', depositSignature: diff --git a/src/guardian/interfaces/staking-module.interface.ts b/src/guardian/interfaces/staking-module.interface.ts index 593a2c83..714a8b38 100644 --- a/src/guardian/interfaces/staking-module.interface.ts +++ b/src/guardian/interfaces/staking-module.interface.ts @@ -3,7 +3,7 @@ import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; export interface StakingModuleData { blockHash: string; unusedKeys: string[]; - vettedKeys: RegistryKey[]; + vettedUnusedKeys: RegistryKey[]; nonce: number; stakingModuleId: number; } diff --git a/src/guardian/staking-module-guard/keys.fixtures.ts b/src/guardian/staking-module-guard/keys.fixtures.ts index b1756895..e69cd5bc 100644 --- a/src/guardian/staking-module-guard/keys.fixtures.ts +++ b/src/guardian/staking-module-guard/keys.fixtures.ts @@ -1,7 +1,7 @@ export const vettedKeysDuplicatesAcrossModules: any = [ { stakingModuleId: 100, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', depositSignature: @@ -24,7 +24,7 @@ export const vettedKeysDuplicatesAcrossModules: any = [ }, { stakingModuleId: 102, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', depositSignature: @@ -47,7 +47,7 @@ export const vettedKeysDuplicatesAcrossModules: any = [ }, { stakingModuleId: 103, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', depositSignature: @@ -64,7 +64,7 @@ export const vettedKeysDuplicatesAcrossModules: any = [ export const vettedKeysDuplicatesAcrossOneModule: any = [ { stakingModuleId: 100, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', depositSignature: @@ -96,7 +96,7 @@ export const vettedKeysDuplicatesAcrossOneModule: any = [ }, { stakingModuleId: 102, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', depositSignature: @@ -111,7 +111,7 @@ export const vettedKeysDuplicatesAcrossOneModule: any = [ { stakingModuleId: 103, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', depositSignature: @@ -128,7 +128,7 @@ export const vettedKeysDuplicatesAcrossOneModule: any = [ export const vettedKeysDuplicatesAcrossOneModuleAndFew: any = [ { stakingModuleId: 100, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', depositSignature: @@ -151,7 +151,7 @@ export const vettedKeysDuplicatesAcrossOneModuleAndFew: any = [ }, { stakingModuleId: 102, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', depositSignature: @@ -183,7 +183,7 @@ export const vettedKeysDuplicatesAcrossOneModuleAndFew: any = [ }, { stakingModuleId: 103, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', depositSignature: @@ -200,7 +200,7 @@ export const vettedKeysDuplicatesAcrossOneModuleAndFew: any = [ export const vettedKeysWithoutDuplicates: any = [ { stakingModuleId: 100, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', depositSignature: @@ -224,7 +224,7 @@ export const vettedKeysWithoutDuplicates: any = [ { stakingModuleId: 102, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', depositSignature: @@ -239,7 +239,7 @@ export const vettedKeysWithoutDuplicates: any = [ { stakingModuleId: 103, - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', depositSignature: 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 2784d72a..0dd804cb 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -44,9 +44,9 @@ export class StakingModuleGuardService { const modulesWithDuplicatedKeysSet = new Set(); const duplicatedKeys = new Map>(); - stakingModulesData.forEach(({ vettedKeys, stakingModuleId }) => { + stakingModulesData.forEach(({ vettedUnusedKeys, stakingModuleId }) => { // check module keys on duplicates across all modules - vettedKeys.forEach((key) => { + vettedUnusedKeys.forEach((key) => { const stakingModules = keyMap.get(key.key); if (!stakingModules) { @@ -154,7 +154,7 @@ export class StakingModuleGuardService { await this.handleKeysIntersections(stakingModuleData, blockData); } else { // it could throw error if kapi returned old data - const usedKeys = await this.getIntersectionBetweenUsedAndUnusedKeys( + const usedKeys = await this.findAlreadyDepositedKeys( validIntersections, blockData, ); @@ -203,8 +203,7 @@ export class StakingModuleGuardService { public excludeInvalidDeposits(intersections: VerifiedDepositEvent[]) { // Exclude deposits with invalid signature over the deposit data - const validIntersections = intersections.filter(({ valid }) => valid); - return validIntersections; + return intersections.filter(({ valid }) => valid); } /** @@ -216,16 +215,10 @@ export class StakingModuleGuardService { blockData: BlockData, validIntersections: VerifiedDepositEvent[], ): Promise { - if (!validIntersections.length) { - return []; - } - // Exclude deposits with Lido withdrawal credentials - const attackIntersections = validIntersections.filter( + return validIntersections.filter( (deposit) => deposit.wc !== blockData.lidoWC, ); - - return attackIntersections; } /** @@ -233,14 +226,14 @@ export class StakingModuleGuardService { * with Lido withdrawal credentials, we need to determine whether this deposit was made by Lido. * If it was indeed made by Lido, we set a metric and skip sending deposit messages in the queue for this iteration. */ - public async getIntersectionBetweenUsedAndUnusedKeys( + public async findAlreadyDepositedKeys( intersectionsWithLidoWC: VerifiedDepositEvent[], blockData: BlockData, ) { const depositedPubkeys = intersectionsWithLidoWC.map( (deposit) => deposit.pubkey, ); - + // if depositedPubkeys == [], /find will return validation error if (!depositedPubkeys.length) { return []; } @@ -249,41 +242,32 @@ export class StakingModuleGuardService { 'Found intersections with lido credentials, need to check used duplicated keys', ); - const alreadyDepositedKeys = await this.getDuplicatedLidoUsedKeys( + const { data, meta } = await this.stakingRouterService.findKeysEntires( depositedPubkeys, + ); + + this.checkCurrentBlockOlderThanPrev( + meta.elBlockSnapshot.blockNumber, blockData.blockNumber, ); - return alreadyDepositedKeys; + return data.filter((key) => key.used); } - /** - * Upon identifying the intersection of keys deposited and unused with Lido withdrawal credentials, - * use the KAPI /v1/keys/find endpoint to locate all keys with duplicates. - * Filter out the used keys, and since used keys cannot be deleted, - * it is sufficient to check if the blockNumber in the new result is greater than the current blockNumber. - */ - // getUsedLidoKeysForUnused ? - private async getDuplicatedLidoUsedKeys( - keys: string[], + private async checkCurrentBlockOlderThanPrev( + currBlockNumber: number, prevBlockNumber: number, - ): Promise { - const { data, meta } = await this.stakingRouterService.findKeysEntires( - keys, - ); - - if (meta.elBlockSnapshot.blockNumber < prevBlockNumber) { + ) { + if (currBlockNumber < prevBlockNumber) { const errorMsg = 'BlockNumber of the current response older than previous response from KAPI'; this.logger.error(errorMsg, { previous: prevBlockNumber, - current: meta.elBlockSnapshot.blockNumber, + current: currBlockNumber, }); throw Error(errorMsg); } - const usedKeys = data.filter((key) => key.used); - - return usedKeys; + return; } /** @@ -418,11 +402,11 @@ export class StakingModuleGuardService { blockData: BlockData, ): Promise<{ key: string; depositSignature: string }[]> { this.logger.log('Start keys validation', { - keysCount: stakingModuleData.vettedKeys.length, + keysCount: stakingModuleData.vettedUnusedKeys.length, }); const validationTimeStart = performance.now(); const invalidKeysList = await this.keysValidationService.findInvalidKeys( - stakingModuleData.vettedKeys, + stakingModuleData.vettedUnusedKeys, blockData.lidoWC, ); const validationTimeEnd = performance.now(); 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 53d7f6e8..fd49537b 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -90,7 +90,7 @@ describe('StakingModuleGuardService', () => { }; const blockData = { unusedKeys, depositedEvents } as any; const matched = stakingModuleGuardService.getKeysIntersections( - { ...stakingModuleData, unusedKeys, vettedKeys: [] }, + { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, blockData, ); @@ -107,7 +107,7 @@ describe('StakingModuleGuardService', () => { }; const blockData = { unusedKeys, depositedEvents } as any; const matched = stakingModuleGuardService.getKeysIntersections( - { ...stakingModuleData, unusedKeys, vettedKeys: [] }, + { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, blockData, ); @@ -123,7 +123,7 @@ describe('StakingModuleGuardService', () => { }; const blockData = { unusedKeys, depositedEvents } as any; const matched = stakingModuleGuardService.getKeysIntersections( - { ...stakingModuleData, unusedKeys, vettedKeys: [] }, + { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, blockData, ); @@ -191,14 +191,14 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => false); await stakingModuleGuardService.checkKeysIntersections( - { ...stakingModuleData, unusedKeys, vettedKeys: [] }, + { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, blockData, ); expect(mockHandleCorrectKeys).not.toBeCalled(); expect(mockHandleKeysIntersections).toBeCalledTimes(1); expect(mockHandleKeysIntersections).toBeCalledWith( - { ...stakingModuleData, unusedKeys, vettedKeys: [] }, + { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, blockData, ); expect(mockSecurityContractIsDepositsPaused).toBeCalledTimes(1); @@ -222,14 +222,14 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => false); await stakingModuleGuardService.checkKeysIntersections( - { ...stakingModuleData, unusedKeys, vettedKeys: [] }, + { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, blockData, ); expect(mockHandleKeysIntersections).not.toBeCalled(); expect(mockHandleCorrectKeys).toBeCalledTimes(1); expect(mockHandleCorrectKeys).toBeCalledWith( - { ...stakingModuleData, unusedKeys, vettedKeys: [] }, + { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, blockData, ); expect(mockSecurityContractIsDepositsPaused).toBeCalledTimes(1); @@ -260,11 +260,11 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => signature); await stakingModuleGuardService.handleCorrectKeys( - { ...stakingModuleData, unusedKeys: [], vettedKeys: [] }, + { ...stakingModuleData, unusedKeys: [], vettedUnusedKeys: [] }, blockData, ); await stakingModuleGuardService.handleCorrectKeys( - { ...stakingModuleData, unusedKeys: [], vettedKeys: [] }, + { ...stakingModuleData, unusedKeys: [], vettedUnusedKeys: [] }, blockData, ); @@ -287,7 +287,7 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => signature); await stakingModuleGuardService.handleCorrectKeys( - { ...stakingModuleData, unusedKeys: [], vettedKeys: [] }, + { ...stakingModuleData, unusedKeys: [], vettedUnusedKeys: [] }, blockData, ); @@ -361,7 +361,7 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => undefined); await stakingModuleGuardService.handleKeysIntersections( - { ...stakingModuleData, unusedKeys: [], vettedKeys: [] }, + { ...stakingModuleData, unusedKeys: [], vettedUnusedKeys: [] }, blockData, ); @@ -383,7 +383,7 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => undefined); await stakingModuleGuardService.handleKeysIntersections( - { ...stakingModuleData, unusedKeys: [], vettedKeys: [] }, + { ...stakingModuleData, unusedKeys: [], vettedUnusedKeys: [] }, blockData, ); @@ -446,21 +446,21 @@ describe('StakingModuleGuardService', () => { { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 1, }, { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 2, }, { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 3, }, @@ -472,14 +472,14 @@ describe('StakingModuleGuardService', () => { { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 1, }, { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 3, }, @@ -500,21 +500,21 @@ describe('StakingModuleGuardService', () => { { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 1, }, { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 2, }, { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 3, }, @@ -535,21 +535,21 @@ describe('StakingModuleGuardService', () => { { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 1, }, { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 2, }, { blockHash: '', unusedKeys: [], - vettedKeys: [], + vettedUnusedKeys: [], nonce: 0, stakingModuleId: 3, }, @@ -565,7 +565,7 @@ describe('StakingModuleGuardService', () => { }); }); - describe('checkVettedKeysDuplicates', () => { + describe('getModulesIdsWithDuplicatedVettedUnusedKeys', () => { const blockData = { blockHash: 'some_hash' } as any; it('should found duplicated keys across two module', () => { @@ -629,7 +629,7 @@ describe('StakingModuleGuardService', () => { }); }); - describe('getIntersectionBetweenUsedAndUnusedKeys', () => { + describe('findAlreadyDepositedKeys', () => { // function that return list from kapi that match keys in parameter it('intersection is empty', async () => { const intersectionsWithLidoWC = []; @@ -639,14 +639,13 @@ describe('StakingModuleGuardService', () => { 'findKeysEntires', ); - const result = - await stakingModuleGuardService.getIntersectionBetweenUsedAndUnusedKeys( - intersectionsWithLidoWC, - { - blockNumber: 1, - blockHash: '0x1234', - } as any, - ); + const result = await stakingModuleGuardService.findAlreadyDepositedKeys( + intersectionsWithLidoWC, + { + blockNumber: 1, + blockHash: '0x1234', + } as any, + ); expect(result).toEqual([]); expect(mockSendMessageFromGuardian).toBeCalledTimes(0); @@ -717,14 +716,13 @@ describe('StakingModuleGuardService', () => { }, })); - const result = - await stakingModuleGuardService.getIntersectionBetweenUsedAndUnusedKeys( - intersectionsWithLidoWC, - { - blockNumber: 0, - blockHash: '0x1234', - } as any, - ); + const result = await stakingModuleGuardService.findAlreadyDepositedKeys( + intersectionsWithLidoWC, + { + blockNumber: 0, + blockHash: '0x1234', + } as any, + ); expect(result.length).toEqual(2); expect(result).toEqual( @@ -799,14 +797,13 @@ describe('StakingModuleGuardService', () => { }, })); - const result = - await stakingModuleGuardService.getIntersectionBetweenUsedAndUnusedKeys( - intersectionsWithLidoWC, - { - blockNumber: 0, - blockHash: '0x1234', - } as any, - ); + const result = await stakingModuleGuardService.findAlreadyDepositedKeys( + intersectionsWithLidoWC, + { + blockNumber: 0, + blockHash: '0x1234', + } as any, + ); expect(result).toEqual([]); expect(mockSendMessageFromGuardian).toBeCalledTimes(1); @@ -863,7 +860,7 @@ describe('StakingModuleGuardService', () => { expect( async () => - await stakingModuleGuardService.getIntersectionBetweenUsedAndUnusedKeys( + await stakingModuleGuardService.findAlreadyDepositedKeys( intersectionsWithLidoWC, { blockNumber: 1, diff --git a/src/staking-router/errors.ts b/src/staking-router/errors.ts new file mode 100644 index 00000000..491bab57 --- /dev/null +++ b/src/staking-router/errors.ts @@ -0,0 +1,10 @@ +export class InconsistentBlockhashError extends Error { + constructor( + message = 'Blockhash of the received keys does not match the blockhash of operators', + ) { + super(message); + this.name = 'InconsistentBlockhashError'; + + Object.setPrototypeOf(this, InconsistentBlockhashError.prototype); + } +} diff --git a/src/staking-router/staking-router.service.ts b/src/staking-router/staking-router.service.ts index ff0bc5e6..1c1394b0 100644 --- a/src/staking-router/staking-router.service.ts +++ b/src/staking-router/staking-router.service.ts @@ -6,6 +6,7 @@ import { SRModuleKeysResponse, SRModule } from 'keys-api/interfaces'; import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; import { StakingModuleData } from 'guardian'; import { RegistryOperator } from 'keys-api/interfaces/RegistryOperator'; +import { InconsistentBlockhashError } from './errors'; @Injectable() export class StakingRouterService { @@ -24,21 +25,22 @@ export class StakingRouterService { blockHash: string; blockNumber: number; }> { - // TODO: add cache by modules nonce + // get list of all unused keys and operators const { operatorsByModules, unusedKeys, blockHash, blockNumber } = - await this.getOperatorsAndUnusedKeysFromKAPI(); - // all staking modules list + await this.getOperatorsAndUnusedKeys(); + + // iterate by modules and filter const stakingModulesData: StakingModuleData[] = operatorsByModules.data.map( ({ operators, module: stakingModule }) => { const { moduleUnusedKeys, moduleVettedKeys } = - this.getModuleOperatorsVettedKeys(operators, unusedKeys.data); + this.getModuleUnusedAndVettedUnusedKeys(operators, unusedKeys.data); return { unusedKeys: moduleUnusedKeys.map((srKey) => srKey.key), nonce: stakingModule.nonce, stakingModuleId: stakingModule.id, blockHash, - vettedKeys: moduleVettedKeys, + vettedUnusedKeys: moduleVettedKeys, }; }, ); @@ -53,7 +55,7 @@ export class StakingRouterService { /** * Request grouped by modules operators and all staking modules keys with meta from KAPI */ - private async getOperatorsAndUnusedKeysFromKAPI() { + private async getOperatorsAndUnusedKeys() { const operatorsByModules = await this.keysApiService.getOperatorListWithModule(); const { blockHash: operatorsBlockHash, blockNumber: operatorsBlockNumber } = @@ -85,13 +87,11 @@ export class StakingRouterService { }, ); - throw new Error( - 'Blockhash of the received keys does not match the blockhash of operators', - ); + throw new InconsistentBlockhashError(); } } - private getModuleOperatorsVettedKeys( + private getModuleUnusedAndVettedUnusedKeys( moduleOperators: RegistryOperator[], allModulesUnusedKeys: RegistryKey[], ) { @@ -100,18 +100,19 @@ export class StakingRouterService { const moduleVettedKeys: RegistryKey[] = []; moduleOperators.forEach((operator) => { - const operatorKeys = this.getSortedOperatorKeys( + // all operator unused keys + const operatorKeys = this.filterAndSortOperatorKeys( allModulesUnusedKeys, operator, ); moduleUnusedKeys.push(...operatorKeys); - const operatorVettedKeys = this.getOperatorVettedKeys( + const operatorVettedUnusedKeys = this.getOperatorVettedUnusedKeys( operatorKeys, operator, ); - moduleVettedKeys.push(...operatorVettedKeys); + moduleVettedKeys.push(...operatorVettedUnusedKeys); }); return { @@ -121,28 +122,28 @@ export class StakingRouterService { } /*** - * @param unusedKeys - keys list of all staking modules + * @param keys - keys list of all staking modules * @param operator - staking module operator * @returns sorted operator's keys list */ - private getSortedOperatorKeys( + private filterAndSortOperatorKeys( keys: RegistryKey[], operator: RegistryOperator, ): RegistryKey[] { - const operatorSortedKeys = keys + const operatorKeys = keys .filter( (key) => key.moduleAddress === operator.moduleAddress && key.operatorIndex === operator.index, ) .sort((a, b) => a.index - b.index); - return operatorSortedKeys; + return operatorKeys; } /*** - * Got sorted unused keys and return vetted keys + * Got sorted by index unused keys and return vetted unused keys */ - private getOperatorVettedKeys( + private getOperatorVettedUnusedKeys( unusedKeys: RegistryKey[], operator: RegistryOperator, ): RegistryKey[] { @@ -154,53 +155,4 @@ export class StakingRouterService { public async findKeysEntires(pubkeys: string[]) { return await this.keysApiService.findKeysEntires(pubkeys); } - - public async getStakingModules() { - return await this.keysApiService.getModulesList(); - } - - public async getStakingModuleUnusedKeys( - blockHash: string, - { id, nonce }: SRModule, - ) { - if (!this.isNeedToUpdateState(id, nonce)) - return this.getStakingRouterKeysCache(id); - - const srResponse = await this.keysApiService.getUnusedModuleKeys(id); - const srModuleBlockHash = srResponse.meta.elBlockSnapshot.blockHash; - - if (srModuleBlockHash !== blockHash) { - this.logger.log('Blockhash of the received keys', { - srModuleBlockHash, - blockHash, - }); - - throw Error( - 'Block hash of the received keys does not match the current block hash', - ); - } - - this.setStakingRouterCache(id, srResponse); - - return srResponse; - } - - protected getStakingRouterKeysCache(stakingModuleId: number) { - return this.stakingRouterCache[stakingModuleId]; - } - - protected setStakingRouterCache( - stakingModuleId: number, - srResponse: SRModuleKeysResponse, - ) { - this.stakingRouterCache[stakingModuleId] = srResponse; - } - - protected isNeedToUpdateState(stakingModuleId: number, nextNonce: number) { - const cache = this.getStakingRouterKeysCache(stakingModuleId); - if (!cache) return true; - - const prevNonce = cache.data.module.nonce; - return prevNonce !== nextNonce; - } } diff --git a/src/staking-router/staking-router.spec.ts b/src/staking-router/staking-router.spec.ts index 2635fbc4..a038c804 100644 --- a/src/staking-router/staking-router.spec.ts +++ b/src/staking-router/staking-router.spec.ts @@ -53,7 +53,7 @@ describe('StakingRouter', () => { '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', '0x8d12ec44816f108df84ef9b03e423a6d8fb0f0a1823c871b123ff41f893a7b372eb038a1ed1ff15083e07a777a5cba50', ], - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', depositSignature: @@ -75,7 +75,7 @@ describe('StakingRouter', () => { '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', ], - vettedKeys: [ + vettedUnusedKeys: [ { key: '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', depositSignature: From ac14b8d5e770b8a460ba3d2af8b981bd4f26f2f6 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 22 Dec 2023 12:18:48 +0400 Subject: [PATCH 33/37] fix: use of lastChangedBlockHash --- src/common/custom-errors.ts | 10 + src/guardian/guardian.service.spec.ts | 2 + .../interfaces/staking-module.interface.ts | 1 + src/guardian/interfaces/state.interface.ts | 1 + .../staking-module-guard.service.ts | 51 +++-- .../staking-module-guard.spec.ts | 191 ++++++++++++++---- src/keys-api/interfaces/ELBlockSnapshot.ts | 5 + .../interfaces/SRKeyListWithModule.ts | 13 -- .../interfaces/SRModuleKeysResponse.ts | 10 - .../interfaces/SRModuleListResponse.ts | 13 -- src/keys-api/keys-api.service.ts | 23 +-- src/staking-router/errors.ts | 10 - src/staking-router/keys.fixtures.ts | 2 + src/staking-router/operators.fixtures.ts | 2 + src/staking-router/staking-router.service.ts | 173 ++++++---------- src/staking-router/staking-router.spec.ts | 29 ++- src/staking-router/vetted-keys.ts | 16 ++ test/helpers/mockKeysApi.ts | 6 +- test/manifest.e2e-spec.ts | 79 ++++++-- 19 files changed, 373 insertions(+), 264 deletions(-) create mode 100644 src/common/custom-errors.ts delete mode 100644 src/keys-api/interfaces/SRKeyListWithModule.ts delete mode 100644 src/keys-api/interfaces/SRModuleKeysResponse.ts delete mode 100644 src/keys-api/interfaces/SRModuleListResponse.ts delete mode 100644 src/staking-router/errors.ts create mode 100644 src/staking-router/vetted-keys.ts diff --git a/src/common/custom-errors.ts b/src/common/custom-errors.ts new file mode 100644 index 00000000..bb7eda8c --- /dev/null +++ b/src/common/custom-errors.ts @@ -0,0 +1,10 @@ +export class InconsistentLastChangedBlockHash extends Error { + constructor( + message = 'Since the last request, data in Kapi has been updated. This may result in inconsistencies between the data from two separate requests.', + ) { + super(message); + this.name = 'InconsistentLastChangedBlockHash'; + + Object.setPrototypeOf(this, InconsistentLastChangedBlockHash.prototype); + } +} diff --git a/src/guardian/guardian.service.spec.ts b/src/guardian/guardian.service.spec.ts index 41f8defa..adfca110 100644 --- a/src/guardian/guardian.service.spec.ts +++ b/src/guardian/guardian.service.spec.ts @@ -61,6 +61,7 @@ const vettedKeysResponse = { stakingModulesData: [ { blockHash: 'some_hash', + lastChangedBlockHash: 'some_hash', unusedKeys: [ '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', @@ -91,6 +92,7 @@ const vettedKeysResponse = { }, { blockHash: 'some_hash', + lastChangedBlockHash: 'some_hash', unusedKeys: [ '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', ], diff --git a/src/guardian/interfaces/staking-module.interface.ts b/src/guardian/interfaces/staking-module.interface.ts index 714a8b38..a600a67a 100644 --- a/src/guardian/interfaces/staking-module.interface.ts +++ b/src/guardian/interfaces/staking-module.interface.ts @@ -6,4 +6,5 @@ export interface StakingModuleData { vettedUnusedKeys: RegistryKey[]; nonce: number; stakingModuleId: number; + lastChangedBlockHash: string; } diff --git a/src/guardian/interfaces/state.interface.ts b/src/guardian/interfaces/state.interface.ts index e39a4c1c..b04f98e1 100644 --- a/src/guardian/interfaces/state.interface.ts +++ b/src/guardian/interfaces/state.interface.ts @@ -2,4 +2,5 @@ export interface ContractsState { blockNumber: number; nonce: number; depositRoot: string; + lastChangedBlockHash: string; } 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 0dd804cb..b8447293 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -10,9 +10,9 @@ import { GuardianMetricsService } from '../guardian-metrics'; import { GuardianMessageService } from '../guardian-message'; import { StakingRouterService } from 'staking-router'; -import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; import { KeysValidationService } from 'guardian/keys-validation/keys-validation.service'; import { performance } from 'perf_hooks'; +import { InconsistentLastChangedBlockHash } from 'common/custom-errors'; @Injectable() export class StakingModuleGuardService { @@ -155,8 +155,8 @@ export class StakingModuleGuardService { } else { // it could throw error if kapi returned old data const usedKeys = await this.findAlreadyDepositedKeys( + stakingModuleData.lastChangedBlockHash, validIntersections, - blockData, ); // if found used keys, Lido already made deposit on this keys @@ -227,8 +227,8 @@ export class StakingModuleGuardService { * If it was indeed made by Lido, we set a metric and skip sending deposit messages in the queue for this iteration. */ public async findAlreadyDepositedKeys( + lastChangedBlockHash: string, intersectionsWithLidoWC: VerifiedDepositEvent[], - blockData: BlockData, ) { const depositedPubkeys = intersectionsWithLidoWC.map( (deposit) => deposit.pubkey, @@ -246,28 +246,26 @@ export class StakingModuleGuardService { depositedPubkeys, ); - this.checkCurrentBlockOlderThanPrev( - meta.elBlockSnapshot.blockNumber, - blockData.blockNumber, + this.isEqualLastChangedBlockHash( + lastChangedBlockHash, + meta.elBlockSnapshot.lastChangedBlockHash, ); return data.filter((key) => key.used); } - private async checkCurrentBlockOlderThanPrev( - currBlockNumber: number, - prevBlockNumber: number, + private isEqualLastChangedBlockHash( + firstRequestHash: string, + secondRequestHash: string, ) { - if (currBlockNumber < prevBlockNumber) { - const errorMsg = - 'BlockNumber of the current response older than previous response from KAPI'; - this.logger.error(errorMsg, { - previous: prevBlockNumber, - current: currBlockNumber, - }); - throw Error(errorMsg); + if (firstRequestHash !== secondRequestHash) { + const error = + 'Since the last request, data in Kapi has been updated. This may result in inconsistencies between the data from two separate requests.'; + + this.logger.error(error, { firstRequestHash, secondRequestHash }); + + throw new InconsistentLastChangedBlockHash(); } - return; } /** @@ -333,9 +331,14 @@ export class StakingModuleGuardService { guardianIndex, } = blockData; - const { nonce, stakingModuleId } = stakingModuleData; + const { nonce, stakingModuleId, lastChangedBlockHash } = stakingModuleData; - const currentContractState = { nonce, depositRoot, blockNumber }; + const currentContractState = { + nonce, + depositRoot, + blockNumber, + lastChangedBlockHash, + }; const lastContractsState = this.lastContractsStateByModuleId[stakingModuleId]; @@ -433,7 +436,15 @@ export class StakingModuleGuardService { ): boolean { if (!firstState || !secondState) return false; if (firstState.depositRoot !== secondState.depositRoot) return false; + + // Check if the nonce values are different. A difference in nonce implies a state change. if (firstState.nonce !== secondState.nonce) return false; + // If the nonce is unchanged, the state might still have changed. + // Therefore, we also need to compare the 'lastChangedBlockHash'. + // It's important to note that it's not possible for the nonce to be different + // while having the same 'lastChangedBlockHash'. + if (firstState.lastChangedBlockHash !== secondState.lastChangedBlockHash) + return false; if ( Math.floor(firstState.blockNumber / GUARDIAN_DEPOSIT_RESIGNING_BLOCKS) !== 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 fd49537b..21fd3a3c 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -24,6 +24,7 @@ import { vettedKeysDuplicatesAcrossOneModuleAndFew, vettedKeysWithoutDuplicates, } from './keys.fixtures'; +import { InconsistentLastChangedBlockHash } from 'common/custom-errors'; jest.mock('../../transport/stomp/stomp.client'); @@ -90,7 +91,12 @@ describe('StakingModuleGuardService', () => { }; const blockData = { unusedKeys, depositedEvents } as any; const matched = stakingModuleGuardService.getKeysIntersections( - { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, blockData, ); @@ -107,7 +113,12 @@ describe('StakingModuleGuardService', () => { }; const blockData = { unusedKeys, depositedEvents } as any; const matched = stakingModuleGuardService.getKeysIntersections( - { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, blockData, ); @@ -123,7 +134,12 @@ describe('StakingModuleGuardService', () => { }; const blockData = { unusedKeys, depositedEvents } as any; const matched = stakingModuleGuardService.getKeysIntersections( - { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, blockData, ); @@ -191,14 +207,24 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => false); await stakingModuleGuardService.checkKeysIntersections( - { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, blockData, ); expect(mockHandleCorrectKeys).not.toBeCalled(); expect(mockHandleKeysIntersections).toBeCalledTimes(1); expect(mockHandleKeysIntersections).toBeCalledWith( - { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, blockData, ); expect(mockSecurityContractIsDepositsPaused).toBeCalledTimes(1); @@ -222,14 +248,24 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => false); await stakingModuleGuardService.checkKeysIntersections( - { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, blockData, ); expect(mockHandleKeysIntersections).not.toBeCalled(); expect(mockHandleCorrectKeys).toBeCalledTimes(1); expect(mockHandleCorrectKeys).toBeCalledWith( - { ...stakingModuleData, unusedKeys, vettedUnusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, blockData, ); expect(mockSecurityContractIsDepositsPaused).toBeCalledTimes(1); @@ -260,11 +296,21 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => signature); await stakingModuleGuardService.handleCorrectKeys( - { ...stakingModuleData, unusedKeys: [], vettedUnusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + }, blockData, ); await stakingModuleGuardService.handleCorrectKeys( - { ...stakingModuleData, unusedKeys: [], vettedUnusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + }, blockData, ); @@ -287,7 +333,12 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => signature); await stakingModuleGuardService.handleCorrectKeys( - { ...stakingModuleData, unusedKeys: [], vettedUnusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + }, blockData, ); @@ -361,7 +412,12 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => undefined); await stakingModuleGuardService.handleKeysIntersections( - { ...stakingModuleData, unusedKeys: [], vettedUnusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + }, blockData, ); @@ -383,7 +439,12 @@ describe('StakingModuleGuardService', () => { .mockImplementation(async () => undefined); await stakingModuleGuardService.handleKeysIntersections( - { ...stakingModuleData, unusedKeys: [], vettedUnusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + }, blockData, ); @@ -396,7 +457,12 @@ describe('StakingModuleGuardService', () => { describe('isSameContractsStates', () => { it('should return true if states are the same', () => { - const state = { depositRoot: '0x1', nonce: 1, blockNumber: 100 }; + const state = { + depositRoot: '0x1', + nonce: 1, + blockNumber: 100, + lastChangedBlockHash: 'hash', + }; const result = stakingModuleGuardService.isSameContractsStates( { ...state }, { ...state }, @@ -405,7 +471,12 @@ describe('StakingModuleGuardService', () => { }); it('should return true if blockNumbers are close', () => { - const state = { depositRoot: '0x1', nonce: 1, blockNumber: 100 }; + const state = { + depositRoot: '0x1', + nonce: 1, + blockNumber: 100, + lastChangedBlockHash: 'hash', + }; const result = stakingModuleGuardService.isSameContractsStates(state, { ...state, blockNumber: state.blockNumber + 1, @@ -414,7 +485,12 @@ describe('StakingModuleGuardService', () => { }); it('should return false if blockNumbers are too far', () => { - const state = { depositRoot: '0x1', nonce: 1, blockNumber: 100 }; + const state = { + depositRoot: '0x1', + nonce: 1, + blockNumber: 100, + lastChangedBlockHash: 'hash', + }; const result = stakingModuleGuardService.isSameContractsStates(state, { ...state, blockNumber: state.blockNumber + 200, @@ -423,7 +499,12 @@ describe('StakingModuleGuardService', () => { }); it('should return false if depositRoot are different', () => { - const state = { depositRoot: '0x1', nonce: 1, blockNumber: 100 }; + const state = { + depositRoot: '0x1', + nonce: 1, + blockNumber: 100, + lastChangedBlockHash: 'hash', + }; const result = stakingModuleGuardService.isSameContractsStates(state, { ...state, depositRoot: '0x2', @@ -432,13 +513,36 @@ describe('StakingModuleGuardService', () => { }); it('should return false if nonce are different', () => { - const state = { depositRoot: '0x1', nonce: 1, blockNumber: 100 }; + // It's important to note that it's not possible for the nonce to be different + // while having the same 'lastChangedBlockHash'. + const state = { + depositRoot: '0x1', + nonce: 1, + blockNumber: 100, + lastChangedBlockHash: 'hash', + }; const result = stakingModuleGuardService.isSameContractsStates(state, { ...state, nonce: 2, }); expect(result).toBeFalsy(); }); + + it('should return false if lastChangedBlockHash are different', () => { + // It's important to note that it's not possible for the nonce to be different + // while having the same 'lastChangedBlockHash'. + const state = { + depositRoot: '0x1', + nonce: 1, + blockNumber: 100, + lastChangedBlockHash: 'hash', + }; + const result = stakingModuleGuardService.isSameContractsStates(state, { + ...state, + lastChangedBlockHash: 'new hash', + }); + expect(result).toBeFalsy(); + }); }); describe('excludeModulesWithDuplicatedKeys', () => { @@ -449,6 +553,7 @@ describe('StakingModuleGuardService', () => { vettedUnusedKeys: [], nonce: 0, stakingModuleId: 1, + lastChangedBlockHash: '', }, { blockHash: '', @@ -456,6 +561,7 @@ describe('StakingModuleGuardService', () => { vettedUnusedKeys: [], nonce: 0, stakingModuleId: 2, + lastChangedBlockHash: '', }, { blockHash: '', @@ -463,6 +569,7 @@ describe('StakingModuleGuardService', () => { vettedUnusedKeys: [], nonce: 0, stakingModuleId: 3, + lastChangedBlockHash: '', }, ]; @@ -475,6 +582,7 @@ describe('StakingModuleGuardService', () => { vettedUnusedKeys: [], nonce: 0, stakingModuleId: 1, + lastChangedBlockHash: '', }, { blockHash: '', @@ -482,6 +590,7 @@ describe('StakingModuleGuardService', () => { vettedUnusedKeys: [], nonce: 0, stakingModuleId: 3, + lastChangedBlockHash: '', }, ]; @@ -496,13 +605,14 @@ describe('StakingModuleGuardService', () => { it('should return list without changes', () => { const moduleIdsWithDuplicateKeys = [4]; - const expectedStakingModules = [ + const expectedStakingModules: StakingModuleData[] = [ { blockHash: '', unusedKeys: [], vettedUnusedKeys: [], nonce: 0, stakingModuleId: 1, + lastChangedBlockHash: '', }, { blockHash: '', @@ -510,6 +620,7 @@ describe('StakingModuleGuardService', () => { vettedUnusedKeys: [], nonce: 0, stakingModuleId: 2, + lastChangedBlockHash: '', }, { blockHash: '', @@ -517,6 +628,7 @@ describe('StakingModuleGuardService', () => { vettedUnusedKeys: [], nonce: 0, stakingModuleId: 3, + lastChangedBlockHash: '', }, ]; @@ -531,13 +643,14 @@ describe('StakingModuleGuardService', () => { it('should return list without changes if duplicated keys were not found', () => { const moduleIdsWithDuplicateKeys = []; - const expectedStakingModules = [ + const expectedStakingModules: StakingModuleData[] = [ { blockHash: '', unusedKeys: [], vettedUnusedKeys: [], nonce: 0, stakingModuleId: 1, + lastChangedBlockHash: '', }, { blockHash: '', @@ -545,6 +658,7 @@ describe('StakingModuleGuardService', () => { vettedUnusedKeys: [], nonce: 0, stakingModuleId: 2, + lastChangedBlockHash: '', }, { blockHash: '', @@ -552,6 +666,7 @@ describe('StakingModuleGuardService', () => { vettedUnusedKeys: [], nonce: 0, stakingModuleId: 3, + lastChangedBlockHash: '', }, ]; @@ -640,11 +755,8 @@ describe('StakingModuleGuardService', () => { ); const result = await stakingModuleGuardService.findAlreadyDepositedKeys( + 'lastHash', intersectionsWithLidoWC, - { - blockNumber: 1, - blockHash: '0x1234', - } as any, ); expect(result).toEqual([]); @@ -712,16 +824,14 @@ describe('StakingModuleGuardService', () => { blockNumber: 0, blockHash: 'hash', timestamp: 12345, + lastChangedBlockHash: 'lastHash', }, }, })); const result = await stakingModuleGuardService.findAlreadyDepositedKeys( + 'lastHash', intersectionsWithLidoWC, - { - blockNumber: 0, - blockHash: '0x1234', - } as any, ); expect(result.length).toEqual(2); @@ -793,23 +903,21 @@ describe('StakingModuleGuardService', () => { blockNumber: 0, blockHash: 'hash', timestamp: 12345, + lastChangedBlockHash: 'lastHash', }, }, })); const result = await stakingModuleGuardService.findAlreadyDepositedKeys( + 'lastHash', intersectionsWithLidoWC, - { - blockNumber: 0, - blockHash: '0x1234', - } as any, ); expect(result).toEqual([]); expect(mockSendMessageFromGuardian).toBeCalledTimes(1); }); - it('should skip if blockNumber that kapi returned is smaller than in blockData ', async () => { + it('should throw error if lastChangedBlockHash that kapi returned is not equal to prev value', async () => { const pubkey1 = '0x1234'; const pubkey2 = '0x56789'; const pubkey3 = '0x3478'; @@ -854,22 +962,19 @@ describe('StakingModuleGuardService', () => { blockNumber: 0, blockHash: 'hash', timestamp: 12345, + lastChangedBlockHash: 'lastHash', }, }, })); + const prevLastChangedBlockHash = 'prevHash'; + expect( - async () => - await stakingModuleGuardService.findAlreadyDepositedKeys( - intersectionsWithLidoWC, - { - blockNumber: 1, - blockHash: '0x1234', - } as any, - ), - ).rejects.toThrowError( - 'BlockNumber of the current response older than previous response from KAPI', - ); + stakingModuleGuardService.findAlreadyDepositedKeys( + prevLastChangedBlockHash, + intersectionsWithLidoWC, + ), + ).rejects.toThrowError(new InconsistentLastChangedBlockHash()); expect(mockSendMessageFromGuardian).toBeCalledTimes(1); }); diff --git a/src/keys-api/interfaces/ELBlockSnapshot.ts b/src/keys-api/interfaces/ELBlockSnapshot.ts index ddf94874..e66f5235 100644 --- a/src/keys-api/interfaces/ELBlockSnapshot.ts +++ b/src/keys-api/interfaces/ELBlockSnapshot.ts @@ -11,4 +11,9 @@ export type ELBlockSnapshot = { * Block timestamp */ timestamp: number; + + /** + * Blockhash from the most recent data update + */ + lastChangedBlockHash: string; }; diff --git a/src/keys-api/interfaces/SRKeyListWithModule.ts b/src/keys-api/interfaces/SRKeyListWithModule.ts deleted file mode 100644 index 9cbdbddd..00000000 --- a/src/keys-api/interfaces/SRKeyListWithModule.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { RegistryKey } from './RegistryKey'; -import type { SRModule } from './SRModule'; - -export type SRKeyListWithModule = { - /** - * Keys of staking router module - */ - keys: Array; - /** - * Detailed Staking Router information - */ - module: SRModule; -}; diff --git a/src/keys-api/interfaces/SRModuleKeysResponse.ts b/src/keys-api/interfaces/SRModuleKeysResponse.ts deleted file mode 100644 index c2e81add..00000000 --- a/src/keys-api/interfaces/SRModuleKeysResponse.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Meta } from './Meta'; -import type { SRKeyListWithModule } from './SRKeyListWithModule'; - -export type SRModuleKeysResponse = { - /** - * Staking router module keys. - */ - data: SRKeyListWithModule; - meta: Meta; -}; diff --git a/src/keys-api/interfaces/SRModuleListResponse.ts b/src/keys-api/interfaces/SRModuleListResponse.ts deleted file mode 100644 index 01adfe66..00000000 --- a/src/keys-api/interfaces/SRModuleListResponse.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { ELBlockSnapshot } from './ELBlockSnapshot'; -import type { SRModule } from './SRModule'; - -export type SRModuleListResponse = { - /** - * List of staking router modules with detailed information - */ - data: Array; - /** - * Execution layer block information - */ - elBlockSnapshot: ELBlockSnapshot; -}; diff --git a/src/keys-api/keys-api.service.ts b/src/keys-api/keys-api.service.ts index e61bc15d..ae575df5 100644 --- a/src/keys-api/keys-api.service.ts +++ b/src/keys-api/keys-api.service.ts @@ -2,11 +2,7 @@ import { Injectable, LoggerService, Inject } from '@nestjs/common'; import { FetchService, RequestInit } from '@lido-nestjs/fetch'; import { AbortController } from 'node-abort-controller'; import { FETCH_REQUEST_TIMEOUT } from './keys-api.constants'; -import { - SRModuleKeysResponse, - SRModuleListResponse, - KeyListResponse, -} from './interfaces'; +import { KeyListResponse } from './interfaces'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { Configuration } from 'common/config'; import { GroupedByModuleOperatorListResponse } from './interfaces/GroupedByModuleOperatorListResponse'; @@ -45,22 +41,6 @@ export class KeysApiService { } } - public async getModulesList() { - const result = await this.fetch('/v1/modules'); - if (!result.data?.length || !result.elBlockSnapshot) - throw Error('Keys API not synced, please wait'); - return result; - } - - public async getUnusedModuleKeys(stakingModuleId: number) { - const result = await this.fetch( - `/v1/modules/${stakingModuleId}/keys?used=false`, - ); - if (!result.data || !result.meta) - throw Error('Keys API not synced, please wait'); - return result; - } - /** * * @param The /v1/keys/find API endpoint returns keys along with their duplicates @@ -74,7 +54,6 @@ export class KeysApiService { }, body: JSON.stringify({ pubkeys }), }); - return result; } diff --git a/src/staking-router/errors.ts b/src/staking-router/errors.ts deleted file mode 100644 index 491bab57..00000000 --- a/src/staking-router/errors.ts +++ /dev/null @@ -1,10 +0,0 @@ -export class InconsistentBlockhashError extends Error { - constructor( - message = 'Blockhash of the received keys does not match the blockhash of operators', - ) { - super(message); - this.name = 'InconsistentBlockhashError'; - - Object.setPrototypeOf(this, InconsistentBlockhashError.prototype); - } -} diff --git a/src/staking-router/keys.fixtures.ts b/src/staking-router/keys.fixtures.ts index d6d63188..90c55aea 100644 --- a/src/staking-router/keys.fixtures.ts +++ b/src/staking-router/keys.fixtures.ts @@ -61,6 +61,8 @@ export const keysAllStakingModules = { blockHash: '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', timestamp: 1701027516, + lastChangedBlockHash: + '0x194ac4fd960ed44cb3db53fe1f5a53e983280fd438aeba607ae04f1bb416b4a1', }, }, }; diff --git a/src/staking-router/operators.fixtures.ts b/src/staking-router/operators.fixtures.ts index fcefbba9..ee27484a 100644 --- a/src/staking-router/operators.fixtures.ts +++ b/src/staking-router/operators.fixtures.ts @@ -78,6 +78,8 @@ export const groupedByModulesOperators = { blockHash: '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', timestamp: 1701027516, + lastChangedBlockHash: + '0x194ac4fd960ed44cb3db53fe1f5a53e983280fd438aeba607ae04f1bb416b4a1', }, }, }; diff --git a/src/staking-router/staking-router.service.ts b/src/staking-router/staking-router.service.ts index 1c1394b0..d32e9e72 100644 --- a/src/staking-router/staking-router.service.ts +++ b/src/staking-router/staking-router.service.ts @@ -2,15 +2,18 @@ import { Injectable, LoggerService, Inject } from '@nestjs/common'; import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; import { Configuration } from 'common/config'; import { KeysApiService } from 'keys-api/keys-api.service'; -import { SRModuleKeysResponse, SRModule } from 'keys-api/interfaces'; -import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; import { StakingModuleData } from 'guardian'; +import { getVettedUnusedKeys } from './vetted-keys'; import { RegistryOperator } from 'keys-api/interfaces/RegistryOperator'; -import { InconsistentBlockhashError } from './errors'; +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'; @Injectable() export class StakingRouterService { - protected stakingRouterCache: Record = {}; + // protected stakingRouterCache: Record = {}; + constructor( @Inject(WINSTON_MODULE_NEST_PROVIDER) protected logger: LoggerService, protected readonly config: Configuration, @@ -25,133 +28,77 @@ export class StakingRouterService { blockHash: string; blockNumber: number; }> { - // get list of all unused keys and operators - const { operatorsByModules, unusedKeys, blockHash, blockNumber } = - await this.getOperatorsAndUnusedKeys(); - - // iterate by modules and filter - const stakingModulesData: StakingModuleData[] = operatorsByModules.data.map( - ({ operators, module: stakingModule }) => { - const { moduleUnusedKeys, moduleVettedKeys } = - this.getModuleUnusedAndVettedUnusedKeys(operators, unusedKeys.data); - - return { - unusedKeys: moduleUnusedKeys.map((srKey) => srKey.key), - nonce: stakingModule.nonce, - stakingModuleId: stakingModule.id, - blockHash, - vettedUnusedKeys: moduleVettedKeys, - }; - }, - ); + const { data: operatorsByModules, meta: operatorsMeta } = + await this.keysApiService.getOperatorListWithModule(); - return { - stakingModulesData, - blockHash, - blockNumber, - }; - } + const { data: unusedKeys, meta: unusedKeysMeta } = + await this.keysApiService.getUnusedKeys(); - /** - * Request grouped by modules operators and all staking modules keys with meta from KAPI - */ - private async getOperatorsAndUnusedKeys() { - const operatorsByModules = - await this.keysApiService.getOperatorListWithModule(); - const { blockHash: operatorsBlockHash, blockNumber: operatorsBlockNumber } = - operatorsByModules.meta.elBlockSnapshot; + const blockHash = operatorsMeta.elBlockSnapshot.blockHash; + const blockNumber = operatorsMeta.elBlockSnapshot.blockNumber; - const unusedKeys = await this.keysApiService.getUnusedKeys(); - const { blockHash: keysBlockHash } = unusedKeys.meta.elBlockSnapshot; + this.isEqualLastChangedBlockHash( + operatorsMeta.elBlockSnapshot.lastChangedBlockHash, + unusedKeysMeta.elBlockSnapshot.lastChangedBlockHash, + ); - this.validateBlockHashMatch(keysBlockHash, operatorsBlockHash); + const stakingModulesData = operatorsByModules.map( + ({ operators, module: stakingModule }) => + this.processModuleData({ + operators, + stakingModule, + unusedKeys, + meta: operatorsMeta, + }), + ); - return { - operatorsByModules, - unusedKeys, - blockHash: operatorsBlockHash, - blockNumber: operatorsBlockNumber, - }; + return { stakingModulesData, blockHash, blockNumber }; } - private validateBlockHashMatch( - keysBlockHash: string, - operatorsBlockHash: string, + private isEqualLastChangedBlockHash( + firstRequestHash: string, + secondRequestHash: string, ) { - if (keysBlockHash !== operatorsBlockHash) { - this.logger.error( - 'Blockhash of the received keys and operators dont match', - { - keysBlockHash, - operatorsBlockHash, - }, - ); + if (firstRequestHash !== secondRequestHash) { + const error = + 'Since the last request, data in Kapi has been updated. This may result in inconsistencies between the data from two separate requests.'; + + this.logger.error(error, { firstRequestHash, secondRequestHash }); - throw new InconsistentBlockhashError(); + throw new InconsistentLastChangedBlockHash(); } } - private getModuleUnusedAndVettedUnusedKeys( - moduleOperators: RegistryOperator[], - allModulesUnusedKeys: RegistryKey[], - ) { - const moduleUnusedKeys: RegistryKey[] = []; - // all module vetted keys - const moduleVettedKeys: RegistryKey[] = []; - - moduleOperators.forEach((operator) => { - // all operator unused keys - const operatorKeys = this.filterAndSortOperatorKeys( - allModulesUnusedKeys, - operator, - ); - - moduleUnusedKeys.push(...operatorKeys); + private processModuleData({ + operators, + stakingModule, + unusedKeys, + meta, + }: { + operators: RegistryOperator[]; + stakingModule: SRModule; + unusedKeys: RegistryKey[]; + meta: Meta; + }): StakingModuleData { + const moduleUnusedKeys = unusedKeys.filter( + (key) => key.moduleAddress === stakingModule.stakingModuleAddress, + ); - const operatorVettedUnusedKeys = this.getOperatorVettedUnusedKeys( - operatorKeys, - operator, - ); - moduleVettedKeys.push(...operatorVettedUnusedKeys); - }); + const moduleVettedUnusedKeys = getVettedUnusedKeys( + operators, + moduleUnusedKeys, + ); return { - moduleUnusedKeys, - moduleVettedKeys, + unusedKeys: moduleUnusedKeys.map((srKey) => srKey.key), + nonce: stakingModule.nonce, + stakingModuleId: stakingModule.id, + blockHash: meta.elBlockSnapshot.blockHash, + lastChangedBlockHash: meta.elBlockSnapshot.lastChangedBlockHash, + vettedUnusedKeys: moduleVettedUnusedKeys, }; } - /*** - * @param keys - keys list of all staking modules - * @param operator - staking module operator - * @returns sorted operator's keys list - */ - private filterAndSortOperatorKeys( - keys: RegistryKey[], - operator: RegistryOperator, - ): RegistryKey[] { - const operatorKeys = keys - .filter( - (key) => - key.moduleAddress === operator.moduleAddress && - key.operatorIndex === operator.index, - ) - .sort((a, b) => a.index - b.index); - return operatorKeys; - } - - /*** - * Got sorted by index unused keys and return vetted unused keys - */ - private getOperatorVettedUnusedKeys( - unusedKeys: RegistryKey[], - operator: RegistryOperator, - ): RegistryKey[] { - const numberOfVettedUnusedKeys = - operator.stakingLimit - operator.usedSigningKeys; - return unusedKeys.slice(0, numberOfVettedUnusedKeys); - } - public async findKeysEntires(pubkeys: string[]) { return await this.keysApiService.findKeysEntires(pubkeys); } diff --git a/src/staking-router/staking-router.spec.ts b/src/staking-router/staking-router.spec.ts index a038c804..4026fa49 100644 --- a/src/staking-router/staking-router.spec.ts +++ b/src/staking-router/staking-router.spec.ts @@ -5,6 +5,7 @@ import { groupedByModulesOperators } from './operators.fixtures'; import { keysAllStakingModules } from './keys.fixtures'; import { ConfigModule } from 'common/config'; import { LoggerModule } from 'common/logger'; +import { InconsistentLastChangedBlockHash } from 'common/custom-errors'; describe('StakingRouter', () => { let stakingRouterService: StakingRouterService; @@ -28,10 +29,9 @@ describe('StakingRouter', () => { stakingRouterService = module.get(StakingRouterService); keysApiService = module.get(KeysApiService); - // keysApiService = module.get(KeysApiService) as jest.Mocked; }); - it('should return correct data when block hashes match', async () => { + it("should return correct data when 'lastChangedBlockHash' values of two requests are identical", async () => { (keysApiService.getOperatorListWithModule as jest.Mock).mockResolvedValue( groupedByModulesOperators, ); @@ -66,6 +66,8 @@ describe('StakingRouter', () => { ], blockHash: '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + lastChangedBlockHash: + '0x194ac4fd960ed44cb3db53fe1f5a53e983280fd438aeba607ae04f1bb416b4a1', stakingModuleId: 1, nonce: 364, }, @@ -89,9 +91,32 @@ describe('StakingRouter', () => { 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, + ...{ + meta: { + elBlockSnapshot: { + lastChangedBlockHash: + '0xabf3d64e85527d0c80eb6b0378316caceed9a24f535f6f28dad008fdfebe82b8', + }, + }, + }, + }); + + expect(stakingRouterService.getStakingModulesData()).rejects.toThrowError( + new InconsistentLastChangedBlockHash(), + ); + }); }); diff --git a/src/staking-router/vetted-keys.ts b/src/staking-router/vetted-keys.ts new file mode 100644 index 00000000..42ee1257 --- /dev/null +++ b/src/staking-router/vetted-keys.ts @@ -0,0 +1,16 @@ +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; +import { RegistryOperator } from 'keys-api/interfaces/RegistryOperator'; + +export function getVettedUnusedKeys( + operators: RegistryOperator[], + unusedKeys: RegistryKey[], +): RegistryKey[] { + return operators.flatMap((operator) => { + const operatorKeys = unusedKeys + .filter((key) => key.operatorIndex === operator.index) + .sort((a, b) => a.index - b.index) + .slice(0, operator.stakingLimit - operator.usedSigningKeys); + + return operatorKeys; + }); +} diff --git a/test/helpers/mockKeysApi.ts b/test/helpers/mockKeysApi.ts index f4eb0245..bab76105 100644 --- a/test/helpers/mockKeysApi.ts +++ b/test/helpers/mockKeysApi.ts @@ -21,10 +21,14 @@ export const mockedModule = (block: ethers.providers.Block, nonce = 6046) => ({ lastDepositBlock: block.number, }); -export const mockedMeta = (block: ethers.providers.Block) => ({ +export const mockedMeta = ( + block: ethers.providers.Block, + lastChangedBlockHash: string, +) => ({ blockNumber: block.number, blockHash: block.hash, timestamp: block.timestamp, + lastChangedBlockHash, }); export const mockedOperators: RegistryOperator[] = [ diff --git a/test/manifest.e2e-spec.ts b/test/manifest.e2e-spec.ts index e7cbc313..1b3d59cf 100644 --- a/test/manifest.e2e-spec.ts +++ b/test/manifest.e2e-spec.ts @@ -231,7 +231,7 @@ describe('ganache e2e tests', () => { }, ]; - const meta = mockedMeta(currentBlock); + const meta = mockedMeta(currentBlock, currentBlock.hash); const stakingModule = mockedModule(currentBlock); mockedKeysApiOperators( @@ -301,7 +301,7 @@ describe('ganache e2e tests', () => { // Mock Keys API again on new block const newBlock = await providerService.provider.getBlock('latest'); - const newMeta = mockedMeta(newBlock); + const newMeta = mockedMeta(newBlock, newBlock.hash); mockedKeysApiOperators( keysApiService, @@ -366,7 +366,7 @@ describe('ganache e2e tests', () => { }, ]; - const meta = mockedMeta(currentBlock); + const meta = mockedMeta(currentBlock, currentBlock.hash); const stakingModule = mockedModule(currentBlock); mockedKeysApiOperators( @@ -423,7 +423,7 @@ describe('ganache e2e tests', () => { // Mock Keys API again on new block const newBlock = await providerService.provider.getBlock('latest'); - const newMeta = mockedMeta(newBlock); + const newMeta = mockedMeta(newBlock, newBlock.hash); mockedKeysApiOperators( keysApiService, @@ -479,7 +479,7 @@ describe('ganache e2e tests', () => { }, ]; - const meta = mockedMeta(currentBlock); + const meta = mockedMeta(currentBlock, currentBlock.hash); const stakingModule = mockedModule(currentBlock); mockedKeysApiOperators( @@ -539,7 +539,7 @@ describe('ganache e2e tests', () => { ); // Mock Keys API again on new block const newBlock = await providerService.provider.getBlock('latest'); - const newMeta = mockedMeta(newBlock); + const newMeta = mockedMeta(newBlock, newBlock.hash); mockedKeysApiOperators( keysApiService, @@ -593,7 +593,7 @@ describe('ganache e2e tests', () => { }, ]; - const meta = mockedMeta(currentBlock); + const meta = mockedMeta(currentBlock, currentBlock.hash); const stakingModule = mockedModule(currentBlock); mockedKeysApiOperators( @@ -643,7 +643,7 @@ describe('ganache e2e tests', () => { // Mock Keys API again on new block const newBlock = await providerService.provider.getBlock('latest'); - const newMeta = mockedMeta(newBlock); + const newMeta = mockedMeta(newBlock, newBlock.hash); mockedKeysApiOperators( keysApiService, @@ -685,7 +685,7 @@ describe('ganache e2e tests', () => { test( 'reorganization', async () => { - // TODOL need attention to this test + // TODO: need attention to this test const tempProvider = new ethers.providers.JsonRpcProvider( `http://127.0.0.1:${GANACHE_PORT}`, ); @@ -710,7 +710,7 @@ describe('ganache e2e tests', () => { }, ]; - const meta = mockedMeta(currentBlock); + const meta = mockedMeta(currentBlock, currentBlock.hash); const stakingModule = mockedModule(currentBlock); mockedKeysApiOperators( @@ -771,7 +771,7 @@ 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); + const newMeta = mockedMeta(newBlock, newBlock.hash); mockedKeysApiOperators( keysApiService, @@ -857,7 +857,7 @@ describe('ganache e2e tests', () => { // mocked curated module const stakingModule = mockedModule(currentBlock); - const meta = mockedMeta(currentBlock); + const meta = mockedMeta(currentBlock, currentBlock.hash); mockedKeysApiOperators( keysApiService, @@ -921,7 +921,7 @@ describe('ganache e2e tests', () => { ]; const newBlock = await tempProvider.getBlock('latest'); - const newMeta = mockedMeta(newBlock); + const newMeta = mockedMeta(newBlock, newBlock.hash); mockedKeysApiOperators( keysApiService, @@ -952,6 +952,51 @@ describe('ganache e2e tests', () => { TESTS_TIMEOUT, ); + test( + 'inconsistent kapi requests data', + async () => { + const tempProvider = new ethers.providers.JsonRpcProvider( + `http://127.0.0.1:${GANACHE_PORT}`, + ); + const currentBlock = await tempProvider.getBlock('latest'); + + // mocked curated module + const stakingModule = mockedModule(currentBlock); + const meta = mockedMeta(currentBlock, currentBlock.hash); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + meta, + ); + + // list of keys for /keys?used=false mock + const unusedKeys = [ + { + key: '0xa9bfaa8207ee6c78644c079ffc91b6e5abcc5eede1b7a06abb8fb40e490a75ea269c178dd524b65185299d2bbd2eb7b2', + depositSignature: + '0xaa5f2a1053ba7d197495df44d4a32b7ae10265cf9e38560a16b782978c0a24271a113c9538453b7e45f35cb64c7adb460d7a9fe8c8ce6b8c80ca42fd5c48e180c73fc08f7d35ba32e39f32c902fd333faf47611827f0b7813f11c4c518dd2e59', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: NOP_REGISTRY, + }, + ]; + + const hashWasChanged = + '0xd921055dbb407e09f64afe5182a64c1bd309fe28f26909a96425cdb6bfc48959'; + const newMeta = mockedMeta(currentBlock, hashWasChanged); + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, newMeta); + + await guardianService.handleNewBlock(); + + expect(sendDepositMessage).toBeCalledTimes(0); + expect(sendPauseMessage).toBeCalledTimes(0); + }, + TESTS_TIMEOUT, + ); + test( 'added unused keys for that deposit was already made', async () => { @@ -1003,7 +1048,7 @@ describe('ganache e2e tests', () => { // mocked curated module const stakingModule = mockedModule(currentBlock); - const meta = mockedMeta(currentBlock); + const meta = mockedMeta(currentBlock, currentBlock.hash); mockedKeysApiOperators( keysApiService, @@ -1048,7 +1093,7 @@ describe('ganache e2e tests', () => { // council will resume deposits to module const newBlock = await tempProvider.getBlock('latest'); - const newMeta = mockedMeta(newBlock); + const newMeta = mockedMeta(newBlock, newBlock.hash); mockedKeysApiOperators( keysApiService, @@ -1126,7 +1171,7 @@ describe('ganache e2e tests', () => { // mocked curated module const stakingModule = mockedModule(currentBlock); - const meta = mockedMeta(currentBlock); + const meta = mockedMeta(currentBlock, currentBlock.hash); mockedKeysApiOperators( keysApiService, @@ -1168,7 +1213,7 @@ describe('ganache e2e tests', () => { }); // mocked curated module - const newMeta = mockedMeta(newBlock); + const newMeta = mockedMeta(newBlock, newBlock.hash); const newStakingModule = mockedModule(newBlock, 6047); mockedKeysApiOperators( From a0e457df71c28abd20b2ad4b5d7537d595044f70 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Fri, 22 Dec 2023 12:56:36 +0400 Subject: [PATCH 34/37] fix: build --- src/keys-api/interfaces/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/keys-api/interfaces/index.ts b/src/keys-api/interfaces/index.ts index da3cc9df..e5384860 100644 --- a/src/keys-api/interfaces/index.ts +++ b/src/keys-api/interfaces/index.ts @@ -1,4 +1,2 @@ -export type { SRModuleKeysResponse } from './SRModuleKeysResponse'; -export type { SRModuleListResponse } from './SRModuleListResponse'; export type { SRModule } from './SRModule'; export type { KeyListResponse } from './KeyListResponse'; From 74bbee096fe4543039d79fad967658d75440f20a Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Sun, 24 Dec 2023 20:51:58 +0400 Subject: [PATCH 35/37] fix: refactoring and tests --- .../guardian-metrics.service.ts | 2 +- .../staking-module-guard.module.ts | 1 - .../staking-module-guard.service.ts | 17 +---- .../staking-module-guard.spec.ts | 1 - src/staking-router/staking-router.service.ts | 4 +- src/staking-router/vetted-keys.spec.ts | 65 +++++++++++++++++++ src/staking-router/vetted-keys.ts | 1 + 7 files changed, 69 insertions(+), 22 deletions(-) create mode 100644 src/staking-router/vetted-keys.spec.ts diff --git a/src/guardian/guardian-metrics/guardian-metrics.service.ts b/src/guardian/guardian-metrics/guardian-metrics.service.ts index 487f902f..230b3cc9 100644 --- a/src/guardian/guardian-metrics/guardian-metrics.service.ts +++ b/src/guardian/guardian-metrics/guardian-metrics.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { VerifiedDepositEvent } from 'contracts/deposit'; import { BlockData, StakingModuleData } from '../interfaces'; import { InjectMetric } from '@willsoto/nestjs-prometheus'; diff --git a/src/guardian/staking-module-guard/staking-module-guard.module.ts b/src/guardian/staking-module-guard/staking-module-guard.module.ts index 505a3720..8ad5478f 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.module.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.module.ts @@ -1,7 +1,6 @@ import { Module } from '@nestjs/common'; import { SecurityModule } from 'contracts/security'; -import { LidoModule } from 'contracts/lido'; import { StakingRouterModule } from 'staking-router'; import { GuardianMetricsModule } from '../guardian-metrics'; 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 b8447293..7359f182 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -12,7 +12,6 @@ import { GuardianMessageService } from '../guardian-message'; import { StakingRouterService } from 'staking-router'; import { KeysValidationService } from 'guardian/keys-validation/keys-validation.service'; import { performance } from 'perf_hooks'; -import { InconsistentLastChangedBlockHash } from 'common/custom-errors'; @Injectable() export class StakingModuleGuardService { @@ -246,7 +245,7 @@ export class StakingModuleGuardService { depositedPubkeys, ); - this.isEqualLastChangedBlockHash( + this.stakingRouterService.isEqualLastChangedBlockHash( lastChangedBlockHash, meta.elBlockSnapshot.lastChangedBlockHash, ); @@ -254,20 +253,6 @@ export class StakingModuleGuardService { return data.filter((key) => key.used); } - private isEqualLastChangedBlockHash( - firstRequestHash: string, - secondRequestHash: string, - ) { - if (firstRequestHash !== secondRequestHash) { - const error = - 'Since the last request, data in Kapi has been updated. This may result in inconsistencies between the data from two separate requests.'; - - this.logger.error(error, { firstRequestHash, secondRequestHash }); - - throw new InconsistentLastChangedBlockHash(); - } - } - /** * Handles the situation when keys have previously deposited copies * @param blockData - collected data from the current block 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 21fd3a3c..ee1ee472 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -29,7 +29,6 @@ import { InconsistentLastChangedBlockHash } from 'common/custom-errors'; jest.mock('../../transport/stomp/stomp.client'); const TEST_MODULE_ID = 1; -const lidoWC = '0x12'; const stakingModuleData = { nonce: 0, diff --git a/src/staking-router/staking-router.service.ts b/src/staking-router/staking-router.service.ts index d32e9e72..d9476e6a 100644 --- a/src/staking-router/staking-router.service.ts +++ b/src/staking-router/staking-router.service.ts @@ -12,8 +12,6 @@ import { InconsistentLastChangedBlockHash } from 'common/custom-errors'; @Injectable() export class StakingRouterService { - // protected stakingRouterCache: Record = {}; - constructor( @Inject(WINSTON_MODULE_NEST_PROVIDER) protected logger: LoggerService, protected readonly config: Configuration, @@ -55,7 +53,7 @@ export class StakingRouterService { return { stakingModulesData, blockHash, blockNumber }; } - private isEqualLastChangedBlockHash( + public isEqualLastChangedBlockHash( firstRequestHash: string, secondRequestHash: string, ) { diff --git a/src/staking-router/vetted-keys.spec.ts b/src/staking-router/vetted-keys.spec.ts new file mode 100644 index 00000000..caa6360e --- /dev/null +++ b/src/staking-router/vetted-keys.spec.ts @@ -0,0 +1,65 @@ +import { getVettedUnusedKeys } from './vetted-keys'; // Replace with your actual module path + +describe('getVettedUnusedKeys', () => { + test('should return an empty array for empty input arrays', () => { + expect(getVettedUnusedKeys([], [])).toEqual([]); + }); + + test('should correctly filter and sort keys for multiple operators', () => { + // totalSigningKeys is used here only to describe cases, + // we don't use is in algorithm in function to determine vetted unused keys + const operators = [ + // 2 vetted unused keys, have some available limit + { index: 1, stakingLimit: 3, usedSigningKeys: 1, totalSigningKeys: 4 }, + // 1 vetted unused key, have some available limit + { index: 2, stakingLimit: 1, usedSigningKeys: 0, totalSigningKeys: 2 }, + // 0 vetted unused keys, staking limit wasnt increased + { index: 3, stakingLimit: 0, usedSigningKeys: 0, totalSigningKeys: 1 }, + // 0 vetted unused keys, staking limit exceeded have one used key + { index: 4, stakingLimit: 1, usedSigningKeys: 1, totalSigningKeys: 2 }, + // 0 vetted unused keys, have staking limit, but don't have keys to deposit + { index: 5, stakingLimit: 1, usedSigningKeys: 0, totalSigningKeys: 0 }, + ] as any; + + const unusedKeys = [ + // operator 1 unused keys + { operatorIndex: 1, index: 1 }, + { operatorIndex: 1, index: 0 }, + { operatorIndex: 1, index: 2 }, + // operator 2 unused keys + { operatorIndex: 2, index: 0 }, + { operatorIndex: 2, index: 1 }, + // operator 3 unused keys + { operatorIndex: 3, index: 0 }, + // operator 4 unused keys + { operatorIndex: 4, index: 0 }, + ] as any; + + const expected = [ + { operatorIndex: 1, index: 0 }, + { operatorIndex: 1, index: 1 }, + { operatorIndex: 2, index: 0 }, + ]; + const result = getVettedUnusedKeys(operators, unusedKeys); + expect(result.length).toEqual(expected.length); + expect(getVettedUnusedKeys(operators, unusedKeys)).toEqual(expected); + }); + + test('should correctly sort keys within operators', () => { + const operators = [ + { index: 1, stakingLimit: 4, usedSigningKeys: 1, totalSigningKeys: 5 }, + ] as any; + const unusedKeys = [ + { operatorIndex: 1, index: 3 }, + { operatorIndex: 1, index: 1 }, + { operatorIndex: 1, index: 2 }, + { operatorIndex: 1, index: 4 }, + ] as any; + const expected = [ + { operatorIndex: 1, index: 1 }, + { operatorIndex: 1, index: 2 }, + { operatorIndex: 1, index: 3 }, + ]; + expect(getVettedUnusedKeys(operators, unusedKeys)).toEqual(expected); + }); +}); diff --git a/src/staking-router/vetted-keys.ts b/src/staking-router/vetted-keys.ts index 42ee1257..cfdc0671 100644 --- a/src/staking-router/vetted-keys.ts +++ b/src/staking-router/vetted-keys.ts @@ -9,6 +9,7 @@ export function getVettedUnusedKeys( const operatorKeys = unusedKeys .filter((key) => key.operatorIndex === operator.index) .sort((a, b) => a.index - b.index) + // stakingLimit limit cant be less than usedSigningKeys .slice(0, operator.stakingLimit - operator.usedSigningKeys); return operatorKeys; From e4bddabe5fdd881c71c3ada826cbf6ac8d9cb294 Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 25 Dec 2023 12:54:41 +0400 Subject: [PATCH 36/37] fix: condition for running invalid keys chek and tests --- .../staking-module-guard.service.ts | 7 +- .../staking-module-guard.spec.ts | 86 +++++++++++++++---- .../staking-router-storage.service.ts | 54 ++++++++++++ test/manifest.e2e-spec.ts | 24 ++++++ 4 files changed, 151 insertions(+), 20 deletions(-) create mode 100644 src/staking-router/staking-router-storage.service.ts 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 7359f182..11c04aa9 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.service.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.service.ts @@ -342,7 +342,8 @@ export class StakingModuleGuardService { if ( !lastContractsState || - currentContractState.nonce !== lastContractsState.nonce + currentContractState.lastChangedBlockHash !== + lastContractsState.lastChangedBlockHash ) { const invalidKeys = await this.getInvalidKeys( stakingModuleData, @@ -422,10 +423,8 @@ export class StakingModuleGuardService { if (!firstState || !secondState) return false; if (firstState.depositRoot !== secondState.depositRoot) return false; - // Check if the nonce values are different. A difference in nonce implies a state change. - if (firstState.nonce !== secondState.nonce) return false; // If the nonce is unchanged, the state might still have changed. - // Therefore, we also need to compare the 'lastChangedBlockHash'. + // Therefore, we need to compare the 'lastChangedBlockHash' instead // It's important to note that it's not possible for the nonce to be different // while having the same 'lastChangedBlockHash'. if (firstState.lastChangedBlockHash !== secondState.lastChangedBlockHash) 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 ee1ee472..5670add9 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -25,6 +25,8 @@ import { vettedKeysWithoutDuplicates, } from './keys.fixtures'; import { InconsistentLastChangedBlockHash } from 'common/custom-errors'; +import { KeysValidationModule } from 'guardian/keys-validation/keys-validation.module'; +import { KeysValidationService } from 'guardian/keys-validation/keys-validation.service'; jest.mock('../../transport/stomp/stomp.client'); @@ -52,6 +54,8 @@ describe('StakingModuleGuardService', () => { let stakingModuleGuardService: StakingModuleGuardService; let guardianMessageService: GuardianMessageService; let stakingRouterService: StakingRouterService; + let keysValidationService: KeysValidationService; + let findInvalidKeys: jest.SpyInstance; beforeEach(async () => { const moduleRef = await Test.createTestingModule({ @@ -67,6 +71,7 @@ describe('StakingModuleGuardService', () => { GuardianMessageModule, RepositoryModule, PrometheusModule, + KeysValidationModule, ], }).compile(); @@ -75,6 +80,8 @@ describe('StakingModuleGuardService', () => { stakingModuleGuardService = moduleRef.get(StakingModuleGuardService); guardianMessageService = moduleRef.get(GuardianMessageService); stakingRouterService = moduleRef.get(StakingRouterService); + keysValidationService = moduleRef.get(KeysValidationService); + findInvalidKeys = jest.spyOn(keysValidationService, 'findInvalidKeys'); jest.spyOn(loggerService, 'log').mockImplementation(() => undefined); jest.spyOn(loggerService, 'warn').mockImplementation(() => undefined); @@ -344,6 +351,69 @@ 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); + + const mockIsSameContractsStates = jest.spyOn( + stakingModuleGuardService, + 'isSameContractsStates', + ); + + const mockSignDepositData = jest + .spyOn(securityService, 'signDepositData') + .mockImplementation(async () => signature); + + await stakingModuleGuardService.handleCorrectKeys( + { + ...stakingModuleData, + lastChangedBlockHash: '0x1', + unusedKeys: [], + vettedUnusedKeys: [], + }, + blockData, + ); + + expect(findInvalidKeys).toBeCalledTimes(1); + + findInvalidKeys.mockClear(); + + await stakingModuleGuardService.handleCorrectKeys( + { + ...stakingModuleData, + lastChangedBlockHash: '0x1', + unusedKeys: [], + vettedUnusedKeys: [], + }, + blockData, + ); + + expect(findInvalidKeys).toBeCalledTimes(0); + findInvalidKeys.mockClear(); + + await stakingModuleGuardService.handleCorrectKeys( + { + ...stakingModuleData, + lastChangedBlockHash: '0x2', + unusedKeys: [], + vettedUnusedKeys: [], + }, + blockData, + ); + + expect(findInvalidKeys).toBeCalledTimes(1); + + expect(mockIsSameContractsStates).toBeCalledTimes(3); + const { results } = mockIsSameContractsStates.mock; + expect(results[0].value).toBeFalsy(); + expect(results[1].value).toBeTruthy(); + expect(results[2].value).toBeFalsy(); + + expect(mockSendMessageFromGuardian).toBeCalledTimes(2); + expect(mockSignDepositData).toBeCalledTimes(2); + }); }); describe('excludeEligibleIntersections', () => { @@ -511,22 +581,6 @@ describe('StakingModuleGuardService', () => { expect(result).toBeFalsy(); }); - it('should return false if nonce are different', () => { - // It's important to note that it's not possible for the nonce to be different - // while having the same 'lastChangedBlockHash'. - const state = { - depositRoot: '0x1', - nonce: 1, - blockNumber: 100, - lastChangedBlockHash: 'hash', - }; - const result = stakingModuleGuardService.isSameContractsStates(state, { - ...state, - nonce: 2, - }); - expect(result).toBeFalsy(); - }); - it('should return false if lastChangedBlockHash are different', () => { // It's important to note that it's not possible for the nonce to be different // while having the same 'lastChangedBlockHash'. diff --git a/src/staking-router/staking-router-storage.service.ts b/src/staking-router/staking-router-storage.service.ts new file mode 100644 index 00000000..efd83281 --- /dev/null +++ b/src/staking-router/staking-router-storage.service.ts @@ -0,0 +1,54 @@ +import { Injectable, LoggerService, Inject } from '@nestjs/common'; +import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; +import { Configuration } from 'common/config'; +import { KeysApiService } from 'keys-api/keys-api.service'; +import { StakingModuleData } from 'guardian'; +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 { SROperatorListWithModule } from 'keys-api/interfaces/SROperatorListWithModule'; +import { ELBlockSnapshot } from 'keys-api/interfaces/ELBlockSnapshot'; + +type UpdatedData = { + operatosByModules: SROperatorListWithModule; + unusedKeys: + meta: Meta; + +} + +@Injectable() +export class StakingRouterService { + constructor( + @Inject(WINSTON_MODULE_NEST_PROVIDER) protected logger: LoggerService, + protected readonly config: Configuration, + protected readonly keysApiService: KeysApiService, + ) {} + + protected stakingRouterCache: Record = {}; + + + async fetchData() { + // fetch operators and modules + const { data: operatorsByModules, meta } = + await this.keysApiService.getOperatorListWithModule(); + + const result = operatorsByModules.filter(({ module: stakingModule }) => { + const cacheLastChangedBlockHash = + this.stakingRouterCache[stakingModule.id].lastChangedBlockHash; + + return cacheLastChangedBlockHash !== stakingModule.lastChangedBlockHash + ? true + : false; + }); + + } + + async fetchOperatorsAndModules() { + + } + + +} \ No newline at end of file diff --git a/test/manifest.e2e-spec.ts b/test/manifest.e2e-spec.ts index 1b3d59cf..470d2053 100644 --- a/test/manifest.e2e-spec.ts +++ b/test/manifest.e2e-spec.ts @@ -80,6 +80,7 @@ import { GanacheProviderModule } from '../src/provider'; import { BlsService } from '../src/bls'; import { GuardianMessageService } from '../src/guardian/guardian-message'; +import { KeyValidatorInterface } from '@lido-nestjs/key-validation'; // Mock rabbit straight away jest.mock('../src/transport/stomp/stomp.client.ts'); @@ -99,6 +100,9 @@ describe('ganache e2e tests', () => { let sendDepositMessage: jest.SpyInstance; let sendPauseMessage: jest.SpyInstance; + let keyValidator: KeyValidatorInterface; + let validateKeys: jest.SpyInstance; + beforeEach(async () => { server = makeServer(FORK_BLOCK, CHAIN_ID, UNLOCKED_ACCOUNTS); await server.listen(GANACHE_PORT); @@ -147,6 +151,7 @@ describe('ganache e2e tests', () => { lidoService = moduleRef.get(LidoService); depositService = moduleRef.get(DepositService); guardianMessageService = moduleRef.get(GuardianMessageService); + keyValidator = moduleRef.get(KeyValidatorInterface); // Initializing needed service instead of the whole app blsService = moduleRef.get(BlsService); @@ -165,6 +170,8 @@ describe('ganache e2e tests', () => { sendPauseMessage = jest .spyOn(guardianMessageService, 'sendPauseMessage') .mockImplementation(() => Promise.resolve()); + + validateKeys = jest.spyOn(keyValidator, 'validateKeys'); }); describe('node checks', () => { @@ -1196,6 +1203,18 @@ describe('ganache e2e tests', () => { await guardianService.handleNewBlock(); + expect(validateKeys).toBeCalledTimes(1); + expect(validateKeys).toBeCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + key: toHexString(pk), + // just some random sign + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + }), + ]), + ); + expect(sendDepositMessage).toBeCalledTimes(0); expect(sendPauseMessage).toBeCalledTimes(0); @@ -1226,8 +1245,13 @@ describe('ganache e2e tests', () => { // list of keys for /keys?used=false mock mockedKeysApiUnusedKeys(keysApiService, [keyWithWrongSign], newMeta); + validateKeys.mockClear(); + await guardianService.handleNewBlock(); + expect(validateKeys).toBeCalledTimes(1); + expect(validateKeys).toBeCalledWith([]); + // should found invalid key and skip again // on this iteration cache will be used expect(sendDepositMessage).toBeCalledTimes(0); From 483f6f413113a0f1cd6b304b145b8a9e190e903e Mon Sep 17 00:00:00 2001 From: Anna Mukharram Date: Mon, 25 Dec 2023 12:59:20 +0400 Subject: [PATCH 37/37] fix: remov --- .../staking-router-storage.service.ts | 54 ------------------- 1 file changed, 54 deletions(-) delete mode 100644 src/staking-router/staking-router-storage.service.ts diff --git a/src/staking-router/staking-router-storage.service.ts b/src/staking-router/staking-router-storage.service.ts deleted file mode 100644 index efd83281..00000000 --- a/src/staking-router/staking-router-storage.service.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Injectable, LoggerService, Inject } from '@nestjs/common'; -import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston'; -import { Configuration } from 'common/config'; -import { KeysApiService } from 'keys-api/keys-api.service'; -import { StakingModuleData } from 'guardian'; -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 { SROperatorListWithModule } from 'keys-api/interfaces/SROperatorListWithModule'; -import { ELBlockSnapshot } from 'keys-api/interfaces/ELBlockSnapshot'; - -type UpdatedData = { - operatosByModules: SROperatorListWithModule; - unusedKeys: - meta: Meta; - -} - -@Injectable() -export class StakingRouterService { - constructor( - @Inject(WINSTON_MODULE_NEST_PROVIDER) protected logger: LoggerService, - protected readonly config: Configuration, - protected readonly keysApiService: KeysApiService, - ) {} - - protected stakingRouterCache: Record = {}; - - - async fetchData() { - // fetch operators and modules - const { data: operatorsByModules, meta } = - await this.keysApiService.getOperatorListWithModule(); - - const result = operatorsByModules.filter(({ module: stakingModule }) => { - const cacheLastChangedBlockHash = - this.stakingRouterCache[stakingModule.id].lastChangedBlockHash; - - return cacheLastChangedBlockHash !== stakingModule.lastChangedBlockHash - ? true - : false; - }); - - } - - async fetchOperatorsAndModules() { - - } - - -} \ No newline at end of file