diff --git a/.gitignore b/.gitignore index b92dc39..609cbf7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,13 @@ *.bak *.original custom.yml +.idea/ +*.iml +*.ipr +*.iws +.vscode/ +.DS_Store +*.log +*.tmp +tmp/ +temp/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index de65eae..fcbf3dc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,14 +10,10 @@ repos: - id: check-shebang-scripts-are-executable - id: check-executables-have-shebangs - id: detect-private-key - - id: check-ast - - id: debug-statements - id: check-merge-conflict - id: check-added-large-files - id: mixed-line-ending - id: check-case-conflict - - id: requirements-txt-fixer - - id: check-toml - repo: https://github.com/jumanjihouse/pre-commit-hooks rev: 3.0.0 @@ -33,13 +29,7 @@ repos: rev: v2.4.1 hooks: - id: codespell - - # Dockerfiles - # Disabled bcs macOS Docker might not be installed - # - repo: https://github.com/hadolint/hadolint - # rev: v2.13.1 - # hooks: - # - id: hadolint-docker + args: ['-L', 'pyton'] # YAML style (beyond syntax) - repo: https://github.com/adrienverge/yamllint diff --git a/README.md b/README.md new file mode 100644 index 0000000..686d746 --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +# Overview + +Docker Compose for TON (The Open Network) liteserver nodes. + +`cp default.env .env`, adjust values for the right network (mainnet/testnet), then `./ethd up`. + +Meant to be used with [central-proxy-docker](https://github.com/CryptoManufaktur-io/central-proxy-docker) for traefik and Prometheus remote write; use `:ext-network.yml` in `COMPOSE_FILE` inside `.env` in that case. + +Add `ton-shared.yml` to `COMPOSE_FILE` to expose liteserver and validator console ports locally instead of via traefik. + +`./ethd install` brings in docker-ce, if you don't have Docker installed already. + +`cp default.env .env` + +`nano .env` and adjust variables, particularly `GLOBAL_CONFIG_URL` and `SNAPSHOT` + +`./ethd up` + +Initial sync: ~10 hours (~4-5 hours with snapshot). + +To update the software, run `./ethd update` and then `./ethd up` + +# Configuration + +## Mainnet + +Basic setup: +```properties +TON_BRANCH=mainnet +GLOBAL_CONFIG_URL=https://ton.org/global.config.json +SNAPSHOT=latest +``` + +With HTTP API: +```properties +COMPOSE_FILE=ton.yml:ton-http-api.yml +TON_API_HTTP_PORT=8081 +``` + +With local RPC access: +```properties +COMPOSE_FILE=ton.yml:ton-shared.yml +``` + +## Testnet + +```properties +TON_BRANCH=testnet +GLOBAL_CONFIG_URL=https://ton.org/testnet-global.config.json +SNAPSHOT=latest_testnet +``` + +## HTTP API + +Enable HTTP/JSON-RPC API in `COMPOSE_FILE`: +```properties +COMPOSE_FILE=ton.yml:ton-http-api.yml +TON_API_HTTP_PORT=8081 +``` + +Test endpoints: +```bash +# masterchain info +curl -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"getMasterchainInfo","params":[]}' \ + http://localhost:8081 + +# health check +curl http://localhost:8081/healthcheck +``` + +# Operations + +## Check Sync Status +```bash +./scripts/check-sync.sh +``` +Exit codes: 0=synced, 1=syncing, 2=error + +## Monitor Logs +```bash +./ethd logs -f ton +``` + +## Node Status +```bash +docker compose exec ton mytonctrl +MyTonCtrl> status +``` + +## Generate Liteserver Config +After full sync: +```bash +docker compose exec ton mytonctrl +MyTonCtrl> installer clcf +``` +Creates `/usr/bin/ton/local.config.json` for client connections. + +# Hardware Requirements + +**Mainnet:** +- 16 cores, 64GB RAM +- 1TB SSD/NVMe (~250GB used, grows over time) +- 1 Gbps, 10TB+ monthly traffic + +**Testnet:** Similar, ~100GB storage + +# Ports + +Default ports (customizable in `.env`): +- `VALIDATOR_PORT` (30001/udp) - P2P networking +- `LITESERVER_PORT` (30003/tcp) - Liteserver connections +- `VALIDATOR_CONSOLE_PORT` (30002/tcp) - Console access +- `TON_API_HTTP_PORT` (8081/tcp) - HTTP API + +Public IP auto-detected on startup. Override with `PUBLIC_IP` in `.env` if needed. + +# Customization + +`custom.yml` can override any settings and is not tracked by git. Add to `COMPOSE_FILE` in `.env` if used. + +See `default.env` for all configuration options. + +## Version + +This is TON Docker v1.0.0 diff --git a/README.md.example b/README.md.example deleted file mode 100644 index 0c0faf1..0000000 --- a/README.md.example +++ /dev/null @@ -1,35 +0,0 @@ -# Overview - -Docker Compose for name-of-project - -Meant to be used with [central-proxy-docker](https://github.com/CryptoManufaktur-io/central-proxy-docker) for traefik -and Prometheus remote write; use `:ext-network.yml` in `COMPOSE_FILE` inside `.env` in that case. - -If you want the RPC ports exposed locally, use `rpc-shared.yml` in `COMPOSE_FILE` inside `.env`. - -## Quick Start - -The `./d` script can be used as a quick-start: - -`./d install` brings in docker-ce, if you don't have Docker installed already. - -`cp default.env .env` - -`nano .env` and adjust variables as needed, particularly mention-important-vars-here - -`./d up` - -## Software update - -To update the software, run `./d update` and then `./d up` - -## Customization - -`custom.yml` is not tracked by git and can be used to override anything in the provided yml files. If you use it, -add it to `COMPOSE_FILE` in `.env` - -## Version - -name-of-project Docker uses a semver scheme. - -This is name-of-project Docker v1.0.0 diff --git a/default.env b/default.env new file mode 100644 index 0000000..0456f25 --- /dev/null +++ b/default.env @@ -0,0 +1,52 @@ +# copy to .env and adjust +COMPOSE_FILE=ton.yml + +# uncomment to override public ip (kubernetes/special networks) +#PUBLIC_IP= + +TON_BRANCH=mainnet + +# testnet: https://ton.org/testnet-global.config.json +GLOBAL_CONFIG_URL=https://ton.org/global.config.json + +MYTONCTRL_VERSION=master +TELEMETRY=true +IGNORE_MINIMAL_REQS=true +MODE=liteserver + +ARCHIVE_TTL=2592000 +STATE_TTL=86400 +VERBOSITY=1 + +LITESERVER_PORT=30003 +VALIDATOR_PORT=30001 +VALIDATOR_CONSOLE_PORT=30002 + +TON_HOST=ton +TON_API_HOST=ton-api +DOMAIN=example.com + +# faster sync (~200GB): latest | latest_testnet | https://dump.ton.org/dumps/latest.tar.lz +SNAPSHOT= + +TON_DOCKERFILE=Dockerfile.binary +TON_DOCKER_TAG=latest +TON_DOCKER_REPO=ghcr.io/ton-blockchain/ton-docker-ctrl + +# add ton-http-api.yml to COMPOSE_FILE to enable +TON_API_HTTP_PORT=8081 +TON_API_LOGS_LEVEL=INFO +TON_API_GET_METHODS_ENABLED=1 +TON_API_JSON_RPC_ENABLED=1 +TON_API_WEBSERVERS_WORKERS=1 +TON_API_CACHE_ENABLED=0 +TON_API_ROOT_PATH=/ +TON_API_TONLIB_PARALLEL_REQUESTS_PER_LITESERVER=50 +TON_API_TONLIB_REQUEST_TIMEOUT=10 +TON_API_LOGS_JSONIFY=0 +TON_API_GUNICORN_FLAGS= + +SCRIPT_TAG= + +# do not adjust +ENV_VERSION=1 diff --git a/default.env.example b/default.env.example deleted file mode 100644 index 47e2064..0000000 --- a/default.env.example +++ /dev/null @@ -1,25 +0,0 @@ -# The settings are in .env, use "nano .env". Don't edit default.env itself. -COMPOSE_FILE=#Some default yml files here - -# Further variables such as RPC, WS ports, P2P ports, NETWORk, SNAPSHOT, LOG_LEVEL, &c - -# Secure web proxy - advanced use, please see instructions -DOMAIN=example.com -RPC_HOST= -RPC_LB=-lb -WS_HOST= -WS_LB=ws-lb - -# IP of the host you want to use in Docker (in case host has several IPs) -HOST_IP= -# IP address to use when host-mapping a port through *-shared.yml. Set this to 127.0.0.1 to restrict the share to localhost -SHARE_IP= - -# External Docker network if using ext-network.yml -DOCKER_EXT_NETWORK=traefik_default - -# Set a Github tag here to pin the script to a version. -SCRIPT_TAG= - -# Used by script update - please do not adjust -ENV_VERSION=1 diff --git a/ethd b/ethd index 0cfc41c..3067a5f 100755 --- a/ethd +++ b/ethd @@ -43,29 +43,23 @@ __free_space=0 __docker_dir="/var/lib/docker" -# Also adjust version() and __prep_conffiles for your chain, look for the word "Adjust" below -# If you are using an init container, adjust start() - version() { # script version grep "^This is" README.md echo __var="COMPOSE_FILE" __get_value_from_env "${__var}" "${__env_file}" "__value" -# Client versions -# Adjust for your clients and how to check their version -# Multiple clients are in multiple case statements. Mutually exclusive clients are in one case statement -# Avoid the use of ;;& +# client versions case "${__value}" in - *x.yml* ) - __docompose exec x-geth x-geth version + *ton.yml* ) + echo "TON_DOCKER_TAG: $(grep -E '^TON_DOCKER_TAG=' "${__env_file}" | cut -d '=' -f2)" + __docompose exec ton validator-engine -V || true ;; esac } __prep_conffiles() { - # Adjust - this is where sample files would be copied to config files that are meant to be bind-mounted return } @@ -117,10 +111,8 @@ __env_migrate() { if [ "${__debug}" -eq 1 ]; then # Find any values in default.env that contain dashes __error=0 while IFS= read -r __line; do - # Skip blank lines and comments [[ -z "${__line}" || "${__line}" =~ ^# ]] && continue - # Warn on dash-containing variable names if [[ "${__line}" =~ ^([A-Za-z0-9_-]+)= ]]; then __varname="${BASH_REMATCH[1]}" if [[ "${__varname}" = *-* ]]; then @@ -131,7 +123,7 @@ __env_migrate() { continue fi else - continue # Doesn't match variable assignment format + continue fi done < "./default.env" if [ "${__error}" -gt 0 ]; then @@ -340,7 +332,6 @@ __update_value_in_env() { awk_exe="awk" fi - # Escape backslashes for safety escaped_value=$(printf '%s' "${new_value}" | sed 's/\\/\\\\/g') # Check if the variable already exists in the .env file @@ -1037,6 +1028,11 @@ cmd() { } +check-sync() { + bash scripts/check-sync.sh "$@" || true +} + + terminate() { # Assume project name and volume are delimited by _ and there is no _ in the volume name. Done to avoid catching project-2 while looking at project if [ -z "$(__dodocker volume ls -q -f "name=^$(basename "$(realpath .)" | tr '[:upper:]' '[:lower:]')_[^_]+$")" ]; then @@ -1136,6 +1132,8 @@ __full_help() { echo " shows logs" echo " cmd " echo " executes an arbitrary Docker Compose command. Use \"cmd help\" to list them" + echo " check-sync" + echo " call the check sync script to check the sync status of the node" echo " terminate" echo " stops the ${__app_name} and destroys all data stores" echo " space" @@ -1237,7 +1235,7 @@ if ! __docompose --help >/dev/null 2>&1; then fi case "$__command" in - help|update|up|start|down|stop|restart|version|logs|cmd|terminate|space) + help|update|up|start|down|stop|restart|version|logs|cmd|check-sync|terminate|space) $__command "$@";; *) echo "Unrecognized command $__command" diff --git a/init/Dockerfile b/init/Dockerfile new file mode 100644 index 0000000..de4e173 --- /dev/null +++ b/init/Dockerfile @@ -0,0 +1,10 @@ +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates bash curl pv plzip tar aria2 \ + && rm -rf /var/lib/apt/lists/* + +COPY ./fetch-snapshot.sh /usr/local/bin/fetch-snapshot.sh +RUN chmod 755 /usr/local/bin/fetch-snapshot.sh + +ENTRYPOINT ["/usr/local/bin/fetch-snapshot.sh"] diff --git a/init/fetch-snapshot.sh b/init/fetch-snapshot.sh new file mode 100755 index 0000000..ef52021 --- /dev/null +++ b/init/fetch-snapshot.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +set -euo pipefail + +TON_WORK_DIR="/var/ton-work" +DB_DIR="${TON_WORK_DIR}/db" +GLOBAL_CONFIG_URL="${GLOBAL_CONFIG_URL:-https://ton.org/global.config.json}" +SNAPSHOT="${SNAPSHOT:-}" + +mkdir -p "${DB_DIR}" + +if [[ -z "${SNAPSHOT}" ]]; then + echo "[ton-init] SNAPSHOT not set; skipping snapshot download (will sync from network)." + touch "${DB_DIR}/dump_done" + exit 0 +fi + +if [[ -f "${DB_DIR}/dump_done" ]]; then + echo "[ton-init] Snapshot already present (${DB_DIR}/dump_done)." + exit 0 +fi + +# parse snapshot: full url or dump name +if [[ "${SNAPSHOT}" == http* ]]; then + URL_DUMP="${SNAPSHOT}" + DUMP_BASE_URL="${SNAPSHOT%/dumps/*}" + DUMP_NAME="${SNAPSHOT##*/}" + DUMP_NAME="${DUMP_NAME%.tar.lz}" +else + DUMP_BASE_URL="https://dump.ton.org" + DUMP_NAME="${SNAPSHOT}" + URL_DUMP="${DUMP_BASE_URL}/dumps/${DUMP_NAME}.tar.lz" +fi + +URL_SIZE="${DUMP_BASE_URL}/dumps/${DUMP_NAME}.tar.size.archive.txt" + +echo "[ton-init] Snapshot: ${DUMP_NAME}" +echo "[ton-init] Size URL: ${URL_SIZE}" +echo "[ton-init] Dump URL: ${URL_DUMP}" + +DUMPSIZE="" +if aria2c --max-tries=5 --retry-wait=5 --console-log-level=error --summary-interval=0 --allow-overwrite=true -d /tmp -o dump_size.txt "${URL_SIZE}" >/dev/null 2>&1; then + DUMPSIZE="$(cat /tmp/dump_size.txt)" + rm -f /tmp/dump_size.txt +fi +if [[ -z "${DUMPSIZE}" ]]; then + echo "[ton-init] Could not fetch dump size from ${URL_SIZE}" + exit 1 +fi + +DISKSPACE="$(df -B1 --output=avail "${TON_WORK_DIR}" | tail -n1 | tr -d ' ')" +NEEDSPACE="$(( 3 * DUMPSIZE ))" + +echo "[ton-init] Available bytes: ${DISKSPACE}" +echo "[ton-init] Required bytes : ${NEEDSPACE}" + +if (( DISKSPACE <= NEEDSPACE )); then + echo "[ton-init] Not enough free space. Need at least ${NEEDSPACE} bytes free on ${TON_WORK_DIR}." + exit 1 +fi + +THREADS="$(nproc || echo 4)" +echo "[ton-init] Using ${THREADS} threads for plzip" + +aria2c \ + --max-tries=10 \ + --retry-wait=5 \ + --max-connection-per-server=4 \ + --split=4 \ + --min-split-size=10M \ + --console-log-level=warn \ + --summary-interval=10 \ + --allow-overwrite=true \ + -d /tmp \ + -o dump.tar.lz \ + "${URL_DUMP}" + +echo "[ton-init] Download complete. Extracting..." +pv /tmp/dump.tar.lz | plzip -d -n"${THREADS}" | tar -xC "${DB_DIR}" +rm -f /tmp/dump.tar.lz + +mkdir -p "${DB_DIR}/static" "${DB_DIR}/import" +touch "${DB_DIR}/dump_done" + +chown -R 1000:1000 "${TON_WORK_DIR}" || true + +echo "[ton-init] Dump download + extract complete." diff --git a/scripts/check-sync.sh b/scripts/check-sync.sh new file mode 100755 index 0000000..69a3bd6 --- /dev/null +++ b/scripts/check-sync.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +set -euo pipefail + +DOCKER_SERVICE="${DOCKER_SERVICE:-ton}" +CONTAINER="${CONTAINER:-}" + +if [[ -z "$CONTAINER" ]]; then + if docker compose version >/dev/null 2>&1; then + CONTAINER="$(docker compose ps -q "$DOCKER_SERVICE" 2>/dev/null | head -n 1)" + if [[ -n "$CONTAINER" ]]; then + CONTAINER="$(docker inspect --format '{{.Name}}' "$CONTAINER" 2>/dev/null | sed 's/^\///')" + fi + fi + + if [[ -z "$CONTAINER" ]]; then + CONTAINER="$(docker ps --filter "label=com.docker.compose.service=${DOCKER_SERVICE}" --format '{{.Names}}' | head -1)" + fi +fi + +if [[ -z "$CONTAINER" ]]; then + echo "ERROR: ${DOCKER_SERVICE} service container is not running" + exit 2 +fi + +STATUS_OUTPUT=$(docker exec "$CONTAINER" bash -c "echo 'status' | /usr/bin/mytonctrl 2>/dev/null" || echo "ERROR") + +if [[ "${STATUS_OUTPUT}" == "ERROR" ]]; then + echo "ERROR: Could not get status from mytonctrl" + exit 2 +fi + +if echo "${STATUS_OUTPUT}" | grep -qi "synchronization complete\|in sync"; then + echo "Synced" + exit 0 +fi + +# check local validator status +if echo "${STATUS_OUTPUT}" | grep -q "Local validator status"; then + if echo "${STATUS_OUTPUT}" | grep -q "Local validator initial sync status"; then + SECONDS_BEHIND=$(echo "${STATUS_OUTPUT}" | grep "Local validator initial sync status" | grep -oE '[0-9]+ s ago' | tail -1 | grep -oE '[0-9]+') + if [ -n "${SECONDS_BEHIND}" ]; then + if [ "${SECONDS_BEHIND}" -le 60 ]; then + echo "Synced (${SECONDS_BEHIND}s behind)" + exit 0 + else + DAYS_BEHIND=$((SECONDS_BEHIND / 86400)) + HOURS_BEHIND=$(((SECONDS_BEHIND % 86400) / 3600)) + MINUTES_BEHIND=$(((SECONDS_BEHIND % 3600) / 60)) + echo "Syncing: ${SECONDS_BEHIND}s (${DAYS_BEHIND}d ${HOURS_BEHIND}h ${MINUTES_BEHIND}m) behind" + exit 1 + fi + fi + fi + + # check masterchain out of sync value + if echo "${STATUS_OUTPUT}" | grep -q "Masterchain out of sync"; then + OUT_OF_SYNC=$(echo "${STATUS_OUTPUT}" | grep "Masterchain out of sync" | grep -oE '[0-9]+' | head -1) + if [ -n "${OUT_OF_SYNC}" ] && [ "${OUT_OF_SYNC}" -le 60 ]; then + echo "Synced (${OUT_OF_SYNC}s out of sync on masterchain)" + exit 0 + else + echo "Syncing: ${OUT_OF_SYNC}s out of sync on masterchain" + exit 1 + fi + fi + + # check shardchain out of sync value + if echo "${STATUS_OUTPUT}" | grep -q "Shardchain out of sync"; then + SHARD_BLOCKS=$(echo "${STATUS_OUTPUT}" | grep "Shardchain out of sync" | grep -oE '[0-9]+' | head -1) + if [ -n "${SHARD_BLOCKS}" ] && [ "${SHARD_BLOCKS}" -le 10 ]; then + echo "Synced (${SHARD_BLOCKS} blocks out of sync on shardchain)" + exit 0 + else + echo "Syncing: ${SHARD_BLOCKS} blocks out of sync on shardchain" + exit 1 + fi + fi + echo "Synced" + exit 0 +fi + +# generic sync check +if echo "${STATUS_OUTPUT}" | grep -qi "out of sync"; then + BLOCKS_BEHIND=$(echo "${STATUS_OUTPUT}" | grep -oE '[0-9]+ blocks' | head -1 | grep -oE '[0-9]+' || echo "unknown") + if [ "${BLOCKS_BEHIND}" = "0" ]; then + echo "Synced (0 blocks behind)" + exit 0 + fi + echo "Syncing: ${BLOCKS_BEHIND} blocks behind" + exit 1 +fi + +echo "Status output:" +echo "${STATUS_OUTPUT}" +echo "" +echo "Unable to determine sync status definitively. Check logs with: ./ethd logs -f ton" +exit 1 diff --git a/ton-http-api.yml b/ton-http-api.yml new file mode 100644 index 0000000..b5b99e2 --- /dev/null +++ b/ton-http-api.yml @@ -0,0 +1,73 @@ +x-logging: &logging + logging: + driver: json-file + options: + max-size: 100m + max-file: "3" + tag: '{{.ImageName}}|{{.Name}}|{{.ImageFullID}}|{{.FullID}}' + +services: + ton-http-api-config: + build: + context: ./ton-http-api + dockerfile: Dockerfile + image: ton-http-api-config:local + restart: "no" + depends_on: + ton: + condition: service_started + environment: + - TON_CONTAINER=${TON_CONTAINER:-} + volumes: + - ton-api-config:/shared-config + - /var/run/docker.sock:/var/run/docker.sock:ro + <<: *logging + + ton-http-api: + build: + context: ./ton-http-api + dockerfile: Dockerfile.api + image: ton-http-api:local + restart: unless-stopped + ports: + - ${TON_API_HTTP_PORT:-8081}:8081 + environment: + - TON_API_CACHE_ENABLED=${TON_API_CACHE_ENABLED:-0} + - TON_API_LOGS_JSONIFY=${TON_API_LOGS_JSONIFY:-0} + - TON_API_LOGS_LEVEL=${TON_API_LOGS_LEVEL:-INFO} + - TON_API_TONLIB_LITESERVER_CONFIG=/shared-config/local.config.json + - TON_API_TONLIB_KEYSTORE=/tmp/ton_keystore/ + - TON_API_TONLIB_PARALLEL_REQUESTS_PER_LITESERVER=${TON_API_TONLIB_PARALLEL_REQUESTS_PER_LITESERVER:-50} + - TON_API_TONLIB_REQUEST_TIMEOUT=${TON_API_TONLIB_REQUEST_TIMEOUT:-10} + - TON_API_GET_METHODS_ENABLED=${TON_API_GET_METHODS_ENABLED:-1} + - TON_API_JSON_RPC_ENABLED=${TON_API_JSON_RPC_ENABLED:-1} + - TON_API_ROOT_PATH=${TON_API_ROOT_PATH:-/} + - TON_API_WEBSERVERS_WORKERS=${TON_API_WEBSERVERS_WORKERS:-1} + - TON_API_GUNICORN_FLAGS=${TON_API_GUNICORN_FLAGS:-} + - TON_CONTAINER=${TON_CONTAINER:-} + volumes: + - ton-api-config:/shared-config:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./scripts:/scripts:ro + healthcheck: + test: curl -sS http://127.0.0.1:8081${TON_API_ROOT_PATH:-/}healthcheck || exit 1 + interval: 30s + timeout: 10s + retries: 3 + start_period: 24h + labels: + - traefik.enable=true + - traefik.http.routers.${TON_API_HOST:-ton-api}.service=${TON_API_HOST:-ton-api} + - traefik.http.routers.${TON_API_HOST:-ton-api}.entrypoints=websecure + - traefik.http.routers.${TON_API_HOST:-ton-api}.rule=Host(`${TON_API_HOST:-ton-api}.${DOMAIN}`) + - traefik.http.routers.${TON_API_HOST:-ton-api}.tls.certresolver=letsencrypt + - traefik.http.services.${TON_API_HOST:-ton-api}.loadbalancer.server.port=8081 + - traefik.http.services.${TON_API_HOST:-ton-api}.loadbalancer.healthcheck.path=${TON_API_ROOT_PATH:-/}healthcheck + - traefik.http.services.${TON_API_HOST:-ton-api}.loadbalancer.healthcheck.interval=30s + depends_on: + ton-http-api-config: + condition: service_completed_successfully + <<: *logging + +volumes: + ton-api-config: diff --git a/ton-http-api/Dockerfile b/ton-http-api/Dockerfile new file mode 100644 index 0000000..c9b3320 --- /dev/null +++ b/ton-http-api/Dockerfile @@ -0,0 +1,8 @@ +FROM alpine:latest + +RUN apk add --no-cache bash docker-cli curl python3 + +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] diff --git a/ton-http-api/Dockerfile.api b/ton-http-api/Dockerfile.api new file mode 100644 index 0000000..82ce82d --- /dev/null +++ b/ton-http-api/Dockerfile.api @@ -0,0 +1,13 @@ +FROM toncenter/ton-http-api:latest + +USER root + +RUN apt-get update && apt-get install -y --no-install-recommends \ + docker.io \ + curl \ + && rm -rf /var/lib/apt/lists/* + +COPY wait-for-sync.sh /wait-for-sync.sh +RUN chmod +x /wait-for-sync.sh + +ENTRYPOINT ["/bin/bash", "/wait-for-sync.sh"] diff --git a/ton-http-api/docker-entrypoint.sh b/ton-http-api/docker-entrypoint.sh new file mode 100755 index 0000000..74f1731 --- /dev/null +++ b/ton-http-api/docker-entrypoint.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +set -euo pipefail + +DOCKER_SERVICE="${DOCKER_SERVICE:-ton}" +TON_CONTAINER="${TON_CONTAINER:-}" +CONFIG_DEST="/shared-config/local.config.json" + +resolve_ton_container() { + if [[ -n "$TON_CONTAINER" ]]; then + return 0 + fi + + TON_CONTAINER="$(docker ps --filter "label=com.docker.compose.service=${DOCKER_SERVICE}" --format '{{.Names}}' | head -1)" + + if [[ -z "$TON_CONTAINER" ]]; then + if docker compose version >/dev/null 2>&1; then + local container_id + container_id="$(docker compose ps -q "$DOCKER_SERVICE" 2>/dev/null | head -n 1)" + if [[ -n "$container_id" ]]; then + TON_CONTAINER="$(docker inspect --format '{{.Name}}' "$container_id" 2>/dev/null | sed 's/^\///')" + fi + fi + fi +} + +echo "[ton-http-api-config] Waiting for TON service '${DOCKER_SERVICE}' to start..." + +for i in $(seq 1 30); do + resolve_ton_container + if [[ -n "$TON_CONTAINER" ]]; then + echo "[ton-http-api-config] TON container found: ${TON_CONTAINER}" + break + fi + echo "[ton-http-api-config] Waiting for TON container... (${i}/30)" + sleep 10 +done + +if [[ -z "$TON_CONTAINER" ]]; then + echo "[ton-http-api-config] ERROR: TON service '${DOCKER_SERVICE}' container is not running" + exit 1 +fi + +echo "[ton-http-api-config] Waiting for TON services to be ready..." + +# wait for validator and mytoncore services to start +for i in $(seq 1 20); do + if docker exec "${TON_CONTAINER}" systemctl is-active validator >/dev/null 2>&1 && \ + docker exec "${TON_CONTAINER}" systemctl is-active mytoncore >/dev/null 2>&1; then + echo "[ton-http-api-config] TON services are running" + break + fi + echo "[ton-http-api-config] Waiting for services... (${i}/20)" + sleep 10 +done + +# wait for mytonctrl to be initialized and usable +echo "[ton-http-api-config] Waiting for mytonctrl to be ready..." +for i in $(seq 1 30); do + if docker exec "${TON_CONTAINER}" bash -c "echo 'exit' | timeout 10 /usr/bin/mytonctrl" >/dev/null 2>&1; then + echo "[ton-http-api-config] mytonctrl is ready" + break + fi + echo "[ton-http-api-config] Waiting for mytonctrl initialization... (${i}/30)" + sleep 10 +done + +# check if config already exists +if docker exec "${TON_CONTAINER}" test -f /usr/bin/ton/local.config.json 2>/dev/null; then + echo "[ton-http-api-config] Config already exists, extracting..." +else + echo "[ton-http-api-config] Generating liteserver config..." + if docker exec "${TON_CONTAINER}" bash -c "echo 'installer clcf' | /usr/bin/mytonctrl" 2>&1 | tee /tmp/clcf.log | grep -i "created"; then + echo "[ton-http-api-config] Config generated successfully" + else + echo "[ton-http-api-config] Config generation output:" + cat /tmp/clcf.log || true + fi + sleep 5 +fi + +# verify and extract config +echo "[ton-http-api-config] Extracting config file..." +if docker exec "${TON_CONTAINER}" test -f /usr/bin/ton/local.config.json 2>/dev/null; then + if docker exec "${TON_CONTAINER}" cat /usr/bin/ton/local.config.json | grep -q "liteservers"; then + docker cp "${TON_CONTAINER}:/usr/bin/ton/local.config.json" "${CONFIG_DEST}" + + # get the TON container's internal IP address + TON_INTERNAL_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "${TON_CONTAINER}") + echo "[ton-http-api-config] TON container internal IP: ${TON_INTERNAL_IP}" + + # replace the public IP with internal IP in the config + # IP is stored as a signed 32-bit integer, we need to replace it + python3 << EOF +import json +import socket +import struct + +# Read the config +with open("${CONFIG_DEST}", 'r') as f: + config = json.load(f) + +# Convert internal IP to signed 32-bit integer format +ip_addr = "${TON_INTERNAL_IP}" +ip_int = struct.unpack('!i', socket.inet_aton(ip_addr))[0] + +# Update the liteserver IP +config['liteservers'][0]['ip'] = ip_int + +# Write back +with open("${CONFIG_DEST}", 'w') as f: + json.dump(config, f, indent=2) + +print(f"Updated liteserver IP to {ip_addr} ({ip_int})") +EOF + + chmod 644 "${CONFIG_DEST}" + echo "[ton-http-api-config] Config exported successfully" + echo "[ton-http-api-config] Liteserver configuration:" + cat "${CONFIG_DEST}" + exit 0 + else + echo "[ton-http-api-config] ERROR: Config file exists but is not valid JSON" + exit 1 + fi +else + echo "[ton-http-api-config] ERROR: Config file not found after generation attempt" + echo "[ton-http-api-config] Check TON container logs: docker logs ${TON_CONTAINER}" + exit 1 +fi diff --git a/ton-http-api/wait-for-sync.sh b/ton-http-api/wait-for-sync.sh new file mode 100755 index 0000000..9d86e9b --- /dev/null +++ b/ton-http-api/wait-for-sync.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -euo pipefail + +DOCKER_SERVICE="${DOCKER_SERVICE:-ton}" +TON_CONTAINER="${TON_CONTAINER:-}" +CHECK_INTERVAL=60 +MAX_WAIT=86400 + +resolve_ton_container() { + if [[ -n "$TON_CONTAINER" ]]; then + return 0 + fi + + TON_CONTAINER="$(docker ps --filter "label=com.docker.compose.service=${DOCKER_SERVICE}" --format '{{.Names}}' | head -1)" + + if [[ -z "$TON_CONTAINER" ]]; then + if docker compose version >/dev/null 2>&1; then + local container_id + container_id="$(docker compose ps -q "$DOCKER_SERVICE" 2>/dev/null | head -n 1)" + if [[ -n "$container_id" ]]; then + TON_CONTAINER="$(docker inspect --format '{{.Name}}' "$container_id" 2>/dev/null | sed 's/^\///')" + fi + fi + fi +} + +echo "[ton-http-api] Waiting for TON node to complete initial sync before starting API..." +echo "[ton-http-api] This may take several hours if syncing from scratch." +echo "[ton-http-api] You can monitor sync progress with: ./ethd check-sync" + +check_sync() { + /scripts/check-sync.sh >/dev/null 2>&1 + # exit code 0 = synced, 1 = syncing, 2 = error + return $? +} + +# wait for sync +ELAPSED=0 +while [ $ELAPSED -lt $MAX_WAIT ]; do + resolve_ton_container + + if [[ -n "$TON_CONTAINER" ]] && check_sync; then + echo "[ton-http-api] TON node is synced! Starting ton-http-api..." + break + fi + + if [[ -n "$TON_CONTAINER" ]]; then + SYNC_STATUS=$(/scripts/check-sync.sh 2>&1 || echo "Checking sync status...") + echo "[ton-http-api] ${SYNC_STATUS} (${ELAPSED}s elapsed)" + else + echo "[ton-http-api] Waiting for TON container to be ready..." + fi + + sleep $CHECK_INTERVAL + ELAPSED=$((ELAPSED + CHECK_INTERVAL)) +done + +if [ $ELAPSED -ge $MAX_WAIT ]; then + echo "[ton-http-api] WARNING: Max wait time reached. Starting anyway..." + echo "[ton-http-api] API may experience errors until sync completes." +fi + +echo "[ton-http-api] Starting gunicorn server..." +# shellcheck disable=SC2086 +exec gunicorn -k uvicorn.workers.UvicornWorker \ + -w "${TON_API_WEBSERVERS_WORKERS:-1}" \ + --bind 0.0.0.0:8081 \ + ${TON_API_GUNICORN_FLAGS} \ + pyTON.main:app diff --git a/ton-shared.yml b/ton-shared.yml new file mode 100644 index 0000000..22be3b0 --- /dev/null +++ b/ton-shared.yml @@ -0,0 +1,7 @@ +# exposes rpc ports locally instead of via traefik +# add to COMPOSE_FILE: ton.yml:ton-shared.yml +services: + ton: + ports: + - ${LITESERVER_PORT:-30003}:${LITESERVER_PORT:-30003}/tcp + - ${VALIDATOR_CONSOLE_PORT:-30002}:${VALIDATOR_CONSOLE_PORT:-30002}/tcp diff --git a/ton.yml b/ton.yml new file mode 100644 index 0000000..9470f34 --- /dev/null +++ b/ton.yml @@ -0,0 +1,70 @@ +x-logging: &logging + logging: + driver: json-file + options: + max-size: 100m + max-file: "3" + tag: '{{.ImageName}}|{{.Name}}|{{.ImageFullID}}|{{.FullID}}' + +services: + ton-init: + restart: "no" + build: + context: ./init + dockerfile: Dockerfile + image: init:ton + pull_policy: never + environment: + - SNAPSHOT=${SNAPSHOT:-} + - GLOBAL_CONFIG_URL=${GLOBAL_CONFIG_URL:-https://ton.org/global.config.json} + volumes: + - ton-work:/var/ton-work + - /etc/localtime:/etc/localtime:ro + <<: *logging + + ton: + restart: unless-stopped + tty: true + build: + context: ./ton + dockerfile: ${TON_DOCKERFILE:-Dockerfile.binary} + args: + - DOCKER_TAG=${TON_DOCKER_TAG} + - DOCKER_REPO=${TON_DOCKER_REPO} + image: ton:${TON_BRANCH:-latest} + pull_policy: never + stop_grace_period: 5m + environment: + - TON_BRANCH=${TON_BRANCH:-latest} + - PUBLIC_IP=${PUBLIC_IP:-} + - GLOBAL_CONFIG_URL=${GLOBAL_CONFIG_URL} + - MYTONCTRL_VERSION=${MYTONCTRL_VERSION:-master} + - TELEMETRY=${TELEMETRY:-true} + - IGNORE_MINIMAL_REQS=${IGNORE_MINIMAL_REQS:-true} + - MODE=${MODE:-liteserver} + - ARCHIVE_TTL=${ARCHIVE_TTL:-2592000} + - STATE_TTL=${STATE_TTL:-86400} + - VERBOSITY=${VERBOSITY:-1} + - LITESERVER_PORT=${LITESERVER_PORT:-} + - VALIDATOR_PORT=${VALIDATOR_PORT:-} + - VALIDATOR_CONSOLE_PORT=${VALIDATOR_CONSOLE_PORT:-} + volumes: + - ton-work:/var/ton-work + - mytoncore:/usr/local/bin/mytoncore + - /etc/localtime:/etc/localtime:ro + ports: + - ${VALIDATOR_PORT:-30001}:${VALIDATOR_PORT:-30001}/udp + labels: + - traefik.enable=true + - traefik.tcp.routers.${TON_HOST:-ton}.service=${TON_HOST:-ton} + - traefik.tcp.routers.${TON_HOST:-ton}.entrypoints=tonls + - traefik.tcp.routers.${TON_HOST:-ton}.rule=HostSNI(`*`) + - traefik.tcp.services.${TON_HOST:-ton}.loadbalancer.server.port=${LITESERVER_PORT:-30003} + depends_on: + ton-init: + condition: service_completed_successfully + <<: *logging + +volumes: + ton-work: + mytoncore: diff --git a/ton/Dockerfile.binary b/ton/Dockerfile.binary new file mode 100644 index 0000000..ab328e6 --- /dev/null +++ b/ton/Dockerfile.binary @@ -0,0 +1,21 @@ +ARG DOCKER_REPO=ghcr.io/ton-blockchain/ton-docker-ctrl +ARG DOCKER_TAG=latest + +FROM ${DOCKER_REPO}:${DOCKER_TAG} + +USER root + +RUN if command -v apt-get >/dev/null 2>&1; then \ + apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates tzdata bash curl wget jq \ + && rm -rf /var/lib/apt/lists/*; \ + elif command -v apk >/dev/null 2>&1; then \ + apk add --no-cache ca-certificates tzdata bash curl wget jq; \ + else \ + echo "No known package manager found; skipping OS packages" >&2; \ + fi + +COPY ./docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod -R 755 /usr/local/bin/docker-entrypoint.sh + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] diff --git a/ton/docker-entrypoint.sh b/ton/docker-entrypoint.sh new file mode 100755 index 0000000..dd81153 --- /dev/null +++ b/ton/docker-entrypoint.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +# auto-detect public ip +if [[ -z "${PUBLIC_IP:-}" ]]; then + echo "[ton] PUBLIC_IP not set, attempting auto-detection..." + PUBLIC_IP=$(curl -4 -s --max-time 10 http://ifconfig.me || curl -4 -s --max-time 10 http://icanhazip.com || curl -4 -s --max-time 10 http://ipinfo.io/ip || echo "") + if [[ -z "${PUBLIC_IP}" ]]; then + echo "[ton] ERROR: Could not auto-detect public IP. Please set PUBLIC_IP environment variable manually." + exit 1 + fi + echo "[ton] Detected public IP: ${PUBLIC_IP}" +else + echo "[ton] Using provided PUBLIC_IP: ${PUBLIC_IP}" +fi +export PUBLIC_IP + +TON_WORK_DIR="/var/ton-work" + +export VALIDATOR_PORT="${VALIDATOR_PORT:-}" +export LITESERVER_PORT="${LITESERVER_PORT:-}" +export VALIDATOR_CONSOLE_PORT="${VALIDATOR_CONSOLE_PORT:-}" + +if [[ -f "${TON_WORK_DIR}/db/mtc_done" ]]; then + if [[ ! -f /etc/systemd/system/validator.service || ! -f /etc/systemd/system/mytoncore.service ]]; then + echo "[ton] mtc_done exists but systemd unit files are missing; forcing reinstall" + rm -f "${TON_WORK_DIR}/db/mtc_done" + rm -rf /usr/src/mytonctrl || true + fi +fi + +exec /scripts/entrypoint.sh "$@"