resolver: cleanup (#1817)

* resolver: cleanup

* resolver: update .testing registry address
This commit is contained in:
sh authored and GitHub committed 2026-06-23 16:30:44 +01:00
1 parent 98391fd677
commit 2dff11a808
9 files changed
+133 -220

No files matched your search

+20 -17
View File
@@ -1,19 +1,22 @@
# Ethereum network: holesky (default test instance) or mainnet
NETWORK=holesky
# ============================================================================
# Required settings — the stack will not start without these.
# ============================================================================
# Checkpoint sync URL — used ONCE on first sync. Must expose the heavy
# /eth/v2/debug/beacon/states/finalized endpoint (most generic beacon APIs
# do not — use a dedicated checkpoint-sync provider).
# Community list: https://eth-clients.github.io/checkpoint-sync-endpoints/
#
# For mainnet, switch to one of:
# https://beaconstate.info
# https://sync-mainnet.beaconcha.in
# https://mainnet-checkpoint-sync.attestant.io
TRUSTED_NODE_URL=https://checkpoint-sync.holesky.ethpandaops.io
# Ethereum network: mainnet (the SNRC `.testing` contracts live on mainnet)
# or holesky (test). Mainnet full sync needs ~1 day and ~1.2 TB NVMe.
NETWORK=mainnet
# Nimbus NAT mode. Default "any" tries UPnP/PMP/auto-detect (often fails on cloud).
# For a stable public node, set explicit external IP:
# NAT=extip:1.2.3.4
# Find your server's public IPv4 with: curl -s ifconfig.me
NAT=any
# Beacon checkpoint-sync URL — used ONCE on first sync. Must expose the heavy
# /eth/v2/debug/beacon/states/finalized endpoint (generic beacon APIs do not;
# use a dedicated checkpoint provider). List: https://eth-clients.github.io/checkpoint-sync-endpoints/
# mainnet: https://mainnet-checkpoint-sync.attestant.io (also beaconstate.info, sync-mainnet.beaconcha.in)
# holesky: https://checkpoint-sync.holesky.ethpandaops.io
TRUSTED_NODE_URL=https://mainnet-checkpoint-sync.attestant.io
# ============================================================================
# Optional overrides — sensible defaults are baked into docker-compose.yml,
# so leave these commented unless you need to change them.
# ============================================================================
# Nimbus NAT (default: any). For a stable public node set an explicit IP:
# NAT=extip:1.2.3.4 # your public IPv4: curl -s ifconfig.me
+109 -198
View File
@@ -1,235 +1,146 @@
# Ethereum stack for SMP names role
# Self-hosted SNRC stack
Reth (execution) + Nimbus (consensus) on Holesky testnet by default.
One `docker compose up` runs the self-hosted SimpleX Namespace (SNRC) backend
against **Ethereum mainnet** (where the `.testing` contracts live):
## Quickstart
| # | Component | What it does |
|---|---|---|
| 1 | **reth + nimbus** | self-hosted Ethereum node (`--minimal` — enough for the resolver's `eth_call` at chain head) |
| 2 | **resolver** | the REST resolver the smp-server's `[NAMES]` role queries (`snrc-resolve.py`) |
## Requirements
- **Docker** + Compose v2.
- **≥ 300 GB NVMe SSD** for `reth --minimal` (~260 GB on mainnet; TLC, not QLC
— QLC stalls during sync) + **32 GB RAM**, fast multi-core CPU.
- **~1 day** for the initial reth sync. The resolver returns errors until reth
has caught up — that's expected.
- Firewall: open p2p ports `30303` (tcp/udp) and `9000` (tcp/udp).
## 1. Configure
Edit `.env` — the defaults work as-is; override only if needed:
```sh
cd scripts/docker/reth-nimbus
docker compose up -d
docker compose logs -f reth nimbus
NETWORK=mainnet # default
TRUSTED_NODE_URL=https://mainnet-checkpoint-sync.attestant.io # default
```
Sync takes a few hours on Holesky, ~1 day on mainnet. When synced:
Everything else (NAT) has a working default baked into `docker-compose.yml`;
uncomment the hints in `.env` only to override.
## 2. Run
```sh
cd scripts/resolver
docker compose up -d
docker compose logs -f reth resolver
```
`depends_on` handles ordering automatically (start node → start resolver).
## 3. Wait for the node to sync
```sh
docker compose logs --tail=20 reth
```
This is the long pole (~1 day on mainnet). Until reth is synced the resolver
returns `502`.
## Verify
Run these once the stack is up (the node-dependent ones pass after sync):
**1. reth is reachable and reporting a block:**
```sh
curl -s -X POST http://127.0.0.1:8545 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' | jq
```
Point smp-server: `[NAMES] ethereum_endpoint: http://127.0.0.1:8545`.
## How the trust bootstrap works
- **Reth** holds Ethereum state and runs the EVM. It does not decide which fork is canonical.
- **Nimbus** follows the beacon chain and tells Reth which payloads to execute.
- Nimbus needs **one trusted starting point** to break the chicken-and-egg of peer-claims. `--trusted-node-url` fetches that checkpoint once from a public beacon API; from that point on every block is verified locally against the validator set.
- The default `TRUSTED_NODE_URL` is publicnode.com (no API key, no rate limits). Replace with any beacon API you trust — only consulted once on first sync.
## Switching to mainnet
Edit `.env`:
```
NETWORK=mainnet
TRUSTED_NODE_URL=https://ethereum-beacon-api.publicnode.com
**2. resolver is healthy:**
```sh
curl -s http://127.0.0.1:8000/health | jq
# → {"ok": true, "rpc": "http://reth:8545", "registries": {"testing": "0x…", "simplex": ""}}
```
Then `docker compose down -v && docker compose up -d` (the `-v` wipes state so Nimbus re-bootstraps against the new network). Reth on mainnet needs ~260 GB pruned NVMe.
**3. resolver resolves a live name** (`foobar.testing` is a populated test name):
```sh
curl -s http://127.0.0.1:8000/resolve/foobar.testing | jq
# → {"name":"foobar.testing","nickname":"Foo","simplexContact":["https://smp16.simplex.im/a#…"], … }
```
## Notes
**Wire your smp-server:** in its `[NAMES]` section set
`resolver_endpoint: http://127.0.0.1:8000` (no auth needed for loopback).
- Reth's RPC is bound to `127.0.0.1:8545` only. For remote access (multiple smp-server hosts → one Reth), put Caddy + Let's Encrypt + Basic auth in front — see `plans/20260522_01_smp_public_namespaces.md` §"Operator deployment".
- 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`.
## Ports (all loopback unless noted)
## SNRC resolver REST API (`snrc-resolve.py`)
| Service | Host | Purpose |
|---|---|---|
| reth JSON-RPC | `127.0.0.1:8545` | smp-server RPC |
| reth p2p | `:30303` tcp/udp | Ethereum sync (open on firewall) |
| nimbus p2p | `:9000` tcp/udp | beacon sync (open on firewall) |
| nimbus REST | `127.0.0.1:5052` | beacon API |
| **resolver** | `127.0.0.1:8000` | SNRC REST (`/resolve`, `/health`) |
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.
## Caveats
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/):
- **All images track `:latest`** (reth, nimbus) — you get upstream fixes on each
`docker compose pull`; re-run the verify checks after pulling.
- All ports bind to loopback; expose only what you put behind a TLS reverse proxy.
## Teardown
```sh
uv run scripts/resolver/snrc-resolve.py
docker compose down # stop, keep all state
docker compose down -v # also wipe volumes → full re-sync
```
`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:
`down -v` wipes the chain data (full re-sync on the next `up`).
---
## Resolver API reference
The resolver (`snrc-resolve.py`, host `127.0.0.1:8000`) is also runnable
standalone for local dev (no Docker), via [`uv`](https://docs.astral.sh/uv/):
```sh
pip install 'eth-hash[pycryptodome]>=0.7'
python scripts/resolver/snrc-resolve.py
uv run scripts/resolver/service/snrc-resolve.py # defaults to local reth + mainnet .testing
```
### Deployed registries
### Response shape
| 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
```jsonc
{
"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"
],
"nickname": "Foo", "website": "https://foo.bar", "location": "",
"simplexContact": ["https://smp16.simplex.im/a#…", "https://smp11…"], // primary first, fallbacks after
"simplexChannel": [],
"eth": null,
"btc": "bc1qpzht4wp64yg7z6sgl07vvrnepyux740juynfcn",
"xmr": "4ANzdVJFxLtCKcBgNGkFSEA41zJFgrTX93LWt9UR6xpg7YNCsdrSV817cw2xKT8NXeS5euBBqTApS2u8kRTxMhyiDGN3Qgt",
"dot": "139GgyEsXDyGLhmhBTPmDmGCyTvTVuLad3YjHax2PWLK6p3s",
"owner": "0xd83bb610fbad567fb5d8755ec162881e46d1fbc9",
"resolver": "0x80fa1903e70af03e79c73fb7feae2fb33aebae01"
"eth": null, "btc": "bc1q…", "xmr": "4ANz…", "dot": "139G…",
"owner": "0xd83b…", "resolver": "0x80fa…"
}
```
`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.
`simplexContact`/`simplexChannel` are arrays (a name can advertise multiple SMP
servers; clients try them in order). On-chain they're a single comma-separated
text record; the resolver splits/trims/drops-empties. Address encodings are
canonical per chain (EIP-55 / bech32 / SS58 / Monero-base58). Subnames work
identically (`bar.foobar.testing`).
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:
### Status codes
```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)
```
| Status | Meaning |
|---|---|
| 200 | resolved |
| 400 | TLD not configured, or not a fully-qualified name |
| 404 | name has no resolver set on the registry |
| 502 | upstream RPC error / reth not synced |
(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.)
### Configuring registries
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) |
Defaults to mainnet `.testing` (`0x03f438…`); `.simplex` is unset until
deployed. Override per TLD via env on the `resolver` service in
`docker-compose.yml` (`SNRC_REGISTRY_TESTING` / `SNRC_REGISTRY_SIMPLEX`), or as
env vars for the standalone script.
+1 -2
View File
@@ -100,7 +100,6 @@ services:
--http.addr 0.0.0.0 --http.port 8545
--http.api eth,net
--rpc.gascap 50000000
--rpc.max-response-size 5
--port 30303
--discovery.port 30303
restart: unless-stopped
@@ -133,7 +132,7 @@ services:
# To change the host port, edit the LEFT side of the port mapping below.
resolver:
build:
context: .
context: ./service
dockerfile: Dockerfile
depends_on:
# reth's `service_started` is sufficient — the resolver tolerates
File renamed without changes.
File renamed without changes.
@@ -35,7 +35,7 @@ Environment:
SNRC_RPC JSON-RPC endpoint (default: http://127.0.0.1:8545)
SNRC_REGISTRY_TESTING ENSRegistry for the .testing deployment
(default: mainnet,
0x03f438da0bd44da3c6c1d0392f8ba183b8b3a7a6)
0x58fc46996d975c57883564648bda5206d1a0102b)
SNRC_REGISTRY_SIMPLEX ENSRegistry for the .simplex deployment
(default: empty — TLD not yet deployed)
SNRC_PORT Listen port (default: 8000)
@@ -80,7 +80,7 @@ PORT = int(os.environ.get("SNRC_PORT", "8000"))
# without duplicating the registry address.
REGISTRIES = {
"testing": os.environ.get("SNRC_REGISTRY_TESTING", "")
or "0x03f438da0bd44da3c6c1d0392f8ba183b8b3a7a6", # mainnet .testing
or "0x58fc46996d975c57883564648bda5206d1a0102b", # mainnet .testing
"simplex": os.environ.get("SNRC_REGISTRY_SIMPLEX", ""), # not deployed yet
}
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Unit tests for snrc-resolve helpers.
Run with `python3 -m unittest scripts/resolver/test_snrc_resolve.py`.
Run with `python3 -m unittest scripts/resolver/service/test_snrc_resolve.py`.
"""
import importlib.util
File renamed without changes.
File renamed without changes.