mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-09-11 22:16:30 +00:00
simplify language
This commit is contained in:
+80
-66
@@ -19,15 +19,15 @@ against **Ethereum mainnet** (where the `.testing` contracts live):
|
||||
|
||||
## 1. Configure
|
||||
|
||||
Edit `.env` — the defaults work as-is; override only if needed:
|
||||
Edit `.env`. The defaults work as they are; change them only if you need to:
|
||||
|
||||
```sh
|
||||
NETWORK=mainnet # default
|
||||
TRUSTED_NODE_URL=https://mainnet-checkpoint-sync.attestant.io # default
|
||||
```
|
||||
|
||||
Everything else (NAT) has a working default baked into `docker-compose.yml`;
|
||||
uncomment the hints in `.env` only to override.
|
||||
Everything else (NAT) already has a working default in `docker-compose.yml`.
|
||||
Uncomment the hints in `.env` only if you need to change one.
|
||||
|
||||
## 2. Run
|
||||
|
||||
@@ -37,7 +37,7 @@ docker compose up -d
|
||||
docker compose logs -f reth resolver
|
||||
```
|
||||
|
||||
`depends_on` handles ordering automatically (start node → start resolver).
|
||||
Compose starts the node before the resolver; `depends_on` takes care of that.
|
||||
|
||||
## 3. Wait for the node to sync
|
||||
|
||||
@@ -45,12 +45,13 @@ docker compose logs -f reth resolver
|
||||
docker compose logs --tail=20 reth
|
||||
```
|
||||
|
||||
This is the long pole (~1 day on mainnet). Until reth is synced the resolver
|
||||
returns `502`.
|
||||
This is the slow step: about a day on mainnet. Until reth has synced, the
|
||||
resolver returns `502`.
|
||||
|
||||
## Verify
|
||||
|
||||
Run these once the stack is up (the node-dependent ones pass after sync):
|
||||
Run the three checks below once the stack is up. The ones that need chain data
|
||||
pass only after the node has synced.
|
||||
|
||||
**1. reth is reachable and reporting a block:**
|
||||
```sh
|
||||
@@ -71,7 +72,7 @@ curl -s http://127.0.0.1:8000/resolve/foobar.testing | jq
|
||||
# → {"name":"foobar.testing","nickname":"Foo","simplexContact":["https://smp16.simplex.im/a#…"], … }
|
||||
```
|
||||
|
||||
**Wire your smp-server:** in its `[NAMES]` section set
|
||||
**Point your smp-server at it:** in its `[NAMES]` section set
|
||||
`resolver_endpoint: http://127.0.0.1:8000` (no auth needed for loopback).
|
||||
|
||||
## Ports (all loopback unless noted)
|
||||
@@ -86,9 +87,10 @@ curl -s http://127.0.0.1:8000/resolve/foobar.testing | jq
|
||||
|
||||
## Caveats
|
||||
|
||||
- **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.
|
||||
- **All images track `:latest`** (reth, nimbus). Each `docker compose pull`
|
||||
brings upstream fixes, so re-run the checks above afterwards.
|
||||
- All ports bind to loopback. Expose only what you put behind a TLS reverse
|
||||
proxy.
|
||||
|
||||
## Teardown
|
||||
|
||||
@@ -103,8 +105,9 @@ docker compose down -v # also wipe volumes → full re-sync
|
||||
|
||||
## 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/):
|
||||
You can also run the resolver (`snrc-resolve.py`, host `127.0.0.1:8000`) on its
|
||||
own for local development, without Docker, using
|
||||
[`uv`](https://docs.astral.sh/uv/):
|
||||
|
||||
```sh
|
||||
uv run scripts/resolver/service/snrc-resolve.py # defaults to local reth + mainnet .testing
|
||||
@@ -126,19 +129,20 @@ uv run scripts/resolver/service/snrc-resolve.py # defaults to local reth + main
|
||||
}
|
||||
```
|
||||
|
||||
`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`).
|
||||
`simplexContact` and `simplexChannel` are arrays, because a name can advertise
|
||||
several SMP servers; clients try them in order. On chain each one is a single
|
||||
text record with the entries joined by `;`. The resolver splits that record,
|
||||
trims each entry and drops the empty ones. Addresses come back in each chain's
|
||||
usual format (EIP-55, bech32, SS58, Monero base58). Subnames work the same way
|
||||
(`bar.foobar.testing`).
|
||||
|
||||
### Registration status and expiry
|
||||
|
||||
`status`, `expires` and `graceEnds` are on every response that got far enough
|
||||
to know them, including a successful resolve — so a client that has just
|
||||
resolved a name already holds its expiry and needs no second request to warn
|
||||
about it. `expires` and `graceEnds` are Unix timestamps in seconds; both are
|
||||
`null` when unknown.
|
||||
A response carries `status`, `expires` and `graceEnds` whenever the resolver
|
||||
got far enough to read them, a successful resolve included. A client that has
|
||||
just resolved a name therefore already has its expiry, and needs no second
|
||||
request to warn about it. `expires` and `graceEnds` are Unix timestamps in
|
||||
seconds, and both are `null` when the resolver could not read them.
|
||||
|
||||
| `status` | Meaning |
|
||||
|---|---|
|
||||
@@ -150,17 +154,19 @@ about it. `expires` and `graceEnds` are Unix timestamps in seconds; both are
|
||||
| `noResolver` | registered, but points nowhere |
|
||||
| `unknown` | no `SNRC_REGISTRAR_<TLD>` configured, so status could not be read |
|
||||
|
||||
The split between `grace` and `expired` mirrors the registrar's own
|
||||
`available(id)` rule (`expires + GRACE_PERIOD < now`), with `GRACE_PERIOD` read
|
||||
from the contract rather than assumed and `now` taken from the latest block's
|
||||
timestamp rather than the host clock — the registrar compares against that same
|
||||
clock, so a skewed machine cannot misstate a registration. Note that
|
||||
`available(id)` alone cannot distinguish these: it is also true for a name
|
||||
nobody ever registered, since `0 + GRACE_PERIOD < now`. A zero expiry is what
|
||||
separates *never taken* from *taken and since released*.
|
||||
The resolver tells `grace` and `expired` apart with the registrar's own
|
||||
`available(id)` rule, `expires + GRACE_PERIOD < now`. The resolver reads
|
||||
`GRACE_PERIOD` from the contract instead of assuming it, and takes `now` from
|
||||
the latest block's timestamp instead of the host clock. The registrar compares
|
||||
against that same block timestamp, so a machine with a wrong clock cannot
|
||||
misreport a registration.
|
||||
|
||||
Subnames report the status of the 2LD they sit under, which is the useful
|
||||
answer — a subname is only as valid as the name above it.
|
||||
`available(id)` on its own cannot tell the two apart, because it is also true
|
||||
for a name nobody ever registered: `0 + GRACE_PERIOD < now`. The resolver uses
|
||||
a zero expiry to tell *never registered* from *registered and since released*.
|
||||
|
||||
A subname reports the status of the 2LD above it. That is the answer a client
|
||||
needs, because a subname is only as good as the name it sits under.
|
||||
|
||||
### Querying by labelhash
|
||||
|
||||
@@ -175,20 +181,23 @@ curl -s "http://127.0.0.1:8000/resolve/[$(printf acme | keccak-256sum | cut -d'
|
||||
```
|
||||
|
||||
This works because namehash is `keccak(parent || keccak(label))`. Passing
|
||||
`keccak(label)` gives the same node, so the resolver reads the same record —
|
||||
and the registrar also keys `nameExpires` and `reservedNames` on the labelhash,
|
||||
so availability needs the label no more than the record does. It learns which
|
||||
name you meant only if it guesses the label and hashes it.
|
||||
`keccak(label)` gives the same node, so the resolver reads the same record. The
|
||||
registrar keys `nameExpires` and `reservedNames` on the labelhash as well, so
|
||||
the status fields do not need the label either. The resolver learns which name
|
||||
you meant only if it guesses the label and hashes it.
|
||||
|
||||
Read the answer by `status`: a name is free exactly when the body says
|
||||
`unregistered` (a 404). Every other answer is a name somebody holds or held
|
||||
recently — note that `noResolver` is also a 404, and it is a taken name.
|
||||
Read the answer from `status`. A name is free only when the body says
|
||||
`unregistered`, which comes with a 404. Every other status means somebody holds
|
||||
the name or held it recently. Watch out for `noResolver`: it is also a 404, but
|
||||
the name is taken.
|
||||
|
||||
Two practical notes. The hash must be keccak-256: `openssl dgst -sha3-256` and
|
||||
`sha3sum` compute SHA3-256, a different function, and produce 64 well-formed
|
||||
hex of nothing you meant. And queries are lowercased before matching, so
|
||||
uppercase hex works too; strict HTTP stacks that reject raw brackets in a path
|
||||
can percent-encode them (`%5B`/`%5D`) — both forms load the same name.
|
||||
The hash must be keccak-256. `openssl dgst -sha3-256` and `sha3sum` compute
|
||||
SHA3-256, which is a different function. They return 64 valid-looking hex
|
||||
characters that point at the wrong node.
|
||||
|
||||
The resolver lowercases the query before matching, so uppercase hex works too.
|
||||
HTTP clients that refuse raw brackets in a path can percent-encode them as
|
||||
`%5B` and `%5D`. Both forms reach the same name.
|
||||
|
||||
Brackets keep the two forms from colliding. `[` and `]` are not valid in a
|
||||
normalised ENS name, and the dApp normalises before it registers, so no name
|
||||
@@ -202,14 +211,14 @@ Only 2LDs can be queried by hash. A 2LD is what a registration buys, so it is
|
||||
the only name worth hiding. Subnames are left out because nobody can race you
|
||||
for one: the owner of the 2LD creates them. In a subname the resolver hashes a
|
||||
`[<64 hex>]` label as written instead of decoding it, so such a query points at
|
||||
a node nobody can own. (ENS tooling reads the bracketed form at any depth; this
|
||||
resolver deliberately does not.)
|
||||
a node nobody can own. ENS tooling accepts the bracketed form at any depth;
|
||||
this resolver does not, on purpose.
|
||||
|
||||
This hides your interest in a name, and nothing more. The registration itself
|
||||
is public, and the controller's commit-reveal protects that step. Guessing
|
||||
stays cheap — for a short or brand-like label the hash is a speed bump, not
|
||||
secrecy — and once the reveal makes the labelhash public, an operator who
|
||||
logged the probe can link the two.
|
||||
is public, and the controller's commit-reveal protects that step. The hash is
|
||||
also easy to guess for a short or well-known label, since an operator can hash
|
||||
candidate labels and compare. And once you register, the reveal publishes the
|
||||
labelhash, so an operator who logged your query can match it to the name.
|
||||
|
||||
### Status codes
|
||||
|
||||
@@ -223,21 +232,26 @@ logged the probe can link the two.
|
||||
|
||||
### Configuring addresses
|
||||
|
||||
Three maps, all per TLD. The **registry** answers *who owns this node* and is
|
||||
what `/resolve` reads records from. The **registrar** (ERC-721) holds
|
||||
`nameExpires` / `GRACE_PERIOD`, and is what every expiry field is read from —
|
||||
without one for a TLD, `/resolve` still works and reports `"status": "unknown"`.
|
||||
The **controller** holds `reservedNames`, and is what the `reserved` status is
|
||||
read from; without one a reserved name reads as `unregistered`.
|
||||
The resolver reads three contracts, each configured per TLD.
|
||||
|
||||
All three default to the mainnet `.testing` deployment; `.simplex` is unset
|
||||
until deployed. Note that the controller default is the **proxy**, not
|
||||
`SimplexControllerImpl`: storage lives in the proxy, so the implementation
|
||||
answers nothing. `deployments.mainnet.testing.json` records it under the ENS
|
||||
role name `ETHRegistrarController` and `verification.mainnet.testing.json`
|
||||
names it `SimplexControllerProxy` — the same address, and the one defaulted to
|
||||
The **registry** answers who owns a node, and `/resolve` reads the records from
|
||||
it. The **registrar** (ERC-721) holds `nameExpires` and `GRACE_PERIOD`, which
|
||||
is where every expiry field comes from. With no registrar for a TLD, `/resolve`
|
||||
still works and reports `"status": "unknown"`. The **controller** holds
|
||||
`reservedNames`, which is where the `reserved` status comes from. With no
|
||||
controller, a reserved name reads as `unregistered`.
|
||||
|
||||
All three default to the mainnet `.testing` deployment. `.simplex` is unset
|
||||
until it is deployed.
|
||||
|
||||
The controller default is the **proxy**, not `SimplexControllerImpl`. Storage
|
||||
lives in the proxy, so the implementation address answers nothing. The two
|
||||
deployment files use different names for that proxy:
|
||||
`deployments.mainnet.testing.json` records it under the ENS role name
|
||||
`ETHRegistrarController`, and `verification.mainnet.testing.json` calls it
|
||||
`SimplexControllerProxy`. Both are the same address, and it is the one used
|
||||
here.
|
||||
|
||||
Override per TLD via env on the `resolver` service in `docker-compose.yml`
|
||||
(`SNRC_REGISTRY_<TLD>` / `SNRC_REGISTRAR_<TLD>` / `SNRC_CONTROLLER_<TLD>`), or
|
||||
as env vars for the standalone script.
|
||||
To override any of them, set `SNRC_REGISTRY_<TLD>`, `SNRC_REGISTRAR_<TLD>` or
|
||||
`SNRC_CONTROLLER_<TLD>` on the `resolver` service in `docker-compose.yml`, or
|
||||
as env vars when you run the script directly.
|
||||
Reference in New Issue
Block a user