diff --git a/README.md b/README.md index ddeecaf9..f47cdc31 100644 --- a/README.md +++ b/README.md @@ -294,6 +294,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/package.json b/package.json index 0f16f2f1..3f16972d 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", @@ -56,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/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/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/contracts/deposit/deposit.service.ts b/src/contracts/deposit/deposit.service.ts index d6402b52..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'; @@ -36,7 +35,6 @@ export class DepositService { private blsService: BlsService, ) {} - @OneAtTime() public async handleNewBlock(blockNumber: number): Promise { if (blockNumber % DEPOSIT_EVENTS_CACHE_UPDATE_BLOCK_RATE !== 0) return; 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-metrics/guardian-metrics.service.ts b/src/guardian/guardian-metrics/guardian-metrics.service.ts index 54801286..230b3cc9 100644 --- a/src/guardian/guardian-metrics/guardian-metrics.service.ts +++ b/src/guardian/guardian-metrics/guardian-metrics.service.ts @@ -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/guardian.service.spec.ts b/src/guardian/guardian.service.spec.ts index 16038281..adfca110 100644 --- a/src/guardian/guardian.service.spec.ts +++ b/src/guardian/guardian.service.spec.ts @@ -24,29 +24,94 @@ import { mockRepository } from 'contracts/repository/repository.mock'; jest.mock('../transport/stomp/stomp.client'); -const TEST_MODULE_ID = 1; +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 stakingModuleResponse = { - data: [ +const vettedKeysResponse = { + blockHash: 'some_hash', + blockNumber: 1, + vettedKeys, + stakingModulesData: [ + { + blockHash: 'some_hash', + lastChangedBlockHash: 'some_hash', + unusedKeys: [ + '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + ], + vettedUnusedKeys: [ + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 100, + }, + { + key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + depositSignature: + '0x898ac7072aa26d983f9ece384c4037966dde614b75dddf982f6a415f3107cb2569b96f6d1c44e608a250ac4bbe908df51473f0de2cf732d283b07d88f3786893124967b8697a8b93d31976e7ac49ab1e568f98db0bbb13384477e8357b6d7e9b', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 101, + }, + ], + nonce: 0, + stakingModuleId: 2, + stakingModuleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + }, { + blockHash: 'some_hash', + lastChangedBlockHash: 'some_hash', + unusedKeys: [ + '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + ], + vettedUnusedKeys: [ + { + key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 5, + }, + ], nonce: 0, - 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', () => { @@ -66,7 +131,6 @@ describe('GuardianService', () => { MockProviderModule.forRoot(), LoggerModule, PrometheusModule, - GuardianModule, RepositoryModule, DepositModule, @@ -101,9 +165,9 @@ describe('GuardianService', () => { }); it('should exit if the previous call is not completed', async () => { - const getStakingModulesMock = jest - .spyOn(stakingRouterService, 'getStakingModules') - .mockImplementation(async () => stakingModuleResponse); + jest + .spyOn(stakingRouterService, 'getStakingModulesData') + .mockImplementation(async () => vettedKeysResponse); const getBlockGuardServiceMock = jest .spyOn(blockGuardService, 'isNeedToProcessNewState') @@ -114,7 +178,6 @@ describe('GuardianService', () => { guardianService.handleNewBlock(), ]); - expect(getStakingModulesMock).toBeCalledTimes(1); expect(getBlockGuardServiceMock).toBeCalledTimes(1); }); }); diff --git a/src/guardian/guardian.service.ts b/src/guardian/guardian.service.ts index 34d13b6e..5eb301a1 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, @@ -22,6 +21,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 { @@ -36,8 +36,6 @@ export class GuardianService implements OnModuleInit { private depositService: DepositService, private securityService: SecurityService, - private providerService: ProviderService, - private stakingRouterService: StakingRouterService, private blockGuardService: BlockGuardService, @@ -96,10 +94,8 @@ 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(); + const { blockHash, blockNumber, stakingModulesData } = + await this.stakingRouterService.getStakingModulesData(); await this.repositoryService.initCachedContracts({ blockHash }); @@ -119,12 +115,15 @@ export class GuardianService implements OnModuleInit { return; } + const stakingModulesCount = stakingModulesData.length; + this.logger.log('Staking modules loaded', { - modulesCount: stakingModules.length, + modulesCount: stakingModulesCount, }); 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, @@ -136,14 +135,24 @@ export class GuardianService implements OnModuleInit { blockHash: blockData.blockHash, }); - await Promise.all( - stakingModules.map(async (stakingRouterModule) => { - const stakingModuleData = - await this.stakingModuleGuardService.getStakingRouterModuleData( - stakingRouterModule, - blockHash, - ); + const modulesIdWithDuplicateKeys: number[] = + this.stakingModuleGuardService.getModulesIdsWithDuplicatedVettedUnusedKeys( + stakingModulesData, + blockData, + ); + const stakingModulesWithoutDuplicates: StakingModuleData[] = + this.stakingModuleGuardService.excludeModulesWithDuplicatedKeys( + stakingModulesData, + modulesIdWithDuplicateKeys, + ); + + this.logger.log('Staking modules without duplicates', { + modulesCount: stakingModulesWithoutDuplicates.length, + }); + + await Promise.all( + stakingModulesWithoutDuplicates.map(async (stakingModuleData) => { await this.stakingModuleGuardService.checkKeysIntersections( stakingModuleData, blockData, @@ -157,7 +166,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/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/interfaces/staking-module.interface.ts b/src/guardian/interfaces/staking-module.interface.ts index fe08e7bd..a600a67a 100644 --- a/src/guardian/interfaces/staking-module.interface.ts +++ b/src/guardian/interfaces/staking-module.interface.ts @@ -1,7 +1,10 @@ +import { RegistryKey } from 'keys-api/interfaces/RegistryKey'; + export interface StakingModuleData { blockHash: string; - isDepositsPaused: boolean; unusedKeys: string[]; + 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/keys-validation/constants.ts b/src/guardian/keys-validation/constants.ts new file mode 100644 index 00000000..1f71acf3 --- /dev/null +++ b/src/guardian/keys-validation/constants.ts @@ -0,0 +1,2 @@ +// TODO: put in config +export const KEYS_LRU_CACHE_SIZE = 32000; 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..dc6b9951 --- /dev/null +++ b/src/guardian/keys-validation/keys-validation.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { KeyValidatorModule } from '@lido-nestjs/key-validation'; +import { KeysValidationService } from './keys-validation.service'; + +@Module({ + imports: [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..00ba0309 --- /dev/null +++ b/src/guardian/keys-validation/keys-validation.service.ts @@ -0,0 +1,136 @@ +import { Inject, Injectable, LoggerService } from '@nestjs/common'; +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 { 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; + 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 }); + } + + /** + * + * Return list of invalid keys + */ + public async findInvalidKeys( + vettedKeys: RegistryKey[], + withdrawalCredentials: string, + ): Promise<{ key: string; depositSignature: string }[]> { + const forkVersion: Uint8Array = await this.forkVersion(); + + 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.updateCache(validatedKeys); + + // this list will not include invalid keys from cache + const invalidKeysFromCurrentValidation = validatedKeys + .filter(([, isValid]) => !isValid) + .map(([key]) => ({ + key: key.key, + 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 [ + ...invalidKeysFromCurrentValidation, + ...unchangedAndInvalidKeysValues, + ]; + } + + private divideKeys(vettedKeys: RegistryKey[]): { + keysNeedingValidation: RegistryKey[]; + unchangedAndInvalidKeys: RegistryKey[]; + } { + const keysNeedingValidation: RegistryKey[] = []; + const unchangedAndInvalidKeys: RegistryKey[] = []; + + vettedKeys.forEach((key) => { + const cachedEntry = this.keysCache.get(key.key); + + if (!cachedEntry || cachedEntry.signature !== key.depositSignature) { + keysNeedingValidation.push(key); + } else if (!cachedEntry.isValid) { + unchangedAndInvalidKeys.push(key); + } + }); + + return { keysNeedingValidation, unchangedAndInvalidKeys }; + } + + private toDepositData( + key: RegistryKey, + withdrawalCredentials: string, + forkVersion: Uint8Array, + ): DepositData { + return { + key: key.key, + depositSignature: key.depositSignature, + withdrawalCredentials: bufferFromHexString(withdrawalCredentials), + genesisForkVersion: Buffer.from(forkVersion.buffer), + }; + } + + private async forkVersion(): Promise { + const chainId = await this.provider.getChainId(); + const forkVersion = GENESIS_FORK_VERSION_BY_CHAIN_ID[chainId]; + + if (!forkVersion) { + throw new Error(`Unsupported chain id ${chainId}`); + } + + return forkVersion; + } + + private async updateCache(validatedKeys: [Key & DepositData, boolean][]) { + validatedKeys.forEach(([key, isValid]) => + this.keysCache.set(key.key, { signature: key.depositSignature, isValid }), + ); + } +} 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/keys.fixtures.ts b/src/guardian/staking-module-guard/keys.fixtures.ts new file mode 100644 index 00000000..e69cd5bc --- /dev/null +++ b/src/guardian/staking-module-guard/keys.fixtures.ts @@ -0,0 +1,254 @@ +export const vettedKeysDuplicatesAcrossModules: any = [ + { + stakingModuleId: 100, + vettedUnusedKeys: [ + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 100, + }, + { + key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + depositSignature: + '0x898ac7072aa26d983f9ece384c4037966dde614b75dddf982f6a415f3107cb2569b96f6d1c44e608a250ac4bbe908df51473f0de2cf732d283b07d88f3786893124967b8697a8b93d31976e7ac49ab1e568f98db0bbb13384477e8357b6d7e9b', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 101, + }, + ], + }, + { + stakingModuleId: 102, + vettedUnusedKeys: [ + { + 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, + vettedUnusedKeys: [ + { + key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: 'another_module', + index: 5, + }, + ], + }, +]; + +export const vettedKeysDuplicatesAcrossOneModule: any = [ + { + stakingModuleId: 100, + vettedUnusedKeys: [ + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 100, + }, + { + key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + depositSignature: + '0x898ac7072aa26d983f9ece384c4037966dde614b75dddf982f6a415f3107cb2569b96f6d1c44e608a250ac4bbe908df51473f0de2cf732d283b07d88f3786893124967b8697a8b93d31976e7ac49ab1e568f98db0bbb13384477e8357b6d7e9b', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 101, + }, + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 102, + }, + ], + }, + { + stakingModuleId: 102, + vettedUnusedKeys: [ + { + key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 5, + }, + ], + }, + + { + stakingModuleId: 103, + vettedUnusedKeys: [ + { + key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: 'another_module', + index: 5, + }, + ], + }, +]; + +export const vettedKeysDuplicatesAcrossOneModuleAndFew: any = [ + { + stakingModuleId: 100, + vettedUnusedKeys: [ + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 100, + }, + { + key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + depositSignature: + '0x898ac7072aa26d983f9ece384c4037966dde614b75dddf982f6a415f3107cb2569b96f6d1c44e608a250ac4bbe908df51473f0de2cf732d283b07d88f3786893124967b8697a8b93d31976e7ac49ab1e568f98db0bbb13384477e8357b6d7e9b', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 101, + }, + ], + }, + { + stakingModuleId: 102, + vettedUnusedKeys: [ + { + 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, + vettedUnusedKeys: [ + { + key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: 'another_module', + index: 5, + }, + ], + }, +]; + +export const vettedKeysWithoutDuplicates: any = [ + { + stakingModuleId: 100, + vettedUnusedKeys: [ + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 100, + }, + { + key: '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + depositSignature: + '0x898ac7072aa26d983f9ece384c4037966dde614b75dddf982f6a415f3107cb2569b96f6d1c44e608a250ac4bbe908df51473f0de2cf732d283b07d88f3786893124967b8697a8b93d31976e7ac49ab1e568f98db0bbb13384477e8357b6d7e9b', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 101, + }, + ], + }, + + { + stakingModuleId: 102, + vettedUnusedKeys: [ + { + key: '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 5, + }, + ], + }, + + { + stakingModuleId: 103, + vettedUnusedKeys: [ + { + key: '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + depositSignature: + '0xb024b67a2f6c579213529e143bd4ebb81c5a2dc385cb526de4a816c8fe0317ebfb38369b08622e9f27e62cce2811679a13a459d4e9a8d7bd00080c36b359c1ca03bdcf4a0fcbbc2e18fe9923d8c4edb503ade58bdefe690760611e3738d5e64f', + operatorIndex: 28, + used: false, + moduleAddress: 'another_module', + index: 5, + }, + ], + }, +]; 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..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,21 +1,21 @@ 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'; 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 6590df66..11c04aa9 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'; @@ -11,7 +10,8 @@ import { GuardianMetricsService } from '../guardian-metrics'; import { GuardianMessageService } from '../guardian-message'; import { StakingRouterService } from 'staking-router'; -import { SRModule } from 'keys-api/interfaces'; +import { KeysValidationService } from 'guardian/keys-validation/keys-validation.service'; +import { performance } from 'perf_hooks'; @Injectable() export class StakingModuleGuardService { @@ -20,50 +20,95 @@ 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 = {}; - public async getStakingRouterModuleData( - stakingRouterModule: SRModule, - blockHash: string, - ): Promise { - const { - data: { - keys, - module: { nonce }, - }, - } = await this.stakingRouterService.getStakingModuleUnusedKeys( - blockHash, - stakingRouterModule, - ); + /** + * @returns List of staking modules id with duplicates + */ + public getModulesIdsWithDuplicatedVettedUnusedKeys( + stakingModulesData: StakingModuleData[], + blockData: BlockData, + ): 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(({ vettedUnusedKeys, stakingModuleId }) => { + // check module keys on duplicates across all modules + vettedUnusedKeys.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); + } + } + }); + }); - const isDepositsPaused = await this.securityService.isDepositsPaused( - stakingRouterModule.id, - { - blockHash, - }, - ); + if (modulesWithDuplicatedKeysSet.size) { + const moduleAddressesWithDuplicatesList: number[] = Array.from( + modulesWithDuplicatedKeysSet, + ); + this.logger.error('Found duplicated vetted keys'); + this.logger.log('Duplicated keys', { + blockHash: blockData.blockHash, + duplicatedKeys: Array.from(duplicatedKeys).map(([key, innerMap]) => ({ + key: key, + stakingModuleIds: Array.from(innerMap.keys()), + })), + moduleAddressesWithDuplicates: moduleAddressesWithDuplicatesList, + }); - return { - nonce, - unusedKeys: keys.map((srKey) => srKey.key), - isDepositsPaused, - stakingModuleId: stakingRouterModule.id, - blockHash, - }; + this.guardianMetricsService.incrDuplicatedVettedUnusedKeysEventCounter(); + return moduleAddressesWithDuplicatesList; + } + + return []; + } + + private hasDuplicateKeys(stakingModules: Map): boolean { + const moduleCounts = Array.from(stakingModules.values()); + + return stakingModules.size > 1 || moduleCounts[0] > 1; + } + + public excludeModulesWithDuplicatedKeys( + stakingModulesData: StakingModuleData[], + modulesIdWithDuplicateKeys: number[], + ): StakingModuleData[] { + return stakingModulesData.filter( + ({ stakingModuleId }) => + !modulesIdWithDuplicateKeys.includes(stakingModuleId), + ); } /** * 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, @@ -76,10 +121,14 @@ 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; this.guardianMetricsService.collectIntersectionsMetrics( @@ -88,7 +137,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 +152,19 @@ export class StakingModuleGuardService { if (isFilteredIntersectionsFound) { await this.handleKeysIntersections(stakingModuleData, blockData); } else { + // it could throw error if kapi returned old data + const usedKeys = await this.findAlreadyDepositedKeys( + stakingModuleData.lastChangedBlockHash, + validIntersections, + ); + + // 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.guardianMetricsService.incrDuplicatedUsedKeysEventCounter(); + return; + } + await this.handleCorrectKeys(stakingModuleData, blockData); } } @@ -131,6 +200,11 @@ export class StakingModuleGuardService { return intersections; } + public excludeInvalidDeposits(intersections: VerifiedDepositEvent[]) { + // Exclude deposits with invalid signature over the deposit data + return intersections.filter(({ valid }) => valid); + } + /** * Excludes invalid deposits and deposits with Lido WC from intersections * @param intersections - list of deposits with keys that were deposited earlier @@ -138,22 +212,45 @@ 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 []; - // Exclude deposits with Lido withdrawal credentials - const { blockHash } = blockData; - const lidoWC = await this.lidoService.getWithdrawalCredentials({ - blockHash, - }); - const attackIntersections = validIntersections.filter( - (deposit) => deposit.wc !== lidoWC, + return validIntersections.filter( + (deposit) => deposit.wc !== blockData.lidoWC, + ); + } + + /** + * 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 findAlreadyDepositedKeys( + lastChangedBlockHash: string, + intersectionsWithLidoWC: VerifiedDepositEvent[], + ) { + const depositedPubkeys = intersectionsWithLidoWC.map( + (deposit) => deposit.pubkey, + ); + // if depositedPubkeys == [], /find will return validation error + if (!depositedPubkeys.length) { + return []; + } + + this.logger.log( + 'Found intersections with lido credentials, need to check used duplicated keys', + ); + + const { data, meta } = await this.stakingRouterService.findKeysEntires( + depositedPubkeys, ); - return attackIntersections; + this.stakingRouterService.isEqualLastChangedBlockHash( + lastChangedBlockHash, + meta.elBlockSnapshot.lastChangedBlockHash, + ); + + return data.filter((key) => key.used); } /** @@ -219,9 +316,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]; @@ -233,7 +335,28 @@ export class StakingModuleGuardService { this.lastContractsStateByModuleId[stakingModuleId] = currentContractState; - if (isSameContractsState) return; + if (isSameContractsState) { + this.logger.log("Contract states didn't change"); + return; + } + + if ( + !lastContractsState || + currentContractState.lastChangedBlockHash !== + lastContractsState.lastChangedBlockHash + ) { + const invalidKeys = await this.getInvalidKeys( + stakingModuleData, + blockData, + ); + if (invalidKeys.length) { + this.logger.error( + 'Found invalid keys, will skip deposits until solving problem', + ); + this.guardianMetricsService.incrInvalidKeysEventCounter(); + return; + } + } const signature = await this.securityService.signDepositData( depositRoot, @@ -263,6 +386,30 @@ 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.vettedUnusedKeys.length, + }); + const validationTimeStart = performance.now(); + const invalidKeysList = await this.keysValidationService.findInvalidKeys( + stakingModuleData.vettedUnusedKeys, + 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 @@ -275,7 +422,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 the nonce is unchanged, the state might still have changed. + // 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) + return false; + if ( Math.floor(firstState.blockNumber / GUARDIAN_DEPOSIT_RESIGNING_BLOCKS) !== Math.floor(secondState.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 4f45e7f1..5670add9 100644 --- a/src/guardian/staking-module-guard/staking-module-guard.spec.ts +++ b/src/guardian/staking-module-guard/staking-module-guard.spec.ts @@ -7,20 +7,31 @@ 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 } from 'staking-router'; +import { StakingRouterModule, StakingRouterService } from 'staking-router'; import { GuardianMetricsModule } from '../guardian-metrics'; import { GuardianMessageModule, GuardianMessageService, } from '../guardian-message'; import { StakingModuleGuardService } from './staking-module-guard.service'; +import { StakingModuleData } from 'guardian/interfaces'; +import { + vettedKeysDuplicatesAcrossModules, + vettedKeysDuplicatesAcrossOneModule, + vettedKeysDuplicatesAcrossOneModuleAndFew, + 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'); const TEST_MODULE_ID = 1; + const stakingModuleData = { nonce: 0, type: 'string', @@ -37,12 +48,14 @@ const stakingModuleData = { isDepositsPaused: false, }; -describe('GuardianService', () => { +describe('StakingModuleGuardService', () => { let loggerService: LoggerService; - let lidoService: LidoService; let securityService: SecurityService; let stakingModuleGuardService: StakingModuleGuardService; let guardianMessageService: GuardianMessageService; + let stakingRouterService: StakingRouterService; + let keysValidationService: KeysValidationService; + let findInvalidKeys: jest.SpyInstance; beforeEach(async () => { const moduleRef = await Test.createTestingModule({ @@ -58,14 +71,17 @@ describe('GuardianService', () => { GuardianMessageModule, RepositoryModule, PrometheusModule, + KeysValidationModule, ], }).compile(); - lidoService = moduleRef.get(LidoService); securityService = moduleRef.get(SecurityService); loggerService = moduleRef.get(WINSTON_MODULE_NEST_PROVIDER); 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); @@ -81,7 +97,12 @@ describe('GuardianService', () => { }; const blockData = { unusedKeys, depositedEvents } as any; const matched = stakingModuleGuardService.getKeysIntersections( - { ...stakingModuleData, unusedKeys }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, blockData, ); @@ -98,7 +119,12 @@ describe('GuardianService', () => { }; const blockData = { unusedKeys, depositedEvents } as any; const matched = stakingModuleGuardService.getKeysIntersections( - { ...stakingModuleData, unusedKeys }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, blockData, ); @@ -114,7 +140,12 @@ describe('GuardianService', () => { }; const blockData = { unusedKeys, depositedEvents } as any; const matched = stakingModuleGuardService.getKeysIntersections( - { ...stakingModuleData, unusedKeys }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, blockData, ); @@ -166,6 +197,7 @@ describe('GuardianService', () => { ...currentBlockData, depositedEvents: { ...currentBlockData.depositedEvents, events }, unusedKeys, + lidoWC, }; const mockHandleCorrectKeys = jest @@ -176,28 +208,38 @@ describe('GuardianService', () => { .spyOn(stakingModuleGuardService, 'handleKeysIntersections') .mockImplementation(async () => undefined); - const mockGetWithdrawalCredentials = jest - .spyOn(lidoService, 'getWithdrawalCredentials') - .mockImplementation(async () => lidoWC); + const mockSecurityContractIsDepositsPaused = jest + .spyOn(securityService, 'isDepositsPaused') + .mockImplementation(async () => false); await stakingModuleGuardService.checkKeysIntersections( - { ...stakingModuleData, unusedKeys }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, blockData, ); expect(mockHandleCorrectKeys).not.toBeCalled(); expect(mockHandleKeysIntersections).toBeCalledTimes(1); expect(mockHandleKeysIntersections).toBeCalledWith( - { ...stakingModuleData, unusedKeys }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, 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') @@ -207,17 +249,32 @@ describe('GuardianService', () => { .spyOn(stakingModuleGuardService, 'handleKeysIntersections') .mockImplementation(async () => undefined); + const mockSecurityContractIsDepositsPaused = jest + .spyOn(securityService, 'isDepositsPaused') + .mockImplementation(async () => false); + await stakingModuleGuardService.checkKeysIntersections( - { ...stakingModuleData, unusedKeys }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, blockData, ); expect(mockHandleKeysIntersections).not.toBeCalled(); expect(mockHandleCorrectKeys).toBeCalledTimes(1); expect(mockHandleCorrectKeys).toBeCalledWith( - { ...stakingModuleData, unusedKeys }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys, + vettedUnusedKeys: [], + }, blockData, ); + expect(mockSecurityContractIsDepositsPaused).toBeCalledTimes(1); }); }); @@ -245,11 +302,21 @@ describe('GuardianService', () => { .mockImplementation(async () => signature); await stakingModuleGuardService.handleCorrectKeys( - { ...stakingModuleData, unusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + }, blockData, ); await stakingModuleGuardService.handleCorrectKeys( - { ...stakingModuleData, unusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + }, blockData, ); @@ -272,26 +339,88 @@ describe('GuardianService', () => { .mockImplementation(async () => signature); await stakingModuleGuardService.handleCorrectKeys( - { ...stakingModuleData, unusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + }, blockData, ); 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', () => { 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]; @@ -352,7 +481,12 @@ describe('GuardianService', () => { .mockImplementation(async () => undefined); await stakingModuleGuardService.handleKeysIntersections( - { ...stakingModuleData, unusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + }, blockData, ); @@ -374,7 +508,12 @@ describe('GuardianService', () => { .mockImplementation(async () => undefined); await stakingModuleGuardService.handleKeysIntersections( - { ...stakingModuleData, unusedKeys: [] }, + { + ...stakingModuleData, + lastChangedBlockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + }, blockData, ); @@ -387,7 +526,12 @@ describe('GuardianService', () => { 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 }, @@ -396,7 +540,12 @@ describe('GuardianService', () => { }); 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, @@ -405,7 +554,12 @@ describe('GuardianService', () => { }); 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, @@ -414,7 +568,12 @@ describe('GuardianService', () => { }); 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', @@ -422,13 +581,455 @@ describe('GuardianService', () => { expect(result).toBeFalsy(); }); - it('should return false if nonce are different', () => { - const state = { depositRoot: '0x1', nonce: 1, blockNumber: 100 }; + 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, - nonce: 2, + lastChangedBlockHash: 'new hash', }); expect(result).toBeFalsy(); }); }); + + describe('excludeModulesWithDuplicatedKeys', () => { + const stakingModules: StakingModuleData[] = [ + { + blockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + nonce: 0, + stakingModuleId: 1, + lastChangedBlockHash: '', + }, + { + blockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + nonce: 0, + stakingModuleId: 2, + lastChangedBlockHash: '', + }, + { + blockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + nonce: 0, + stakingModuleId: 3, + lastChangedBlockHash: '', + }, + ]; + + it('should exclude modules', () => { + const moduleIdsWithDuplicateKeys = [2]; + const expectedStakingModules: StakingModuleData[] = [ + { + blockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + nonce: 0, + stakingModuleId: 1, + lastChangedBlockHash: '', + }, + { + blockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + nonce: 0, + stakingModuleId: 3, + lastChangedBlockHash: '', + }, + ]; + + const result = stakingModuleGuardService.excludeModulesWithDuplicatedKeys( + stakingModules, + moduleIdsWithDuplicateKeys, + ); + + expect(result.length).toEqual(2); + expect(result).toEqual(expect.arrayContaining(expectedStakingModules)); + }); + + it('should return list without changes', () => { + const moduleIdsWithDuplicateKeys = [4]; + const expectedStakingModules: StakingModuleData[] = [ + { + blockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + nonce: 0, + stakingModuleId: 1, + lastChangedBlockHash: '', + }, + { + blockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + nonce: 0, + stakingModuleId: 2, + lastChangedBlockHash: '', + }, + { + blockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + nonce: 0, + stakingModuleId: 3, + lastChangedBlockHash: '', + }, + ]; + + const result = stakingModuleGuardService.excludeModulesWithDuplicatedKeys( + stakingModules, + moduleIdsWithDuplicateKeys, + ); + + expect(result.length).toEqual(3); + expect(result).toEqual(expect.arrayContaining(expectedStakingModules)); + }); + + it('should return list without changes if duplicated keys were not found', () => { + const moduleIdsWithDuplicateKeys = []; + const expectedStakingModules: StakingModuleData[] = [ + { + blockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + nonce: 0, + stakingModuleId: 1, + lastChangedBlockHash: '', + }, + { + blockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + nonce: 0, + stakingModuleId: 2, + lastChangedBlockHash: '', + }, + { + blockHash: '', + unusedKeys: [], + vettedUnusedKeys: [], + nonce: 0, + stakingModuleId: 3, + lastChangedBlockHash: '', + }, + ]; + + const result = stakingModuleGuardService.excludeModulesWithDuplicatedKeys( + stakingModules, + moduleIdsWithDuplicateKeys, + ); + + expect(result.length).toEqual(3); + expect(result).toEqual(expect.arrayContaining(expectedStakingModules)); + }); + }); + + describe('getModulesIdsWithDuplicatedVettedUnusedKeys', () => { + const blockData = { blockHash: 'some_hash' } as any; + + it('should found duplicated keys across two module', () => { + const result = + stakingModuleGuardService.getModulesIdsWithDuplicatedVettedUnusedKeys( + vettedKeysDuplicatesAcrossModules, + blockData, + ); + + const addressesOfModulesWithDuplicateKeys = [100, 102]; + + // 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.getModulesIdsWithDuplicatedVettedUnusedKeys( + vettedKeysDuplicatesAcrossOneModule, + blockData, + ); + + const addressesOfModulesWithDuplicateKeys = [100]; + expect(result).toEqual( + expect.arrayContaining(addressesOfModulesWithDuplicateKeys), + ); + expect(result.length).toEqual(1); + }); + + it('should found duplicated keys across one module and few', () => { + const result = + stakingModuleGuardService.getModulesIdsWithDuplicatedVettedUnusedKeys( + vettedKeysDuplicatesAcrossOneModuleAndFew, + blockData, + ); + + const addressesOfModulesWithDuplicateKeys = [100, 102]; + 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.getModulesIdsWithDuplicatedVettedUnusedKeys( + vettedKeysWithoutDuplicates, + blockData, + ); + + const addressesOfModulesWithDuplicateKeys = []; + + expect(result).toEqual( + expect.arrayContaining(addressesOfModulesWithDuplicateKeys), + ); + expect(result.length).toEqual(0); + }); + }); + + describe('findAlreadyDepositedKeys', () => { + // 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, + 'findKeysEntires', + ); + + const result = await stakingModuleGuardService.findAlreadyDepositedKeys( + 'lastHash', + intersectionsWithLidoWC, + ); + + expect(result).toEqual([]); + expect(mockSendMessageFromGuardian).toBeCalledTimes(0); + }); + + it('should return keys list if deposits with lido wc were made by lido', async () => { + const pubkeyWithUsedKey1 = '0x1234'; + const pubkeyWithoutUsedKey = '0x56789'; + const pubkeyWithUsedKey2 = '0x3478'; + const lidoWC = '0x12'; + const intersectionsWithLidoWC = [ + { 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 + .spyOn(stakingRouterService, 'findKeysEntires') + .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, + lastChangedBlockHash: 'lastHash', + }, + }, + })); + + const result = await stakingModuleGuardService.findAlreadyDepositedKeys( + 'lastHash', + intersectionsWithLidoWC, + ); + + 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, 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 + .spyOn(stakingRouterService, 'findKeysEntires') + .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, + lastChangedBlockHash: 'lastHash', + }, + }, + })); + + const result = await stakingModuleGuardService.findAlreadyDepositedKeys( + 'lastHash', + intersectionsWithLidoWC, + ); + + expect(result).toEqual([]); + expect(mockSendMessageFromGuardian).toBeCalledTimes(1); + }); + + 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'; + const lidoWC = '0x12'; + const intersectionsWithLidoWC = [ + { 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 + .spyOn(stakingRouterService, 'findKeysEntires') + .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, + lastChangedBlockHash: 'lastHash', + }, + }, + })); + + const prevLastChangedBlockHash = 'prevHash'; + + expect( + 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/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/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/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..e5384860 100644 --- a/src/keys-api/interfaces/index.ts +++ b/src/keys-api/interfaces/index.ts @@ -1,3 +1,2 @@ -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..ae575df5 100644 --- a/src/keys-api/keys-api.service.ts +++ b/src/keys-api/keys-api.service.ts @@ -1,10 +1,11 @@ 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 { 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 +15,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 +30,7 @@ export class KeysApiService { `${baseUrl}${url}`, { signal, + ...requestInit, }, ); clearTimeout(timer); @@ -39,19 +41,31 @@ 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'); + /** + * + * @param The /v1/keys/find API endpoint returns keys along with their duplicates + * @returns + */ + public async findKeysEntires(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 getUnusedModuleKeys(stakingModuleId: number) { - const result = await this.fetch( - `/v1/modules/${stakingModuleId}/keys?used=false`, + public async getUnusedKeys() { + const result = await this.fetch(`/v1/keys?used=false`); + return result; + } + + public async getOperatorListWithModule() { + const result = await this.fetch( + `/v1/operators`, ); - if (!result.data || !result.meta) - throw Error('Keys API not synced, please wait'); return result; } } diff --git a/src/staking-router/keys.fixtures.ts b/src/staking-router/keys.fixtures.ts new file mode 100644 index 00000000..90c55aea --- /dev/null +++ b/src/staking-router/keys.fixtures.ts @@ -0,0 +1,68 @@ +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, + lastChangedBlockHash: + '0x194ac4fd960ed44cb3db53fe1f5a53e983280fd438aeba607ae04f1bb416b4a1', + }, + }, +}; diff --git a/src/staking-router/operators.fixtures.ts b/src/staking-router/operators.fixtures.ts new file mode 100644 index 00000000..ee27484a --- /dev/null +++ b/src/staking-router/operators.fixtures.ts @@ -0,0 +1,85 @@ +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, + lastChangedBlockHash: + '0x194ac4fd960ed44cb3db53fe1f5a53e983280fd438aeba607ae04f1bb416b4a1', + }, + }, +}; diff --git a/src/staking-router/staking-router.service.ts b/src/staking-router/staking-router.service.ts index b769ded2..d9476e6a 100644 --- a/src/staking-router/staking-router.service.ts +++ b/src/staking-router/staking-router.service.ts @@ -2,63 +2,102 @@ 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 { 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'; @Injectable() export class StakingRouterService { - protected stakingRouterCache: Record = {}; constructor( @Inject(WINSTON_MODULE_NEST_PROVIDER) protected logger: LoggerService, protected readonly config: Configuration, protected readonly keysApiService: KeysApiService, ) {} - 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); + /** + * Return staking module data and block information + */ + public async getStakingModulesData(): Promise<{ + stakingModulesData: StakingModuleData[]; + blockHash: string; + blockNumber: number; + }> { + const { data: operatorsByModules, meta: operatorsMeta } = + await this.keysApiService.getOperatorListWithModule(); - const srResponse = await this.keysApiService.getUnusedModuleKeys(id); - const srModuleBlockHash = srResponse.meta.elBlockSnapshot.blockHash; + const { data: unusedKeys, meta: unusedKeysMeta } = + await this.keysApiService.getUnusedKeys(); - if (srModuleBlockHash !== blockHash) { - this.logger.log('Blockhash of the received keys', { - srModuleBlockHash, - blockHash, - }); + const blockHash = operatorsMeta.elBlockSnapshot.blockHash; + const blockNumber = operatorsMeta.elBlockSnapshot.blockNumber; - throw Error( - 'Blockhash of the received keys does not match the current blockhash', - ); - } - - this.setStakingRouterCache(id, srResponse); + this.isEqualLastChangedBlockHash( + operatorsMeta.elBlockSnapshot.lastChangedBlockHash, + unusedKeysMeta.elBlockSnapshot.lastChangedBlockHash, + ); - return srResponse; - } + const stakingModulesData = operatorsByModules.map( + ({ operators, module: stakingModule }) => + this.processModuleData({ + operators, + stakingModule, + unusedKeys, + meta: operatorsMeta, + }), + ); - protected getStakingRouterKeysCache(stakingModuleId: number) { - return this.stakingRouterCache[stakingModuleId]; + return { stakingModulesData, blockHash, blockNumber }; } - protected setStakingRouterCache( - stakingModuleId: number, - srResponse: SRModuleKeysResponse, + public isEqualLastChangedBlockHash( + firstRequestHash: string, + secondRequestHash: string, ) { - this.stakingRouterCache[stakingModuleId] = srResponse; + 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(); + } } - protected isNeedToUpdateState(stakingModuleId: number, nextNonce: number) { - const cache = this.getStakingRouterKeysCache(stakingModuleId); - if (!cache) return true; + 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 moduleVettedUnusedKeys = getVettedUnusedKeys( + operators, + moduleUnusedKeys, + ); + + return { + unusedKeys: moduleUnusedKeys.map((srKey) => srKey.key), + nonce: stakingModule.nonce, + stakingModuleId: stakingModule.id, + blockHash: meta.elBlockSnapshot.blockHash, + lastChangedBlockHash: meta.elBlockSnapshot.lastChangedBlockHash, + vettedUnusedKeys: moduleVettedUnusedKeys, + }; + } - const prevNonce = cache.data.module.nonce; - return prevNonce !== nextNonce; + 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 new file mode 100644 index 00000000..4026fa49 --- /dev/null +++ b/src/staking-router/staking-router.spec.ts @@ -0,0 +1,122 @@ +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'; +import { InconsistentLastChangedBlockHash } from 'common/custom-errors'; + +describe('StakingRouter', () => { + 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); + }); + + it("should return correct data when 'lastChangedBlockHash' values of two requests are identical", async () => { + (keysApiService.getOperatorListWithModule as jest.Mock).mockResolvedValue( + groupedByModulesOperators, + ); + (keysApiService.getUnusedKeys as jest.Mock).mockResolvedValue( + keysAllStakingModules, + ); + + const result = await stakingRouterService.getStakingModulesData(); + + // Assertions + expect(result).toEqual({ + blockNumber: 400153, + blockHash: + '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + stakingModulesData: [ + { + unusedKeys: [ + '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + '0x911dd3091cfb1b42c960e4f343ea98d9ee6a1dc8ef215afa976fb557bd627a901717c0008bc33a0bfea15f0dfe9c5d01', + '0x8d12ec44816f108df84ef9b03e423a6d8fb0f0a1823c871b123ff41f893a7b372eb038a1ed1ff15083e07a777a5cba50', + ], + vettedUnusedKeys: [ + { + key: '0x9948d2becf42e9f76922bc6f664545e6f50401050af95785a984802d32a95c4c61f8e3de312b78167f86e047f83a7796', + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + moduleAddress: '0x595F64Ddc3856a3b5Ff4f4CC1d1fb4B46cFd2bAC', + index: 100, + }, + ], + blockHash: + '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + lastChangedBlockHash: + '0x194ac4fd960ed44cb3db53fe1f5a53e983280fd438aeba607ae04f1bb416b4a1', + stakingModuleId: 1, + nonce: 364, + }, + { + unusedKeys: [ + '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', + '0x84e85db03bee714dbecf01914460d9576b7f7226030bdbeae9ee923bf5f8e01eec4f7dfe54aa7eca6f4bccce59a0bf42', + '0x84ff489c1e07c75ac635914d4fa20bb37b30f7cf37a8fb85298a88e6f45daab122b43a352abce2132bdde96fd4a01599', + ], + vettedUnusedKeys: [ + { + key: '0x83fc58f68d913481e065c928b040ae8b157ef2b32371b7df93d40188077c619dc789d443c18ac4a9b7e76de5ed6c8247', + depositSignature: + '0xa13833d96f4b98291dbf428cb69e7a3bdce61c9d20efcdb276423c7d6199ebd10cf1728dbd418c592701a41983cb02330e736610be254f617140af48a9d20b31cdffdd1d4fc8c0776439fca3330337d33042768acf897000b9e5da386077be44', + operatorIndex: 28, + used: false, + moduleAddress: '0x11a93807078f8BB880c1BD0ee4C387537de4b4b6', + index: 4, + }, + ], + nonce: 69, + blockHash: + '0x40c697def4d4f7233b75149ab941462582bb5f035b5089f7c6a3d7849222f47c', + lastChangedBlockHash: + '0x194ac4fd960ed44cb3db53fe1f5a53e983280fd438aeba607ae04f1bb416b4a1', + stakingModuleId: 2, + }, + ], + }); + }); + + 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.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 new file mode 100644 index 00000000..cfdc0671 --- /dev/null +++ b/src/staking-router/vetted-keys.ts @@ -0,0 +1,17 @@ +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) + // stakingLimit limit cant be less than usedSigningKeys + .slice(0, operator.stakingLimit - operator.usedSigningKeys); + + return operatorKeys; + }); +} diff --git a/test/helpers/mockKeysApi.ts b/test/helpers/mockKeysApi.ts index 196efb3e..bab76105 100644 --- a/test/helpers/mockKeysApi.ts +++ b/test/helpers/mockKeysApi.ts @@ -1,55 +1,88 @@ 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) => ({ + nonce, + 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, + lastChangedBlockHash: string, +) => ({ + blockNumber: block.number, + blockHash: block.hash, + timestamp: block.timestamp, + lastChangedBlockHash, +}); - 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, + }, +]; - jest.spyOn(keysApiService, 'getModulesList').mockImplementation(async () => ({ - data: [mockedModule], - elBlockSnapshot: mockedMeta, +export const mockedKeysApiOperators = ( + keysApiService: KeysApiService, + mockedOperators: RegistryOperator[], + mockedModule: SRModule, + mockedMeta: ELBlockSnapshot, +) => { + jest + .spyOn(keysApiService, 'getOperatorListWithModule') + .mockImplementation(async () => ({ + data: [{ operators: mockedOperators, module: mockedModule }], + meta: { + 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, 'getUnusedModuleKeys') + .spyOn(keysApiService, 'findKeysEntires') .mockImplementation(async () => ({ - data: { - keys: mockedKeys, - module: mockedModule, - }, + data: mockedKeys, meta: { elBlockSnapshot: mockedMeta, }, diff --git a/test/manifest.e2e-spec.ts b/test/manifest.e2e-spec.ts index b60b2c9d..470d2053 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 @@ -71,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'); @@ -90,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); @@ -138,8 +151,9 @@ describe('ganache e2e tests', () => { lidoService = moduleRef.get(LidoService); depositService = moduleRef.get(DepositService); guardianMessageService = moduleRef.get(GuardianMessageService); + keyValidator = moduleRef.get(KeyValidatorInterface); - // Initialising needed service instead of the whole app + // Initializing needed service instead of the whole app blsService = moduleRef.get(BlsService); await blsService.onModuleInit(); @@ -156,6 +170,8 @@ describe('ganache e2e tests', () => { sendPauseMessage = jest .spyOn(guardianMessageService, 'sendPauseMessage') .mockImplementation(() => Promise.resolve()); + + validateKeys = jest.spyOn(keyValidator, 'validateKeys'); }); describe('node checks', () => { @@ -202,6 +218,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 +227,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, currentBlock.hash); + const stakingModule = mockedModule(currentBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + meta, + ); + + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, meta); + mockedKeysWithDuplicates(keysApiService, unusedKeys, meta); await depositService.setCachedEvents({ data: [ @@ -268,10 +307,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, newBlock.hash); + + 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 +353,7 @@ describe('ganache e2e tests', () => { ); const currentBlock = await tempProvider.getBlock('latest'); + // mock kapi response const goodDepositMessage = { pubkey: pk, withdrawalCredentials: fromHexString(GOOD_WC), @@ -311,7 +362,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, currentBlock.hash); + const stakingModule = mockedModule(currentBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + meta, + ); + + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, meta); await depositService.setCachedEvents({ data: [], @@ -358,7 +430,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, newBlock.hash); + + 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 +467,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 +475,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, currentBlock.hash); + const stakingModule = mockedModule(currentBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + meta, + ); + + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, meta); await depositService.setCachedEvents({ data: [], @@ -421,16 +524,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 +544,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, newBlock.hash); + + 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 +580,7 @@ describe('ganache e2e tests', () => { ); const currentBlock = await tempProvider.getBlock('latest'); + // no diff const goodDepositMessage = { pubkey: pk, withdrawalCredentials: fromHexString(GOOD_WC), @@ -482,7 +589,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, currentBlock.hash); + const stakingModule = mockedModule(currentBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + meta, + ); + + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, meta); const goodDepositData = { ...goodDepositMessage, @@ -521,7 +649,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, newBlock.hash); + + 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 +692,7 @@ describe('ganache e2e tests', () => { test( 'reorganization', async () => { + // TODO: need attention to this test const tempProvider = new ethers.providers.JsonRpcProvider( `http://127.0.0.1:${GANACHE_PORT}`, ); @@ -566,7 +706,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, currentBlock.hash); + const stakingModule = mockedModule(currentBlock); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + meta, + ); + + mockedKeysApiUnusedKeys(keysApiService, unusedKeys, meta); const goodDepositData = { ...goodDepositMessage, @@ -616,7 +777,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, newBlock.hash); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + newMeta, + ); + + mockedKeysApiUnusedKeys(keysApiService, [], newMeta); // Run a cycle and wait for possible changes await guardianService.handleNewBlock(); @@ -631,8 +802,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 +812,451 @@ 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, 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, + }, + { + key: '0xa9bfaa8207ee6c78644c079ffc91b6e5abcc5eede1b7a06abb8fb40e490a75ea269c178dd524b65185299d2bbd2eb7b2', + depositSignature: + '0xaa5f2a1053ba7d197495df44d4a32b7ae10265cf9e38560a16b782978c0a24271a113c9538453b7e45f35cb64c7adb460d7a9fe8c8ce6b8c80ca42fd5c48e180c73fc08f7d35ba32e39f32c902fd333faf47611827f0b7813f11c4c518dd2e59', + 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: '0xa9bfaa8207ee6c78644c079ffc91b6e5abcc5eede1b7a06abb8fb40e490a75ea269c178dd524b65185299d2bbd2eb7b2', + depositSignature: + '0xaa5f2a1053ba7d197495df44d4a32b7ae10265cf9e38560a16b782978c0a24271a113c9538453b7e45f35cb64c7adb460d7a9fe8c8ce6b8c80ca42fd5c48e180c73fc08f7d35ba32e39f32c902fd333faf47611827f0b7813f11c4c518dd2e59', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: NOP_REGISTRY, + }, + ]; + + const newBlock = await tempProvider.getBlock('latest'); + const newMeta = mockedMeta(newBlock, newBlock.hash); + + 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( + '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 () => { + 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, currentBlock.hash); + + 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, newBlock.hash); + + 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'); + + 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, currentBlock.hash); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + stakingModule, + meta, + ); + + const keyWithWrongSign = { + key: toHexString(pk), + // just some random sign + depositSignature: + '0x8bf4401a354de243a3716ee2efc0bde1ded56a40e2943ac7c50290bec37e935d6170b21e7c0872f203199386143ef12612a1488a8e9f1cdf1229c382f29c326bcbf6ed6a87d8fbfe0df87dacec6632fc4709d9d338f4cf81e861d942c23bba1e', + operatorIndex: 0, + used: false, + index: 0, + moduleAddress: NOP_REGISTRY, + }; + // list of keys for /keys?used=false mock + mockedKeysApiUnusedKeys(keysApiService, [keyWithWrongSign], meta); + mockedKeysWithDuplicates(keysApiService, [], meta); + + 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); + + 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, newBlock.hash); + const newStakingModule = mockedModule(newBlock, 6047); + + mockedKeysApiOperators( + keysApiService, + mockedOperators, + newStakingModule, + newMeta, + ); + + // 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); + expect(sendPauseMessage).toBeCalledTimes(0); + }, + TESTS_TIMEOUT, + ); }); diff --git a/yarn.lock b/yarn.lock index 396241b0..e80121ca 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" @@ -4682,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" @@ -5015,6 +5465,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 +5483,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 +5519,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 +5737,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 +5816,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"