diff --git a/dappnode_package.json b/dappnode_package.json index 926126f..d6939bf 100644 --- a/dappnode_package.json +++ b/dappnode_package.json @@ -5,6 +5,11 @@ "version": "v1.2.3", "arg": "UPSTREAM_VERSION" }, + { + "repo": "ssvlabs/ssv-dkg", + "version": "v3.0.3", + "arg": "DKG_UPSTREAM_VERSION" + }, { "repo": "dappnode/staker-package-scripts", "version": "v0.1.2", @@ -32,6 +37,7 @@ "minimumDappnodeVersion": "0.2.101" }, "license": "Apache-2.0", + "mainService": "operator", "links": { "Readme": "https://github.com/dappnode/DAppNodePackage-Anchor", "Documentation": "https://anchor.sigmaprime.io/introduction/", @@ -46,6 +52,11 @@ "name": "keys", "path": "/root/keys", "service": "operator" + }, + { + "name": "dkg-output", + "path": "/data/dkg/output", + "service": "dkg" } ] } diff --git a/dkg/Dockerfile b/dkg/Dockerfile new file mode 100644 index 0000000..4ee42c0 --- /dev/null +++ b/dkg/Dockerfile @@ -0,0 +1,27 @@ +ARG DKG_UPSTREAM_VERSION +FROM ssvlabs/ssv-dkg:${DKG_UPSTREAM_VERSION} + +WORKDIR / + +ARG NETWORK +ARG DKG_PORT +ARG STAKER_SCRIPTS_VERSION + +RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community \ + yq inotify-tools jq curl + +COPY dkg-config.yml /ssv-dkg/config/dkg-config.yml +COPY entrypoint.sh /usr/local/bin/entrypoint.sh + +RUN chmod +x /usr/local/bin/entrypoint.sh + +ENV DKG_PORT=${DKG_PORT} \ + DKG_DATA_DIR=/data/dkg \ + OPERATOR_DATA_DIR=/root/keys \ + NETWORK=${NETWORK} \ + METRICS_PORT=15000 \ + DKG_CONFIG_DIR=/ssv-dkg/config \ + STAKER_SCRIPTS_URL=https://github.com/dappnode/staker-package-scripts/releases/download/${STAKER_SCRIPTS_VERSION} + +ADD ${STAKER_SCRIPTS_URL}/dvt_lsd_tools.sh /etc/profile.d/ +ENTRYPOINT [ "entrypoint.sh" ] diff --git a/dkg/dkg-config.yml b/dkg/dkg-config.yml new file mode 100644 index 0000000..49e8e44 --- /dev/null +++ b/dkg/dkg-config.yml @@ -0,0 +1,8 @@ +privKey: +privKeyPassword: +port: +storeShare: true +logLevel: +logFormat: json +logLevelFormat: capitalColor +logFilePath: /data/dkg.log diff --git a/dkg/entrypoint.sh b/dkg/entrypoint.sh new file mode 100644 index 0000000..d8b2adf --- /dev/null +++ b/dkg/entrypoint.sh @@ -0,0 +1,128 @@ +#!/bin/sh + +OPERATOR_CONFIG_DIR=${OPERATOR_DATA_DIR} +DKG_LOGS_DIR=${DKG_DATA_DIR}/logs +DKG_OUTPUT_DIR=${DKG_DATA_DIR}/output +DKG_CERT_DIR=${DKG_DATA_DIR}/ssl + +PRIVATE_KEY_FILE=${OPERATOR_CONFIG_DIR}/encrypted_private_key.json +PRIVATE_KEY_PASSWORD_FILE=${OPERATOR_CONFIG_DIR}/password.txt +OPERATOR_ID_FILE=/data/dkg/operator_id.txt +DKG_CONFIG_FILE=${DKG_CONFIG_DIR}/dkg-config.yml +DKG_LOG_FILE=${DKG_LOGS_DIR}/dkg.log + +CERT_FILE="$DKG_CERT_DIR/tls.crt" +KEY_FILE="$DKG_CERT_DIR/tls.key" + +# To use staker scripts +# shellcheck disable=SC1091 +. /etc/profile + +assign_execution_endpoint() { + EXECUTION_LAYER=$(get_execution_rpc_api_url_from_global_env "$NETWORK") + export EXECUTION_LAYER +} + +create_directories() { + mkdir -p "${DKG_CONFIG_DIR}" "${DKG_LOGS_DIR}" "${DKG_OUTPUT_DIR}" +} + +wait_for_private_key() { + echo "[INFO] Waiting for the operator service to create the private key file..." + while [ ! -f "${PRIVATE_KEY_FILE}" ]; do + echo "[INFO] Waiting for ${PRIVATE_KEY_FILE} to be created..." + inotifywait -e create -qq "$(dirname "${PRIVATE_KEY_FILE}")" + done + + echo "[INFO] Private key file found." + + if [ ! -f "${PRIVATE_KEY_PASSWORD_FILE}" ]; then + echo "[ERROR] ${PRIVATE_KEY_PASSWORD_FILE} not found. Cannot continue without the private key password file. Restarting dkg service..." + exit 1 # To avoid restart loop + fi +} + +get_operator_id() { + if [ -z "${OPERATOR_ID}" ]; then + + # Read operator ID from the file if it exists and is not empty + if [ -f "${OPERATOR_ID_FILE}" ] && [ -s "${OPERATOR_ID_FILE}" ]; then + OPERATOR_ID=$(cat "${OPERATOR_ID_FILE}") + echo "[INFO] Using OPERATOR_ID from the file: ${OPERATOR_ID}" + else + fetch_operator_id_from_api + fi + else + echo "[INFO] Using provided OPERATOR_ID: ${OPERATOR_ID}" + echo "${OPERATOR_ID}" >"${OPERATOR_ID_FILE}" + fi +} + +fetch_operator_id_from_api() { + echo "[INFO] OPERATOR_ID not provided. Fetching OPERATOR_ID from the API..." + + PUBLIC_KEY=$(jq -r '.pubKey' "${PRIVATE_KEY_FILE}") + + # If the PUBLIC_KEY is empty, try extracting using the '.publicKey' field (for previous SSV versions) + if [ -z "$PUBLIC_KEY" ] || [ "$PUBLIC_KEY" = "null" ]; then + PUBLIC_KEY=$(jq -r '.publicKey' "${PRIVATE_KEY_FILE}") + fi + + # Fetch the operator ID using the public key (retry 50 times with a delay of 10mins) + RESPONSE=$(curl --retry 50 --retry-delay 600 "https://api.ssv.network/api/v4/${NETWORK}/operators/public_key/${PUBLIC_KEY}") + + OPERATOR_ID=$(echo "${RESPONSE}" | jq -r '.data.id') + + # Check if OPERATOR_ID is successfully retrieved + if [ -z "${OPERATOR_ID}" ] || [ "${OPERATOR_ID}" = "null" ]; then + echo "[ERROR] Failed to fetch OPERATOR_ID from the API. Is your operator registered on the SSV network?" + echo "[INFO] Once registered, set OPERATOR_ID in the package config to perform the DKG or restart the dkg service to retry fetching it from the SSV API." + sleep 10m # To allow restoring backup + exit 0 + else + echo "[INFO] Successfully fetched OPERATOR_ID: ${OPERATOR_ID}" + echo "${OPERATOR_ID}" >"${OPERATOR_ID_FILE}" + fi +} + +generate_tls_cert() { + echo "[INFO] Generating TLS certificates..." + + mkdir -p "$DKG_CERT_DIR" + + # Generate a self-signed SSL certificate only if it doesn't exist + if [ ! -f "$CERT_FILE" ] || [ ! -f "$KEY_FILE" ]; then + echo "[INFO] Certificate or key file not found. Generating new SSL certificate and key." + openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \ + -keyout "$KEY_FILE" -out "$CERT_FILE" \ + -subj "/C=IL/ST=Tel Aviv/L=Tel Aviv/O=Coin-Dash Ltd/CN=*.ssvlabs.io" + else + echo "[INFO] Existing SSL certificate and key found. Using them." + fi +} + +start_dkg() { + exec /bin/ssv-dkg start-operator \ + --operatorID "${OPERATOR_ID}" \ + --configPath ".${DKG_CONFIG_FILE}" \ + --logFilePath ".${DKG_LOG_FILE}" \ + --logLevel "${LOG_LEVEL}" \ + --outputPath ".${DKG_OUTPUT_DIR}" \ + --port "${DKG_PORT}" \ + --privKey ".${PRIVATE_KEY_FILE}" \ + --privKeyPassword ".${PRIVATE_KEY_PASSWORD_FILE}" \ + --serverTLSCertPath ".${CERT_FILE}" \ + --ethEndpointURL "${EXECUTION_LAYER}" \ + --serverTLSKeyPath ".${KEY_FILE}" +} + +main() { + create_directories + assign_execution_endpoint + wait_for_private_key + get_operator_id + generate_tls_cert + start_dkg +} + +main diff --git a/docker-compose.yml b/docker-compose.yml index 83654b6..9a74b62 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,21 @@ services: EXTRA_OPTS: "" EXISTING_PASSWORD: "" NEW_PASSWORD: "" + dkg: + build: + context: dkg + args: + DKG_UPSTREAM_VERSION: v3.1.0 + STAKER_SCRIPTS_VERSION: v0.1.2 + restart: on-failure + volumes: + - operator-keys:/root/keys + - dkg-data:/data/dkg + environment: + LOG_LEVEL: info + OPERATOR_ID: "" + EXTRA_OPTS: "" volumes: operator-data: {} operator-keys: {} + dkg-data: {} diff --git a/operator/entrypoint.sh b/operator/entrypoint.sh index 4de744c..4b9702b 100755 --- a/operator/entrypoint.sh +++ b/operator/entrypoint.sh @@ -11,8 +11,32 @@ BEACON_NODES=$(get_beacon_api_url_from_global_env "$NETWORK") PASSWORD_FILE_PATH="/root/keys/password.txt" KEY_FILE_PATH="/root/keys/encrypted_private_key.json" +PUBLIC_KEY_FILE_PATH="/root/keys/public_key.txt" sleep 1 +post_pubkey_to_dappmanager() { + PUBLIC_KEY=$(cat "$PUBLIC_KEY_FILE_PATH") + + if [ -z "$PUBLIC_KEY" ]; then + echo "[ERROR - entrypoint] Public key not found at ${PUBLIC_KEY_FILE_PATH}, skipping Dappmanager post" + return 1 + fi + + curl --connect-timeout 5 \ + --max-time 10 \ + --silent \ + --retry 5 \ + --retry-delay 0 \ + --retry-max-time 40 \ + -X POST "http://dappmanager.dappnode/data-send?key=OperatorPublicKey&data=${PUBLIC_KEY}" || \ + { + echo "[ERROR - entrypoint] Failed to post public key to Dappmanager" + } + + echo "[INFO - entrypoint] Use this public key to register your node on the SSV network:" + echo "PUBLIC_KEY=${PUBLIC_KEY}" +} + # If Import Operator, save the EXISTING_PASSWORD to the password.txt at first import # Later when Anchor is starting it will use --password-file flag to decrypt the private key if [ "${SETUP_MODE}" = "Import Operator" ]; then @@ -51,6 +75,8 @@ else KEY_FILE="" fi +post_pubkey_to_dappmanager + FLAGS="--network=${NETWORK} \ --data-dir=/root/.anchor \ --beacon-nodes=${BEACON_NODES} \ diff --git a/package_variants/hoodi/docker-compose.yml b/package_variants/hoodi/docker-compose.yml index 50ecf96..345ca22 100644 --- a/package_variants/hoodi/docker-compose.yml +++ b/package_variants/hoodi/docker-compose.yml @@ -9,3 +9,10 @@ services: - "9102:9102/tcp" - "9102:9102/udp" - "9103:9103/udp" + dkg: + build: + args: + NETWORK: hoodi + DKG_PORT: 15514 + ports: + - "15514:15514" diff --git a/package_variants/mainnet/docker-compose.yml b/package_variants/mainnet/docker-compose.yml index 23a81fe..e23e78a 100644 --- a/package_variants/mainnet/docker-compose.yml +++ b/package_variants/mainnet/docker-compose.yml @@ -9,3 +9,10 @@ services: - "9100:9100/tcp" - "9100:9100/udp" - "9101:9101/udp" + dkg: + build: + args: + NETWORK: mainnet + DKG_PORT: 15516 + ports: + - "15516:15516"