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

ADD: Fetching Validators for CSM & DVT Solutions #2099

Draft
wants to merge 25 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3c9f074
ADD: Fetching Validators for DVT Solutions
NeoPlays Oct 25, 2024
914192f
ADD: ssv & charon to sidebar
MaxTheGeeek Oct 29, 2024
e6bdb19
ADD: dvt property to frontend state
NeoPlays Oct 30, 2024
1645dae
ADD: first structure for obol, ssv and csm
MaxTheGeeek Nov 6, 2024
47c73c3
FIX: reading the keys on staking
MaxTheGeeek Nov 11, 2024
cfcb6e5
ADD: status row
MaxTheGeeek Nov 11, 2024
eab5852
Merge branch 'main' into pr/2099
MaxTheGeeek Nov 11, 2024
a0b2fe1
FIX conflicts
MaxTheGeeek Nov 11, 2024
a1ed06c
ADD: stereumPlus btn on login
MaxTheGeeek Nov 19, 2024
adc6621
ADD: dvt icon to dvt keys
MaxTheGeeek Nov 21, 2024
8d7eb62
ADD: filter obol, csm & ssv from validator panel
MaxTheGeeek Nov 21, 2024
696ed70
REFACTOR: dvt keys conditions
MaxTheGeeek Nov 25, 2024
3a35565
ADD: statis value to the stats
MaxTheGeeek Nov 25, 2024
c9bd9e4
ADD: additional csm queue icon for dvt keys
MaxTheGeeek Nov 28, 2024
52f2236
Merge branch 'main' into validator
NeoPlays Nov 28, 2024
95b3138
ADD: DVT Stats
NeoPlays Nov 28, 2024
d833d8e
fix typo
NeoPlays Nov 28, 2024
ddb2af8
FIX: format
MaxTheGeeek Nov 28, 2024
777a1af
FIX: Attestation Performance for Obol
NeoPlays Nov 29, 2024
3dd4064
ADD: get the in queue keys and additional icon to queue keys
MaxTheGeeek Nov 29, 2024
51ad547
FIX: inQueue stat
NeoPlays Dec 2, 2024
3f75ecb
FIX: csm queue status
NeoPlays Dec 3, 2024
96be464
FIX: importing key config error
MaxTheGeeek Dec 3, 2024
933b2ab
Merge branch 'validator' of https://github.com/NeoPlays/ethereum-node…
MaxTheGeeek Dec 3, 2024
fe8b195
ADD: inQueue status to key state
MaxTheGeeek Dec 3, 2024
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added launcher/public/img/stereumPlus/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 30 additions & 2 deletions launcher/public/output.css
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
}

/*
! tailwindcss v3.4.13 | MIT License | https://tailwindcss.com
! tailwindcss v3.4.14 | MIT License | https://tailwindcss.com
*/

/*
Expand Down Expand Up @@ -631,7 +631,7 @@ video {

/* Make elements with the HTML hidden attribute stay hidden by default */

