diff --git a/scripts/resolver/.env b/scripts/resolver/.env index f36893b87..3114c3365 100644 --- a/scripts/resolver/.env +++ b/scripts/resolver/.env @@ -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 diff --git a/scripts/resolver/README.md b/scripts/resolver/README.md index 78df84b9c..88fa6fde5 100644 --- a/scripts/resolver/README.md +++ b/scripts/resolver/README.md @@ -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`) | -```sh -cd scripts/docker/reth-nimbus -docker compose up -d -docker compose logs -f reth nimbus -``` +## Requirements -Sync takes a few hours on Holesky, ~1 day on mainnet. When synced: +- **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). -```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}' -``` +## 1. Configure -Point smp-server: `[NAMES] ethereum_endpoint: http://127.0.0.1:8545`. +Edit `.env` — the defaults work as-is; override only if needed: -## 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 +```sh +NETWORK=mainnet # default +TRUSTED_NODE_URL=https://mainnet-checkpoint-sync.attestant.io # default ``` -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. - -## Notes - -- 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`. - -## 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. +Everything else (NAT) has a working default baked into `docker-compose.yml`; +uncomment the hints in `.env` only to override. -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/): +## 2. Run ```sh -uv run scripts/resolver/snrc-resolve.py +cd scripts/resolver +docker compose up -d +docker compose logs -f reth resolver ``` -`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: +`depends_on` handles ordering automatically (start node → start resolver). + +## 3. Wait for the node to sync ```sh -pip install 'eth-hash[pycryptodome]>=0.7' -python scripts/resolver/snrc-resolve.py +docker compose logs --tail=20 reth ``` -### Deployed registries - -| TLD | Network | ENSRegistry address | -|------------|------------------|----------------------------------------------| -| `.testing` | Ethereum mainnet | `0x03f438da0bd44da3c6c1d0392f8ba183b8b3a7a6` | -| `.simplex` | — (not deployed) | — | +This is the long pole (~1 day on mainnet). Until reth is synced the resolver +returns `502`. -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. +## Verify -### 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: +Run these once the stack is up (the node-dependent ones pass after sync): +**1. reth is reachable and reporting a block:** ```sh -./scripts/resolver/snrc-resolve.py +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}' | jq ``` -Output on startup: - +**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": ""}} ``` -snrc-resolve listening on 0.0.0.0:8000 - RPC = http://127.0.0.1:8545 - Registries: - .testing = 0x03f438da0bd44da3c6c1d0392f8ba183b8b3a7a6 - .simplex = (not configured) - GET /resolve/ GET /health + +**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#…"], … } ``` -Override the listen port or bind address with `SNRC_PORT` / `SNRC_BIND`. +**Wire your smp-server:** in its `[NAMES]` section set +`resolver_endpoint: http://127.0.0.1:8000` (no auth needed for loopback). -### Running in Docker +## Ports (all loopback unless noted) -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`: +| 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`) | -```sh -docker compose up -d resolver -docker compose logs -f resolver -curl -s http://127.0.0.1:8000/health -``` +## Caveats -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. +- **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. -To change the host-side port, edit the LEFT side of the port mapping in -`docker-compose.yml`: +## Teardown -```yaml -resolver: - ports: - - "127.0.0.1:8000:8000" # host:container +```sh +docker compose down # stop, keep all state +docker compose down -v # also wipe volumes → full re-sync ``` -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. +`down -v` wipes the chain data (full re-sync on the next `up`). -The image declares a `HEALTHCHECK` against `/health`; `docker compose ps` -will mark the service `(healthy)` once reth is queryable. +--- -### Resolving a name +## Resolver API reference -`foobar.testing` is registered on mainnet with every text and -multicoin record populated (useful as a smoke-test target): +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 -curl -s http://127.0.0.1:8000/resolve/foobar.testing | jq . +uv run scripts/resolver/service/snrc-resolve.py # defaults to local reth + mainnet .testing ``` -```json +### Response shape + +```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. - -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.) +`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`). -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 -``` +### Status codes -Queries for a TLD with no registry configured return HTTP 400 with the -list of supported TLDs. +| 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 | -### Error responses +### Configuring registries -| 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. diff --git a/scripts/resolver/docker-compose.yml b/scripts/resolver/docker-compose.yml index 06f0d27d8..570b63df3 100644 --- a/scripts/resolver/docker-compose.yml +++ b/scripts/resolver/docker-compose.yml @@ -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 diff --git a/scripts/resolver/Dockerfile b/scripts/resolver/service/Dockerfile similarity index 100% rename from scripts/resolver/Dockerfile rename to scripts/resolver/service/Dockerfile diff --git a/scripts/resolver/pyproject.toml b/scripts/resolver/service/pyproject.toml similarity index 100% rename from scripts/resolver/pyproject.toml rename to scripts/resolver/service/pyproject.toml diff --git a/scripts/resolver/snrc-resolve.py b/scripts/resolver/service/snrc-resolve.py similarity index 99% rename from scripts/resolver/snrc-resolve.py rename to scripts/resolver/service/snrc-resolve.py index 8b5f21e70..ffddbeb02 100755 --- a/scripts/resolver/snrc-resolve.py +++ b/scripts/resolver/service/snrc-resolve.py @@ -35,7 +35,7 @@ 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 @@ # 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 } diff --git a/scripts/resolver/test_snrc_resolve.py b/scripts/resolver/service/test_snrc_resolve.py similarity index 97% rename from scripts/resolver/test_snrc_resolve.py rename to scripts/resolver/service/test_snrc_resolve.py index 64e924c06..2bb42f991 100644 --- a/scripts/resolver/test_snrc_resolve.py +++ b/scripts/resolver/service/test_snrc_resolve.py @@ -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 diff --git a/scripts/resolver/ens-lookup.py b/scripts/resolver/tools/ens-lookup.py similarity index 100% rename from scripts/resolver/ens-lookup.py rename to scripts/resolver/tools/ens-lookup.py diff --git a/scripts/resolver/progress.py b/scripts/resolver/tools/progress.py similarity index 100% rename from scripts/resolver/progress.py rename to scripts/resolver/tools/progress.py