|
| 1 | +# Claude Agent Guide: lido-charon-distributed-validator-node (LCDVN) |
| 2 | + |
| 3 | +This repo is the Docker Compose launcher for one node of an Obol Distributed Validator (DV) cluster operating inside the **Lido Simple DVT module** (staking module ID 2 on mainnet). It is a near-twin of `charon-distributed-validator-node` (CDVN) with Lido-specific additions: the `validator-ejector` service and the Lido EasyTrack / oracle wiring. |
| 4 | + |
| 5 | +> This repo is a **deployment guide, not a canonical deployment**. Lido Simple DVT operators are expected to fork/clone and adapt for their own ops — version-pin client images, tune monitoring, add HA, etc. Don't treat it as turnkey production config. |
| 6 | +
|
| 7 | +For non-Lido DV operators use `charon-distributed-validator-node` instead. Module coverage: **Simple DVT only**. Lido CSM and stVault are **not** yet optimised for this repo. |
| 8 | + |
| 9 | +## Quickstart |
| 10 | + |
| 11 | +```bash |
| 12 | +# Pick a network |
| 13 | +cp .env.sample.hoodi .env # or .env.sample.mainnet |
| 14 | + |
| 15 | +# If using commit-boost (instead of mev-boost): |
| 16 | +cp commit-boost/config.toml.sample.hoodi commit-boost/config.toml |
| 17 | + |
| 18 | +# Edit .env to set your Lido-specific values (see below), client selection, monitoring. |
| 19 | +# Drop a completed .charon/ (from DKG) into the repo root. |
| 20 | +docker compose up -d |
| 21 | +docker compose logs -f charon |
| 22 | +``` |
| 23 | + |
| 24 | +**Prerequisite:** a `.charon/` directory from a completed DKG ceremony must exist in the repo root before start. No DKG yet → run one via [launchpad.obol.org](https://launchpad.obol.org) first. |
| 25 | + |
| 26 | +## Mandatory Lido-specific env vars |
| 27 | + |
| 28 | +All exist in `.env.sample.*` — uncomment and set: |
| 29 | + |
| 30 | +| Var | What | Where to find | |
| 31 | +|-----|------|---------------| |
| 32 | +| `VE_OPERATOR_ID` | Your Lido Simple DVT operator ID | [operators.lido.fi](https://operators.lido.fi) (mainnet) / Lido testnet dashboard (hoodi) | |
| 33 | +| `VE_EASY_TRACK_MOTION_CREATOR_ADDRESSES_ALLOWLIST` | Your cluster's Lido Operator SAFE manager address(es), JSON array | The SAFE you registered with Lido when onboarding | |
| 34 | +| `VE_STAKING_MODULE_ID` | Pre-set to `2` (Simple DVT) — don't change | `.env.sample.*` | |
| 35 | +| `VE_LOCATOR_ADDRESS`, `VE_ORACLE_ADDRESSES_ALLOWLIST`, `VE_EASY_TRACK_ADDRESS` | Lido contract addresses for the network | Pre-populated per network in the env samples — treat as config, not secret | |
| 36 | + |
| 37 | +To enable Obol's log collection (helps the core team diagnose cluster issues), uncomment `MONITORING=${MONITORING:-monitoring},monitoring-log-collector`. |
| 38 | + |
| 39 | +## validator-ejector |
| 40 | + |
| 41 | +Lido publishes validator exit signals via on-chain oracles. The `validator-ejector` service in this stack watches the CL/EL, listens for signed exit messages targeting this operator, and forwards them to the DV so Charon + VCs can cooperate to exit the validator. This is the **key Lido-specific piece** — without it, a DV running under Lido Simple DVT can't honour Lido's exit protocol and risks slashing or socialised losses. |
| 42 | + |
| 43 | +If `validator-ejector` won't start or logs oracle-address mismatches, double-check `VE_ORACLE_ADDRESSES_ALLOWLIST` against the network's Lido deployment (they rotate occasionally). |
| 44 | + |
| 45 | +## Client stack (same shape as CDVN) |
| 46 | + |
| 47 | +Modular via `.env` knobs: `EL`, `CL`, `VC`, `MEV`. Defaults ship reasonable choices per network. |
| 48 | + |
| 49 | +> **Pick non-default clients where you can.** Same reasoning as CDVN — every operator running defaults deepens client supermajorities and raises correlated-failure risk for Ethereum and for the cluster. Within a Lido Simple DVT cluster, coordinate with your co-operators (step 6 of the README): pick EL/CL/VC combinations *different* from theirs so a single client bug can't take the whole cluster offline. |
| 50 | +
|
| 51 | +## Networks |
| 52 | + |
| 53 | +Env samples shipped: `.env.sample.mainnet`, `.env.sample.hoodi`. |
| 54 | + |
| 55 | +- **mainnet** — Lido Simple DVT live module, `VE_STAKING_MODULE_ID=2`. |
| 56 | +- **hoodi** — Lido's current testnet target. |
| 57 | + |
| 58 | +The README still mentions `.env.sample.holesky` — this is **stale**. Holesky is dead; the sample doesn't ship and shouldn't be used. |
| 59 | + |
| 60 | +## Compose layout |
| 61 | + |
| 62 | +Same structure as CDVN, plus Lido-specific services: |
| 63 | + |
| 64 | +- `docker-compose.yml` — Charon, validator-ejector, monitoring base. |
| 65 | +- `compose-el.yml` / `compose-cl.yml` / `compose-vc.yml` / `compose-mev.yml` — client variants wired via `EL`/`CL`/`VC`/`MEV` in `.env`. |
| 66 | +- `compose-monitoring.yml`, `compose-debug.yml` — observability add-ons. |
| 67 | +- `commit-boost/` — commit-boost config samples per network (requires `MEV=mev-commitboost`). |
| 68 | +- `alloy/` — log-collection config. |
| 69 | + |
| 70 | +## Standard workflows |
| 71 | + |
| 72 | +### Verify cluster health after setup |
| 73 | +```bash |
| 74 | +docker compose exec charon charon alpha test <beacon|validator|peers|infra> |
| 75 | +``` |
| 76 | +Use the global `test-a-dv-cluster` skill to interpret failures. |
| 77 | + |
| 78 | +### Update to a new release |
| 79 | +```bash |
| 80 | +docker compose down |
| 81 | +git stash # save local .env / custom overrides |
| 82 | +git pull |
| 83 | +git checkout v0.X.Y # pin to a release tag |
| 84 | +git stash apply |
| 85 | +docker compose up -d |
| 86 | +``` |
| 87 | + |
| 88 | +### Switching a client |
| 89 | +Same as CDVN: `docker compose down`, edit the `EL`/`CL`/`VC`/`MEV` line in `.env`, `docker compose up -d`. For commit-boost, also update `commit-boost/config.toml`. |
| 90 | + |
| 91 | +### Use an external beacon node + execution client (skip the local EL/CL stack) |
| 92 | + |
| 93 | +When the operator already runs a BN/EL elsewhere and doesn't want Docker Compose to start its own, set both clients to the `-none` sentinel and point Charon + validator-ejector at the external endpoints: |
| 94 | + |
| 95 | +```bash |
| 96 | +EL=el-none |
| 97 | +CL=cl-none |
| 98 | + |
| 99 | +# Charon → external BN / EL |
| 100 | +CHARON_BEACON_NODE_ENDPOINTS=https://your-bn.example:5052 |
| 101 | +CHARON_EXECUTION_CLIENT_RPC_ENDPOINT=https://your-el.example:8545 |
| 102 | + |
| 103 | +# Lido-specific: validator-ejector has its own endpoint vars (see gotcha below) |
| 104 | +VE_BEACON_NODE_URL=https://your-bn.example:5052 |
| 105 | +VE_EXECUTION_NODE_URL=https://your-el.example:8545 |
| 106 | +``` |
| 107 | + |
| 108 | +`el-none` / `cl-none` don't match any Compose profile, so nothing local gets started for those layers. The VC still runs locally and talks to Charon as usual — no VC-side changes needed. |
| 109 | + |
| 110 | +**Lido gotcha (validator-ejector):** `VE_BEACON_NODE_URL` and `VE_EXECUTION_NODE_URL` default to `http://${CL}:5052` and `http://${EL}:8545` in the env samples. If `CL`/`EL` are set to `cl-none`/`el-none` and these two vars aren't overridden, they resolve to `http://cl-none:5052` / `http://el-none:8545` — hostnames that don't exist — and validator-ejector fails to start. CDVN doesn't have this trap; it's specific to this repo. (The defaults could be reworked so this isn't needed — tracked as a future fix, leaving as-is for now.) |
| 111 | + |
| 112 | +**Auth to hosted providers:** Charon supports optional HTTP headers on BN requests via `CHARON_BEACON_NODE_HEADERS` (see the env sample). Use it to pass API keys when the external BN requires auth. The env sample warns the headers are sent to primary and fallback endpoints alike, so only wire in BNs you trust. |
| 113 | + |
| 114 | +**Mixed cases** (local CL + external EL, or vice versa) aren't covered explicitly — same pattern, but only set the one `-none` sentinel and only override the endpoint var(s) for the external side. Don't forget the matching `VE_*` override. |
| 115 | + |
| 116 | +### Run two DV stacks on one host, sharing the BN/EL from one of them |
| 117 | + |
| 118 | +Common ops pattern: stack A runs a full EL+CL+Charon+VC+validator-ejector. Stack B runs only Charon+VC+validator-ejector (with `EL=el-none`/`CL=cl-none`) and reuses stack A's BN/EL to avoid running two sync'd clients on the same box. Each stack is **its own DV cluster** — separate `.charon/` from a separate DKG, separate `VE_OPERATOR_ID`, separate validator-ejector instance. They just happen to share client infrastructure. |
| 119 | + |
| 120 | +Goal: stack B's Charon reaches stack A's `lighthouse` / `nethermind` (or whichever clients) by service name, with no extra host-port publishing. Achieved by attaching stack B's containers to stack A's Docker network as a secondary network, while keeping stack B's own `dvnode` network for internal comms (VC ↔ Charon ↔ validator-ejector). |
| 121 | + |
| 122 | +**Stack A** — no structural changes. Just pin the Compose project name so the network name is predictable: |
| 123 | + |
| 124 | +```bash |
| 125 | +# stack-a/.env |
| 126 | +COMPOSE_PROJECT_NAME=stack-a # → network becomes "stack-a_dvnode" |
| 127 | +``` |
| 128 | + |
| 129 | +Start stack A first so the network exists before stack B comes up. |
| 130 | + |
| 131 | +**Stack B** — set `EL=el-none`, `CL=cl-none`, override endpoint vars to stack A's service names, and change the Charon P2P host port so it doesn't clash with stack A's 3610: |
| 132 | + |
| 133 | +```bash |
| 134 | +# stack-b/.env |
| 135 | +COMPOSE_PROJECT_NAME=stack-b |
| 136 | +EL=el-none |
| 137 | +CL=cl-none |
| 138 | +CHARON_BEACON_NODE_ENDPOINTS=http://lighthouse:5052 # service name on stack A's network |
| 139 | +CHARON_EXECUTION_CLIENT_RPC_ENDPOINT=http://nethermind:8545 |
| 140 | +VE_BEACON_NODE_URL=http://lighthouse:5052 |
| 141 | +VE_EXECUTION_NODE_URL=http://nethermind:8545 |
| 142 | +CHARON_PORT_P2P_TCP=3611 # must differ from stack A's 3610 |
| 143 | +``` |
| 144 | + |
| 145 | +Add a `docker-compose.override.yml` in stack B that attaches Charon + validator-ejector to stack A's network as a secondary: |
| 146 | + |
| 147 | +```yaml |
| 148 | +# stack-b/docker-compose.override.yml |
| 149 | +services: |
| 150 | + charon: |
| 151 | + networks: |
| 152 | + - dvnode |
| 153 | + - stack-a |
| 154 | + validator-ejector: |
| 155 | + networks: |
| 156 | + - dvnode |
| 157 | + - stack-a |
| 158 | + |
| 159 | +networks: |
| 160 | + stack-a: |
| 161 | + external: true |
| 162 | + name: stack-a_dvnode # must match stack A's <COMPOSE_PROJECT_NAME>_dvnode |
| 163 | +``` |
| 164 | +
|
| 165 | +**Why a secondary network rather than `external: true` on `dvnode` itself:** if both stacks share one network as `dvnode`, the service aliases `charon`, `validator-ejector`, and the VC name collide — Docker's embedded DNS will return IPs from either stack for those names, and stack B's VC could end up talking to stack A's Charon. Keeping each stack's own `dvnode` private, and only joining a shared secondary network for BN/EL access, avoids that. Adjust VCs similarly if you ever need them to reach across (normally you don't — the VC talks to Charon only, on the private network). |
| 166 | + |
| 167 | +**Port conflicts to watch on the host:** |
| 168 | +- `CHARON_PORT_P2P_TCP` (3610 default) — must differ per stack. |
| 169 | +- `MONITORING_PORT_GRAFANA` (3000), `MONITORING_PORT_LOKI`, any other published monitoring ports — only relevant if stack B runs its own monitoring. Usually skip monitoring on stack B and let stack A's Prometheus scrape stack B's Charon over the shared network. |
| 170 | +- Charon's validator API (3600) and metrics (3620) are **not** published to the host by default — network-internal only — so they don't collide. |
| 171 | + |
| 172 | +**Validator-ejector gotcha still applies:** the `VE_*` overrides above are required, for the same reason as the external-BN section above. |
| 173 | + |
| 174 | +**Prerequisites:** |
| 175 | +- Same Docker host. |
| 176 | +- Same Docker engine version high enough to support secondary networks on a service (any recent Compose v2 is fine). |
| 177 | +- Stack A up before stack B — otherwise stack B's `external: true` lookup fails. |
| 178 | + |
| 179 | +## Monitoring & alerts |
| 180 | + |
| 181 | +Same stack as CDVN: Prometheus + Grafana + Loki + Alloy. Remote-write and Loki push target Obol's hosted monitoring; Discord alerts via `ALERT_DISCORD_IDS`. See the `obol-monitoring` skill for deep diagnostics. |
| 182 | + |
| 183 | +Lido operators should keep `monitoring-log-collector` enabled so the core team can correlate validator-ejector / exit issues across the Simple DVT module. |
| 184 | + |
| 185 | +## Ports |
| 186 | + |
| 187 | +Charon: 3600 (validator API) / 3610 (p2p tcp) / 3620 (metrics). EL/CL ports overridable via `EL_PORT_*` / `CL_PORT_*` env vars. |
| 188 | + |
| 189 | +## Deployment best practices |
| 190 | + |
| 191 | +Obol maintains a [deployment best practices guide](https://docs.obol.org/run-a-dv/prepare/deployment-best-practices) covering hardware sizing, networking, monitoring, backups, key handling, and operational hygiene. **Proactively offer to audit the user's setup against it** — walk through `.env` values, client pinning, monitoring + Discord alerting, `validator-ejector` health, and `.charon/` backup posture, then surface concrete improvements. Lido Simple DVT operators are held to a higher reliability bar than solo DVs (socialised slashing risk, exit-protocol SLAs), so the review should weight monitoring coverage and exit-path readiness heavily. |
| 192 | + |
| 193 | +## Related products |
| 194 | + |
| 195 | +- **`charon-distributed-validator-node`** — (CDVN) non-Lido stock DV stack. Same shape, simpler config. |
| 196 | +- **Obol Stack + `helm-charts/charts/dv-pod`** — Kubernetes-native path; Lido SDVT operators running on k8s should evaluate this. |
| 197 | +- **DappNode** (`dappnode/DAppNodePackage-obol-generic`) — third-party package to run a DV on a DappNode. |
| 198 | + |
| 199 | +## Key docs |
| 200 | + |
| 201 | +- Obol: https://docs.obol.org/docs/int/key-concepts |
| 202 | +- Obol errors: https://docs.obol.org/docs/faq/errors |
| 203 | +- Lido Simple DVT: https://operators.lido.fi/modules/simple-dvt |
| 204 | +- validator-ejector (Lido): https://github.com/lidofinance/validator-ejector |
| 205 | +- Deployment best practices: https://docs.obol.org/run-a-dv/prepare/deployment-best-practices |
| 206 | +- Launchpad: https://launchpad.obol.org |
| 207 | +- Canonical agent index: https://obol.org/llms.txt |
0 commit comments