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

Process validator performance data #346

Merged
merged 22 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
31 changes: 13 additions & 18 deletions packages/brain/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import { params } from "./params.js";
import {
CronJob,
reloadValidators,
trackValidatorsPerformance,
trackValidatorsPerformanceCron,
sendProofsOfValidation,
getSecondsToNextEpoch
startWithinTenFirstPercentageOfEpoch
} from "./modules/cron/index.js";
import { PostgresClient } from "./modules/apiClients/index.js";
import { brainConfig } from "./modules/config/index.js";
Expand Down Expand Up @@ -102,29 +102,24 @@ 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,
executionClient,
consensusClient
})

// executes once every epoch
export const trackValidatorsPerformanceCronTask = new CronJob(slotsPerEpoch * secondsPerSlot * 1000, () =>
trackValidatorsPerformanceCron({ brainDb, postgresClient, beaconchainApi, executionClient, consensusClient })
);
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();
startWithinTenFirstPercentageOfEpoch({
minGenesisTime,
secondsPerSlot,
slotsPerEpoch,
jobFunction: trackValidatorsPerformanceCronTask
});

// Graceful shutdown
function handle(signal: string): void {
logger.info(`${signal} received. Shutting down...`);
reloadValidatorsCron.stop();
proofOfValidationCron.stop();
trackValidatorsPerformanceCronTask.stop();
brainDb.close();
postgresClient.close().catch((err) => logger.error(`Error closing postgres client`, err)); // postgresClient db connection is the only external resource that needs to be closed
uiServer.close();
Expand Down
6 changes: 2 additions & 4 deletions packages/brain/src/modules/apiClients/beaconchain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,15 @@ import {
BeaconchainSyncingStatusGetResponse,
BeaconchainSyncCommitteePostResponse,
BeaconchainBlockRewardsGetResponse,
BeaconchainProposerDutiesGetResponse
BeaconchainProposerDutiesGetResponse,
BlockId
} from "./types.js";
import { StandardApi } from "../standard.js";
import path from "path";
import { ApiParams } from "../types.js";
import { Network } from "@stakingbrain/common";
import { BeaconchainApiError } from "./error.js";

// 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
2 changes: 2 additions & 0 deletions packages/brain/src/modules/apiClients/beaconchain/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,5 @@ export interface BeaconchainLivenessPostResponse {
is_live: boolean;
}[];
}

export type BlockId = "head" | "genesis" | "finalized" | string | `0x${string}`;
76 changes: 65 additions & 11 deletions packages/brain/src/modules/apiClients/postgres/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ enum Columns {
liveness = "liveness",
blockProposalStatus = "block_proposal_status",
syncCommitteeRewards = "sync_comittee_rewards",
attestationsRewards = "attestations_rewards",
attestationsTotalRewards = "attestations_total_rewards",
error = "error"
}

