Skip to content

Commit

Permalink
Merge pull request #271 from lidofinance/develop
Browse files Browse the repository at this point in the history
dev to main
  • Loading branch information
eddort authored Oct 31, 2024
2 parents 0ac715d + be7210a commit 77ecd8f
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 45 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lido-council-daemon",
"version": "3.2.0",
"version": "3.3.0",
"description": "Lido Council Daemon",
"author": "Lido team",
"private": true,
Expand Down
4 changes: 4 additions & 0 deletions src/common/ts-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* eslint-disable @typescript-eslint/ban-types */
export type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends Function ? T[P] : DeepReadonly<T[P]>;
};
6 changes: 3 additions & 3 deletions src/contracts/data-bus/data-bus.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class DataBusService {

const dataBusClient = new DataBusClient(this.dataBusAddress, this.wallet);
this.dsmMessageSender = new DSMMessageSender(dataBusClient);
await this.monitorGuardianBalance();
await this.monitorGuardianDataBusBalance();
this.subscribeToEVMChainUpdates();
}

Expand All @@ -64,7 +64,7 @@ export class DataBusService {
const provider = this.provider;
provider.on('block', async (blockNumber) => {
if (blockNumber % DATA_BUS_BALANCE_UPDATE_BLOCK_RATE !== 0) return;
await this.monitorGuardianBalance().catch((error) =>
await this.monitorGuardianDataBusBalance().catch((error) =>
this.logger.error(error),
);
});
Expand All @@ -77,7 +77,7 @@ export class DataBusService {
* Updates the account balance metric.
*/
@OneAtTime()
public async monitorGuardianBalance() {
public async monitorGuardianDataBusBalance() {
const balanceWei = await this.getAccountBalance();
const balanceETH = formatEther(balanceWei);
const { chainId } = await this.provider.getNetwork();
Expand Down
7 changes: 5 additions & 2 deletions src/guardian/duplicates/keys-duplication-checker.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Inject, Injectable, LoggerService } from '@nestjs/common';
import { DeepReadonly } from 'common/ts-utils';
import {
SigningKeyEvent,
SigningKeyEventsGroupWithStakingModules,
Expand Down Expand Up @@ -37,7 +38,7 @@ export class KeysDuplicationCheckerService {
* - `unresolved`: An array of `RegistryKey` objects for which no corresponding events were found.
*/
public async getDuplicatedKeys(
keys: RegistryKey[],
keys: DeepReadonly<RegistryKey[]>,
blockData: BlockData,
): Promise<{ duplicates: RegistryKey[]; unresolved: RegistryKey[] }> {
if (keys.length === 0) {
Expand Down Expand Up @@ -79,7 +80,9 @@ export class KeysDuplicationCheckerService {
* @returns An array of tuples where each tuple contains a pubkey string and an array of
* `RegistryKey` objects that share that pubkey. Only keys with duplicates are included.
*/
public getDuplicateKeyGroups(keys: RegistryKey[]): [string, RegistryKey[]][] {
public getDuplicateKeyGroups(
keys: DeepReadonly<RegistryKey[]>,
): [string, RegistryKey[]][] {
const keyMap = keys.reduce((acc, key) => {
const duplicateKeys = acc.get(key.key) || [];
duplicateKeys.push(key);
Expand Down
4 changes: 1 addition & 3 deletions src/guardian/guardian.constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { CronExpression } from '@nestjs/schedule';

export const GUARDIAN_DEPOSIT_RESIGNING_BLOCKS = 10;
export const GUARDIAN_DEPOSIT_JOB_NAME = 'guardian-deposit-job';
export const GUARDIAN_DEPOSIT_JOB_DURATION = CronExpression.EVERY_5_SECONDS;
export const GUARDIAN_DEPOSIT_JOB_DURATION_MS = 5000;
export const MIN_KAPI_VERSION = '2.2.0';
export const GUARDIAN_PING_BLOCKS_PERIOD = 300;
55 changes: 25 additions & 30 deletions src/guardian/guardian.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ import {
import { compare } from 'compare-versions';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
import { SchedulerRegistry } from '@nestjs/schedule';
import { CronJob } from 'cron';
import { DepositRegistryService } from 'contracts/deposits-registry';
import { SecurityService } from 'contracts/security';
import { RepositoryService } from 'contracts/repository';
import {
GUARDIAN_DEPOSIT_JOB_DURATION,
GUARDIAN_DEPOSIT_JOB_DURATION_MS,
GUARDIAN_DEPOSIT_JOB_NAME,
} from './guardian.constants';
import { OneAtTime } from 'common/decorators';
Expand All @@ -39,6 +38,7 @@ import { SigningKeysRegistryService } from 'contracts/signing-keys-registry';
import { InjectMetric } from '@willsoto/nestjs-prometheus';
import { METRIC_JOB_DURATION } from 'common/prometheus';
import { Histogram } from 'prom-client';
import { DeepReadonly } from 'common/ts-utils';

@Injectable()
export class GuardianService implements OnModuleInit {
Expand Down Expand Up @@ -126,25 +126,22 @@ export class GuardianService implements OnModuleInit {
* Subscribes to the staking router modules updates
*/
public subscribeToModulesUpdates() {
const cron = new CronJob(GUARDIAN_DEPOSIT_JOB_DURATION, () => {
this.handleNewBlock().catch((error) => {
this.logger.error(error);
});
});

this.logger.log('GuardianService subscribed to Ethereum events');
const interval = setInterval(
() => this.handleNewBlock().catch((error) => this.logger.error(error)),
GUARDIAN_DEPOSIT_JOB_DURATION_MS,
);

cron.start();
this.schedulerRegistry.addInterval(GUARDIAN_DEPOSIT_JOB_NAME, interval);

this.schedulerRegistry.addCronJob(GUARDIAN_DEPOSIT_JOB_NAME, cron);
this.logger.log('GuardianService subscribed to Ethereum events');
}

/**
* Handles the appearance of a new block in the network
*/
@OneAtTime()
public async handleNewBlock(): Promise<void> {
this.logger.log('New staking router state cycle start');
this.logger.log('Beginning of the processing of the new Guardian cycle');

try {
const endTimer = this.jobDurationMetric
Expand Down Expand Up @@ -176,17 +173,12 @@ export class GuardianService implements OnModuleInit {
.startTimer();

// fetch all lido keys
const { data: lidoKeys, meta: secondRequestMeta } =
await this.keysApiService.getKeys();
const { data: lidoKeys } = await this.keysApiService.getKeys(
firstRequestMeta,
);

endTimerKeysReq();

// check that there were no updates in Keys Api between two requests
this.keysApiService.verifyMetaDataConsistency(
firstRequestMeta.lastChangedBlockHash,
secondRequestMeta.elBlockSnapshot.lastChangedBlockHash,
);

// contracts initialization
await this.repositoryService.initCachedContracts({ blockHash });

Expand Down Expand Up @@ -215,22 +207,27 @@ export class GuardianService implements OnModuleInit {
return;
}

// To avoid blocking the pause, run the following tasks asynchronously:
// updating the SigningKeyAdded events cache, checking keys, handling the unvetting of keys,
// and sending deposit messages to the queue.
// To avoid blocking the pause due to a potentially lengthy SigningKeyAdded
// events cache update, which can occur when the modules list changes:
// run key checks and send deposit messages to the queue without waiting.
this.handleKeys(stakingModulesData, blockData, lidoKeys)
.catch(this.logger.error)
.finally(() => endTimer());
.finally(() => {
this.logger.log('End of unvetting and deposits processing by Guardian');
endTimer()
});
} catch (error) {
this.logger.error('Staking router state update error');
this.logger.error('Guardian cycle processing error');
this.logger.error(error);
} finally {
this.logger.log('End of pause processing by Guardian');
}
}

private async collectData(
stakingModules: SRModule[],
meta: ELBlockSnapshot,
lidoKeys: RegistryKey[],
lidoKeys: DeepReadonly<RegistryKey[]>,
) {
const { blockHash, blockNumber } = meta;

Expand Down Expand Up @@ -265,7 +262,7 @@ export class GuardianService implements OnModuleInit {
private async handleKeys(
stakingModulesData: StakingModuleData[],
blockData: BlockData,
lidoKeys: RegistryKey[],
lidoKeys: DeepReadonly<RegistryKey[]>,
) {
// check lido keys
await this.checkKeys(stakingModulesData, blockData, lidoKeys);
Expand All @@ -292,14 +289,12 @@ export class GuardianService implements OnModuleInit {
blockHash,
blockNumber,
});

this.logger.log('New staking router state cycle end');
}

private async checkKeys(
stakingModulesData: StakingModuleData[],
blockData: BlockData,
lidoKeys: RegistryKey[],
lidoKeys: DeepReadonly<RegistryKey[]>,
) {
const stakingRouterModuleAddresses = stakingModulesData.map(
(stakingModule) => stakingModule.stakingModuleAddress,
Expand Down
66 changes: 63 additions & 3 deletions src/keys-api/keys-api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ import { Configuration } from 'common/config';
import { GroupedByModuleOperatorListResponse } from './interfaces/GroupedByModuleOperatorListResponse';
import { InconsistentLastChangedBlockHash } from 'common/custom-errors';
import { SRModuleListResponse } from './interfaces/SRModuleListResponse';
import { ELBlockSnapshot } from './interfaces/ELBlockSnapshot';
import { DeepReadonly } from 'common/ts-utils';

@Injectable()
export class KeysApiService {
// Do not use this value in a straightforward manner
// It can be used in parallel at the moment in `getKeys`
private cachedKeys?: DeepReadonly<KeyListResponse>;
constructor(
@Inject(WINSTON_MODULE_NEST_PROVIDER) protected logger: LoggerService,
protected readonly config: Configuration,
Expand Down Expand Up @@ -85,10 +90,65 @@ export class KeysApiService {
}

/**
* The /v1/keys endpoint returns full list of keys
* Retrieves keys, using cache if valid.
* @param elBlockSnapshot ELBlockSnapshot with the current block hash for cache validation.
* @returns Cached or newly fetched keys.
*/
public async getKeys() {
const result = await this.fetch<KeyListResponse>(`/v1/keys`);
public async getKeys(
elBlockSnapshot: ELBlockSnapshot,
): Promise<DeepReadonly<KeyListResponse>> {
if (!this.cachedKeys) {
return this.updateCachedKeys(elBlockSnapshot);
}

const { lastChangedBlockHash: cachedHash } =
this.cachedKeys.meta.elBlockSnapshot;
const { lastChangedBlockHash: currentHash } = elBlockSnapshot;

if (cachedHash !== currentHash) {
return this.updateCachedKeys(elBlockSnapshot);
}

this.logger.debug?.(
'Keys are obtained from cache, no data update required',
{
elBlockSnapshot,
cachedELBlockSnapshot: this.cachedKeys.meta.elBlockSnapshot,
},
);
return this.cachedKeys;
}

/**
* Fetches new keys from the /v1/keys endpoint and updates cache.
* @returns The newly fetched keys.
*/
private async updateCachedKeys(
elBlockSnapshot: ELBlockSnapshot,
): Promise<DeepReadonly<KeyListResponse>> {
this.logger.log('Updating keys from KeysAPI', {
elBlockSnapshot,
cachedELBlockSnapshot: this.cachedKeys?.meta.elBlockSnapshot,
});

// delete old cache to optimize memory performance
this.cachedKeys = undefined;

const result = await this.fetch<DeepReadonly<KeyListResponse>>(`/v1/keys`);

this.logger.log('Keys successfully updated in cache from KeysAPI', {
elBlockSnapshot,
newELBlockSnapshot: result.meta.elBlockSnapshot,
});

// check that there were no updates in Keys Api between two requests
this.verifyMetaDataConsistency(
elBlockSnapshot.lastChangedBlockHash,
result.meta.elBlockSnapshot.lastChangedBlockHash,
);

this.cachedKeys = result;
// return exactly `result` because this function can be used in parallel
return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import { ELBlockSnapshot } from 'keys-api/interfaces/ELBlockSnapshot';
import { METRIC_JOB_DURATION } from 'common/prometheus';
import { InjectMetric } from '@willsoto/nestjs-prometheus';
import { Histogram } from 'prom-client';
import { DeepReadonly } from 'common/ts-utils';

type State = {
stakingModules: SRModule[];
meta: ELBlockSnapshot;
lidoKeys: RegistryKey[];
lidoKeys: DeepReadonly<RegistryKey[]>;
};

@Injectable()
Expand Down Expand Up @@ -71,7 +72,7 @@ export class StakingModuleDataCollectorService {
*/
public async checkKeys(
stakingModulesData: StakingModuleData[],
lidoKeys: RegistryKey[],
lidoKeys: DeepReadonly<RegistryKey[]>,
blockData: BlockData,
): Promise<void> {
const endTimerDuplicates = this.jobDurationMetric
Expand Down Expand Up @@ -203,7 +204,7 @@ export class StakingModuleDataCollectorService {

private getModuleVettedUnusedKeys(
stakingModuleAddress: string,
lidoKeys: RegistryKey[],
lidoKeys: DeepReadonly<RegistryKey[]>,
) {
const vettedUnusedKeys = lidoKeys.filter(
(key) =>
Expand Down

0 comments on commit 77ecd8f

Please sign in to comment.