Skip to content

Commit

Permalink
Implement validators performance tracker (#329)
Browse files Browse the repository at this point in the history
* implement validators perforamnce

* add seconds per slot

* fix db initialize

* remove sync comitee rewards and block proposal

* add table name prop

* add slots per epoch

* update naming

* add slot nullish

* Start cron at the beggining pf the epoch (#331)

* start cron at the beggining of the epoch

* add comment

* add delete table method (#335)

* add block proposal duties into db (#336)

* add block proposal duties into db

* remove unnecesary log

* Save validator indexes in db (#337)

* save validator indexes in db

* fix comment

* write on db

* add update in db

* fix lint issue

* fix lint issue

* add get table size in bytes

* reorganize code (#338)

* add engine 20.11.0

* Reorg track validators performance cron  (#340)

* Add log prefix

* reorg code

* simplify code get active validator indexes

---------

Co-authored-by: Marc Font <[email protected]>
  • Loading branch information
pablomendezroyo and Marketen authored Sep 19, 2024
1 parent 8d9bd72 commit f162d7d
Show file tree
Hide file tree
Showing 35 changed files with 881 additions and 343 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"type": "module",
"private": true,
"engines": {
"node": "20.x"
"node": ">=20.11.0"
},
"workspaces": [
"packages/*"
Expand Down
20 changes: 19 additions & 1 deletion packages/brain/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import { startUiServer, startLaunchpadApi } from "./modules/apiServers/index.js"
import * as dotenv from "dotenv";
import process from "node:process";
import { params } from "./params.js";
import { CronJob, reloadValidators, sendProofsOfValidation } from "./modules/cron/index.js";
import {
CronJob,
reloadValidators,
trackValidatorsPerformance,
sendProofsOfValidation,
getSecondsToNextEpoch
} from "./modules/cron/index.js";
import { PostgresClient } from "./modules/apiClients/index.js";
import { brainConfig } from "./modules/config/index.js";

Expand Down Expand Up @@ -41,6 +47,9 @@ export const {
validatorsMonitorUrl,
shareCronInterval,
postgresUrl,
minGenesisTime,
secondsPerSlot,
slotsPerEpoch,
tlsCert
} = brainConfig();
logger.debug(
Expand Down Expand Up @@ -93,6 +102,15 @@ const proofOfValidationCron = new CronJob(shareCronInterval, () =>
sendProofsOfValidation(signerApi, brainDb, dappnodeSignatureVerifierApi, shareDataWithDappnode)
);
proofOfValidationCron.start();
const trackValidatorsPerformanceCron = new CronJob(slotsPerEpoch * secondsPerSlot * 1000, () =>
// once every epoch
trackValidatorsPerformance({ brainDb, postgresClient, beaconchainApi, minGenesisTime, secondsPerSlot })
);
const secondsToNextEpoch = getSecondsToNextEpoch({ minGenesisTime, secondsPerSlot });
// start the cron within the first minute of an epoch
// If it remains more than 1 minute then wait for the next epoch (+ 10 seconds of margin)
if (secondsToNextEpoch > 60) setTimeout(() => trackValidatorsPerformanceCron.start(), (secondsToNextEpoch + 10) * 1000);
else trackValidatorsPerformanceCron.start();

// Graceful shutdown
function handle(signal: string): void {
Expand Down
27 changes: 24 additions & 3 deletions packages/brain/src/modules/apiClients/beaconchain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ import {
BeaconchainLivenessPostResponse,
BeaconchainSyncingStatusGetResponse,
BeaconchainSyncCommitteePostResponse,
BeaconchainBlockRewardsGetResponse
BeaconchainBlockRewardsGetResponse,
BeaconchainProposerDutiesGetResponse
} from "./types.js";
import { StandardApi } from "../standard.js";
import path from "path";
import { ApiParams } from "../types.js";
import { Network } from "@stakingbrain/common";

type BlockId = "head" | "genesis" | "finalized" | "slot" | `0x${string}`;
// TODO: BlockId can also be a simple slot in the form of a string. Is this type still necessary?
type BlockId = "head" | "genesis" | "finalized" | string | `0x${string}`;

export class BeaconchainApi extends StandardApi {
private SLOTS_PER_EPOCH: number;
Expand Down Expand Up @@ -268,6 +270,25 @@ export class BeaconchainApi extends StandardApi {
}
}

/**
* Retrieve block proposal duties for the specified epoch. This will return a list of 32 elements, each element corresponding to a slot in the epoch.
* If the epoch requested is not yet finalized, a chain reorg is possible and the duties may change.
*
* @param epoch The epoch to get the proposer duties from
* @see https://ethereum.github.io/beacon-APIs/#/Validator/getProposerDuties
*/
public async getProposerDuties({ epoch }: { epoch: string }): Promise<BeaconchainProposerDutiesGetResponse> {
try {
return await this.request({
method: "GET",
endpoint: path.join(this.validatorEndpoint, "duties", "proposer", epoch)
});
} catch (e) {
e.message += `Error getting (GET) proposer duties from beaconchain. `;
throw e;
}
}

/**
* Retrieve block reward info for a single block
*
Expand Down Expand Up @@ -340,7 +361,7 @@ export class BeaconchainApi extends StandardApi {
* @params blockId Block identifier. Can be one of: "head" (canonical head in node's view), "genesis", "finalized", <slot>, <hex encoded blockRoot with 0x prefix>.
* @example head
*/
private async getBlockHeader({ blockId }: { blockId: BlockId }): Promise<BeaconchainBlockHeaderGetResponse> {
public async getBlockHeader({ blockId }: { blockId: BlockId }): Promise<BeaconchainBlockHeaderGetResponse> {
try {
return await this.request({
method: "GET",
Expand Down
28 changes: 20 additions & 8 deletions packages/brain/src/modules/apiClients/beaconchain/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ export interface BeaconchainSyncingStatusGetResponse {
};
}

export interface TotalRewards {
validator_index: string;
head: string;
target: string;
source: string;
inclusion_delay: string;
inactivity: string;
}

export interface BeaconchainAttestationRewardsPostResponse {
execution_optimistic: boolean;
finalized: boolean;
Expand All @@ -51,17 +60,20 @@ export interface BeaconchainAttestationRewardsPostResponse {
inclusion_delay: string;
inactivity: string;
}[];
total_rewards: {
validator_index: string;
head: string;
target: string;
source: string;
inclusion_delay: string;
inactivity: string;
}[];
total_rewards: TotalRewards[];
};
}

export interface BeaconchainProposerDutiesGetResponse {
dependent_root: string; // The block root that the response is dependent on.
execution_optimistic: boolean; // Indicates whether the response references an unverified execution payload.
data: {
pubkey: string; // The validator's BLS public key, 48-bytes, hex encoded with 0x prefix.
validator_index: string; // The index of the validator in the validator registry.
slot: string; // The slot at which the validator must propose a block.
}[];
}

export interface BeaconchainSyncCommitteePostResponse {
execution_optimistic: boolean;
finalized: boolean;
Expand Down
67 changes: 53 additions & 14 deletions packages/brain/src/modules/apiClients/postgres/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import logger from "../../logger/index.js";
import { BlockProposalStatus, ValidatorPerformance } from "./types.js";

export class PostgresClient {
private readonly tableName = "validators_performance";
private sql: postgres.Sql;

/**
Expand All @@ -19,6 +20,22 @@ export class PostgresClient {
});
}

/**
* Get table size from the database in bytes.
*/
public async getTableSize(): Promise<number> {
const query = `
SELECT pg_total_relation_size('${this.tableName}');
`;
try {
const result = await this.sql.unsafe(query);
return result[0].pg_total_relation_size;
} catch (err) {
err.message = "Error getting table size: " + err.message;
throw err;
}
}

/**
* Initializes the database by creating the required table if it does not exist.
* The table will have the following columns:
Expand All @@ -34,18 +51,25 @@ export class PostgresClient {
*/
public async initialize() {
const query = `
CREATE TABLE IF NOT EXISTS validator_performance (
validator_index BIGINT NOT NULL,
epoch BIGINT NOT NULL,
slot BIGINT NOT NULL,
liveness BOOLEAN,
block_proposal_status ENUM('${BlockProposalStatus.Missed}', '${BlockProposalStatus.Proposed}', '${BlockProposalStatus.Unchosen}'),
sync_comittee_rewards BIGINT,
attestations_rewards JSONB,
error TEXT,
PRIMARY KEY (validator_index, epoch)
);
`;
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'block_proposal_status') THEN
CREATE TYPE block_proposal_status AS ENUM('${BlockProposalStatus.Missed}', '${BlockProposalStatus.Proposed}', '${BlockProposalStatus.Unchosen}');
END IF;
END $$;
CREATE TABLE IF NOT EXISTS ${this.tableName} (
validator_index BIGINT NOT NULL,
epoch BIGINT NOT NULL,
slot BIGINT,
liveness BOOLEAN,
block_proposal_status block_proposal_status,
sync_comittee_rewards BIGINT,
attestations_rewards JSONB,
error TEXT,
PRIMARY KEY (validator_index, epoch)
);
`;
try {
await this.sql.unsafe(query);
logger.info("Table created or already exists.");
Expand All @@ -55,6 +79,21 @@ CREATE TABLE IF NOT EXISTS validator_performance (
}
}

/**
* Delete database table.
*/
public async deleteDatabaseTable() {
const query = `
DROP TABLE IF EXISTS ${this.tableName};
`;
try {
await this.sql.unsafe(query);
logger.info("Table deleted.");
} catch (err) {
logger.error("Error deleting table:", err);
}
}

/**
* Inserts the given performance data into the database.
*
Expand All @@ -63,14 +102,14 @@ CREATE TABLE IF NOT EXISTS validator_performance (
*/
public async insertPerformanceData(data: ValidatorPerformance): Promise<void> {
const query = `
INSERT INTO validator_performance (validator_index, epoch, slot, liveness, block_proposal_status, sync_comittee_rewards, attestations_rewards, error)
INSERT INTO ${this.tableName} (validator_index, epoch, slot, liveness, block_proposal_status, sync_comittee_rewards, attestations_rewards, error)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`;
try {
await this.sql.unsafe(query, [
data.validatorIndex,
data.epoch,
data.slot,
data.slot ?? null,
data.liveness ?? null,
data.blockProposalStatus ?? null,
data.syncCommitteeRewards ?? null,
Expand Down
9 changes: 5 additions & 4 deletions packages/brain/src/modules/apiClients/postgres/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
export enum BlockProposalStatus {
Missed = "missed",
Proposed = "proposed",
Unchosen = "unchosen"
Missed = "Missed",
Proposed = "Proposed",
Unchosen = "Unchosen",
Error = "Error"
}

export interface ValidatorPerformance {
validatorIndex: number;
epoch: number;
slot: number;
slot?: number;
liveness?: boolean;
blockProposalStatus?: BlockProposalStatus;
syncCommitteeRewards?: number;
Expand Down
2 changes: 2 additions & 0 deletions packages/brain/src/modules/config/networks/gnosis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export const gnosisBrainConfig = (
shareCronInterval: 24 * 60 * 60 * 1000, // 1 day in ms
minGenesisTime: 1638968400, // Dec 8, 2021, 13:00 UTC
postgresUrl: "postgres://postgres:[email protected]:5432/web3signer-gnosis",
secondsPerSlot: 5,
slotsPerEpoch: 16,
tlsCert: tlsCert(consensusClientSelected)
};
};
2 changes: 2 additions & 0 deletions packages/brain/src/modules/config/networks/holesky.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export const holeskyBrainConfig = (
shareCronInterval: 24 * 60 * 60 * 1000, // 1 day in ms
minGenesisTime: 1695902100, // Sep-28-2023 11:55:00 +UTC
postgresUrl: "postgres://postgres:[email protected]:5432/web3signer",
secondsPerSlot: 12,
slotsPerEpoch: 32,
tlsCert: tlsCert(consensusClientSelected)
};
};
2 changes: 2 additions & 0 deletions packages/brain/src/modules/config/networks/lukso.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export const luksoBrainConfig = (
shareCronInterval: 24 * 60 * 60 * 1000, // 1 day in ms
minGenesisTime: 1684856400, // Tuesday, 23 May 2023 15:40:00 GMT
postgresUrl: "postgres://postgres:[email protected]:5432/web3signer",
secondsPerSlot: 12,
slotsPerEpoch: 32,
tlsCert: tlsCert(consensusClientSelected)
};
};
2 changes: 2 additions & 0 deletions packages/brain/src/modules/config/networks/mainnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export const mainnetBrainConfig = (
shareCronInterval: 24 * 60 * 60 * 1000, // 1 day in ms
minGenesisTime: 1606824000,
postgresUrl: "postgres://postgres:[email protected]:5432/web3signer-mainnet",
secondsPerSlot: 12,
slotsPerEpoch: 32,
tlsCert: tlsCert(consensusClientSelected)
};
};
2 changes: 2 additions & 0 deletions packages/brain/src/modules/config/networks/prater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export const praterBrainConfig = (
shareCronInterval: 24 * 60 * 60 * 1000, // 1 day in ms
minGenesisTime: 1614588812, // Mar-01-2021 08:53:32 AM +UTC
postgresUrl: "postgres://postgres:[email protected]:5432/web3signer",
secondsPerSlot: 12,
slotsPerEpoch: 32,
tlsCert: tlsCert(consensusClientSelected)
};
};
2 changes: 2 additions & 0 deletions packages/brain/src/modules/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ export interface BrainConfig {
shareCronInterval: number;
minGenesisTime: number;
postgresUrl: string;
secondsPerSlot: number;
slotsPerEpoch: number;
tlsCert: Buffer | null;
}
5 changes: 3 additions & 2 deletions packages/brain/src/modules/cron/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { CronJob } from "./cron.js";
export { reloadValidators } from "./reloadValidators.js";
export { sendProofsOfValidation } from "./sendProofsOfValidation.js";
export { reloadValidators } from "./reloadValidators/index.js";
export { sendProofsOfValidation } from "./sendProofsOfValidation/index.js";
export { trackValidatorsPerformance, getSecondsToNextEpoch } from "./trackValidatorsPerformance/index.js";
Loading

0 comments on commit f162d7d

Please sign in to comment.