Obol Setup
Setting Up a Distributed Validator Cluster with Obol Network and StakeWise Vaults
Follow these steps to set up a Distributed Validator Cluster using Obol Network and integrate it with StakeWise Vaults.
Prerequisites
Before proceeding, ensure you have the following:
- Install Docker Engine ↗
- Ensure Docker is running:
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
Important - Keystore Password Configuration
When using the create-keys command, add the --per-keystore-password flag to generate a keystore with a separate password file for each keystore. This is necessary for the normal operation of the Charon CLI, especially when splitting keys.
Step 1: Setup Obol Cluster
Charon ↗ is Obol's implementation of DVT that acts as a middleware between validator clients and the beacon node, intercepting and proxying API traffic. It uses an event-driven, multi-component architecture where each component handles a specific task in the signing workflow—from scheduling duties and reaching consensus, to collecting partial signatures and aggregating them into valid beacon chain signatures.
Create Environment Configuration
First, create a .env file with Charon settings:
Address Format
Addresses must be in lowercase.
cat <<EOF > .env
export VAULT_CONTRACT_ADDR=[ENTER YOUR VAULT CONTRACT ADDRESS HERE]
export FEE_RECIPIENT_ADDR=[ENTER YOUR BLOCK REWARD RECIPIENT ADDRESS HERE]
export NETWORK=[ENTER YOUR NETWORK NAME]
EOF
Finding Your Addresses
- Vault Contract Address: Found in the Vault page URL or Details section
- Block Reward Recipient: Located in the "Block reward recipient" field on your Vault page settings
Create the Cluster
Split your validator keys across multiple nodes:
source .env
docker run --rm -v "$(pwd):/opt/charon" -v "$HOME/.stakewise:/.stakewise" obolnetwork/charon:v1.7.1 \
create cluster \
--name="cluster-name" \
--cluster-dir=".charon/cluster/" \
--withdrawal-addresses=$VAULT_CONTRACT_ADDR \
--fee-recipient-addresses=$FEE_RECIPIENT_ADDR \
--nodes 3 \
--network $NETWORK \
--split-existing-keys \
--split-keys-dir /.stakewise/$VAULT_CONTRACT_ADDR/keystores
Command Flags Explained
| Flag | Description |
|---|---|
--name | The cluster name |
--cluster-dir | Parent directory containing .charon subdirectories from the required threshold of nodes (default "./") |
--withdrawal-addresses | Comma separated list of Ethereum addresses to receive returned stake and rewards. Provide single address or one per validator |
--fee-recipient-addresses | Comma separated list of fee recipient addresses. Provide single address or one per validator. Must match the "Block reward recipient" address on your Vault page |
--nodes | Number of Charon nodes in the cluster. Minimum is 3 |
--network | Ethereum network. Options: mainnet, hoodi, gnosis, chiado (default "mainnet") |
--split-existing-keys | Split existing validator private key into distributed key shares |
--split-keys-dir | Directory containing keys to split. Expects keystore-*.json and keystore-*.txt. Requires --split-existing-keys |
Example Output
***************** WARNING: Splitting keys **********************
Please make sure any existing validator has been shut down for
at least 2 finalised epochs before starting the charon cluster,
otherwise slashing could occur.
****************************************************************
Created charon cluster:
--split-existing-keys=true
/opt/charon/.charon/cluster/
├─ node[0-2]/ Directory for each node
│ ├─ charon-enr-private-key Charon networking private key for node authentication
│ ├─ cluster-lock.json Cluster lock file signed by all nodes
│ ├─ deposit-data.json Deposit data to activate a Distributed Validator
│ ├─ validator_keys Validator keystores and password
│ │ ├─ keystore-*.json Validator private share key for duty signing
│ │ ├─ keystore-*.txt Keystore password files for keystore-*.json
Verify Cluster Creation
Check that your cluster was created successfully:
ls -la .charon/cluster/
You should see node0/, node1/, and node2/ directories.
Backup Required
Backup the ./.charon folder before proceeding to deployment.
Step 2: Run Nodes, Validators and Charon Client
This docker-compose.yml launches a full Obol/Charon distributed validator stack for Ethereum mainnet,
consisting of Geth (execution client), Lighthouse (consensus client), MEV-boost, Charon (relay + 3 nodes), and validator clients (3x Teku: one per Charon node) (vc0/vc1/vc2).
All services run on a shared cluster Docker network and are wired so that validator clients talk to Charon, and Charon talks to Lighthouse and Geth.
docker-compose.yml
x-logging: &logging
logging:
driver: json-file
options:
max-size: 10m
max-file: "3"
tag: "{{.ImageName}}|{{.Name}}|{{.ImageFullID}}|{{.FullID}}"
networks:
cluster:
# Base config shared by all Charon-based services (relay + nodes)
x-node-base:
# Pegged charon version (update this for each release).
&node-base
# Main Charon image (version can be overridden via CHARON_VERSION env var)
image: obolnetwork/charon:${CHARON_VERSION:-v1.7.1}
restart: unless-stopped
networks: [ cluster ]
depends_on: [ relay ]
volumes:
# Shared Charon config and keys directory
- ./.charon:/opt/charon/.charon/
# Common environment variables shared by all Charon nodes
x-node-env:
&node-env
# Consensus client (Beacon node) endpoint (Lighthouse in this stack)
CHARON_BEACON_NODE_ENDPOINTS: ${CHARON_BEACON_NODE_ENDPOINTS:-http://lighthouse:6000}
CHARON_LOG_LEVEL: ${CHARON_LOG_LEVEL:-info}
CHARON_LOG_FORMAT: ${CHARON_LOG_FORMAT:-console}
CHARON_BUILDER_API: true
# P2P configuration
CHARON_P2P_EXTERNAL_HOSTNAME: ${CHARON_P2P_EXTERNAL_HOSTNAME:-} # Empty default required to avoid warnings.
CHARON_P2P_RELAYS: ${CHARON_P2P_RELAYS:-http://relay:3640/enr}
CHARON_P2P_TCP_ADDRESS: ${CHARON_P2P_TCP_ADDRESS:-0.0.0.0:3610}
# Where the validator API is exposed for validator clients (Teku)
CHARON_VALIDATOR_API_ADDRESS: ${CHARON_VALIDATOR_API_ADDRESS:-0.0.0.0:3600}
services:
mev-boost:
image: flashbots/mev-boost:v1.10.1
restart: always
command: >
-mainnet
-relays
# List of mainnet MEV relays this instance will talk to
https://0xac6e77dfe25ecd6110b8e780608cce0dab71fdd5ebea22a16c0205200f2f8e2e3ad3b71d3499c54ad14d6c21b41a37ae@boost-relay.flashbots.net,https://0xa1559ace749633b997cb3fdacffb890aeebdb0f5a3b6aaa7eeeaf1a38af0a8fe88b9e4b1f61f236d2e64d95733327a62@relay.ultrasound.money,https://0x8b5d2e73e2a3a55c6c87b8b6eb92e0149a125c852751db1422fa951e42a09b82c142c3ea98d0d9930b056a3bc9896b8f@bloxroute.max-profit.blxrbdn.com,https://0xa7ab7a996c8584251c8f925da3170bdfd6ebc75d50f5ddc4050a6fdc77f2a3b5fce2cc750d0865e05d7228af97d69561@agnostic-relay.net,https://0xb0b07cd0abef743db4260b0ed50619cf6ad4d82064cb4fbec9d3ec530f7c5e6793d9f286c4e082c0244ffb9f2658fe88@bloxroute.regulated.blxrbdn.com,https://0xa15b52576bcbf1072f4a011c0f99f9fb6c66f3e1ff321f11f461d15e31b1cb359caa092c71bbded0bae5b5ea401aab7e@aestus.live
-addr
0.0.0.0:18551
-loglevel
info
-json
networks: [ cluster ]
geth:
image: ethereum/client-go:v1.16.7
restart: always
command: >
--mainnet
--syncmode=snap
--datadir=/data
--db.engine=pebble
--authrpc.jwtsecret=/data/jwtsecret --authrpc.addr=0.0.0.0 --authrpc.port=8551 --authrpc.vhosts=*
--http --http.addr=0.0.0.0 --http.port=8445 --http.corsdomain=* --http.vhosts=*
--port=30303
--ipcdisable
volumes: ["./data/geth:/data"]
ports:
- 30303:30303/tcp
- 30303:30303/udp
networks: [ cluster ]
lighthouse:
image: sigp/lighthouse:v8.0.1
restart: always
command: >
lighthouse
bn
--staking
--datadir=/data
--network=mainnet
--execution-endpoint=http://geth:8551
--execution-jwt=/data/jwtsecret
--checkpoint-sync-url=https://mainnet-checkpoint-sync.attestant.io/
--slots-per-restore-point=8192
--http
--http-port=6000
--http-address=0.0.0.0
--http-allow-origin=*
--builder http://mev-boost:18551
--port=30304
--enr-udp-port=30305
--disable-upnp
ulimits:
nofile:
soft: "1000000"
hard: "1000000"
volumes: ["./data/lighthouse:/data"]
ports:
- 30304:30304/tcp
- 30304:30304/udp
- 30305:30305/udp
networks: [ cluster ]
relay:
<<: *node-base
command: relay
depends_on: []
environment:
<<: *node-env
CHARON_HTTP_ADDRESS: 0.0.0.0:3640
CHARON_DATA_DIR: /opt/charon/relay
CHARON_P2P_RELAYS: ""
CHARON_P2P_EXTERNAL_HOSTNAME: relay
volumes:
- ./data/relay:/opt/charon/relay:rw
# CHARON NODES: 3-node distributed validator cluster (node0, node1, node2)
node0:
<<: *node-base
environment:
<<: *node-env
CHARON_PRIVATE_KEY_FILE: /opt/charon/.charon/cluster/node0/charon-enr-private-key
CHARON_LOCK_FILE: /opt/charon/.charon/cluster/node0/cluster-lock.json
CHARON_P2P_EXTERNAL_HOSTNAME: node0
node1:
<<: *node-base
environment:
<<: *node-env
CHARON_PRIVATE_KEY_FILE: /opt/charon/.charon/cluster/node1/charon-enr-private-key
CHARON_LOCK_FILE: /opt/charon/.charon/cluster/node1/cluster-lock.json
CHARON_P2P_EXTERNAL_HOSTNAME: node1
node2:
<<: *node-base
environment:
<<: *node-env
CHARON_PRIVATE_KEY_FILE: /opt/charon/.charon/cluster/node2/charon-enr-private-key
CHARON_LOCK_FILE: /opt/charon/.charon/cluster/node2/cluster-lock.json
CHARON_P2P_EXTERNAL_HOSTNAME: node2
# TEKU VALIDATOR CLIENTS: one VC per Charon node (vc0, vc1, vc2)
#
# Each Teku instance:
# - connects to its local Charon node’s validator API
# - uses validator key shares stored under .charon/cluster/nodeX/validator_keys
vc0-teku:
image: consensys/teku:${TEKU_VERSION:-25.11.0}
networks: [ cluster ]
depends_on: [ node0 ]
restart: unless-stopped
command: |
validator-client
--network=auto
--beacon-node-api-endpoint="http://node0:3600"
--validators-proposer-default-fee-recipient=[ENTER YOUR BLOCK REWARD RECIPIENT ADDRESS HERE]
--validators-builder-registration-default-enabled=true
--validator-keys="/opt/charon/validator_keys:/opt/charon/validator_keys"
--validators-keystore-locking-enabled=false
volumes:
- .charon/cluster/node0/validator_keys:/opt/charon/validator_keys
- ./data/vc0:/opt/charon/teku
vc1-teku:
image: consensys/teku:${TEKU_VERSION:-25.11.0}
networks: [ cluster ]
depends_on: [ node1 ]
restart: unless-stopped
command: |
validator-client
--network=auto
--beacon-node-api-endpoint="http://node1:3600"
--validators-proposer-default-fee-recipient=[ENTER YOUR BLOCK REWARD RECIPIENT ADDRESS HERE]
--validators-builder-registration-default-enabled=true
--validator-keys="/opt/charon/validator_keys:/opt/charon/validator_keys"
--validators-keystore-locking-enabled=false
volumes:
- .charon/cluster/node1/validator_keys:/opt/charon/validator_keys
- ./data/vc1:/opt/charon/teku
vc2-teku:
image: consensys/teku:${TEKU_VERSION:-25.11.0}
networks: [ cluster ]
depends_on: [ node2 ]
restart: unless-stopped
command: |
validator-client
--network=auto
--beacon-node-api-endpoint="http://node2:3600"
--validators-proposer-default-fee-recipient=[ENTER YOUR BLOCK REWARD RECIPIENT ADDRESS HERE]
--validators-builder-registration-default-enabled=true
--validator-keys="/opt/charon/validator_keys:/opt/charon/validator_keys"
--validators-keystore-locking-enabled=false
volumes:
- .charon/cluster/node2/validator_keys:/opt/charon/validator_keys
- ./data/vc2:/opt/charon/teku
Block Reward Recipient Address
Replace [ENTER YOUR BLOCK REWARD RECIPIENT ADDRESS HERE] inside docker-compose.yml file with your Block reward recipient address.
- Copy the
docker-compose.ymlfile below to the same directory as your.charonfolder - Update the fee recipient address
- Run
docker compose up -d
You can run all components on a single server or distribute across multiple servers.
📚 Additional Resources
For more information, refer to the Obol Documentation ↗.