Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions scripts/resolver/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
189 changes: 189 additions & 0 deletions scripts/resolver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,192 @@ Then `docker compose down -v && docker compose up -d` (the `-v` wipes state so N
- Ports 30303/9000 are p2p — open on your firewall for sync.
- `jwt.hex` is generated on first run by the `jwt-init` service and shared between Reth and Nimbus via the `jwt` volume.
- To wipe state and re-sync: `docker compose down -v`.

## SNRC resolver REST API (`snrc-resolve.py`)

The companion script `snrc-resolve.py` exposes the SimpleX Namespace
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.

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
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

| TLD | Network | ENSRegistry address |
|------------|------------------|----------------------------------------------|
| `.testing` | Ethereum mainnet | `0x03f438da0bd44da3c6c1d0392f8ba183b8b3a7a6` |
| `.simplex` | — (not deployed) | — |

Each TLD is an independent ENS-shaped deployment with its own
`ENSRegistry`. The resolver dispatches by the queried name's rightmost
label, so a single instance can serve both TLDs concurrently once
`.simplex` launches.

### Running

With Reth bound to `127.0.0.1:8545` (the default Quickstart layout
above), no env vars are required — the script defaults to that RPC and
to the mainnet `.testing` registry:

```sh
./scripts/resolver/snrc-resolve.py
```

Output on startup:

```
snrc-resolve listening on 0.0.0.0:8000
RPC = http://127.0.0.1:8545
Registries:
.testing = 0x03f438da0bd44da3c6c1d0392f8ba183b8b3a7a6
.simplex = (not configured)
GET /resolve/<name> GET /health
```

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
multicoin record populated (useful as a smoke-test target):

```sh
curl -s http://127.0.0.1:8000/resolve/foobar.testing | jq .
```

```json
{
"name": "foobar.testing",
"nickname": "Foo",
"website": "https://foo.bar",
"location": "",
"simplexContact": [
"https://smp16.simplex.im/a#Q_F00BA7",
"https://smp11.simplex.im/a#Q_F00BA8"
],
"simplexChannel": [],
"eth": null,
"btc": "bc1qpzht4wp64yg7z6sgl07vvrnepyux740juynfcn",
"xmr": "4ANzdVJFxLtCKcBgNGkFSEA41zJFgrTX93LWt9UR6xpg7YNCsdrSV817cw2xKT8NXeS5euBBqTApS2u8kRTxMhyiDGN3Qgt",
"dot": "139GgyEsXDyGLhmhBTPmDmGCyTvTVuLad3YjHax2PWLK6p3s",
"owner": "0xd83bb610fbad567fb5d8755ec162881e46d1fbc9",
"resolver": "0x80fa1903e70af03e79c73fb7feae2fb33aebae01"
}
```

`simplexContact` and `simplexChannel` are arrays so a name can advertise
multiple SMP servers for redundancy. Clients SHOULD try the URLs in
order; the first entry is the primary and the rest are fallbacks. The
on-chain text record stores them as a single comma-separated string
(`"url1,url2,url3"`); this resolver splits, trims whitespace, and drops
empty entries before returning.

All field names are lowercase-initial and contain no dots, so they map
directly onto Haskell record fields and can be consumed via aeson's
`Generic`-derived `FromJSON` without a key-rewriting layer. Equivalent
Haskell record:

```haskell
data SnrcRecord = SnrcRecord
{ name :: Text
, nickname :: Text
, website :: Text
, location :: Text
, simplexContact :: [Text]
, simplexChannel :: [Text]
, eth :: Maybe Text
, btc :: Maybe Text
, xmr :: Maybe Text
, dot :: Maybe Text
, owner :: Text
, resolver :: Text
} deriving (Generic, FromJSON)
```

(The on-chain text-record keys still use the ENSIP-5 dot convention —
`simplex.contact` and `simplex.channel`. Only the resolver's JSON
surface camelCases them.)

Address encoding matches each chain's canonical user-facing form:
EIP-55 mixed-case for `eth`, bech32/bech32m for `btc` segwit/taproot
(base58check for legacy P2PKH/P2SH), SS58 with Polkadot prefix 0 for
`dot`, Monero-base58 for `xmr`. Unrecognised payloads fall back to
`0x`-prefixed hex.

#### Subnames

Subnames work exactly the same. try `bar.foobar.testing`.

```sh
curl -s http://127.0.0.1:8000/health
# → {"ok": true, "rpc": "http://127.0.0.1:8545", "registries": {"testing": "0x…", "simplex": ""}}
```

### Pointing at multiple deployments

Once `.simplex` deploys, point a single resolver instance at both
registries — requests are dispatched by the rightmost label:

```sh
SNRC_REGISTRY_SIMPLEX=0x...mainnet-simplex-ENSRegistry... \
./scripts/resolver/snrc-resolve.py
```

Queries for a TLD with no registry configured return HTTP 400 with the
list of supported TLDs.

### Error responses

| Status | When |
|--------|-----------------------------------------------------------------------|
| 400 | TLD not configured (`/resolve/foo.simplex` while `.simplex` is empty) or path not a fully-qualified name |
| 404 | Name has no resolver set on the registry (`ENSRegistry.resolver(node)` is zero) |
| 502 | Upstream RPC error / unreachable (Reth not running or not synced) |
28 changes: 28 additions & 0 deletions scripts/resolver/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions scripts/resolver/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading