Skip to content

ADD: Pluto as an Obol distributed validator client - #2311

Merged
NeoPlays merged 12 commits into
stereum-dev:mainfrom
NeoPlays:pluto
Sep 2, 2026
Merged

NeoPlays merged 12 commits into
stereum-dev:mainfrom
NeoPlays:pluto

Conversation

@NeoPlays

@NeoPlays NeoPlays commented Sep 2, 2026

Copy link
Copy Markdown
Member

ADD: Pluto as an Obol distributed validator client

Adds support for Nethermind's Pluto, a Rust reimplementation of Obol's Charon, as a first-class Stereum service, plus an in-place migration path for existing Charon clusters and a few fixes found along the way.

Pluto support

PlutoService extends CharonService rather than NodeService, because Pluto implements Charon's run interface exactly: same flags, same CHARON_* env vars, same .charon layout relative to the image's /opt/charon workdir. It therefore inherits every helper unchanged (ENR handling, DKG, cluster lock) and overrides only the image and entrypoint (/app/bin/pluto).

Verified against Pluto v0.1.4's own CLI source, not just run: create enr, enr, dkg --definition-file --publish, --feature-set-enable=json_requests, and --data-dir defaulting to .charon with the key still named charon-enr-private-key. So ENR generation and the DKG flow in ObolModal.vue work untouched.

A shared isObolDVTService() predicate in src/share/ replaces the ~40 hard-coded service === "CharonService" checks across ServiceManager, ValidatorAccountManager, Monitoring, OneClickInstall, the four validator-client classes, LidoObolExitService, and ~15 Vue components. PlutoService is added to the Prometheus scrape job (same metric names, same port).

Being a separate service class rather than an image swap on CharonService is deliberate: auto-updates key off the service name, so reusing CharonService would write Charon tags onto the Pluto repo.

Charon to Pluto switch is in place, not a reinstall

The generic SWITCH CLIENT path installs a replacement and then runs delete-service, whose Remove Service Files task would delete .charon: ENR key, cluster lock, and validator key shares. So switchServices() detects a DV-to-DV switch and swaps the client in place instead: same service id, ports, volumes, dependencies and command, stopped then started so manage-service reconciles ufw, and the previous service excluded from the delete pass. autoupdate is carried over from the stored YAML instead of being reset. SwitchContent.vue now offers a DV client only the other DV client; it previously offered Lighthouse VC etc., which would have run that destructive path.

P2P external IP expert option (all 16 p2p clients)

Behind Docker, Pluto never advertises the address its autonat confirmed, so it stays relay-only, worth 15-30x p2p latency on a real cluster. Rather than baking a guess into the config, this is an expert option that can be auto-detected or typed by hand, and cleared to remove the flag.

Each client's flag was verified against that client's own CLI source:

Flag Clients
--nat + extip: Geth, Erigon, Reth, OpGeth, OpErigon, OpReth
--enr-address Lighthouse, Grandine
--p2p-host-ip / --p2p-advertised-ip / --enr.ip Prysm / Teku / Lodestar
--p2p-host / --Network.ExternalIp / --nat.extip Besu / Nethermind / Ethrex
--p2p.advertise.ip / --p2p-external-ip OpNode / Pluto

Seven clients already shipped an option for this, so those keep their existing command strings (Nimbus carries extip: inside the command) and just gain the detect button, with no duplicate rows. A new valuePrefix field handles the geth family's extip: scheme, which the generic text writer couldn't produce.

Lodestar NODE_OPTIONS

Exposes Lodestar's Node heap limit for tuning on the beacon (which already ships --max-old-space-size=8192) and lets the validator be given one. This needed a fix to ExpertWindow's isENV path: it previously only reached the env branch when the YAML had no -- flags, so on Lodestar it would have appended NODE_OPTIONS: into the command list, and the validator's inline env: {} would have become invalid YAML.

Fixes found while testing

  • FIX: Read Obol cluster stats for Pluto and tolerate empty metrics. One empty Prometheus result killed all six Obol alerts; a labelled counter has no series until first incremented, so that's normal. Also: Pluto's app_monitoring_readyz carries no cluster_peer label (it reports the name via app_peer_name), and readyz 6 is accepted for Pluto because Charon deleted that check in v1.10.0 (#4359) as a false positive.
  • FIX: Convert TB and PB in the volume usage chart. The unit ladder stopped at MB/KB, so 1.2 TB rendered as 1.2 GB.
  • FIX: Don't query the node from the login screen. backToLogin cleared settingsStandalone before navigating, mounting BaseLayout for a tick while logged out; and an empty SSH pool surfaced as Cannot read properties of undefined (reading 'exec').
  • FIX: Guard missing settings keys and the unreachable credits API. savedConfig.logBackups.value threw when the key was absent, so the "write the default" fallback never ran. The credits endpoints answer 404, so they now return [] instead of undefined.
  • FIX: Allow data: audio in the CSP so UI sounds play. No media-src, so the inlined base64 wav sound effects fell back to default-src 'self' and were blocked.
  • FIX: Use Ethrex's own logo for its service icon. The existing icon was an ETHREX wordmark placeholder; re-cut from the project's own logo.

Testing

251 unit tests, 34 suites, eslint and prettier --check clean. Every one of the 10 commits was verified independently in a separate worktree: lint, format and tests all pass at each, with test counts rising monotonically (225 to 251).

New coverage: Pluto's command surface and Charon-compatibility, the in-place swap in both directions (preserving id, volumes, command, dependencies and autoupdate), the per-client external-IP flags plus a guard that no client has two options writing the same command, the storage unit ladder, the SSH not-connected guard, and the credits fetchers.

@NeoPlays
NeoPlays merged commit 702f668 into stereum-dev:main Sep 2, 2026
5 checks passed
@NeoPlays
NeoPlays deleted the pluto branch September 10, 2026 11:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant