mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-11-04 10:47:51 +00:00
iterate /goal
This commit is contained in:
1 parent
861e9890c6
commit
1d82c0815d
13 files changed
+165
-50
No files matched your search
@@ -13,7 +13,7 @@ primitives. The client consumer is the wallet recovery scan in simplex-chat.
|
||||
```
|
||||
GET /v2/owned-by/<address>?offset=N resolver, JSON OwnedNames
|
||||
ROWN <address> <offset> -> ROWND SMP v23 (nameOwnedSMPVersion)
|
||||
ownedNames server, agent client, agent
|
||||
ownedSimplexNames agent, over ownedNames in the server
|
||||
```
|
||||
|
||||
The agent returns the relay it used alongside the answer, so a caller can pass
|
||||
@@ -22,7 +22,10 @@ seed to one relay would tell that relay the accounts belong to one wallet; the
|
||||
scan therefore asks one account per relay, and `getNextNameServer` avoids the
|
||||
hosts already used where the configured set allows. `proxiedSMPRelayVersion` is
|
||||
pinned to the current version so ROWN can be proxied at all — a scan that falls
|
||||
back to a direct session hands the relay its address with its IP.
|
||||
back to a direct session hands the relay its address with its IP. The pin is
|
||||
necessary but not sufficient: under the default `SPMUnknown` proxy mode a
|
||||
configured names server is a known host, so the scan reaches it directly, as
|
||||
RSLV already does.
|
||||
|
||||
Enumeration is read off the ERC-721 registrar (`balanceOf`,
|
||||
`tokenOfOwnerByIndex`, `labelOf`), not from registration logs: the token is the
|
||||
@@ -70,8 +73,13 @@ cannot be enumerated to be dusted.
|
||||
- **Nonce and balance are not carried over SMP.** The resolver reports them, and
|
||||
`OwnedNames` keeps only the names, the flag and the cursor, so a client cannot
|
||||
yet see why an account is in use.
|
||||
- **No caching.** One owned-by is many `eth_call`s, so it is forked on the
|
||||
server the same way RSLV is, and nothing is memoised.
|
||||
- **No caching, and no batching.** One owned-by is a `balanceOf` per configured
|
||||
TLD, a grace period for each that lists a name, three `eth_call`s per name
|
||||
listed, and the block, nonce and balance, all issued one at a time, so it is
|
||||
forked on
|
||||
the server the same way RSLV is and nothing is memoised. The page size trades
|
||||
against both the relay's response cap and its timeout, and it bounds names per
|
||||
registrar rather than per page, so a second TLD doubles a full page.
|
||||
- **Enumeration is not atomic.** `balanceOf` and each `tokenOfOwnerByIndex` are
|
||||
separate calls at `latest`, so a transfer between them can duplicate or skip an
|
||||
entry, and successive pages can straddle blocks. Pinning every call to one
|
||||
|
||||
@@ -70,6 +70,7 @@ Version 21, 2026-07-05
|
||||
- [Resolver commands](#resolver-commands)
|
||||
- [Resolve name command](#resolve-name-command)
|
||||
- [Name record response](#name-record-response)
|
||||
- [Names owned by an address command](#names-owned-by-an-address-command)
|
||||
- [Transport connection with the SMP router](#transport-connection-with-the-SMP-router)
|
||||
- [General transport protocol considerations](#general-transport-protocol-considerations)
|
||||
- [TLS transport encryption](#tls-transport-encryption)
|
||||
@@ -86,7 +87,7 @@ It's designed with the focus on communication security and integrity, under the
|
||||
|
||||
It is designed as a low level protocol for other application protocols to solve the problem of secure and private message transmission, making [MITM attack][1] very difficult at any part of the message transmission system.
|
||||
|
||||
This document describes SMP protocol version 22. Versions 1-5 are discontinued. The version history:
|
||||
This document describes SMP protocol version 23. Versions 1-5 are discontinued. The version history:
|
||||
|
||||
- v1: binary protocol encoding
|
||||
- v2: message flags (used to control notifications)
|
||||
@@ -109,6 +110,7 @@ This document describes SMP protocol version 22. Versions 1-5 are discontinued.
|
||||
- v20: public namespaces resolver (RSLV command, RNAME response) — direct or forwarded via PFWD
|
||||
- v21: server public information in handshake
|
||||
- v22: `RNAME` says whether a name can be registered, not only what it resolves to
|
||||
- v23: the names an address owns (ROWN command, ROWND response) — direct or forwarded via PFWD
|
||||
|
||||
## Introduction
|
||||
|
||||
@@ -1453,7 +1455,8 @@ companion REST resolver process (`scripts/resolver/snrc-resolve.py`) that
|
||||
queries the SNRC contract on Ethereum; alternative backings (different chains,
|
||||
DHT, etc.) are valid as long as they expose the documented HTTP shape (`GET
|
||||
/v2/resolve/<query>` returning a `NameRegistration` on 200 for every
|
||||
registration shape, 400 for unknown TLDs, 502 for upstream failures) or
|
||||
registration shape, 400 for unknown TLDs, 502 for upstream failures, and from
|
||||
SMP v23 `GET /v2/owned-by/<address>?offset=N` returning an `ownedNames`) or
|
||||
substitute a different transport returning the same JSON. The resolver API is
|
||||
versioned separately from this protocol: `/v1/resolve/<name>` returns a bare
|
||||
`NameRecord` and is what relays before v22 call as `/resolve/<name>`.
|
||||
@@ -1623,6 +1626,42 @@ The names router caps the resolver response it will accept
|
||||
stays within the SMP proxied transmission budget of 16224 bytes; a response over
|
||||
the cap is `ERR NAME RESOLVER`.
|
||||
|
||||
#### Names owned by an address command
|
||||
|
||||
`ROWN` asks which names an address holds, and whether the account has been used
|
||||
at all, so a device restoring a wallet seed can find the accounts already in
|
||||
use. It is unauthenticated and accepted direct or forwarded, as `RSLV` is, from
|
||||
v23.
|
||||
|
||||
```abnf
|
||||
rown = %s"ROWN" SP address offset
|
||||
address = length "0x" 40HEXDIG ; EIP-55 checksummed, length-prefixed
|
||||
length = 1*1 OCTET ; 42
|
||||
offset = 4*4 OCTET ; Word32, network byte order: where to resume listing
|
||||
```
|
||||
|
||||
```abnf
|
||||
rownd = %s"ROWND" SP ownedNames
|
||||
```
|
||||
|
||||
`ownedNames` is a UTF-8 JSON object consuming the remainder of the transmission.
|
||||
|
||||
| Field | JSON type | Constraints |
|
||||
|---|---|---|
|
||||
| `names` | array | the names the address holds, each with `name` (absent when the registrar recorded no label), `labelhash`, `expires` and `status` |
|
||||
| `inUse` | boolean | whether the account has been used at all |
|
||||
| `nextOffset` | number | cursor to resume from, absent when the listing is complete |
|
||||
|
||||
Enumeration is not maintained on expiry, so a lapsed name stays listed and is
|
||||
told apart by its `status`.
|
||||
|
||||
`inUse` is what the registry could see on its own chain — the account's nonce,
|
||||
its balance, and every name it holds, which is not only the names listed here:
|
||||
a later page of a held account still reports `inUse` true with an empty `names`. Holding a name is only one way for an account
|
||||
to be in use, and an account used only for other tokens, or on another chain,
|
||||
reads as unused, so a client MUST NOT treat `inUse` as false meaning the account
|
||||
has never been used.
|
||||
|
||||
## Transport connection with the SMP router
|
||||
|
||||
### General transport protocol considerations
|
||||
|
||||
@@ -93,7 +93,7 @@ curl -s http://127.0.0.1:8000/v2/resolve/foobar.testing | jq
|
||||
| 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 (`/v2/resolve`, `/resolve`, `/health`) |
|
||||
| **resolver** | `127.0.0.1:8000` | SNRC REST (`/v2/resolve`, `/v2/owned-by`, `/resolve`, `/health`) |
|
||||
|
||||
## Caveats
|
||||
|
||||
@@ -121,12 +121,13 @@ standalone for local dev (no Docker), via [`uv`](https://docs.astral.sh/uv/):
|
||||
uv run scripts/resolver/service/snrc-resolve.py # defaults to local reth + mainnet .testing
|
||||
```
|
||||
|
||||
Three routes, versioned separately from the protocol so each only changes when
|
||||
Four routes, versioned separately from the protocol so each only changes when
|
||||
its own shape does:
|
||||
|
||||
| Route | Called by | Answers |
|
||||
|---|---|---|
|
||||
| `/v2/resolve/<query>` | routers from SMP v22 | a `NameRegistration` |
|
||||
| `/v2/owned-by/<address>` | routers from SMP v23 | the names an address owns |
|
||||
| `/resolve/<name>` | routers before SMP v22 | a name record, flat |
|
||||
| `/health` | anyone | readiness |
|
||||
|
||||
@@ -165,6 +166,32 @@ label, so a hashed query cannot be answered with a name. See
|
||||
A subname reports the expiry and grace of the 2LD above it, since that is what
|
||||
bounds its lifetime. A subname nobody created reports as not registered.
|
||||
|
||||
### v2: `/v2/owned-by/<address>?offset=N`
|
||||
|
||||
The body is the SMP protocol's `OwnedNames`: the names the address holds, and
|
||||
`inUse`, whether the account has been used at all. The router decodes it as is
|
||||
and forwards it, so the fields are specified with the wire, in the **Names owned
|
||||
by an address command** section of
|
||||
[`protocol/simplex-messaging.md`](../../protocol/simplex-messaging.md).
|
||||
|
||||
Enumeration comes off the registrar's ERC-721 index, so a name acquired by
|
||||
transfer counts, and a lapsed one stays listed until someone re-registers it:
|
||||
every entry carries its `status` rather than being filtered out.
|
||||
|
||||
`offset` is the cursor to resume from, and it counts per registrar, so a page
|
||||
holds up to `SNRC_MAX_OWNED` names for each configured TLD. `nextOffset` is the
|
||||
next cursor, or null when the listing is complete. `inUse` covers every name the
|
||||
address holds, not only the page, so a later page reports it true with an empty
|
||||
`names`.
|
||||
|
||||
| Status | Meaning |
|
||||
|---|---|
|
||||
| 200 | the names the address holds, with `inUse` |
|
||||
| 400 | `badAddress`, `badOffset`, `noRegistrarConfigured` |
|
||||
| 502 | `upstreamError` |
|
||||
|
||||
Error bodies carry `address`, a fixed `error` code and a `message`.
|
||||
|
||||
### v1: `/resolve/<name>`
|
||||
|
||||
What routers before SMP v22 call. Its shape is unrelated to v2's: the record is
|
||||
@@ -374,3 +401,8 @@ here.
|
||||
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.
|
||||
|
||||
`SNRC_MAX_OWNED` (default 16) caps the names `/v2/owned-by` returns per
|
||||
registrar per page. A router will not read a body over 16000 bytes, and a client
|
||||
cannot ask for a smaller page, so keep it times the number of configured TLDs
|
||||
well under that.
|
||||
@@ -44,6 +44,8 @@ Environment:
|
||||
SNRC_CONTROLLER_<TLD> SimplexController (proxy) for the TLD; reservations,
|
||||
and through its `prices()` oracle what registering costs
|
||||
(default: mainnet for .testing, empty for .simplex)
|
||||
SNRC_MAX_OWNED Names /v2/owned-by returns per registrar per page
|
||||
(default: 16)
|
||||
SNRC_PORT Listen port (default: 8000)
|
||||
SNRC_BIND Bind address (default: 0.0.0.0)
|
||||
|
||||
@@ -124,8 +126,10 @@ COIN_DOT = 354
|
||||
|
||||
ZERO_ADDR = "0x0000000000000000000000000000000000000000"
|
||||
|
||||
# Names returned per /owned-by page; a scan follows `nextOffset` for the rest.
|
||||
MAX_OWNED = int(os.environ.get("SNRC_MAX_OWNED", "256"))
|
||||
# Names per registrar per /owned-by page, so a page holds this many times the
|
||||
# number of configured TLDs. A relay caps the body it reads at 16000 bytes and a
|
||||
# client cannot ask for a smaller page, so keep the product well under it.
|
||||
MAX_OWNED = max(1, int(os.environ.get("SNRC_MAX_OWNED", "16")))
|
||||
|
||||
# The registry prices in attoUSD (1e-18 USD); the protocol carries US cents.
|
||||
|
||||
@@ -893,7 +897,8 @@ def owned_by(address: str, offset: int = 0):
|
||||
Enumeration is read off the ERC-721 registrar, so a name acquired by
|
||||
transfer counts, and a lapsed one stays listed until re-registered - it is
|
||||
reported with its `status`, not filtered. `inUse` is what a recovery scan
|
||||
asks, derived from this chain's nonce and balance and the names above: an
|
||||
asks, derived from this chain's nonce and balance and every name it holds,
|
||||
not only the page returned: an
|
||||
account holding only other tokens is not seen, and neither is one used on
|
||||
another chain.
|
||||
"""
|
||||
@@ -921,23 +926,23 @@ def owned_by(address: str, offset: int = 0):
|
||||
}
|
||||
|
||||
now = chain_now()
|
||||
names, truncated = [], False
|
||||
names, truncated, total_held = [], False, 0
|
||||
for tld, registrar in configured.items():
|
||||
grace = grace_period(registrar)
|
||||
held = decode_uint(eth_call(registrar, selector("balanceOf(address)") + encode_address(address)))
|
||||
first = min(offset, held)
|
||||
last = min(first + MAX_OWNED, held)
|
||||
total_held += held
|
||||
last = min(offset + MAX_OWNED, held)
|
||||
if last < held:
|
||||
truncated = True
|
||||
for i in range(first, last):
|
||||
grace = grace_period(registrar) if last > offset else 0
|
||||
for i in range(offset, last):
|
||||
token = decode_uint(
|
||||
eth_call(registrar, selector("tokenOfOwnerByIndex(address,uint256)") + encode_address(address) + encode_uint(i))
|
||||
)
|
||||
expires = decode_uint(eth_call(registrar, selector("nameExpires(uint256)") + encode_uint(token)))
|
||||
# A label of "" means the token is real but its name is not
|
||||
# recoverable from chain state - registered before labels were
|
||||
# recorded. Reported without a name rather than silently dropped.
|
||||
label = decode_bytes(eth_call(registrar, selector("labelOf(uint256)") + encode_uint(token))).decode()
|
||||
# No label means the token is real but its name is not recoverable
|
||||
# from chain state - registered before labels were recorded.
|
||||
# Reported without a name rather than silently dropped.
|
||||
label = registered_label(registrar, token)
|
||||
names.append(
|
||||
{
|
||||
"name": (label + "." + tld) if label else None,
|
||||
@@ -957,12 +962,13 @@ def owned_by(address: str, offset: int = 0):
|
||||
"names": names,
|
||||
"nonce": nonce,
|
||||
"balance": str(balance),
|
||||
"inUse": bool(names) or nonce > 0 or balance > 0,
|
||||
# every name the address holds, not just this page: a later page of a
|
||||
# held name must not read as an account that owns nothing
|
||||
"inUse": total_held > 0 or nonce > 0 or balance > 0,
|
||||
"offset": offset,
|
||||
# `nextOffset` is the cursor to resume from, or null when the listing is
|
||||
# complete, so "there is more" and "how to get it" are one answer.
|
||||
"nextOffset": offset + MAX_OWNED if truncated else None,
|
||||
"truncated": truncated,
|
||||
"checkedTlds": sorted(configured),
|
||||
}
|
||||
|
||||
@@ -995,7 +1001,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
try:
|
||||
offset = int(parse_qs(parsed.query).get("offset", ["0"])[0])
|
||||
except ValueError:
|
||||
self._respond(400, {"address": address, "error": "badOffset"})
|
||||
self._respond(400, {"address": address, "error": "badOffset", "message": "offset is a decimal position in the listing"})
|
||||
return
|
||||
try:
|
||||
status, body = owned_by(address, offset)
|
||||
@@ -1058,7 +1064,7 @@ def main():
|
||||
)
|
||||
for tld, addr in REGISTRIES.items():
|
||||
sys.stderr.write(f" .{tld:<8s} = {addr or '(not configured)'}\n")
|
||||
sys.stderr.write(" GET /v2/resolve/<name> GET /v1/resolve/<name> GET /health\n")
|
||||
sys.stderr.write(" GET /v2/resolve/<name> GET /v2/owned-by/<address> GET /v1/resolve/<name> GET /health\n")
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
|
||||
@@ -1117,7 +1117,6 @@ class OwnedByTests(unittest.TestCase):
|
||||
def test_more_names_than_a_page_carry_the_cursor_to_resume_from(self):
|
||||
snrc.eth_call = self._chain(snrc.MAX_OWNED + 1, int(time.time()) + 86400)
|
||||
_, body = snrc.owned_by(self.ADDR)
|
||||
self.assertTrue(body["truncated"])
|
||||
self.assertEqual(body["nextOffset"], snrc.MAX_OWNED)
|
||||
self.assertEqual(len(body["names"]), snrc.MAX_OWNED)
|
||||
|
||||
@@ -1135,6 +1134,22 @@ class OwnedByTests(unittest.TestCase):
|
||||
_, body = snrc.owned_by(self.ADDR)
|
||||
self.assertEqual(body["names"][0]["status"], "grace")
|
||||
|
||||
def test_a_page_past_the_end_still_reports_the_account_in_use(self):
|
||||
"""inUse is a property of the account, not of the page: an empty later
|
||||
page must not read as an account that owns nothing."""
|
||||
snrc.eth_call = self._chain(1, int(time.time()) + 86400)
|
||||
_, body = snrc.owned_by(self.ADDR, snrc.MAX_OWNED)
|
||||
self.assertEqual(body["names"], [])
|
||||
self.assertTrue(body["inUse"])
|
||||
|
||||
def test_a_label_that_is_not_utf8_is_reported_not_fatal(self):
|
||||
"""The registrar stores the label bytes unchecked, so one bad label
|
||||
must not take down the whole listing."""
|
||||
snrc.eth_call = self._chain(1, int(time.time()) + 86400, label=b"\xff\xfe")
|
||||
status, body = snrc.owned_by(self.ADDR)
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(len(body["names"]), 1)
|
||||
|
||||
def test_a_negative_offset_is_refused(self):
|
||||
snrc.eth_call = self._chain(1, int(time.time()) + 86400)
|
||||
status, body = snrc.owned_by(self.ADDR, -1)
|
||||
|
||||
@@ -2044,17 +2044,15 @@ ownedNames c nm userId server addr offset =
|
||||
-- | Pick a names-capable server for the user (the agent owns server selection,
|
||||
-- accounting for the names role). nameSrvs is opt-in (a plain list); empty means
|
||||
-- no server resolves names - a declared agent error, never a fallback.
|
||||
-- Servers already used are avoided where the set allows: one relay asked about
|
||||
-- every account of a scan would learn they belong to one wallet.
|
||||
-- Servers already used are avoided where the set allows, operator first: one
|
||||
-- operator asked about every account of a scan learns they are one wallet.
|
||||
getNextNameServer :: AgentClient -> UserId -> [SMPServer] -> AM SMPServer
|
||||
getNextNameServer c userId usedSrvs =
|
||||
liftIO (TM.lookupIO userId (userServers c :: TMap UserId (UserServers 'PSMP))) >>= \case
|
||||
Just UserServers {nameSrvs} -> case L.nonEmpty nameSrvs of
|
||||
Just srvs -> protoServer <$> pickServer (fromMaybe srvs $ L.nonEmpty $ L.filter (isUnusedServer usedHosts) srvs)
|
||||
Just srvs -> protoServer . snd <$> getNextServer_ srvs (usedOperatorsHosts srvs usedSrvs)
|
||||
Nothing -> throwE NO_NAME_SERVERS
|
||||
Nothing -> throwE $ INTERNAL "unknown userId - no user servers"
|
||||
where
|
||||
usedHosts = S.unions $ map serverHosts usedSrvs
|
||||
|
||||
enableQueueNotifications :: AgentClient -> RcvQueue -> SMP.NtfPublicAuthKey -> SMP.RcvNtfPublicDhKey -> AM (SMP.NotifierId, SMP.RcvNtfPublicDhKey)
|
||||
enableQueueNotifications c rq@RcvQueue {rcvId, rcvPrivateKey} notifierKey rcvNtfPublicDhKey =
|
||||
|
||||
@@ -1081,8 +1081,9 @@ directResolveName c nm name
|
||||
r -> throwE $ unexpectedResponse r
|
||||
| otherwise = throwE $ PCETransportError TEVersion
|
||||
|
||||
-- | Names an address owns, via PFWD. A scan reveals which accounts belong to
|
||||
-- one wallet, so hiding the client IP matters more here than for one name.
|
||||
-- | Names an address owns, via PFWD, when the network config selects a proxy. A
|
||||
-- scan reveals which accounts belong to one wallet, so hiding the client IP
|
||||
-- matters more here than for one name.
|
||||
proxyOwnedNames :: SMPClient -> NetworkRequestMode -> ProxiedRelay -> Address -> Word32 -> ExceptT SMPClientError IO (Either ProxyClientError OwnedNames)
|
||||
proxyOwnedNames c nm proxiedRelay addr offset
|
||||
| prVersion proxiedRelay >= nameOwnedSMPVersion =
|
||||
|
||||
@@ -94,9 +94,9 @@ data NameRegistration
|
||||
|
||||
-- | What the registry holds for an address: the names it owns, and whether the
|
||||
-- account is in use, which holding a name is only one way to be. `ownInUse` is
|
||||
-- what the resolver could see on its own chain - the nonce, the balance and the
|
||||
-- names - so an account used only for other tokens, or on another chain, reads
|
||||
-- as unused.
|
||||
-- what the resolver could see on its own chain - the nonce, the balance and
|
||||
-- every name the address holds, not only the ones listed here - so an account
|
||||
-- used only for other tokens, or on another chain, reads as unused.
|
||||
data OwnedNames = OwnedNames
|
||||
{ ownNames :: [OwnedName],
|
||||
ownInUse :: Bool,
|
||||
|
||||
@@ -1509,6 +1509,10 @@ client
|
||||
answered = \case
|
||||
NRRegistered {} -> True
|
||||
_ -> v >= nameAvailSMPVersion
|
||||
resolverMsg :: VersionSMP -> NamesEnv -> Command Resolver -> M s BrokerMsg
|
||||
resolverMsg v nenv = \case
|
||||
RSLV d -> resolveNameMsg v nenv d
|
||||
ROWN addr offset -> ownedNamesMsg nenv addr offset
|
||||
-- Forked for the same reason as RSLV: one owned-by is many eth_calls.
|
||||
ownedNamesMsg :: NamesEnv -> Address -> Word32 -> M s BrokerMsg
|
||||
ownedNamesMsg nenv addr offset = do
|
||||
@@ -1532,12 +1536,9 @@ client
|
||||
SEND flags msgBody -> response <$> withQueue_ False err (sendMessage flags msgBody)
|
||||
Cmd SIdleClient PING -> pure $ response (corrId, NoEntity, PONG)
|
||||
Cmd SProxyService (RFWD encBlock) -> (response . (corrId, NoEntity,) =<<) <$> processForwardedCommand encBlock
|
||||
Cmd SResolver (RSLV d) -> rslvNamesEnv >>= \case
|
||||
Cmd SResolver command -> rslvNamesEnv >>= \case
|
||||
Nothing -> pure $ response (corrId, NoEntity, ERR (NAME NO_RESOLVER))
|
||||
Just nenv -> forkCmd serverResolverConcurrency corrId NoEntity (resolveNameMsg (thVersion thParams') nenv d)
|
||||
Cmd SResolver (ROWN addr offset) -> rslvNamesEnv >>= \case
|
||||
Nothing -> pure $ response (corrId, NoEntity, ERR (NAME NO_RESOLVER))
|
||||
Just nenv -> forkCmd serverResolverConcurrency corrId NoEntity (ownedNamesMsg nenv addr offset)
|
||||
Just nenv -> forkCmd serverResolverConcurrency corrId NoEntity (resolverMsg (thVersion thParams') nenv command)
|
||||
Cmd SSenderLink command -> case command of
|
||||
LKEY k -> withQueue $ \q qr -> checkMode QMMessaging qr $ secureQueue_ q k $>> getQueueLink_ q qr
|
||||
LGET -> withQueue $ \q qr -> checkContact qr $ getQueueLink_ q qr
|
||||
@@ -2165,10 +2166,10 @@ client
|
||||
-- rejectOrVerify filters allowed commands, no need to repeat it here.
|
||||
Left r -> pure $ Just r
|
||||
Right t''@(_, (corrId', entId', cmd')) -> case cmd' of
|
||||
Cmd SResolver (RSLV d) -> lift $ rslvNamesEnv >>= \case
|
||||
Cmd SResolver command -> lift $ rslvNamesEnv >>= \case
|
||||
Nothing -> pure $ Just (corrId', entId', ERR (NAME NO_RESOLVER))
|
||||
Just nenv -> forkCmd serverResolverConcurrency corrId NoEntity $ do
|
||||
msg <- resolveNameMsg (thVersion clntTHParams) nenv d
|
||||
msg <- resolverMsg (thVersion clntTHParams) nenv command
|
||||
either ERR id <$> runExceptT (encodeResp (corrId', entId', msg))
|
||||
-- INTERNAL because processCommand never returns Nothing for sender commands;
|
||||
-- `fst` drops the empty message only returned for SUB.
|
||||
@@ -2190,6 +2191,7 @@ client
|
||||
Cmd SSenderLink (LKEY _) -> True
|
||||
Cmd SSenderLink LGET -> True
|
||||
Cmd SResolver (RSLV _) -> True
|
||||
Cmd SResolver (ROWN _ _) -> True
|
||||
_ -> False
|
||||
verified = \case
|
||||
VRVerified q -> Right (q, t'')
|
||||
|
||||
@@ -43,8 +43,8 @@ import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import Data.Text (Text)
|
||||
import Data.Word (Word32)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Word (Word32)
|
||||
import Network.HTTP.Client
|
||||
( HttpException,
|
||||
Manager,
|
||||
|
||||
@@ -465,11 +465,11 @@ prometheusMetrics sm rtm ts =
|
||||
in "# Names\n\
|
||||
\# -----\n\
|
||||
\\n\
|
||||
\# HELP simplex_smp_names_reqs Total RSLV requests forwarded to this server.\n\
|
||||
\# HELP simplex_smp_names_reqs Total resolver requests (RSLV and ROWN) forwarded to this server.\n\
|
||||
\# TYPE simplex_smp_names_reqs counter\n\
|
||||
\simplex_smp_names_reqs " <> mshow _rslvReqs <> "\n# rslvReqs\n\
|
||||
\\n\
|
||||
\# HELP simplex_smp_names_success NameRecord resolved, or availability answered.\n\
|
||||
\# HELP simplex_smp_names_success NameRecord resolved, availability answered, or names owned listed.\n\
|
||||
\# TYPE simplex_smp_names_success counter\n\
|
||||
\simplex_smp_names_success " <> mshow _rslvSucc <> "\n# rslvSucc\n\
|
||||
\\n\
|
||||
@@ -481,7 +481,7 @@ prometheusMetrics sm rtm ts =
|
||||
\# TYPE simplex_smp_names_resolver_errs counter\n\
|
||||
\simplex_smp_names_resolver_errs " <> mshow _rslvResolverErrs <> "\n# rslvResolverErrs\n\
|
||||
\\n\
|
||||
\# HELP simplex_smp_names_disabled RSLV requests rejected because no resolver is configured (names role off).\n\
|
||||
\# HELP simplex_smp_names_disabled Resolver requests rejected because no resolver is configured (names role off).\n\
|
||||
\# TYPE simplex_smp_names_disabled counter\n\
|
||||
\simplex_smp_names_disabled " <> mshow _rslvDisabled <> "\n# rslvDisabled\n\
|
||||
\\n"
|
||||
|
||||
@@ -47,8 +47,8 @@ withResolverServerDelayed delayMs handler action = do
|
||||
let (st, body) = handler (pathInfo req)
|
||||
send $ responseLBS st [(hContentType, "application/json")] body
|
||||
|
||||
-- | The resolver API is versioned on its own: v2 answers with NameRegistration
|
||||
-- JSON, which is the only shape the server asks for.
|
||||
-- | The resolver API is versioned on its own: v2 answers a name with
|
||||
-- NameRegistration JSON, and an address with OwnedNames.
|
||||
resolveResp :: Status -> LB.ByteString -> [Text] -> (Status, LB.ByteString)
|
||||
resolveResp st body = \case
|
||||
["health"] -> (ok200, "{}")
|
||||
|
||||
+16
-2
@@ -103,6 +103,7 @@ rslvTests = do
|
||||
describe "RSLV forwarded (PFWD)" $ do
|
||||
it "PFWD-wrapped RSLV reaches resolver via proxy (PCEProtocolError (NAME RESOLVER))" testRslvForwarded
|
||||
it "PFWD-wrapped RSLV success returns RNAME (record JSON frames over the proxy)" testRslvForwardedSuccess
|
||||
it "PFWD-wrapped ROWN reaches the resolver, so a scan need not go direct" testRownForwarded
|
||||
describe "RSLV success path (RNAME response)" $ do
|
||||
it "returns RNAME with NameRecord" testRslvSuccess
|
||||
describe "RSLV availability (RNAME response)" $ do
|
||||
@@ -182,7 +183,11 @@ testRslvVersion =
|
||||
_ -> expectationFailure $ "expected Left (PCETransportError TEVersion), got: " <> show r
|
||||
|
||||
forwardedResolveAlice :: IO (Either SMPClientError (Either ProxyClientError SMP.NameResponse))
|
||||
forwardedResolveAlice = do
|
||||
forwardedResolveAlice = forwardedToRelay $ \pc sess -> proxyResolveName pc NRMInteractive sess (domain "alice.simplex")
|
||||
|
||||
-- | Run one proxied resolver command over a PFWD session to the second relay.
|
||||
forwardedToRelay :: (SMPClient -> ProxiedRelay -> ExceptT SMPClientError IO a) -> IO (Either SMPClientError a)
|
||||
forwardedToRelay proxiedCmd = do
|
||||
g <- C.newRandom
|
||||
ts <- getCurrentTime
|
||||
let proxyServ = SMPServer testHost testPort testKeyHash
|
||||
@@ -191,7 +196,7 @@ forwardedResolveAlice = do
|
||||
pcE <- getProtocolClient g NRMInteractive (1, proxyServ, Nothing) cfg' [] Nothing ts (\_ -> pure ())
|
||||
pc <- either (fail . show) pure pcE
|
||||
sess <- runExceptT' (connectSMPProxiedRelay pc NRMInteractive relayServ Nothing)
|
||||
runExceptT (proxyResolveName pc NRMInteractive sess (domain "alice.simplex"))
|
||||
runExceptT (proxiedCmd pc sess)
|
||||
|
||||
testRslvForwarded :: IO ()
|
||||
testRslvForwarded =
|
||||
@@ -207,6 +212,15 @@ testRslvForwardedSuccess =
|
||||
Right (Right NameResponse {registration = NRRegistered {nameRecord}}) -> nameRecord `shouldBe` testNameRecord
|
||||
_ -> expectationFailure $ "expected Right (Right NRRegistered), got: " <> show r
|
||||
|
||||
-- | Without this the scan has to fall back to a direct session, handing the
|
||||
-- relay the address together with the client IP.
|
||||
testRownForwarded :: IO ()
|
||||
testRownForwarded =
|
||||
withProxyAndResolver (status200, ownedBody) $
|
||||
forwardedToRelay (\pc sess -> proxyOwnedNames pc NRMInteractive sess testAddr 0) >>= \r -> case r of
|
||||
Right (Right owned) -> map onName (ownNames owned) `shouldBe` [Just "alice.testing"]
|
||||
_ -> expectationFailure $ "expected Right (Right OwnedNames), got: " <> show r
|
||||
|
||||
testRslvSuccess :: IO ()
|
||||
testRslvSuccess =
|
||||
withResolverServer (status200, registeredBody testNameRecord) $
|
||||
|
||||
Reference in new issue
Block a user