From b10250a327c635bb41a2bf1704c444f4ce070080 Mon Sep 17 00:00:00 2001 From: NeoPlays <80448387+NeoPlays@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:39:44 +0200 Subject: [PATCH 01/12] ADD: Expert option to detect and set the P2P external IP --- launcher/src/backend/ServiceManager.js | 17 +++ .../tests/unit/ExternalIpOption.test.js | 107 ++++++++++++++++++ launcher/src/background.js | 4 + .../UI/node-page/sections/ExpertWindow.vue | 48 +++++++- launcher/src/store/ControlService.js | 4 + launcher/src/store/services.js | 50 +++++++- 6 files changed, 226 insertions(+), 4 deletions(-) create mode 100644 launcher/src/backend/tests/unit/ExternalIpOption.test.js diff --git a/launcher/src/backend/ServiceManager.js b/launcher/src/backend/ServiceManager.js index 834b041e38..55d150eaab 100755 --- a/launcher/src/backend/ServiceManager.js +++ b/launcher/src/backend/ServiceManager.js @@ -969,6 +969,23 @@ export class ServiceManager { } } + /** + * @returns {string|null} the node's publicly reachable IPv4 address + */ + async resolveExternalIp() { + const services = ["https://api.ipify.org", "https://ifconfig.me/ip", "https://icanhazip.com"]; + for (const url of services) { + try { + const result = await this.nodeConnection.sshService.exec(`curl -4 -fsS --max-time 5 ${url}`); + const ip = result.stdout?.trim(); + if (/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) return ip; + } catch (err) { + log.warn(`Couldn't resolve the external IP via ${url}:`, err); + } + } + return null; + } + //args: network, installDir, port, executionClients, checkpointURL, consensusClients, mevboost, relays // for external -> source, jwtToken // chainId -> for devnet getService(name, args) { let ports; diff --git a/launcher/src/backend/tests/unit/ExternalIpOption.test.js b/launcher/src/backend/tests/unit/ExternalIpOption.test.js new file mode 100644 index 0000000000..239ad587ab --- /dev/null +++ b/launcher/src/backend/tests/unit/ExternalIpOption.test.js @@ -0,0 +1,107 @@ +import { createPinia, setActivePinia } from "pinia"; +import { useServices } from "@/store/services"; + +// Flag each client uses to advertise its address to peers. Seven clients already +// shipped an option for this before the detect button existed, and their command +// strings are kept as they were - Nimbus in particular carries the "extip:" +// scheme inside the command rather than as a valuePrefix. +const EXPECTED = { + // pre-existing options, now with autoDetect added + LighthouseBeaconService: ["--enr-address", ""], + GrandineBeaconService: ["--enr-address", ""], + PrysmBeaconService: ["--p2p-host-ip", ""], + TekuBeaconService: ["--p2p-advertised-ip", ""], + LodestarBeaconService: ["--enr.ip", ""], + BesuService: ["--p2p-host", ""], + NimbusBeaconService: ["--nat:extip:", ""], + // added, geth derived: "--nat any|none|upnp|pmp|extip:" + GethService: ["--nat", "extip:"], + ErigonService: ["--nat", "extip:"], + RethService: ["--nat", "extip:"], + OpGethService: ["--nat", "extip:"], + OpErigonService: ["--nat", "extip:"], + OpRethService: ["--nat", "extip:"], + // added, plain address + NethermindService: ["--Network.ExternalIp", ""], + EthrexService: ["--nat.extip", ""], + OpNodeBeaconService: ["--p2p.advertise.ip", ""], +}; + +// External EC/CC only point at a node someone else runs, and L2Geth replicates +// from the sequencer instead of speaking devp2p, so none of them advertise an +// address of their own. +const WITHOUT_P2P = ["ExternalExecutionService", "ExternalConsensusService", "L2GethService"]; + +let allServices; + +beforeEach(() => { + setActivePinia(createPinia()); + allServices = useServices().allServices; +}); + +const optionsFor = (service) => allServices.find((s) => s.service === service)?.expertOptions ?? []; + +const externalIpOptionsOf = (service, command) => optionsFor(service).filter((o) => o.commands?.[0] === command); + +test("every client that speaks p2p offers an external ip option", () => { + const p2pServices = allServices + .filter((s) => ["execution", "consensus"].includes(s.category)) + .map((s) => s.service) + .filter((service) => !WITHOUT_P2P.includes(service)); + + expect(p2pServices.length).toBeGreaterThan(0); + + // a client added to the catalog without its flag fails here rather than + // silently shipping without the option + for (const service of p2pServices) { + expect(Object.keys(EXPECTED)).toContain(service); + } +}); + +test("each client gets its own flag and value prefix", () => { + for (const [service, [command, valuePrefix]] of Object.entries(EXPECTED)) { + const [option] = externalIpOptionsOf(service, command); + + expect(option).toBeDefined(); + expect(option.type).toMatch("text"); + expect(option.valuePrefix ?? "").toBe(valuePrefix); + // drives the detect button in ExpertWindow + expect(option.autoDetect).toMatch("externalIp"); + } +}); + +test("exactly one option per client edits the external ip flag", () => { + // seven clients already had an option for this; adding a second one gave them + // two identical rows in expert mode + for (const [service, [command]] of Object.entries(EXPECTED)) { + expect(externalIpOptionsOf(service, command)).toHaveLength(1); + } +}); + +test("no client has two options writing the same command", () => { + for (const client of allServices) { + const commands = (client.expertOptions ?? []).flatMap((o) => o.commands ?? []); + expect(new Set(commands).size).toBe(commands.length); + } +}); + +test("options are independent objects", () => { + const geth = externalIpOptionsOf("GethService", "--nat")[0]; + const reth = externalIpOptionsOf("RethService", "--nat")[0]; + + // both are built from the same factory with the same arguments, so a shared + // reference would leak one client's entered address into the other + expect(geth).not.toBe(reth); + + geth.changeValue = "80.249.121.1"; + expect(reth.changeValue).toBeNull(); +}); + +test("flags containing dots are escaped in the pattern", () => { + expect(externalIpOptionsOf("NethermindService", "--Network.ExternalIp")[0].pattern[0]).toMatch("(- --Network\\.ExternalIp=)(.*)(\\n)"); + + // the escaped pattern still matches the line it describes + const pattern = new RegExp(externalIpOptionsOf("EthrexService", "--nat.extip")[0].pattern[0]); + expect(" - --nat.extip=80.249.121.1\n").toMatch(pattern); + expect(" - --natXextip=80.249.121.1\n").not.toMatch(pattern); +}); diff --git a/launcher/src/background.js b/launcher/src/background.js index e847de5acf..2b3954f824 100755 --- a/launcher/src/background.js +++ b/launcher/src/background.js @@ -810,6 +810,10 @@ ipcMain.handle("handleOTPChange", async (event, args) => { ); }); +ipcMain.handle("getExternalIp", async () => { + return await serviceManager.resolveExternalIp(); +}); + ipcMain.handle("fetchObolCharonAlerts", async () => { return await monitoring.fetchObolCharonAlerts(); }); diff --git a/launcher/src/components/UI/node-page/sections/ExpertWindow.vue b/launcher/src/components/UI/node-page/sections/ExpertWindow.vue index 9e1e377add..50e9335914 100755 --- a/launcher/src/components/UI/node-page/sections/ExpertWindow.vue +++ b/launcher/src/components/UI/node-page/sections/ExpertWindow.vue @@ -146,7 +146,22 @@ @click="buttonOn(option)" /> --> - + detect + diff --git a/launcher/src/composables/version.js b/launcher/src/composables/version.js index c115c8f93b..dbeec49df2 100644 --- a/launcher/src/composables/version.js +++ b/launcher/src/composables/version.js @@ -34,7 +34,15 @@ export async function useUpdateCheck() { if (!nodeHeaderStore.updating) { nodeHeaderStore.searchingForUpdates = true; - let services = await useBackendServices(); + let services; + try { + services = await useBackendServices(); + } catch (error) { + // reachable with no node connected, e.g. settings opened from the login screen + console.log("Couldn't read services, skipping the update check:", error.message); + nodeHeaderStore.searchingForUpdates = false; + return; + } let versions = {}; try { From a6f4718618fc550c2b7887a311c2a09402004760 Mon Sep 17 00:00:00 2001 From: NeoPlays <80448387+NeoPlays@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:10:41 +0200 Subject: [PATCH 11/12] CHORE: Bump default client image versions --- launcher/src/backend/ethereum-services/BesuService.js | 2 +- launcher/src/backend/ethereum-services/CharonService.js | 2 +- launcher/src/backend/ethereum-services/ErigonService.js | 2 +- launcher/src/backend/ethereum-services/EthrexService.js | 2 +- .../src/backend/ethereum-services/FlashbotsMevBoostService.js | 2 +- launcher/src/backend/ethereum-services/GethService.js | 2 +- launcher/src/backend/ethereum-services/GrafanaService.js | 2 +- launcher/src/backend/ethereum-services/GrandineBeaconService.js | 2 +- launcher/src/backend/ethereum-services/KeysAPIService.js | 2 +- launcher/src/backend/ethereum-services/L2GethService.js | 2 +- launcher/src/backend/ethereum-services/LidoObolExitService.js | 2 +- .../src/backend/ethereum-services/LighthouseBeaconService.js | 2 +- .../src/backend/ethereum-services/LighthouseValidatorService.js | 2 +- launcher/src/backend/ethereum-services/LodestarBeaconService.js | 2 +- .../src/backend/ethereum-services/LodestarValidatorService.js | 2 +- launcher/src/backend/ethereum-services/NethermindService.js | 2 +- launcher/src/backend/ethereum-services/NimbusBeaconService.js | 2 +- .../src/backend/ethereum-services/NimbusValidatorService.js | 2 +- launcher/src/backend/ethereum-services/OpErigonService.js | 2 +- launcher/src/backend/ethereum-services/OpGethService.js | 2 +- launcher/src/backend/ethereum-services/OpNodeBeaconService.js | 2 +- launcher/src/backend/ethereum-services/OpRethService.js | 2 +- .../backend/ethereum-services/PrometheusNodeExporterService.js | 2 +- launcher/src/backend/ethereum-services/PrometheusService.js | 2 +- launcher/src/backend/ethereum-services/PrysmBeaconService.js | 2 +- launcher/src/backend/ethereum-services/PrysmValidatorService.js | 2 +- launcher/src/backend/ethereum-services/RethService.js | 2 +- launcher/src/backend/ethereum-services/SSVDKGService.js | 2 +- launcher/src/backend/ethereum-services/SSVNetworkService.js | 2 +- launcher/src/backend/ethereum-services/TekuBeaconService.js | 2 +- launcher/src/backend/ethereum-services/TekuValidatorService.js | 2 +- .../src/backend/ethereum-services/ValidatorEjectorService.js | 2 +- launcher/src/backend/ethereum-services/Web3SignerService.js | 2 +- 33 files changed, 33 insertions(+), 33 deletions(-) diff --git a/launcher/src/backend/ethereum-services/BesuService.js b/launcher/src/backend/ethereum-services/BesuService.js index 6431565ccd..58604cb09c 100755 --- a/launcher/src/backend/ethereum-services/BesuService.js +++ b/launcher/src/backend/ethereum-services/BesuService.js @@ -16,7 +16,7 @@ export class BesuService extends NodeService { service.id, // id 1, // configVersion "hyperledger/besu", // image - "22.7.6", // imageVersion + "26.8.1", // imageVersion [ `--network=${network}`, `--data-path=${dataDir}`, diff --git a/launcher/src/backend/ethereum-services/CharonService.js b/launcher/src/backend/ethereum-services/CharonService.js index fb3ee2977a..cc04045b34 100755 --- a/launcher/src/backend/ethereum-services/CharonService.js +++ b/launcher/src/backend/ethereum-services/CharonService.js @@ -21,7 +21,7 @@ export class CharonService extends NodeService { service.id, // id 1, // configVersion "obolnetwork/charon", // image - "v0.19.0", // imageVersion + "v1.11.0", // imageVersion [ "run", `--beacon-node-endpoints=${beaconNodes}`, diff --git a/launcher/src/backend/ethereum-services/ErigonService.js b/launcher/src/backend/ethereum-services/ErigonService.js index 9f1bf8ccd8..0bc5060be3 100755 --- a/launcher/src/backend/ethereum-services/ErigonService.js +++ b/launcher/src/backend/ethereum-services/ErigonService.js @@ -17,7 +17,7 @@ export class ErigonService extends NodeService { service.id, // id 1, // configVersion "erigontech/erigon", // image - "v2.60.8", // imageVersion + "v3.6.0", // imageVersion [ `erigon`, `--chain=${network}`, diff --git a/launcher/src/backend/ethereum-services/EthrexService.js b/launcher/src/backend/ethereum-services/EthrexService.js index 20119f2dd9..f5c415538f 100644 --- a/launcher/src/backend/ethereum-services/EthrexService.js +++ b/launcher/src/backend/ethereum-services/EthrexService.js @@ -16,7 +16,7 @@ export class EthrexService extends NodeService { service.id, // id 1, // configVersion "ghcr.io/lambdaclass/ethrex", // image - "9.0.0", // imageVersion + "25.0.0", // imageVersion [ `--network=${network}`, `--datadir=${dataDir}`, diff --git a/launcher/src/backend/ethereum-services/FlashbotsMevBoostService.js b/launcher/src/backend/ethereum-services/FlashbotsMevBoostService.js index af838dd9ee..3be985085d 100755 --- a/launcher/src/backend/ethereum-services/FlashbotsMevBoostService.js +++ b/launcher/src/backend/ethereum-services/FlashbotsMevBoostService.js @@ -10,7 +10,7 @@ export class FlashbotsMevBoostService extends NodeService { null, // id 1, // configVersion image, // image - "v0.8.2", // imageVersion + "1.12", // imageVersion null, // command ["/app/mev-boost", "-addr", "0.0.0.0:18550", `-${network}`, "-relay-check", "-relays", `${relayLinks}`], // entrypoint null, // env diff --git a/launcher/src/backend/ethereum-services/GethService.js b/launcher/src/backend/ethereum-services/GethService.js index 5c5754ee48..5754d2b41b 100755 --- a/launcher/src/backend/ethereum-services/GethService.js +++ b/launcher/src/backend/ethereum-services/GethService.js @@ -23,7 +23,7 @@ export class GethService extends NodeService { service.id, // id 1, // configVersion "ethereum/client-go", // image - "v1.14.10-amd64", // imageVersion + "v1.17.5", // imageVersion cmd, // command ["geth"], // entrypoint null, // env diff --git a/launcher/src/backend/ethereum-services/GrafanaService.js b/launcher/src/backend/ethereum-services/GrafanaService.js index 509e16b88d..05981d0bd0 100755 --- a/launcher/src/backend/ethereum-services/GrafanaService.js +++ b/launcher/src/backend/ethereum-services/GrafanaService.js @@ -25,7 +25,7 @@ export class GrafanaService extends NodeService { service.id, // id 1, // configVersion image, // image - "9.1.6", // imageVersion + "13.2.0", // imageVersion "/run.sh", // command null, // entrypoint null, // env diff --git a/launcher/src/backend/ethereum-services/GrandineBeaconService.js b/launcher/src/backend/ethereum-services/GrandineBeaconService.js index bfa0f5b9fd..8461e9f1ea 100644 --- a/launcher/src/backend/ethereum-services/GrandineBeaconService.js +++ b/launcher/src/backend/ethereum-services/GrandineBeaconService.js @@ -39,7 +39,7 @@ export class GrandineBeaconService extends NodeService { service.id, //id 1, // configVersion image, //image - "1.0.0", //imageVersion + "2.0.6", //imageVersion [ "grandine", `--network=${network}`, diff --git a/launcher/src/backend/ethereum-services/KeysAPIService.js b/launcher/src/backend/ethereum-services/KeysAPIService.js index 8d569b9974..b2b234c997 100755 --- a/launcher/src/backend/ethereum-services/KeysAPIService.js +++ b/launcher/src/backend/ethereum-services/KeysAPIService.js @@ -12,7 +12,7 @@ export class KeysAPIService extends NodeService { service.id, // id 1, // configVersion image, // image - "dev", // imageVersion + "4.0.4", // imageVersion [], // command [], // entrypoint { diff --git a/launcher/src/backend/ethereum-services/L2GethService.js b/launcher/src/backend/ethereum-services/L2GethService.js index be40faaa98..a66c981458 100644 --- a/launcher/src/backend/ethereum-services/L2GethService.js +++ b/launcher/src/backend/ethereum-services/L2GethService.js @@ -15,7 +15,7 @@ export class L2GethService extends NodeService { service.id, // id 1, // configVersion "ethereumoptimism/l2geth", // image - "0.5.31", // imageVersion + "latest", // imageVersion ["--vmodule=eth/*=5,miner=4,rpc=5,rollup=4,consensus/clique=1", `--datadir=${dataDir}`, "--allow-insecure-unlock", "--gcmode=full"], // command ["geth"], // entrypoint { diff --git a/launcher/src/backend/ethereum-services/LidoObolExitService.js b/launcher/src/backend/ethereum-services/LidoObolExitService.js index 16aa2b8be8..4f63c274eb 100644 --- a/launcher/src/backend/ethereum-services/LidoObolExitService.js +++ b/launcher/src/backend/ethereum-services/LidoObolExitService.js @@ -30,7 +30,7 @@ export class LidoObolExitService extends NodeService { service.id, // id 1, // configVersion image, // image - "latest", // imageVersion + "v0.5.2", // imageVersion [ "run", `--beacon-node-url=${consensusClients[0] ? consensusClients[0].buildConsensusClientHttpEndpointUrl() : ""}`, diff --git a/launcher/src/backend/ethereum-services/LighthouseBeaconService.js b/launcher/src/backend/ethereum-services/LighthouseBeaconService.js index 0eaec7e975..8aa88f275e 100755 --- a/launcher/src/backend/ethereum-services/LighthouseBeaconService.js +++ b/launcher/src/backend/ethereum-services/LighthouseBeaconService.js @@ -40,7 +40,7 @@ export class LighthouseBeaconService extends NodeService { service.id, //id 1, // configVersion image, //image - "v3.1.2", //imageVersion + "v8.2.2", //imageVersion [ "lighthouse", "bn", diff --git a/launcher/src/backend/ethereum-services/LighthouseValidatorService.js b/launcher/src/backend/ethereum-services/LighthouseValidatorService.js index 00e7de77af..5e1af08b46 100755 --- a/launcher/src/backend/ethereum-services/LighthouseValidatorService.js +++ b/launcher/src/backend/ethereum-services/LighthouseValidatorService.js @@ -29,7 +29,7 @@ export class LighthouseValidatorService extends NodeService { service.id, //id 1, //configVersion image, //image - "v3.1.2", //imageVersion + "v8.2.2", //imageVersion [ "lighthouse", "vc", diff --git a/launcher/src/backend/ethereum-services/LodestarBeaconService.js b/launcher/src/backend/ethereum-services/LodestarBeaconService.js index e45ae895f9..0c93f4ff5f 100755 --- a/launcher/src/backend/ethereum-services/LodestarBeaconService.js +++ b/launcher/src/backend/ethereum-services/LodestarBeaconService.js @@ -40,7 +40,7 @@ export class LodestarBeaconService extends NodeService { service.id, //id 1, // configVersion image, //image - "v1.3.0", //imageVersion + "v1.46.0", //imageVersion [ `beacon`, `--network=${network}`, diff --git a/launcher/src/backend/ethereum-services/LodestarValidatorService.js b/launcher/src/backend/ethereum-services/LodestarValidatorService.js index cc2906c970..5a588ff9b9 100755 --- a/launcher/src/backend/ethereum-services/LodestarValidatorService.js +++ b/launcher/src/backend/ethereum-services/LodestarValidatorService.js @@ -28,7 +28,7 @@ export class LodestarValidatorService extends NodeService { service.id, //id 1, //configVersion image, //image - "v1.3.0", //imageVersion + "v1.46.0", //imageVersion [ `validator`, `--network=${network}`, diff --git a/launcher/src/backend/ethereum-services/NethermindService.js b/launcher/src/backend/ethereum-services/NethermindService.js index 78067d4b5f..b48b93fcb8 100755 --- a/launcher/src/backend/ethereum-services/NethermindService.js +++ b/launcher/src/backend/ethereum-services/NethermindService.js @@ -17,7 +17,7 @@ export class NethermindService extends NodeService { service.id, // id 1, // configVersion "nethermind/nethermind", // image - "1.16.1", // imageVersion + "1.39.3", // imageVersion [ `--config=${network}`, "--log=info", diff --git a/launcher/src/backend/ethereum-services/NimbusBeaconService.js b/launcher/src/backend/ethereum-services/NimbusBeaconService.js index 7c8506fe08..023d1f05c8 100755 --- a/launcher/src/backend/ethereum-services/NimbusBeaconService.js +++ b/launcher/src/backend/ethereum-services/NimbusBeaconService.js @@ -38,7 +38,7 @@ export class NimbusBeaconService extends NodeService { service.id, // id, 2, // configVersion image, // image, - "multiarch-v22.10.0", // imageVersion, + "multiarch-v26.8.0", // imageVersion, [ `--network=${network}`, `--data-dir=${dataDir}`, diff --git a/launcher/src/backend/ethereum-services/NimbusValidatorService.js b/launcher/src/backend/ethereum-services/NimbusValidatorService.js index 7813e3ec4e..79774d64f8 100755 --- a/launcher/src/backend/ethereum-services/NimbusValidatorService.js +++ b/launcher/src/backend/ethereum-services/NimbusValidatorService.js @@ -32,7 +32,7 @@ export class NimbusValidatorService extends NodeService { service.id, // id, 1, // configVersion image, // image, - "multiarch-v23.4.0", // imageVersion, + "multiarch-v26.8.0", // imageVersion, [ "--log-level=INFO", `--data-dir=${dataDir}`, diff --git a/launcher/src/backend/ethereum-services/OpErigonService.js b/launcher/src/backend/ethereum-services/OpErigonService.js index 623af04955..0ba8f4e8b6 100644 --- a/launcher/src/backend/ethereum-services/OpErigonService.js +++ b/launcher/src/backend/ethereum-services/OpErigonService.js @@ -30,7 +30,7 @@ export class OpErigonService extends NodeService { service.id, // id 1, // configVersion "testinprod/op-erigon", // image - "v2.60.10-0.8.2", // imageVersion + "latest", // imageVersion cmd, // command ["/usr/local/bin/erigon"], // entrypoint null, // env diff --git a/launcher/src/backend/ethereum-services/OpGethService.js b/launcher/src/backend/ethereum-services/OpGethService.js index 7385027467..32a41c091c 100644 --- a/launcher/src/backend/ethereum-services/OpGethService.js +++ b/launcher/src/backend/ethereum-services/OpGethService.js @@ -30,7 +30,7 @@ export class OpGethService extends NodeService { service.id, // id 1, // configVersion "us-docker.pkg.dev/oplabs-tools-artifacts/images/op-geth", // image - "v1.101411.4", // imageVersion + "latest", // imageVersion cmd, // command ["geth"], // entrypoint null, // env diff --git a/launcher/src/backend/ethereum-services/OpNodeBeaconService.js b/launcher/src/backend/ethereum-services/OpNodeBeaconService.js index a0791ee992..ccaea4a6ae 100644 --- a/launcher/src/backend/ethereum-services/OpNodeBeaconService.js +++ b/launcher/src/backend/ethereum-services/OpNodeBeaconService.js @@ -64,7 +64,7 @@ export class OpNodeBeaconService extends NodeService { service.id, //id 1, // configVersion image, //image - "v1.10.3", //imageVersion + "v1.19.5", //imageVersion [ "op-node", `--l2=${l2Execution}`, diff --git a/launcher/src/backend/ethereum-services/OpRethService.js b/launcher/src/backend/ethereum-services/OpRethService.js index 67e7eb8b45..425e30ccd7 100644 --- a/launcher/src/backend/ethereum-services/OpRethService.js +++ b/launcher/src/backend/ethereum-services/OpRethService.js @@ -21,7 +21,7 @@ export class OpRethService extends NodeService { service.id, // id 1, // configVersion "ghcr.io/paradigmxyz/op-reth", // image - "v1.2.1", // imageVersion + "latest", // imageVersion cmd, // command ["/usr/local/bin/op-reth"], // entrypoint null, // env diff --git a/launcher/src/backend/ethereum-services/PrometheusNodeExporterService.js b/launcher/src/backend/ethereum-services/PrometheusNodeExporterService.js index 457d5f1880..857f9e7fc2 100755 --- a/launcher/src/backend/ethereum-services/PrometheusNodeExporterService.js +++ b/launcher/src/backend/ethereum-services/PrometheusNodeExporterService.js @@ -11,7 +11,7 @@ export class PrometheusNodeExporterService extends NodeService { null, // id 1, // configVersion image, // image - "v1.3.1", // imageVersion + "v1.12.1", // imageVersion ["--path.rootfs=/host"], // command ["/bin/node_exporter"], // entrypoint null, // env diff --git a/launcher/src/backend/ethereum-services/PrometheusService.js b/launcher/src/backend/ethereum-services/PrometheusService.js index 94e4d868fb..680e9478ae 100755 --- a/launcher/src/backend/ethereum-services/PrometheusService.js +++ b/launcher/src/backend/ethereum-services/PrometheusService.js @@ -20,7 +20,7 @@ export class PrometheusService extends NodeService { service.id, // id 1, // configVersion image, // image - "v2.38.0", // imageVersion + "v3.14.0", // imageVersion 'sh -c "/bin/prometheus --config.file=/etc/prometheus/prometheus.yml --web.enable-lifecycle --storage.tsdb.retention.time=15d"', // command null, // entrypoint null, // env diff --git a/launcher/src/backend/ethereum-services/PrysmBeaconService.js b/launcher/src/backend/ethereum-services/PrysmBeaconService.js index c0cecfe6cf..3e1c9ab3a2 100755 --- a/launcher/src/backend/ethereum-services/PrysmBeaconService.js +++ b/launcher/src/backend/ethereum-services/PrysmBeaconService.js @@ -63,7 +63,7 @@ export class PrysmBeaconService extends NodeService { service.id, //id 1, // configVersion image, //image - "v5.0.4", //imageVersion + "v7.1.8", //imageVersion cmd, // command ["/app/cmd/beacon-chain/beacon-chain"], //entrypoint null, //env diff --git a/launcher/src/backend/ethereum-services/PrysmValidatorService.js b/launcher/src/backend/ethereum-services/PrysmValidatorService.js index 5dd4b1cd4a..b7c01f12a0 100755 --- a/launcher/src/backend/ethereum-services/PrysmValidatorService.js +++ b/launcher/src/backend/ethereum-services/PrysmValidatorService.js @@ -46,7 +46,7 @@ export class PrysmValidatorService extends NodeService { service.id, //id 1, // configVersion image, //image - "v5.0.4", //imageVersion + "v7.1.8", //imageVersion cmd, //command ["/app/cmd/validator/validator"], // entrypoint null, // env diff --git a/launcher/src/backend/ethereum-services/RethService.js b/launcher/src/backend/ethereum-services/RethService.js index 920d1e2d81..def24315ba 100644 --- a/launcher/src/backend/ethereum-services/RethService.js +++ b/launcher/src/backend/ethereum-services/RethService.js @@ -16,7 +16,7 @@ export class RethService extends NodeService { service.id, // id 1, // configVersion "ghcr.io/paradigmxyz/reth", // image - "v0.1.0-alpha.4", // imageVersion + "v2.5.1", // imageVersion [ `node`, `--chain=${network}`, diff --git a/launcher/src/backend/ethereum-services/SSVDKGService.js b/launcher/src/backend/ethereum-services/SSVDKGService.js index 0359521a53..ff84a70cf2 100644 --- a/launcher/src/backend/ethereum-services/SSVDKGService.js +++ b/launcher/src/backend/ethereum-services/SSVDKGService.js @@ -41,7 +41,7 @@ ethEndpointURL: ${ethEndpointURL} #HTTP Address of Execution Node`; service.id, //id 1, //configVersion image, //image - "v2.1.0", //imageVersion + "v3.1.1", //imageVersion ["start-operator", "--configPath=./data/config.yaml"], //command ["/entry-point.sh"], // entrypoint null, // env diff --git a/launcher/src/backend/ethereum-services/SSVNetworkService.js b/launcher/src/backend/ethereum-services/SSVNetworkService.js index 8fe2bfc6a5..9551cdd19a 100755 --- a/launcher/src/backend/ethereum-services/SSVNetworkService.js +++ b/launcher/src/backend/ethereum-services/SSVNetworkService.js @@ -62,7 +62,7 @@ SSVAPIPort: 16000`; service.id, // id 1, // configVersion image, //image - "v0.3.4", //imageVersion + "v2.4.3", //imageVersion "make BUILD_PATH=/go/bin/ssvnode start-node && docker logs ssv_node", // command null, // entrypoint { diff --git a/launcher/src/backend/ethereum-services/TekuBeaconService.js b/launcher/src/backend/ethereum-services/TekuBeaconService.js index a01bb22ec7..03d250030c 100755 --- a/launcher/src/backend/ethereum-services/TekuBeaconService.js +++ b/launcher/src/backend/ethereum-services/TekuBeaconService.js @@ -38,7 +38,7 @@ export class TekuBeaconService extends NodeService { service.id, // id 2, // configVersion image, // image - "22.10.1", // imageVersion + "26.8.0", // imageVersion [ `--network=${network}`, "--p2p-enabled=true", diff --git a/launcher/src/backend/ethereum-services/TekuValidatorService.js b/launcher/src/backend/ethereum-services/TekuValidatorService.js index c591eb63be..5f88ef892c 100755 --- a/launcher/src/backend/ethereum-services/TekuValidatorService.js +++ b/launcher/src/backend/ethereum-services/TekuValidatorService.js @@ -28,7 +28,7 @@ export class TekuValidatorService extends NodeService { service.id, // id 1, // configVersion image, // image - "23.4.0", // imageVersion + "26.8.0", // imageVersion [ "vc", `--beacon-node-api-endpoint=${beaconNodes}`, diff --git a/launcher/src/backend/ethereum-services/ValidatorEjectorService.js b/launcher/src/backend/ethereum-services/ValidatorEjectorService.js index 361dbd502f..7d6f5f06ea 100755 --- a/launcher/src/backend/ethereum-services/ValidatorEjectorService.js +++ b/launcher/src/backend/ethereum-services/ValidatorEjectorService.js @@ -41,7 +41,7 @@ export class ValidatorEjectorService extends NodeService { service.id, // id 1, // configVersion image, // image - "dev", // imageVersion + "2.1.0", // imageVersion [], // command [], // entrypoint { diff --git a/launcher/src/backend/ethereum-services/Web3SignerService.js b/launcher/src/backend/ethereum-services/Web3SignerService.js index bdaa8602fe..7e1b91b206 100755 --- a/launcher/src/backend/ethereum-services/Web3SignerService.js +++ b/launcher/src/backend/ethereum-services/Web3SignerService.js @@ -20,7 +20,7 @@ export class Web3SignerService extends NodeService { service.id, // id 1, // configVersion image, // image - "23.2.1", // imageVersion + "26.7.0", // imageVersion [ `--key-store-path=${keysDir}`, `--data-path=${dataDir}`, From b330364804ce1dd2e0f61a840e6252b800107138 Mon Sep 17 00:00:00 2001 From: NeoPlays <80448387+NeoPlays@users.noreply.github.com> Date: Wed, 2 Sep 2026 16:15:17 +0200 Subject: [PATCH 12/12] FIX: Refresh what dependents record when a client is swapped in place --- launcher/src/backend/ServiceManager.js | 36 +++++++++--- .../backend/tests/unit/ServiceManager.test.js | 55 ++++++++++++++++++- 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/launcher/src/backend/ServiceManager.js b/launcher/src/backend/ServiceManager.js index 4c5c2bdf4a..dca0e23027 100755 --- a/launcher/src/backend/ServiceManager.js +++ b/launcher/src/backend/ServiceManager.js @@ -869,6 +869,22 @@ export class ServiceManager { return command + newValue; } + /** + * buildConfiguration() always reports autoupdate as enabled, so carry over + * whatever the service was configured with. Falls back to enabled, which is + * what writing the configuration directly would have done anyway. + */ + async writeServiceConfigurationKeepingAutoupdate(service) { + const configuration = service.buildConfiguration(); + try { + const stored = await this.nodeConnection.readServiceConfiguration(service.id); + configuration.autoupdate = stored?.autoupdate ?? true; + } catch (err) { + log.warn(`Couldn't read the stored autoupdate flag of ${service.id}, keeping it enabled:`, err?.message ?? err); + } + await this.nodeConnection.writeServiceConfiguration(configuration); + } + async deleteService(task, _tasks, services, ssvConfigs) { let serviceToDelete = services.find((service) => service.id === task.service.config.serviceID); let dependents = []; @@ -884,7 +900,7 @@ export class ServiceManager { if (service.service === "SSVNetworkService") { this.removeSSVNetworkConnection(service, serviceToDelete, ssvConfigs); } - this.nodeConnection.writeServiceConfiguration(service.buildConfiguration()); + await this.writeServiceConfigurationKeepingAutoupdate(service); } if (serviceToDelete.service === "Web3SignerService") { await this.nodeConnection.sshService.exec( @@ -1016,16 +1032,22 @@ export class ServiceManager { swapped.imageVersion = await this.latestImageVersion(target, current.network, canonical.imageVersion); swapped.entrypoint = canonical.entrypoint; - // buildConfiguration() always reports autoupdate as enabled - const configuration = swapped.buildConfiguration(); - const storedConfiguration = await this.nodeConnection.readServiceConfiguration(serviceID); - configuration.autoupdate = storedConfiguration?.autoupdate ?? true; + log.info(`Switching ${current.service} ${serviceID} to ${target} (${swapped.image}:${swapped.imageVersion})`); - log.info(`Switching ${current.service} ${serviceID} to ${target} (${configuration.image})`); + // Dependents persist the client's name alongside its id. They hold a + // reference to `current`, so renaming it is what their minimal + // configurations will serialise. + const dependents = services.filter((service) => + Object.values(service.dependencies).some((deps) => deps.some((d) => d?.id === serviceID)) + ); + current.service = target; // "restarted" skips the role's firewall task, so stop then start await this.manageServiceState(serviceID, "stopped"); - await this.nodeConnection.writeServiceConfiguration(configuration); + await this.writeServiceConfigurationKeepingAutoupdate(swapped); + for (const dependent of dependents) { + await this.writeServiceConfigurationKeepingAutoupdate(dependent); + } await this.manageServiceState(serviceID, "started"); } diff --git a/launcher/src/backend/tests/unit/ServiceManager.test.js b/launcher/src/backend/tests/unit/ServiceManager.test.js index c0dc97bdd4..7385966818 100755 --- a/launcher/src/backend/tests/unit/ServiceManager.test.js +++ b/launcher/src/backend/tests/unit/ServiceManager.test.js @@ -370,6 +370,29 @@ const beaconConfig = (id) => { }; }; +const validatorConfig = (id, dvId) => { + return { + service: "LodestarValidatorService", + id: id, + configVersion: 1, + command: ["validator", `--beaconNodes=http://stereum-${dvId}:3600`, "--distributed"], + entrypoint: [], + env: {}, + image: "chainsafe/lodestar:v1.46.0", + ports: [], + volumes: ["/opt/stereum/lodestar-" + id + ":/opt/app/validator"], + user: "2000", + network: "hoodi", + autoupdate: false, + dependencies: { + executionClients: [], + consensusClients: [{ id: dvId, service: "CharonService" }], + mevboost: [], + otherServices: [], + }, + }; +}; + const charonConfig = (id) => { return { service: "CharonService", @@ -422,7 +445,8 @@ const switchMocks = (configs) => { test("switchServices swaps charon for pluto in place", async () => { const config = charonConfig("charon-id"); - const { NodeConnection, writeServiceConfiguration, runPlaybook } = switchMocks([config, beaconConfig("lh-id")]); + const vc = validatorConfig("vc-id", "charon-id"); + const { NodeConnection, writeServiceConfiguration, runPlaybook } = switchMocks([config, beaconConfig("lh-id"), vc]); const sm = new ServiceManager(NodeConnection.NodeConnection()); const handledInPlace = await sm.switchServices({ @@ -457,6 +481,35 @@ test("switchServices swaps charon for pluto in place", async () => { expect(runPlaybook.mock.calls.map((c) => c[1].stereum_args.manage_service.state)).toEqual(["stopped", "started"]); }); +test("switchServices refreshes what dependents recorded about the client", async () => { + const config = charonConfig("charon-id"); + const vc = validatorConfig("vc-id", "charon-id"); + const { NodeConnection, writeServiceConfiguration } = switchMocks([config, beaconConfig("lh-id"), vc]); + const sm = new ServiceManager(NodeConnection.NodeConnection()); + + await sm.switchServices({ + id: "charon-id", + setupId: "setup-id", + service: { service: "CharonService", category: "validator", config: { serviceID: "charon-id", network: "hoodi" } }, + data: { itemToInstall: { service: "PlutoService" }, data: {} }, + }); + + const written = writeServiceConfiguration.mock.calls.map((c) => c[0]); + const dependent = written.find((c) => c.id === "vc-id"); + + // dependents persist the client's name next to its id, so a stale name would + // outlive the swap + expect(dependent).toBeDefined(); + expect(dependent.dependencies.consensusClients).toEqual([{ id: "charon-id", service: "PlutoService" }]); + + // rewriting a dependent must not quietly re-enable its auto updates + expect(dependent.autoupdate).toBe(false); + + // and nothing else about it changes + expect(dependent.command).toEqual(vc.command); + expect(dependent.image).toMatch("chainsafe/lodestar:v1.46.0"); +}); + test("switchServices swaps pluto back to charon in place", async () => { const config = charonConfig("pluto-id"); config.service = "PlutoService";