Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement validators performance tracker #329

Merged
merged 19 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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