[hidden] {
[hidden]:where(:not([hidden="until-found"])) {
display: none;
}

Expand Down Expand Up @@ -1943,6 +1943,10 @@ video {
max-height: 15rem;
}

.max-h-7{
max-height: 1.75rem;
}

.max-h-8{
max-height: 2rem;
}
Expand Down Expand Up @@ -2431,6 +2435,10 @@ video {
width: 90%;
}

.w-auto{
width: auto;
}

.w-fit{
width: -webkit-fit-content;
width: -moz-fit-content;
Expand Down Expand Up @@ -3239,6 +3247,11 @@ video {
border-bottom-left-radius: 0.125rem;
}

.rounded-l-full{
border-top-left-radius: 9999px;
border-bottom-left-radius: 9999px;
}

.rounded-l-md{
border-top-left-radius: 0.375rem;
border-bottom-left-radius: 0.375rem;
Expand Down Expand Up @@ -3615,6 +3628,11 @@ video {
border-top-color: rgb(255 255 255 / var(--tw-border-opacity));
}

.bg-\[\#093A4C\]{
--tw-bg-opacity: 1;
background-color: rgb(9 58 76 / var(--tw-bg-opacity));
}

.bg-\[\#0F1217\]{
--tw-bg-opacity: 1;
background-color: rgb(15 18 23 / var(--tw-bg-opacity));
Expand Down Expand Up @@ -3870,6 +3888,11 @@ video {
background-color: rgb(41 46 50 / var(--tw-bg-opacity));
}

.bg-\[\#2a2c30\]{
--tw-bg-opacity: 1;
background-color: rgb(42 44 48 / var(--tw-bg-opacity));
}

.bg-\[\#2a2e30\]{
--tw-bg-opacity: 1;
background-color: rgb(42 46 48 / var(--tw-bg-opacity));
Expand Down Expand Up @@ -3955,6 +3978,11 @@ video {
background-color: rgb(52 52 52 / var(--tw-bg-opacity));
}

.bg-\[\#363934\]{
--tw-bg-opacity: 1;
background-color: rgb(54 57 52 / var(--tw-bg-opacity));
}

.bg-\[\#387272\]{
--tw-bg-opacity: 1;
background-color: rgb(56 114 114 / var(--tw-bg-opacity));
Expand Down
83 changes: 83 additions & 0 deletions launcher/src/backend/Monitoring.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as crypto from "crypto";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import axios from "axios";
const { powerMonitor } = require("electron");

const globalMonitoringCache = {
Expand Down Expand Up @@ -3761,4 +3762,86 @@ export class Monitoring {
this.globalMonitoringCache.idleTimerRunning = false;
}
}

async getObolClusterInformation(serviceID) {
const serviceInfos = await this.getServiceInfos("CharonService");
const charon = serviceInfos.find((service) => service.config.serviceID === serviceID);
if (!charon) {
log.info("No such Charon found!");
return {};
}

const queries = {
app_monitoring_readyz: `app_monitoring_readyz{instance=~".*${serviceID}.*"}`, // for Cluster Peer
cluster_attestation_performance: `(sum(increase(core_tracker_success_duties_total{instance=~".*${serviceID}.*",duty="attester"}[1h])) / sum(increase(core_tracker_expect_duties_total{instance=~".*${serviceID}.*",duty="attester"}[1h])) > 0) * 100`,
cluster_attestation_participation: `core_tracker_participation{instance=~".*${serviceID}.*",duty="attester"}`,
cluster_operators: `cluster_operators{instance=~".*${serviceID}.*"}`,
cluster_threshold: `cluster_threshold{instance=~".*${serviceID}.*"}`,
cluster_validators: `cluster_validators{instance=~".*${serviceID}.*"}`,
};

const queryPromises = Object.entries(queries).map(([key, query]) => {
return this.queryPrometheus(encodeURIComponent(query)).then((result) => ({ key, result }));
});

const results = await Promise.all(queryPromises);

const stats = {};

results.forEach((metric) => {
if (metric.result.status != "success") {
return;
}
switch (metric.key) {
case "app_monitoring_readyz":
stats.nodeStatus = metric.result.data.result[0].value[1] === "1" ? "ACTIVE" : "INACTIVE";
stats.peerName = metric.result.data.result[0].metric.cluster_peer;
break;
case "cluster_attestation_performance":
stats.attestationPerformance = metric.result.data.result[0].value[1];
break;
case "cluster_attestation_participation":
stats.attestationParticipation = metric.result.data.result.reduce((acc, curr) => acc + parseInt(curr.value[1]), 0);
break;
case "cluster_operators":
stats.operators = parseInt(metric.result.data.result[0].value[1]);
break;
case "cluster_threshold":
stats.threshold = parseInt(metric.result.data.result[0].value[1]);
break;
case "cluster_validators":
stats.validators = parseInt(metric.result.data.result[0].value[1]);
break;
}
});
return stats;
}

async getSSVClusterInformation(serviceID) {
const serviceInfos = await this.getServiceInfos("SSVNetworkService");
const ssv = serviceInfos.find((service) => service.config.serviceID === serviceID);
if (!ssv) {
log.info("No such SSV found!");
return {};
}
const operator = await this.nodeConnection.getSSVLastKnownOperatorId(serviceID);
if (!operator) {
log.info("No SSV Operator found!");
return {};
}
const stats = {};

stats.operator = operator;

const data = await axios.get(`https://api.ssv.network/api/v4/${ssv.config.network}/operators/` + operator);
if (data.status !== 200) {
log.error("Error fetching SSV Operator Information:\n" + JSON.stringify(data.data, null, 2));
return {};
}

stats.private = data.data.is_private;
stats.status = data.data.status;
stats.performance = data.data.performance["24h"];
return stats;
}
}
84 changes: 70 additions & 14 deletions launcher/src/backend/ValidatorAccountManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,23 +221,38 @@ export class ValidatorAccountManager {
this.nodeConnection.taskManager.otherTasksHandler(ref, `Listing Keys`);
try {
let client = await this.nodeConnection.readServiceConfiguration(serviceID);
const result = await this.keymanagerAPI(client, "GET", "/eth/v1/keystores");
let data = {};
if (client.service === "CharonService" || client.service === "SSVNetworkService") {
const keys = await this.getDVTKeys(serviceID);
data.data = keys.map((dv) => {
return {
validating_pubkey: client.service === "CharonService" ? dv.distributed_public_key : "0x" + dv.public_key,
derivation_path: "",
readonly: false,
dvt: true,
};
});
//Push successful task
this.nodeConnection.taskManager.otherTasksHandler(ref, `Get Keys`, true, JSON.stringify(data.data, null, 2));
} else {
const result = await this.keymanagerAPI(client, "GET", "/eth/v1/keystores");

//Error handling
if (SSHService.checkExecError(result) && result.stderr) throw SSHService.extractExecError(result);
if (!result.stdout)
throw `ReturnCode: ${result.rc}\nStderr: ${result.stderr}\nStdout: ${result.stdout}\nIs Your Consensus Client Running?`;
//Error handling
if (SSHService.checkExecError(result) && result.stderr) throw SSHService.extractExecError(result);
if (!result.stdout)
throw `ReturnCode: ${result.rc}\nStderr: ${result.stderr}\nStdout: ${result.stdout}\nIs Your Consensus Client Running?`;

const data = JSON.parse(result.stdout);
if (data.data === undefined) {
if (data.code === undefined || data.message === undefined) {
throw "Undexpected Error: " + result;
data = JSON.parse(result.stdout);
if (data.data === undefined) {
if (data.code === undefined || data.message === undefined) {
throw "Undexpected Error: " + result;
}
throw data.code + " " + data.message;
}
throw data.code + " " + data.message;
}

//Push successful task
this.nodeConnection.taskManager.otherTasksHandler(ref, `Get Keys`, true, result.stdout);
//Push successful task
this.nodeConnection.taskManager.otherTasksHandler(ref, `Get Keys`, true, result.stdout);
}

if (!data.data) data.data = [];
this.writeKeys(data.data.map((key) => key.validating_pubkey));
Expand Down Expand Up @@ -1068,7 +1083,7 @@ export class ValidatorAccountManager {
let charonClient = services.find((service) => service.service === "CharonService");
if (!charonClient) throw "Couldn't find CharonService";
const dataDir = path.posix.join(charonClient.getDataDir(), ".charon");
this.nodeConnection.sshService.exec(`rm -rf ${dataDir}`);
await this.nodeConnection.sshService.exec(`rm -rf ${dataDir}`);
const result = await this.nodeConnection.sshService.uploadDirectorySSH(path.normalize(localPath), dataDir);
if (result) {
log.info("Obol Backup uploaded from: ", localPath);
Expand All @@ -1077,4 +1092,45 @@ export class ValidatorAccountManager {
log.error("Error uploading Obol Backup: ", err);
}
}

async getDVTKeys(serviceID) {
const service = (await this.serviceManager.readServiceConfigurations()).find((s) => s.id === serviceID);
if (!service) throw new Error(`Service with id ${serviceID} not found`);
switch (service.service) {
case "CharonService": {
const result = await this.nodeConnection.sshService.exec(service.getReadClusterLockCommand());
const clusterLock = JSON.parse(result.stdout);
return clusterLock.distributed_validators;
}
case "SSVNetworkService": {
const ssvConfig = await this.nodeConnection.getSSVTotalConfig(serviceID);
//Get Operator ID
const response = await axios.get(
`https://api.ssv.network/api/v4/${service.network}/operators/public_key/` + ssvConfig.privateKeyFileData.publicKey
);
if (response.status !== 200 && !response?.data?.data?.id)
throw new Error(`Couldn't get Operator ID from SSV Network ${response.status} ${response.statusText}`);
const operatorID = response.data.data.id;

//get pagination info
let result = await axios.get(
`https://api.ssv.network/api/v4/${service.network}/validators/in_operator/${operatorID}?page=${1}&perPage=100`
);
if (result.status !== 200) throw new Error(`Couldn't get Validator Keys from SSV Network ${result.status} ${result.statusText}`);

//get all pages and concat them
for (let i = 2; i <= result.data.pagination.pages; i++) {
const page = await axios.get(
`https://api.ssv.network/api/v4/${service.network}/validators/in_operator/${operatorID}?page=${i}&perPage=100`
);
if (page.status !== 200) throw new Error(`Couldn't get Validator Keys from SSV Network ${page.status} ${page.statusText}`);
result.data.validators = result.data.validators.concat(page.data.validators);
}

return result.data.validators;
}
default:
throw new Error(`Service ${service.service} not supported`);
}
}
}
4 changes: 4 additions & 0 deletions launcher/src/backend/ethereum-services/CharonService.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ export class CharonService extends NodeService {
return `ls -1 -a ${this.getDataDir()}/.charon`;
}

getReadClusterLockCommand() {
return `cat ${this.getDataDir()}/.charon/cluster-lock.json`;
}

//definitionFile as URL or Path to file (default ".charon/cluster-definition.json" by dkg command)
getDKGCommand(definitionFile) {
return `docker run -u 0 --name "dkg-container" -d -v "${this.getDataDir()}:/opt/charon" ${this.image + ":" + this.imageVersion} dkg ${
Expand Down
8 changes: 8 additions & 0 deletions launcher/src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,14 @@ ipcMain.handle("getCSMQueue", async (event, args) => {
return await checkSigningKeys(args.keysArray, monitoring);
});

ipcMain.handle("getObolClusterInformation", async (event, args) => {
return await monitoring.getObolClusterInformation(args.serviceID);
});

ipcMain.handle("getSSVClusterInformation", async (event, args) => {
return await monitoring.getSSVClusterInformation(args.serviceID);
});

// Scheme must be registered before the app is ready
protocol.registerSchemesAsPrivileged([{ scheme: "app", privileges: { secure: true, standard: true } }]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,29 +46,39 @@
@mouseleave="footerStore.cursorLocation = ''"
/>
</div>
<button
class="w-full h-[50px] self-end col-start-1 col-span-full row-start-11 row-span-2 bg-gray-200 rounded-md px-4 py-2 flex justify-center items-center shadow-lg shadow-black active:shadow-none active:scale-95 cursor-pointer space-x-4 transition-all duration-200 ease-in-out hover:bg-[#336666] text-gray-800 hover:text-gray-100"
@click="serverLogin"
@mouseenter="footerStore.cursorLocation = `${t('serverList.addServer')}`"
@mouseleave="footerStore.cursorLocation = ''"
>
<img
class="w-7 h-7 border border-gray-500 bg-teal-500 rounded-full p-1"
src="/img/icon/server-management-icons/plus.png"
alt="Add Icon"
/>
<span class="text-sm text-left uppercase font-bold">{{ $t("multiServer.addServer") }}</span>
</button>
<div class="col-start-1 col-span-full row-start-11 row-span-2 self-end grid grid-cols-12 gap-x-2">
<button
class="w-full h-[50px] self-end col-start-6 col-span-full row-start-11 row-span-2 bg-gray-200 rounded-md px-4 py-2 flex justify-start items-center shadow-lg shadow-black active:shadow-none active:scale-95 cursor-pointer space-x-4 transition-all duration-200 ease-in-out hover:bg-[#336666] text-gray-800 hover:text-gray-100"
@click="serverLogin"
@mouseenter="footerStore.cursorLocation = `${t('serverList.addServer')}`"
@mouseleave="footerStore.cursorLocation = ''"
>
<img
class="w-7 h-7 border border-gray-500 bg-teal-500 rounded-full p-1"
src="/img/icon/server-management-icons/plus.png"
alt="Add Icon"
/>
<span class="text-sm text-left uppercase font-bold">{{ $t("multiServer.addServer") }}</span>
</button>

<div
class="w-full h-[50px] self-end col-start-1 col-end-6 row-start-11 row-span-2 flex justify-center items-center bg-[#093A4C] rounded-md px-4 py-2 cursor-pointer space-x-2 transition-all duration-200 ease-in-out hover:bg-[#336666] shadow-lg shadow-black active:shadow-none"
@click="getToStereumPlusLogin"
>
<span class="text-xs text-gray-200 font-normal font-sans"> GET SERVER </span>
<img class="w-auto h-6 border rounded-[4px]" src="/img/stereumPlus/logo.png" alt="Server Icon" />
</div>
</div>
</div>
</template>
<script setup>
import ServerRow from "./ServerRow.vue";
import i18n from "@/includes/i18n";
import ControlService from "@/store/ControlService";
import { useServers } from "@/store/servers";
import { useControlStore } from "@/store/theControl";
import { onMounted, watch, ref } from "vue";
import { useFooter } from "@/store/theFooter";
import i18n from "@/includes/i18n";
import { onMounted, ref, watch } from "vue";
import ServerRow from "./ServerRow.vue";

const t = i18n.global.t;

Expand Down Expand Up @@ -116,6 +126,10 @@ onMounted(async () => {

//Methods

const getToStereumPlusLogin = () => {
window.open("https://stereumplus.com/", "_blank");
};

const loadStoredConnections = async () => {
serverStore.savedServers = await ControlService.readConfig();

Expand Down
Loading
Loading