diff --git a/compose-vc.yml b/compose-vc.yml index 5e3e8483..a86d42b7 100644 --- a/compose-vc.yml +++ b/compose-vc.yml @@ -12,9 +12,11 @@ services: vc-lodestar: profiles: [vc-lodestar] image: chainsafe/lodestar:${VC_LODESTAR_VERSION:-v1.46.0} - depends_on: [charon] - entrypoint: /opt/lodestar/run.sh + depends_on: + charon: + condition: service_healthy networks: [dvnode] + entrypoint: /opt/lodestar/run.sh environment: BEACON_NODE_ADDRESS: http://charon:3600 NETWORK: ${NETWORK} @@ -25,6 +27,7 @@ services: volumes: - ./lodestar/run.sh:/opt/lodestar/run.sh - .charon/validator_keys:/home/charon/validator_keys + - .charon/vc-config:/home/charon/vc-config:ro - ./data/lodestar:/opt/data # Keep data in lodestar and not vc-lodestar for backwards compatibility restart: unless-stopped @@ -41,7 +44,9 @@ services: context: nimbus args: VERSION: ${VC_NIMBUS_VERSION:-multiarch-v26.7.0} - depends_on: [charon] + depends_on: + charon: + condition: service_healthy networks: [dvnode] environment: BEACON_NODE_ADDRESS: http://charon:3600 @@ -51,6 +56,7 @@ services: volumes: - ./nimbus/run.sh:/home/user/data/run.sh - .charon/validator_keys:/home/validator_keys + - .charon/vc-config:/home/charon/vc-config:ro - ./data/vc-nimbus:/home/user/data restart: unless-stopped @@ -62,20 +68,28 @@ services: vc-prysm: profiles: [vc-prysm] - image: offchainlabs/prysm-validator:${VC_PRYSM_VERSION:-v7.1.8} + image: "prysm-validator-client:${VC_PRYSM_VERSION:-v7.1.8}" + build: + context: prysm + args: + VERSION: ${VC_PRYSM_VERSION:-v7.1.8} platform: "linux/amd64" - depends_on: [charon] + depends_on: + charon: + condition: service_healthy networks: [dvnode] entrypoint: /home/prysm/run.sh environment: BEACON_NODE_ADDRESS: http://charon:3600 NETWORK: ${NETWORK} + BUILDER_API_ENABLED: ${BUILDER_API_ENABLED:-true} labels: - "alloy-monitored=${VC_PRYSM_ALLOY_MONITORED:-true}" volumes: - ./prysm/run.sh:/home/prysm/run.sh - ./data/vc-prysm:/data/vc - .charon/validator_keys:/home/charon/validator_keys + - .charon/vc-config:/home/charon/vc-config:ro restart: unless-stopped # _ _ @@ -86,24 +100,25 @@ services: vc-teku: profiles: [vc-teku] - image: consensys/teku:${VC_TEKU_VERSION:-26.8.0} - command: | - validator-client - --beacon-node-api-endpoint "http://charon:3600" - --network="${NETWORK}" - --data-base-path=/home/data - --validator-keys="/opt/charon/validator_keys:/opt/charon/validator_keys" - --validators-keystore-locking-enabled false - --validators-external-signer-slashing-protection-enabled true - --validators-builder-registration-default-enabled ${BUILDER_API_ENABLED:-true} - --validators-proposer-default-fee-recipient "0x0000000000000000000000000000000000000000" - --Xobol-dvt-integration-enabled true - --Xvalidator-client-beacon-api-executor-threads=50 - depends_on: [charon] + image: "teku-validator-client:${VC_TEKU_VERSION:-26.8.0}" + build: + context: teku + args: + VERSION: ${VC_TEKU_VERSION:-26.8.0} + depends_on: + charon: + condition: service_healthy networks: [dvnode] + entrypoint: /opt/teku/run.sh + environment: + BEACON_NODE_ADDRESS: http://charon:3600 + NETWORK: ${NETWORK} + BUILDER_API_ENABLED: ${BUILDER_API_ENABLED:-true} labels: - "alloy-monitored=${VC_TEKU_ALLOY_MONITORED:-true}" volumes: + - ./teku/run.sh:/opt/teku/run.sh - .charon/validator_keys:/opt/charon/validator_keys + - .charon/vc-config:/opt/charon/vc-config:ro - ./data/vc-teku:/home/data restart: unless-stopped diff --git a/lodestar/run.sh b/lodestar/run.sh index 8841cca8..b230db0e 100755 --- a/lodestar/run.sh +++ b/lodestar/run.sh @@ -40,6 +40,35 @@ done echo "Processed all keys imported=${IMPORTED_COUNT}, existing=${EXISTING_COUNT}, total=$(ls /home/charon/validator_keys/keystore-*.json | wc -l)" +# Render Lodestar's proposer settings from the charon-generated canonical config when +# available: entries only carry fields diverging from default_config, absent fields +# fall back to it. Lodestar only accepts yml/yaml file extensions; JSON is valid YAML. +PROPOSER_SETTINGS="" +if [ -f /home/charon/vc-config/proposer-config.json ]; then + echo "proposer-config.json found, rendering lodestar proposer settings" + node -e ' + const fs = require("fs"); + const src = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); + const d = src.default_config; + const out = { + proposer_config: {}, + default_config: {fee_recipient: d.fee_recipient, builder: {gas_limit: d.gas_limit}}, + }; + for (const [pubkey, entry] of Object.entries(src.proposer_config || {})) { + out.proposer_config[pubkey] = { + fee_recipient: entry.fee_recipient ?? d.fee_recipient, + builder: {gas_limit: entry.gas_limit ?? d.gas_limit}, + }; + } + fs.writeFileSync(process.argv[2], JSON.stringify(out)); + ' /home/charon/vc-config/proposer-config.json /tmp/proposer-config.yml + PROPOSER_SETTINGS="--proposerSettingsFile=/tmp/proposer-config.yml" +else + echo "proposer-config.json not found, running without proposer settings" +fi + +# Word splitting of $PROPOSER_SETTINGS is intentional, it is empty or a single flag. +# shellcheck disable=SC2086 exec node /usr/app/packages/cli/bin/lodestar validator \ --dataDir="$DATA_DIR" \ --keystoresDir="$KEYSTORES_DIR" \ @@ -51,4 +80,5 @@ exec node /usr/app/packages/cli/bin/lodestar validator \ --beaconNodes="$BEACON_NODE_ADDRESS" \ --builder="$BUILDER_API_ENABLED" \ --builder.selection="$BUILDER_SELECTION" \ - --distributed + --distributed \ + $PROPOSER_SETTINGS diff --git a/nimbus/Dockerfile b/nimbus/Dockerfile index ad27351a..c671ab4e 100755 --- a/nimbus/Dockerfile +++ b/nimbus/Dockerfile @@ -6,4 +6,9 @@ FROM statusim/nimbus-validator-client:${VERSION} COPY --from=nimbusbn /home/user/nimbus_beacon_node /home/user/nimbus_beacon_node +# jq is used by run.sh to render per-validator proposer config files. +USER root +RUN apt-get update && apt-get install -y --no-install-recommends jq && rm -rf /var/lib/apt/lists/* +USER user + ENTRYPOINT ["/home/user/data/run.sh"] diff --git a/nimbus/run.sh b/nimbus/run.sh index 464f535b..b77c6875 100755 --- a/nimbus/run.sh +++ b/nimbus/run.sh @@ -35,6 +35,28 @@ rm -r ${tmpkeys} echo "Imported all keys" +if [[ -f /home/charon/vc-config/proposer-config.json ]]; then + echo "proposer-config.json found, rendering per-validator proposer settings" + + # Resolve each imported validator's settings: proposer_config entries only carry + # fields diverging from default_config, absent fields fall back to it. + config=/home/charon/vc-config/proposer-config.json + for f in /home/validator_keys/keystore-*.json; do + pubkey="0x$(jq -r .pubkey "${f}")" + fee_recipient=$(jq -r --arg pk "${pubkey}" '.proposer_config[$pk].fee_recipient // .default_config.fee_recipient' "${config}") + gas_limit=$(jq -r --arg pk "${pubkey}" '.proposer_config[$pk].gas_limit // .default_config.gas_limit' "${config}") + + for dir in "/home/user/data/validators/${pubkey}" "/home/user/data/validators/${pubkey#0x}"; do + if [[ -d "${dir}" ]]; then + echo "${fee_recipient}" >"${dir}/suggested_fee_recipient.hex" + echo "${gas_limit}" >"${dir}/suggested_gas_limit.json" + fi + done + done +else + echo "proposer-config.json not found, running without proposer settings" +fi + # Now run nimbus VC exec /home/user/nimbus_validator_client \ --data-dir=/home/user/data \ @@ -42,5 +64,5 @@ exec /home/user/nimbus_validator_client \ --doppelganger-detection=false \ --metrics \ --metrics-address=0.0.0.0 \ - --payload-builder=${BUILDER_API_ENABLED} \ + --payload-builder="${BUILDER_API_ENABLED}" \ --distributed diff --git a/prometheus/prometheus.yml.example b/prometheus/prometheus.yml.example index bdb3b85b..00e99e5b 100644 --- a/prometheus/prometheus.yml.example +++ b/prometheus/prometheus.yml.example @@ -45,7 +45,12 @@ scrape_configs: - job_name: "validator-client" static_configs: - - targets: ["lodestar:5064","vc-lodestar:5064"] + - targets: + - "lodestar:5064" + - "vc-lodestar:5064" + - "vc-teku:8008" + - "vc-prysm:8081" + - "vc-nimbus:8108" # Debug - job_name: "node-exporter" static_configs: diff --git a/prysm/Dockerfile b/prysm/Dockerfile new file mode 100644 index 00000000..973787de --- /dev/null +++ b/prysm/Dockerfile @@ -0,0 +1,8 @@ +ARG VERSION + +FROM ghcr.io/jqlang/jq:1.8.0 AS jq + +FROM offchainlabs/prysm-validator:${VERSION} + +# jq is used by run.sh to render Prysm's proposer settings from the charon-generated config. +COPY --from=jq /jq /usr/local/bin/jq diff --git a/prysm/run.sh b/prysm/run.sh index afbd3990..c937c7e0 100755 --- a/prysm/run.sh +++ b/prysm/run.sh @@ -41,13 +41,37 @@ rm -r ${tmpkeys} echo "Imported all keys" +# Render Prysm's proposer settings from the charon-generated canonical config when available: +# entries only carry fields diverging from default_config, absent fields fall back to it. +PROPOSER_SETTINGS=() +if [[ -f /home/charon/vc-config/proposer-config.json ]]; then + echo "proposer-config.json found, rendering prysm proposer settings" + jq --argjson enabled "${BUILDER_API_ENABLED}" ' + .default_config as $d | + { + proposer_config: (.proposer_config | map_values({ + fee_recipient: (.fee_recipient // $d.fee_recipient), + builder: {enabled: $enabled, gas_limit: (.gas_limit // $d.gas_limit)} + })), + default_config: { + fee_recipient: $d.fee_recipient, + builder: {enabled: $enabled, gas_limit: $d.gas_limit} + } + }' /home/charon/vc-config/proposer-config.json >/tmp/prysm-proposer-settings.json + PROPOSER_SETTINGS+=(--proposer-settings-file="/tmp/prysm-proposer-settings.json") +else + echo "proposer-config.json not found, running without proposer settings" +fi + # Now run prysm VC -/app/cmd/validator/validator --wallet-dir="$WALLET_DIR" \ +exec /app/cmd/validator/validator --wallet-dir="$WALLET_DIR" \ --accept-terms-of-use=true \ --datadir="/data/vc" \ --wallet-password-file="/wallet-password.txt" \ --enable-beacon-rest-api \ + --monitoring-host=0.0.0.0 \ --beacon-rest-api-provider="${BEACON_NODE_ADDRESS}" \ --beacon-rpc-provider="${BEACON_NODE_ADDRESS}" \ --"${NETWORK}" \ - --distributed + --distributed \ + "${PROPOSER_SETTINGS[@]}" diff --git a/teku/Dockerfile b/teku/Dockerfile new file mode 100644 index 00000000..2b09b72f --- /dev/null +++ b/teku/Dockerfile @@ -0,0 +1,8 @@ +ARG VERSION + +FROM ghcr.io/jqlang/jq:1.8.0 AS jq + +FROM consensys/teku:${VERSION} + +# jq is used by run.sh to render Teku's proposer config from the charon-generated one. +COPY --from=jq /jq /usr/local/bin/jq diff --git a/teku/run.sh b/teku/run.sh new file mode 100755 index 00000000..1758824a --- /dev/null +++ b/teku/run.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Render Teku's proposer config from the charon-generated canonical config when available: +# entries only carry fields diverging from default_config, absent fields fall back to it. +# Otherwise fall back to a zero default fee recipient, which charon overrides pre-gloas. +PROPOSER_CONFIG=() +if [[ -f /opt/charon/vc-config/proposer-config.json ]]; then + echo "proposer-config.json found, rendering teku proposer config" + jq --argjson enabled "${BUILDER_API_ENABLED}" ' + .default_config as $d | + { + proposer_config: (.proposer_config | map_values({ + fee_recipient: (.fee_recipient // $d.fee_recipient), + builder: {enabled: $enabled, gas_limit: (.gas_limit // $d.gas_limit)} + })), + default_config: { + fee_recipient: $d.fee_recipient, + builder: {enabled: $enabled, gas_limit: $d.gas_limit} + } + }' /opt/charon/vc-config/proposer-config.json >/tmp/teku-proposer-config.json + PROPOSER_CONFIG+=(--validators-proposer-config="/tmp/teku-proposer-config.json") +else + echo "proposer-config.json not found, running with zero default fee recipient" + PROPOSER_CONFIG+=(--validators-proposer-default-fee-recipient="0x0000000000000000000000000000000000000000") +fi + +exec /opt/teku/bin/teku validator-client \ + --beacon-node-api-endpoint "${BEACON_NODE_ADDRESS}" \ + --network="${NETWORK}" \ + --data-base-path=/home/data \ + --validator-keys="/opt/charon/validator_keys:/opt/charon/validator_keys" \ + --validators-keystore-locking-enabled false \ + --validators-external-signer-slashing-protection-enabled true \ + --validators-builder-registration-default-enabled "${BUILDER_API_ENABLED}" \ + --Xobol-dvt-integration-enabled true \ + --Xvalidator-client-beacon-api-executor-threads=50 \ + --metrics-enabled=true \ + --metrics-interface=0.0.0.0 \ + --metrics-port=8008 \ + --metrics-host-allowlist="*" \ + "${PROPOSER_CONFIG[@]}"