From 095805602eba92829ea8d71d3e1f0b8d3cdfbd2e Mon Sep 17 00:00:00 2001 From: daverolo <107847185+daverolo@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:01:12 +0100 Subject: [PATCH 1/3] REFACTOR: enable ssv holesky clickinstall --- launcher/src/backend/ethereum-services/SSVNetworkService.js | 3 ++- launcher/src/store/nodeManage.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/launcher/src/backend/ethereum-services/SSVNetworkService.js b/launcher/src/backend/ethereum-services/SSVNetworkService.js index a2ef77604..d5401771b 100755 --- a/launcher/src/backend/ethereum-services/SSVNetworkService.js +++ b/launcher/src/backend/ethereum-services/SSVNetworkService.js @@ -18,7 +18,8 @@ export class SSVNetworkService extends NodeService { ssv: # The SSV network to join to # Mainnet = Network: mainnet (default) - # Testnet = Network: jato-v2 + # Testnet (Goerli) = Network: jato-v2 + # Testnet (Holesky) = Network: holesky Network: ${network === "goerli" ? "jato-v2" : network} ValidatorOptions: diff --git a/launcher/src/store/nodeManage.js b/launcher/src/store/nodeManage.js index 60c233ad1..470f384f1 100755 --- a/launcher/src/store/nodeManage.js +++ b/launcher/src/store/nodeManage.js @@ -207,7 +207,7 @@ export const useNodeManage = defineStore("nodeManage", { icon: "/img/icon/click-installation/testnet-icon.png", currencyIcon: "/img/icon/control/goETH_Currency_Symbol.png", dataEndpoint: "https://holesky.beaconcha.in/api/v1", - support: ["staking", "stereum on arm", "mev boost"], + support: ["staking", "ssv.network", "stereum on arm", "mev boost"], }, ], currentNetwork: {}, From aa48dab4bfab84540d65026870d43b8f77bf8e29 Mon Sep 17 00:00:00 2001 From: daverolo <107847185+daverolo@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:01:31 +0100 Subject: [PATCH 2/3] REFACTOR: add holesky support for ssv operator url --- launcher/src/components/UI/services-modal/SsvDashboard.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/src/components/UI/services-modal/SsvDashboard.vue b/launcher/src/components/UI/services-modal/SsvDashboard.vue index 9a8b77c5e..7c59a07df 100755 --- a/launcher/src/components/UI/services-modal/SsvDashboard.vue +++ b/launcher/src/components/UI/services-modal/SsvDashboard.vue @@ -76,7 +76,7 @@ export default { async getURL() { const grafana = this.installedServices.find((service) => service.service === "GrafanaService"); this.ssvNetworkUrl.operatorUrl = `https://${ - this.currentNetwork.network === "goerli" ? "goerli." : "" + ["goerli", "holesky"].includes(this.currentNetwork.network) ? this.currentNetwork.network + "." : "" }explorer.ssv.network/operators/${this.operatorData?.id ? this.operatorData?.id : ""}`; this.ssvNetworkUrl.grafanaDashboardUrl = grafana.linkUrl ? grafana.linkUrl + "/d/QNiMrdoVz/node-dashboard?orgId=1" From 3e9903fa4094ed5c426f5baf74698160eb13dac9 Mon Sep 17 00:00:00 2001 From: daverolo <107847185+daverolo@users.noreply.github.com> Date: Mon, 4 Dec 2023 21:11:38 +0100 Subject: [PATCH 3/3] REFACTOR: get ssv public key from keystore for encrypted keys --- launcher/src/backend/NodeConnection.js | 132 ++++++++++++++---- launcher/src/background.js | 4 + .../components/UI/services-modal/SsvModal.vue | 6 + launcher/src/store/ControlService.js | 6 +- 4 files changed, 118 insertions(+), 30 deletions(-) diff --git a/launcher/src/backend/NodeConnection.js b/launcher/src/backend/NodeConnection.js index 6ebdb809c..75b33e010 100755 --- a/launcher/src/backend/NodeConnection.js +++ b/launcher/src/backend/NodeConnection.js @@ -336,17 +336,17 @@ export class NodeConnection { " ANSIBLE_LOAD_CALLBACK_PLUGINS=1\ ANSIBLE_STDOUT_CALLBACK=stereumjson\ ANSIBLE_LOG_FOLDER=/tmp/" + - playbookRunRef + - "\ + playbookRunRef + + "\ ansible-playbook\ --connection=local\ --inventory 127.0.0.1,\ --extra-vars " + - StringUtils.escapeStringForShell(extraVarsJson) + - "\ + StringUtils.escapeStringForShell(extraVarsJson) + + "\ " + - this.settings.stereum.settings.controls_install_path + - "/ansible/controls/genericPlaybook.yaml\ + this.settings.stereum.settings.controls_install_path + + "/ansible/controls/genericPlaybook.yaml\ " ); } catch (err) { @@ -448,6 +448,83 @@ export class NodeConnection { return serviceYAML.stdout; } + async readSSVKeystoreConfig(serviceID) { + try { + const service = await this.readServiceConfiguration(serviceID); + let configPath = ServiceVolume.buildByConfig( + service.volumes.find((v) => v.split(":").slice(-1) == "/data") + ).destinationPath; + if (configPath.endsWith("/")) configPath = configPath.slice(0, -1, ""); //if path ends with '/' remove it + + let ssvNetworkConfig = await this.sshService.exec(`cat ${configPath}/config.yaml`); + if (SSHService.checkExecError(ssvNetworkConfig)) { + throw new Error( + "Failed reading SSV network config to get keystore keystore from service " + + serviceID + + ": " + + SSHService.extractExecError(ssvNetworkConfig) + ); + } + let ssvNetworkConfigParsed = YAML.parse(ssvNetworkConfig.stdout); + + const regex = /^(\.\/|)data\//; // string starts with "./data/" or "data/" + let keyStorePasswordFile = ssvNetworkConfigParsed.KeyStore.PasswordFile; + if (regex.test(keyStorePasswordFile)) { + keyStorePasswordFile = configPath + "/" + keyStorePasswordFile.replace(regex, ""); + } + let keyStorePrivateKeyFile = ssvNetworkConfigParsed.KeyStore.PrivateKeyFile; + if (regex.test(keyStorePrivateKeyFile)) { + keyStorePrivateKeyFile = configPath + "/" + keyStorePrivateKeyFile.replace(regex, ""); + } + + let keyStorePasswordFileRequest = await this.sshService.exec(`cat "${keyStorePasswordFile}"`); + if (SSHService.checkExecError(keyStorePasswordFileRequest) || keyStorePasswordFileRequest.rc) { + log.error( + "Can't read SSV keystore password file content from service " + serviceID, + keyStorePasswordFileRequest.stderr + ); + throw new Error( + "Can't read SSV keystore password file content from service " + + serviceID + + ": " + + keyStorePasswordFileRequest.stderr + ); + } + let keyStorePasswordFileContent = keyStorePasswordFileRequest.stdout; + + let keyStorePrivateKeyFileRequest = await this.sshService.exec(`cat "${keyStorePrivateKeyFile}"`); + if (SSHService.checkExecError(keyStorePrivateKeyFileRequest) || keyStorePrivateKeyFileRequest.rc) { + log.error( + "Can't read SSV keystore private key file content from service " + serviceID, + keyStorePrivateKeyFileRequest.stderr + ); + throw new Error( + "Can't read SSV keystore private key file content from service " + + serviceID + + ": " + + keyStorePrivateKeyFileRequest.stderr + ); + } + let keyStorePrivateKeyFileContent = keyStorePrivateKeyFileRequest.stdout; + + return { + passwordFilePath: keyStorePasswordFile, + passwordFileData: keyStorePasswordFileContent.trim(), + privateKeyFilePath: keyStorePrivateKeyFile, + privateKeyFileData: (() => { + try { + return JSON.parse(keyStorePrivateKeyFileContent); + } catch (e) { + return keyStorePrivateKeyFileContent; + } + })(), + }; + } catch (err) { + log.error("Can't read SSV keystore from service " + serviceID, err); + throw new Error("Can't read SSV keystore from service " + serviceID + ": " + err); + } + } + async readSSVNetworkConfig(serviceID) { let SSVNetworkConfig; try { @@ -593,10 +670,10 @@ export class NodeConnection { } configStatus = await this.sshService.exec( "echo -e " + - StringUtils.escapeStringForShell(service.data.trim()) + - " > /etc/stereum/services/" + - service.id + - ".yaml" + StringUtils.escapeStringForShell(service.data.trim()) + + " > /etc/stereum/services/" + + service.id + + ".yaml" ); } catch (err) { this.taskManager.otherSubTasks.push({ @@ -642,10 +719,10 @@ export class NodeConnection { try { configStatus = await this.sshService.exec( "echo -e " + - StringUtils.escapeStringForShell(YAML.stringify(serviceConfiguration)) + - " > /etc/stereum/services/" + - serviceConfiguration.id + - ".yaml" + StringUtils.escapeStringForShell(YAML.stringify(serviceConfiguration)) + + " > /etc/stereum/services/" + + serviceConfiguration.id + + ".yaml" ); } catch (err) { this.taskManager.otherSubTasks.push({ @@ -667,9 +744,9 @@ export class NodeConnection { this.taskManager.finishedOtherTasks.push({ otherRunRef: ref }); throw new Error( "Failed writing service configuration " + - serviceConfiguration.id + - ": " + - SSHService.extractExecError(configStatus) + serviceConfiguration.id + + ": " + + SSHService.extractExecError(configStatus) ); } this.taskManager.otherSubTasks.push({ @@ -1139,7 +1216,7 @@ export class NodeConnection { log.info(" Could not connect.\n" + (retry.maxTries - retry.counter) + " tries left."); } } - log.info("OUT OF WHILE LOOP") + log.info("OUT OF WHILE LOOP"); if (retry.connected) { await this.establish(this.taskManager); this.taskManager.otherTasksHandler(ref, "Connected", true); @@ -1238,13 +1315,14 @@ export class NodeConnection { async dumpDockerLogs() { try { const services = await this.listServices(); - log.info(services) + log.info(services); const containerIds = services.map((service) => service.ID); - const logsPromises = containerIds.map(async (containerId) => { try { - let jsonFilePathsResult = await this.sshService.exec(`ls /var/lib/docker/containers/${containerId}/${containerId}*`); + let jsonFilePathsResult = await this.sshService.exec( + `ls /var/lib/docker/containers/${containerId}/${containerId}*` + ); if (SSHService.checkExecError(jsonFilePathsResult)) { throw new Error("Failed reading docker logs: " + SSHService.extractExecError(jsonFilePathsResult)); @@ -1253,25 +1331,21 @@ export class NodeConnection { const jsonFilePaths = jsonFilePathsResult.stdout.split("\n").filter((i) => i); for (const jsonFilePath of jsonFilePaths) { - const logs = await this.sshService.exec(`cat ${jsonFilePath}`); return { containerId, logs }; } } catch (err) { - log.error("Failed to dump Docker Logs: ", err) - return { containerId, logs: '' }; + log.error("Failed to dump Docker Logs: ", err); + return { containerId, logs: "" }; } }); const allLogs = await Promise.all(logsPromises); return allLogs; - } catch (err) { - log.error("Failed to dump Docker Logs: ", err) - return [{ containerId: 'ERROR', logs: err }]; + log.error("Failed to dump Docker Logs: ", err); + return [{ containerId: "ERROR", logs: err }]; } - - } } diff --git a/launcher/src/background.js b/launcher/src/background.js index fe62f7aaa..a990e610a 100755 --- a/launcher/src/background.js +++ b/launcher/src/background.js @@ -380,6 +380,10 @@ ipcMain.handle("restartServer", async () => { return await nodeConnection.restartServer(); }); +ipcMain.handle("readSSVKeystoreConfig", async (event, args) => { + return await nodeConnection.readSSVKeystoreConfig(args); +}); + ipcMain.handle("readSSVNetworkConfig", async (event, args) => { return await nodeConnection.readSSVNetworkConfig(args); }); diff --git a/launcher/src/components/UI/services-modal/SsvModal.vue b/launcher/src/components/UI/services-modal/SsvModal.vue index 443e3f633..070ea0fc5 100755 --- a/launcher/src/components/UI/services-modal/SsvModal.vue +++ b/launcher/src/components/UI/services-modal/SsvModal.vue @@ -117,6 +117,12 @@ export default { this.pubkey = ssvConfig.ssv_pk; try { + if (!this.pubkey) { + let ssvKeystoreConfig = await ControlService.readSSVKeystoreConfig(ssv.config.serviceID); + if (ssvKeystoreConfig.privateKeyFileData.publicKey) { + this.pubkey = ssvKeystoreConfig.privateKeyFileData.publicKey; + } + } let network = ssvConfig.network === "goerli" ? "prater" : ssvConfig.network; let response = await axios.get(`https://api.ssv.network/api/v4/${network}/operators/public_key/` + this.pubkey); if (!response.data.data) diff --git a/launcher/src/store/ControlService.js b/launcher/src/store/ControlService.js index 1535d9ebb..a2694a0e1 100755 --- a/launcher/src/store/ControlService.js +++ b/launcher/src/store/ControlService.js @@ -345,6 +345,10 @@ class ControlService extends EventEmitter { return await this.promiseIpc.send("restartServer"); } + async readSSVKeystoreConfig(args) { + return await this.promiseIpc.send("readSSVKeystoreConfig", args); + } + async readSSVNetworkConfig(args) { return await this.promiseIpc.send("readSSVNetworkConfig", args); } @@ -447,7 +451,7 @@ class ControlService extends EventEmitter { async beaconchainMonitoringModification(args) { return await this.promiseIpc.send("beaconchainMonitoringModification", args); } - + async removeBeaconchainMonitoring(args) { return await this.promiseIpc.send("removeBeaconchainMonitoring", args); }