Expand Down Expand Up @@ -123,9 +123,9 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} (
${Columns.consensusClient} ${this.CONSENSUS_CLIENT} NOT NULL,
${Columns.slot} BIGINT,
${Columns.liveness} BOOLEAN,
${Columns.blockProposalStatus} ${this.BLOCK_PROPOSAL_STATUS},
${Columns.blockProposalStatus} ${this.BLOCK_PROPOSAL_STATUS} NOT NULL,
${Columns.syncCommitteeRewards} BIGINT,
${Columns.attestationsRewards} JSONB,
${Columns.attestationsTotalRewards} JSONB NOT NULL,
${Columns.error} TEXT,
PRIMARY KEY (${Columns.validatorIndex}, ${Columns.epoch})
);
Expand All @@ -150,12 +150,12 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} (
* Inserts the given performance data into the database.
*
* @param data - The performance data to insert.
* @example insertPerformanceData({ validatorIndex: 1, epoch: 1, slot: 1, liveness: true, blockProposalStatus: "missed", syncCommitteeRewards: 100, attestationsRewards: { attestation1: 10, attestation2: 20 } })
* @example insertPerformanceData({ validatorIndex: 1, epoch: 1, slot: 1, liveness: true, blockProposalStatus: "missed", syncCommitteeRewards: 100, attestationsTotalRewards: { attestation1: 10, attestation2: 20 } })
*/
public async insertPerformanceData(data: ValidatorPerformance): Promise<void> {
const query = `
INSERT INTO ${this.tableName} (${Columns.validatorIndex}, ${Columns.epoch}, ${Columns.slot}, ${Columns.liveness}, ${Columns.blockProposalStatus}, ${Columns.syncCommitteeRewards}, ${Columns.attestationsRewards}, ${Columns.error})
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
INSERT INTO ${this.tableName} (${Columns.validatorIndex}, ${Columns.epoch}, ${Columns.executionClient}, ${Columns.consensusClient}, ${Columns.slot}, ${Columns.liveness}, ${Columns.blockProposalStatus}, ${Columns.syncCommitteeRewards}, ${Columns.attestationsTotalRewards}, ${Columns.error})
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
`;

await this.sql.unsafe(query, [
Expand All @@ -165,15 +165,16 @@ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
data.consensusClient,
data.slot ?? null,
data.liveness ?? null,
data.blockProposalStatus ?? null,
data.blockProposalStatus,
data.syncCommitteeRewards ?? null,
JSON.stringify(data.attestationsRewards) ?? null, // JSONB expects a string
JSON.stringify(data.attestationsTotalRewards), // JSONB expects a string
data.error ?? null
]);
}

/**
* Get the validators data for the given validator indexes from all epochs.
* Get the validators data for the given validator indexes from all epochs. In order to improve data process
* it will return a map with the validator index as key and the performance data as value.
*
* @param validatorIndexes - The indexes of the validators to get the data for.
* @returns The performance data for the given validators.
Expand All @@ -185,7 +186,7 @@ WHERE ${Columns.validatorIndex} = ANY($1)
`;

const result = await this.sql.unsafe(query, [validatorIndexes]);
// TODO: add type for result
// TODO: add type to result
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return result.map((row: any) => ({
validatorIndex: row.validator_index,
Expand All @@ -196,11 +197,64 @@ WHERE ${Columns.validatorIndex} = ANY($1)
liveness: row.liveness,
blockProposalStatus: row.block_proposal_status,
syncCommitteeRewards: row.sync_comittee_rewards,
attestationsRewards: row.attestations_rewards,
attestationsTotalRewards: JSON.parse(row.attestations_total_rewards),
error: row.error
}));
}

/**
* Get tje validators data for the given validator indexes and an epoch start and end range. In order to improve data process
* it will return a map with the validator index as key and the performance data as value.
*
* @param validatorIndexes - The indexes of the validators to get the data for.
* @param startEpoch - The start epoch number.
* @param endEpoch - The end epoch number.
* @returns The performance data for the given validators.
*/
public async getValidatorsDataMapForEpochRange({
validatorIndexes,
startEpoch,
endEpoch
}: {
validatorIndexes: string[];
startEpoch: number;
endEpoch: number;
}): Promise<Map<string, ValidatorPerformance[]>> {
const query = `
SELECT * FROM ${this.tableName}
WHERE ${Columns.validatorIndex} = ANY($1)
AND ${Columns.epoch} >= $2
AND ${Columns.epoch} <= $3
`;

const result = await this.sql.unsafe(query, [validatorIndexes, startEpoch, endEpoch]);
// TODO: add type to result
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return result.reduce((map: Map<string, ValidatorPerformance[]>, row: any) => {
const key = row.validator_index;
const performanceData = {
validatorIndex: row.validator_index,
epoch: row.epoch,
executionClient: row.execution_client,
consensusClient: row.consensus_client,
slot: row.slot,
liveness: row.liveness,
blockProposalStatus: row.block_proposal_status,
syncCommitteeRewards: row.sync_comittee_rewards,
attestationsTotalRewards: JSON.parse(row.attestations_total_rewards),
error: row.error
};

if (map.has(key)) {
map.get(key)?.push(performanceData);
} else {
map.set(key, [performanceData]);
}

return map;
}, new Map<string, ValidatorPerformance[]>());
}

/**
* Method to close the database connection.
*/
Expand Down
13 changes: 11 additions & 2 deletions packages/brain/src/modules/apiClients/postgres/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,24 @@ export enum BlockProposalStatus {
Error = "Error"
}

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

export interface ValidatorPerformance {
validatorIndex: number;
epoch: number;
executionClient: ExecutionClient;
consensusClient: ConsensusClient;
blockProposalStatus: BlockProposalStatus;
attestationsTotalRewards: AttestationsTotalRewards;
slot?: number;
liveness?: boolean;
blockProposalStatus?: BlockProposalStatus;
syncCommitteeRewards?: number;
attestationsRewards?: object;
error?: string;
}
2 changes: 1 addition & 1 deletion packages/brain/src/modules/config/networks/gnosis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const gnosisBrainConfig = (
consensusClient,
isMevBoostSet,
executionClientUrl: "http://execution.gnosis.dncore.dappnode:8545",
validatorUrl: "http://validator.gnosis.dncore.dappnode:3500",
validatorUrl: `${consensusClient === "teku" ? "https" : "http"}://validator.gnosis.dncore.dappnode:3500`,
beaconchainUrl: "http:/beacon-chain.gnosis.dncore.dappnode:3500",
blockExplorerUrl: "https://gnosischa.in",
signerUrl: "http://web3signer.web3signer-gnosis.dappnode:9000",
Expand Down
2 changes: 1 addition & 1 deletion packages/brain/src/modules/config/networks/holesky.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const holeskyBrainConfig = (
consensusClient,
isMevBoostSet,
executionClientUrl: "http://execution.holesky.dncore.dappnode:8545",
validatorUrl: "http://validator.holesky.dncore.dappnode:3500",
validatorUrl: `${consensusClient === "teku" ? "https" : "http"}://validator.holesky.dncore.dappnode:3500`,
beaconchainUrl: "http:/beacon-chain.holesky.dncore.dappnode:3500",
blockExplorerUrl: "https://holesky.beaconcha.in",
signerUrl: "http://web3signer.web3signer-holesky.dappnode:9000",
Expand Down
2 changes: 1 addition & 1 deletion packages/brain/src/modules/config/networks/lukso.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const luksoBrainConfig = (
consensusClient,
isMevBoostSet,
executionClientUrl: "http://execution.lukso.dncore.dappnode:8545",
validatorUrl: "http://validator.lukso.dncore.dappnode:3500",
validatorUrl: `${consensusClient === "teku" ? "https" : "http"}://validator.lukso.dncore.dappnode:3500`,
beaconchainUrl: "http:/beacon-chain.lukso.dncore.dappnode:3500",
blockExplorerUrl: "https://explorer.consensus.mainnet.lukso.network/",
signerUrl: "http://web3signer.web3signer-lukso.dappnode:9000",
Expand Down
2 changes: 1 addition & 1 deletion packages/brain/src/modules/config/networks/mainnet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const mainnetBrainConfig = (
consensusClient,
isMevBoostSet,
executionClientUrl: "http://execution.mainnet.dncore.dappnode:8545",
validatorUrl: "http://validator.mainnet.dncore.dappnode:3500",
validatorUrl: `${consensusClient === "teku" ? "https" : "http"}://validator.mainnet.dncore.dappnode:3500`,
beaconchainUrl: "http:/beacon-chain.mainnet.dncore.dappnode:3500",
blockExplorerUrl: "https://beaconcha.in",
signerUrl: "http://web3signer.web3signer.dappnode:9000",
Expand Down
2 changes: 1 addition & 1 deletion packages/brain/src/modules/config/networks/prater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const praterBrainConfig = (
consensusClient,
isMevBoostSet,
executionClientUrl: "http://execution.prater.dncore.dappnode:8545",
validatorUrl: "http://validator.prater.dncore.dappnode:3500",
validatorUrl: `${consensusClient === "teku" ? "https" : "http"}://validator.prater.dncore.dappnode:3500`,
beaconchainUrl: "http:/beacon-chain.prater.dncore.dappnode:3500",
blockExplorerUrl: "https://prater.beaconcha.in",
signerUrl: "http://web3signer.web3signer-prater.dappnode:9000",
Expand Down
5 changes: 4 additions & 1 deletion packages/brain/src/modules/cron/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export { CronJob } from "./cron.js";
export { reloadValidators } from "./reloadValidators/index.js";
export { sendProofsOfValidation } from "./sendProofsOfValidation/index.js";
export { trackValidatorsPerformance, getSecondsToNextEpoch } from "./trackValidatorsPerformance/index.js";
export {
trackValidatorsPerformanceCron,
startWithinTenFirstPercentageOfEpoch
} from "./trackValidatorsPerformance/index.js";

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,22 @@ import { logPrefix } from "./logPrefix.js";
*/
export async function getActiveValidatorsLoadedInBrain({
beaconchainApi,
brainDb,
activeValidatorsIndexes
brainDb
}: {
beaconchainApi: BeaconchainApi;
brainDb: BrainDataBase;
activeValidatorsIndexes: string[];
}): Promise<void> {
}): Promise<string[]> {
const validatorIndexes = await getValidatorIndexesAndSaveInDb({ beaconchainApi, brainDb });
if (validatorIndexes.length === 0) return;
(
if (validatorIndexes.length === 0) return [];
return (
await beaconchainApi.postStateValidators({
body: {
ids: validatorIndexes,
statuses: validatorIndexes.map(() => ValidatorStatus.ACTIVE_ONGOING)
},
stateId: "finalized"
})
).data.forEach((validator) => activeValidatorsIndexes.push(validator.index));
).data.map((validator) => validator.index.toString());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,22 @@ import { TotalRewards } from "../../apiClients/types.js";
*
* @param {BeaconchainApi} beaconchainApi - Beaconchain API client.
* @param {string} epoch - The epoch to get the rewards.
* @param {string[]} validatorIndexes - Array of validator indexes.
* @param {string[]} activeValidatorsIndexes - Array of active validator indexes.
* @returns {TotalRewards[]} - Array of total rewards for the validators.
*/
export async function getAttestationsTotalRewards({
beaconchainApi,
epoch,
validatorIndexes,
totalRewards
activeValidatorsIndexes
}: {
beaconchainApi: BeaconchainApi;
epoch: string;
validatorIndexes: string[];
totalRewards: TotalRewards[];
}): Promise<void> {
(
activeValidatorsIndexes: string[];
}): Promise<TotalRewards[]> {
return (
await beaconchainApi.getAttestationsRewards({
epoch,
pubkeysOrIndexes: validatorIndexes
pubkeysOrIndexes: activeValidatorsIndexes
})
).data.total_rewards.forEach((reward) => totalRewards.push(reward));
).data.total_rewards;
}
Loading