Skip to content

Commit

Permalink
feat: duplicated vetted keys checks
Browse files Browse the repository at this point in the history
  • Loading branch information
Amuhar committed Nov 28, 2023
1 parent 2896930 commit 849d33a
Show file tree
Hide file tree
Showing 15 changed files with 559 additions and 43 deletions.
2 changes: 2 additions & 0 deletions src/contracts/deposit/deposit.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export class DepositService {
private blsService: BlsService,
) {}

// Why OneAtTime() ? this function is executed in guardian.service inside another function with OneAtTime() in cron job
@OneAtTime()
public async handleNewBlock(blockNumber: number): Promise<void> {
if (blockNumber % DEPOSIT_EVENTS_CACHE_UPDATE_BLOCK_RATE !== 0) return;
Expand Down Expand Up @@ -336,6 +337,7 @@ export class DepositService {

const mergedEvents = cachedEvents.data.concat(freshEvents);

// TODO: Why we don't cache freshEvents?
return {
events: mergedEvents,
startBlock: cachedEvents.headers.startBlock,
Expand Down
27 changes: 14 additions & 13 deletions src/guardian/guardian.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,9 @@ export class GuardianService implements OnModuleInit {
this.logger.log('New staking router state cycle start');

try {
const {
elBlockSnapshot: { blockHash, blockNumber },
data: stakingModules,
} = await this.stakingRouterService.getStakingModules();
// TODO: rename
const { blockHash, blockNumber, vettedKeys, stakingModulesData } =
await this.stakingRouterService.getVettedAndUnusedKeys();

await this.repositoryService.initCachedContracts({ blockHash });

Expand All @@ -119,8 +118,10 @@ export class GuardianService implements OnModuleInit {
return;
}

const stakingModulesNumber = stakingModulesData.length;

this.logger.log('Staking modules loaded', {
modulesCount: stakingModules.length,
modulesCount: stakingModulesNumber,
});

await this.depositService.handleNewBlock(blockNumber);
Expand All @@ -136,14 +137,14 @@ export class GuardianService implements OnModuleInit {
blockHash: blockData.blockHash,
});

await Promise.all(
stakingModules.map(async (stakingRouterModule) => {
const stakingModuleData =
await this.stakingModuleGuardService.getStakingRouterModuleData(
stakingRouterModule,
blockHash,
);
// TODO: check only if one of nonce changed
await this.stakingModuleGuardService.checkVettedKeysDuplicates(
vettedKeys,
blockData,
);

await Promise.all(
stakingModulesData.map(async (stakingModuleData) => {
await this.stakingModuleGuardService.checkKeysIntersections(
stakingModuleData,
blockData,
Expand All @@ -157,7 +158,7 @@ export class GuardianService implements OnModuleInit {
);

await this.guardianMessageService.pingMessageBroker(
stakingModules.map(({ id }) => id),
stakingModulesData.map(({ stakingModuleId }) => stakingModuleId),
blockData,
);

Expand Down
4 changes: 3 additions & 1 deletion src/guardian/interfaces/staking-module.interface.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { RegistryKey } from 'keys-api/interfaces/RegistryKey';

export interface StakingModuleData {
blockHash: string;
isDepositsPaused: boolean;
unusedKeys: string[];
vettedKeys: RegistryKey[];
nonce: number;
stakingModuleId: number;
}
115 changes: 89 additions & 26 deletions src/guardian/staking-module-guard/staking-module-guard.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { GuardianMessageService } from '../guardian-message';

import { StakingRouterService } from 'staking-router';
import { SRModule } from 'keys-api/interfaces';

Check warning on line 14 in src/guardian/staking-module-guard/staking-module-guard.service.ts

View workflow job for this annotation

GitHub Actions / App tests

'SRModule' is defined but never used
import { RegistryKey } from 'keys-api/interfaces/RegistryKey';

@Injectable()
export class StakingModuleGuardService {
Expand All @@ -30,34 +31,55 @@ export class StakingModuleGuardService {
private lastContractsStateByModuleId: Record<number, ContractsState | null> =
{};

public async getStakingRouterModuleData(
stakingRouterModule: SRModule,
blockHash: string,
): Promise<StakingModuleData> {
const {
data: {
keys,
module: { nonce },
},
} = await this.stakingRouterService.getStakingModuleUnusedKeys(
blockHash,
stakingRouterModule,
);
// public async getStakingRouterModuleData(
// stakingRouterModule: SRModule,
// blockHash: string,
// ): Promise<StakingModuleData> {
// const {
// data: {
// keys,
// module: { nonce },
// },
// } = await this.stakingRouterService.getStakingModuleUnusedKeys(
// blockHash,
// stakingRouterModule,
// );

// const isDepositsPaused = await this.securityService.isDepositsPaused(
// stakingRouterModule.id,
// {
// blockHash,
// },
// );

// return {
// nonce,
// unusedKeys: keys.map((srKey) => srKey.key),
// stakingModuleId: stakingRouterModule.id,
// blockHash,
// };
// }

const isDepositsPaused = await this.securityService.isDepositsPaused(
stakingRouterModule.id,
{
blockHash,
},
/**
* Check vetted among staking modules
*/
public async checkVettedKeysDuplicates(
vettedKeys: RegistryKey[],
blockData: BlockData,
): Promise<void> {
const uniqueKeys = new Set();
const duplicatedKeys = vettedKeys.filter(
(vettedKey) => uniqueKeys.size === uniqueKeys.add(vettedKey.key).size,
);

return {
nonce,
unusedKeys: keys.map((srKey) => srKey.key),
isDepositsPaused,
stakingModuleId: stakingRouterModule.id,
blockHash,
};
if (duplicatedKeys.length) {
this.logger.warn('Found duplicated vetted key', {
blockHash: blockData.blockHash,
duplicatedKeys,
});
//TODO: set metric
throw Error('Found duplicated vetted key');
}
}

/**
Expand Down Expand Up @@ -88,14 +110,32 @@ 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;
}

if (isFilteredIntersectionsFound) {
await this.handleKeysIntersections(stakingModuleData, blockData);
} else {
const usedKeys = await this.handleIntersectionBetweenUsedAndUnusedKeys(
keysIntersections,
);

// if found used keys, Lido already made deposit on this keys
if (usedKeys.length) {
this.logger.log('Found that we already deposited on these keys');
// set metric ccouncil_daemon_used_duplicate
return;
}

await this.handleCorrectKeys(stakingModuleData, blockData);
}
}
Expand Down Expand Up @@ -156,6 +196,29 @@ export class StakingModuleGuardService {
return attackIntersections;
}

public async handleIntersectionBetweenUsedAndUnusedKeys(
intersectionsWithLidoWC: VerifiedDepositEvent[],
) {
const depositedPubkeys = intersectionsWithLidoWC.map(
(deposit) => deposit.pubkey,
);

if (depositedPubkeys.length) {
console.log('deposited pubkeys', depositedPubkeys);
this.logger.log(
'Found intersections with lido credentials, need to check duplicated keys',
);

const keys = await this.stakingRouterService.getKeysWithDuplicates(
depositedPubkeys,
);
const usedKeys = keys.data.filter((key) => key.used);
return usedKeys;
}

return [];
}

/**
* Handles the situation when keys have previously deposited copies
* @param blockData - collected data from the current block
Expand Down
10 changes: 10 additions & 0 deletions src/keys-api/interfaces/GroupedByModuleOperatorListResponse.ts
Original file line number Diff line number Diff line change
@@ -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;
};
7 changes: 7 additions & 0 deletions src/keys-api/interfaces/KeyListResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { Meta } from './Meta';
import { RegistryKey } from './RegistryKey';

export type KeyListResponse = {
data: Array<RegistryKey>;
meta: Meta;
};
5 changes: 5 additions & 0 deletions src/keys-api/interfaces/RegistryKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ export type RegistryKey = {
* Key index in contract
*/
index: number;

/**
* Staking module address
*/
moduleAddress: string;
};
38 changes: 38 additions & 0 deletions src/keys-api/interfaces/RegistryOperator.ts
Original file line number Diff line number Diff line change
@@ -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;
};
13 changes: 13 additions & 0 deletions src/keys-api/interfaces/SROperatorListWithModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { RegistryOperator } from './RegistryOperator';
import type { SRModule } from './SRModule';

export type SROperatorListWithModule = {
/**
* Operators of staking router module
*/
operators: Array<RegistryOperator>;
/**
* Detailed Staking Router information
*/
module: SRModule;
};
1 change: 1 addition & 0 deletions src/keys-api/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export type { SRModuleKeysResponse } from './SRModuleKeysResponse';
export type { SRModuleListResponse } from './SRModuleListResponse';
export type { SRModule } from './SRModule';
export type { KeyListResponse } from './KeyListResponse';
41 changes: 38 additions & 3 deletions src/keys-api/keys-api.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { Injectable, LoggerService, Inject } from '@nestjs/common';
import { FetchService } from '@lido-nestjs/fetch';
import { FetchService, RequestInit } from '@lido-nestjs/fetch';
import { AbortController } from 'node-abort-controller';
import { FETCH_REQUEST_TIMEOUT } from './keys-api.constants';
import { SRModuleKeysResponse, SRModuleListResponse } from './interfaces';
import {
SRModuleKeysResponse,
SRModuleListResponse,
KeyListResponse,
} from './interfaces';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
import { Configuration } from 'common/config';
import { GroupedByModuleOperatorListResponse } from './interfaces/GroupedByModuleOperatorListResponse';

@Injectable()
export class KeysApiService {
Expand All @@ -14,7 +19,7 @@ export class KeysApiService {
protected readonly fetchService: FetchService,
) {}

protected async fetch<Response>(url: string) {
protected async fetch<Response>(url: string, requestInit?: RequestInit) {
const controller = new AbortController();
const { signal } = controller;

Expand All @@ -29,6 +34,7 @@ export class KeysApiService {
`${baseUrl}${url}`,
{
signal,
...requestInit,
},
);
clearTimeout(timer);
Expand All @@ -54,4 +60,33 @@ export class KeysApiService {
throw Error('Keys API not synced, please wait');
return result;
}

/**
*
* @param The /v1/keys/find KAPI endpoint returns a key along with its duplicates
* @returns
*/
public async getKeysWithDuplicates(pubkeys: string[]) {
const result = await this.fetch<KeyListResponse>(`/v1/keys/find`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ pubkeys }),
});

return result;
}

public async getUnusedKeys() {
const result = await this.fetch<KeyListResponse>(`/v1/keys?used=false`);
return result;
}

public async getOperatorListWithModule() {
const result = await this.fetch<GroupedByModuleOperatorListResponse>(
`/v1/operators`,
);
return result;
}
}
Loading

0 comments on commit 849d33a

Please sign in to comment.