diff --git a/scripts/resolver/Dockerfile b/scripts/resolver/Dockerfile new file mode 100644 index 000000000..8df898aac --- /dev/null +++ b/scripts/resolver/Dockerfile @@ -0,0 +1,48 @@ +# syntax=docker/dockerfile:1.7 +# ---------- builder ---------- +# Use the official uv image (Astral) on top of a slim Python base. +# uv resolves and installs the lockfile-free pyproject.toml in seconds and +# produces a portable .venv we can copy into the runtime stage. +FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS builder + +ENV UV_LINK_MODE=copy \ + UV_COMPILE_BYTECODE=1 \ + UV_PYTHON_DOWNLOADS=never \ + UV_NO_PROGRESS=1 + +WORKDIR /app + +# Install deps first (separate layer) — script edits won't bust this cache. +COPY pyproject.toml ./ +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --no-dev --no-install-project + +# Script is added after the dep layer for cache friendliness. +COPY snrc-resolve.py ./ + +# ---------- runtime ---------- +# Slim runtime — only the venv + script. No uv, no apt. +FROM python:3.13-slim AS runtime + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PATH="/app/.venv/bin:$PATH" + +# Non-root user (matches resolver privacy posture: it has no need for root). +RUN groupadd --system --gid 10001 snrc && \ + useradd --system --uid 10001 --gid snrc --no-create-home --shell /usr/sbin/nologin snrc + +WORKDIR /app +COPY --from=builder --chown=snrc:snrc /app /app + +USER snrc:snrc + +EXPOSE 8000 + +# Liveness check hits the script's own /health route. ThreadingHTTPServer is +# fast enough that 3s is generous for a localhost probe; restart if it stops +# responding entirely. +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD ["python", "-c", "import urllib.request, sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=3).status == 200 else 1)"] + +ENTRYPOINT ["python", "snrc-resolve.py"] diff --git a/scripts/resolver/README.md b/scripts/resolver/README.md index f80923814..49d1a4e89 100644 --- a/scripts/resolver/README.md +++ b/scripts/resolver/README.md @@ -52,10 +52,21 @@ Registry (SNRC) over a small JSON HTTP API. It talks to the same local Reth + Nimbus stack described above (set `NETWORK=mainnet` in `.env`), reading the SNRC contracts directly on Ethereum mainnet. -Install the only runtime dependency (same as `ens-lookup.py`): +Dependencies are declared inline (PEP 723) at the top of `snrc-resolve.py` +and in a sibling `pyproject.toml`. The simplest local run uses +[`uv`](https://docs.astral.sh/uv/): ```sh -pip install --break-system-packages 'eth-hash[pycryptodome]' +uv run scripts/resolver/snrc-resolve.py +``` + +`uv` resolves and caches `eth-hash[pycryptodome]` on first run. No +virtualenv juggling, no `--break-system-packages`. If you'd rather +manage Python deps yourself: + +```sh +pip install 'eth-hash[pycryptodome]>=0.7' +python scripts/resolver/snrc-resolve.py ``` ### Deployed registries @@ -93,6 +104,38 @@ snrc-resolve listening on 0.0.0.0:8000 Override the listen port or bind address with `SNRC_PORT` / `SNRC_BIND`. +### Running in Docker + +The compose file ships a `resolver` service alongside reth and nimbus. +`docker compose up -d` builds the image from `Dockerfile` (multi-stage, +non-root, `uv`-based) and exposes the API on `127.0.0.1:8000`: + +```sh +docker compose up -d resolver +docker compose logs -f resolver +curl -s http://127.0.0.1:8000/health +``` + +The container points `SNRC_RPC` at `http://reth:8545` (the compose-internal +DNS name) so the resolver and reth share the bridge network without +exposing reth's RPC to the host beyond loopback. + +To change the host-side port, edit the LEFT side of the port mapping in +`docker-compose.yml`: + +```yaml +resolver: + ports: + - "127.0.0.1:8000:8000" # host:container +``` + +The registry address defaults to mainnet `.testing` — to override (Holesky, +a private deployment, or future `.simplex`), uncomment and set the values +in `docker-compose.yml` under the resolver service's `environment:` block. + +The image declares a `HEALTHCHECK` against `/health`; `docker compose ps` +will mark the service `(healthy)` once reth is queryable. + ### Resolving a name `foobar.testing` is registered on mainnet with every text and diff --git a/scripts/resolver/docker-compose.yml b/scripts/resolver/docker-compose.yml index 15fee9029..06f0d27d8 100644 --- a/scripts/resolver/docker-compose.yml +++ b/scripts/resolver/docker-compose.yml @@ -127,6 +127,34 @@ services: --nat=${NAT:-any} restart: unless-stopped + # SNRC REST resolver. Talks to reth on the compose-internal network, + # exposes /resolve and /health on 127.0.0.1:8000 by default. The + # smp-server points its [NAMES] resolver_endpoint at this URL. + # To change the host port, edit the LEFT side of the port mapping below. + resolver: + build: + context: . + dockerfile: Dockerfile + depends_on: + # reth's `service_started` is sufficient — the resolver tolerates + # eth_call failures gracefully (returns 502 with the error body), so + # starting before reth has finished snapshot replay just yields a few + # 502s until the chain is queryable. The upstream reth image doesn't + # ship a HEALTHCHECK, so we can't gate on healthy. + reth: + condition: service_started + environment: + SNRC_RPC: http://reth:8545 + SNRC_BIND: 0.0.0.0 + # Registry addresses cascade through the script's own defaults + # (mainnet `.testing`; `.simplex` unconfigured). Set explicitly here + # only if you're deploying against a different network or contract. + # SNRC_REGISTRY_TESTING: 0x... + # SNRC_REGISTRY_SIMPLEX: 0x... + ports: + - "127.0.0.1:8000:8000" + restart: unless-stopped + volumes: reth-data: nimbus-data: diff --git a/scripts/resolver/pyproject.toml b/scripts/resolver/pyproject.toml new file mode 100644 index 000000000..cba4a289b --- /dev/null +++ b/scripts/resolver/pyproject.toml @@ -0,0 +1,13 @@ +[project] +name = "snrc-resolve" +version = "0.1.0" +description = "SimpleX Namespace (SNRC) resolver — REST API over ENS-shaped Ethereum registries" +readme = "README.md" +requires-python = ">=3.11" +license = "AGPL-3.0-only" +dependencies = [ + "eth-hash[pycryptodome]>=0.7", +] + +[tool.uv] +package = false diff --git a/scripts/resolver/snrc-resolve.py b/scripts/resolver/snrc-resolve.py index 7614958c9..469d9f22c 100755 --- a/scripts/resolver/snrc-resolve.py +++ b/scripts/resolver/snrc-resolve.py @@ -1,4 +1,10 @@ #!/usr/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "eth-hash[pycryptodome]>=0.7", +# ] +# /// """SimpleX Namespace (SNRC) resolver — REST API. Resolves names like `alice.testing` / `bob.simplex` against the SNRC @@ -33,8 +39,9 @@ Environment: Each TLD is a separate SNRC deployment with its own ENSRegistry; the resolver dispatches by the queried name's rightmost label. -Same dependency surface as ens-lookup.py: - pip install --break-system-packages 'eth-hash[pycryptodome]' +Dependencies are declared inline (PEP 723) at the top of this file. Run with: + uv run snrc-resolve.py # uv resolves & caches deps; one-line setup + python snrc-resolve.py # if eth-hash[pycryptodome] is already installed Addresses are returned in each chain's canonical presentation: eth EIP-55 mixed-case checksummed hex (e.g. 0xEa65A0…1572) @@ -62,11 +69,13 @@ PORT = int(os.environ.get("SNRC_PORT", "8000")) # Each TLD is its own SNRC deployment with its own ENSRegistry. Dispatch # happens on the rightmost label of the queried name. Empty / unset means # "not deployed" — requests for that TLD return 400 with a clear error. +# `... or "..."` makes the script's defaults the single source of truth: +# unset AND empty-string both fall through to the literal. docker-compose +# can therefore pass `SNRC_REGISTRY_TESTING=${SNRC_REGISTRY_TESTING:-}` +# without duplicating the registry address. REGISTRIES = { - "testing": os.environ.get( - "SNRC_REGISTRY_TESTING", - "0x03f438da0bd44da3c6c1d0392f8ba183b8b3a7a6", # mainnet .testing - ), + "testing": os.environ.get("SNRC_REGISTRY_TESTING", "") + or "0x03f438da0bd44da3c6c1d0392f8ba183b8b3a7a6", # mainnet .testing "simplex": os.environ.get("SNRC_REGISTRY_SIMPLEX", ""), # not deployed yet }