iterate /goal

This commit is contained in:
Alain Brenzikofer committed 2026-09-23 07:58:53 +02:00
1 parent 861e9890c6
commit 1d82c0815d
13 files changed
+165 -50

No files matched your search

+12 -4
View File
@@ -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
+41 -2
View File
@@ -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
+34 -2
View File
@@ -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.
+22 -16
View File
@@ -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:
+16 -1
View File
@@ -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)
+3 -5
View File
@@ -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 =
+3 -2
View File
@@ -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 =
+3 -3
View File
@@ -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,
+9 -7
View File
@@ -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,
+3 -3
View File
@@ -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"
+2 -2
View File
@@ -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
View File
@@ -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) $