core: align resolver docs/tests with RSLV errors

This commit is contained in:
shum
2026-06-11 14:51:56 +00:00
parent ae42ab130c
commit 7836f71d7a
2 changed files with 42 additions and 19 deletions
+23 -18
View File
@@ -4633,11 +4633,12 @@ processChatCommand cxt nm = \case
-- | Failure modes for 'resolveOnUserServers' / 'iterateResolvers'.
data ResolveError
= -- | No enabled server can resolve: every candidate answered CMD UNKNOWN /
-- PROHIBITED (does not speak RSLV), or none were configured / reachable.
-- Iterating across unsupported servers is privacy-safe -- the client degrades
-- RSLV to a no-op for relays below namesSMPVersion (see Protocol.hs), so an
-- unsupported relay never receives the queried name.
= -- | No enabled server can resolve: every candidate answered CMD UNKNOWN
-- (predates RSLV) or CMD PROHIBITED (speaks RSLV but has no resolver
-- configured), or none were configured / reachable. Iterating across these is
-- safe: a CMD UNKNOWN relay never received the name (the client degrades RSLV
-- to a no-op below namesSMPVersion, see Protocol.hs); a CMD PROHIBITED relay
-- did receive it but is one the user already trusts as an SMP server.
ResolverUnavailable
| -- | AUTH from a name-capable server. Every name server reads the same on-chain state, so we trust the first one's no.
NameNotRegistered
@@ -4664,13 +4665,15 @@ enabledSMPServersForUser user = do
| otherwise = Nothing
-- | Resolve a SimpleX name by trying the user's enabled SMP servers in order.
-- Transport-level failures (NETWORK, TIMEOUT, host-unreachable) and unsupported
-- servers (CMD UNKNOWN / PROHIBITED, i.e. relays that don't speak RSLV) both fall
-- through to the next server. Skipping an unsupported relay discloses nothing:
-- the client degrades RSLV to a no-op below namesSMPVersion, so the name is never
-- sent to it. A definitive answer from a name-capable relay terminates iteration:
-- AUTH is definitive NotFound (every name server reads the same on-chain state);
-- any other definite error surfaces as ResolverTransport.
-- Transport-level failures (NETWORK, TIMEOUT, host-unreachable) and servers that
-- cannot resolve (CMD UNKNOWN -- predates RSLV; or CMD PROHIBITED -- speaks RSLV
-- but has no resolver configured) all fall through to the next server. A CMD
-- UNKNOWN relay never received the name (the client degrades RSLV below
-- namesSMPVersion); a CMD PROHIBITED relay did, but it is one the user already
-- trusts as an SMP server. A definitive answer from a name-capable relay
-- terminates iteration: AUTH is definitive NotFound (every name server reads the
-- same on-chain state); any other definite error (e.g. INTERNAL on a resolver
-- backend failure) surfaces as ResolverTransport.
-- Privacy: a name-capable relay does see the queried name, so once one has
-- answered we do not broadcast the miss to every other operator the user has.
resolveOnUserServers :: User -> SimplexNameDomain -> CM (Either ResolveError NameRecord)
@@ -4858,12 +4861,14 @@ iterateResolvers servers resolve = go servers
isNotRegistered = \case
SMP _ SMP.AUTH -> True
_ -> False
-- A server that doesn't speak RSLV answers CMD UNKNOWN (predates the
-- command, e.g. the official servers) or CMD PROHIBITED (knows it but gates
-- on namesSMPVersion) -- directly, or wrapped by a proxy. We skip it and try
-- the next server: the client degrades RSLV to a no-op below namesSMPVersion
-- (Protocol.hs), so an unsupported relay never received the queried name.
-- ResolverUnavailable is returned only when no server can resolve.
-- A server that cannot resolve answers CMD UNKNOWN -- it predates RSLV (e.g.
-- an old official server), and the client degraded RSLV to a no-op below
-- namesSMPVersion so it never received the name -- or CMD PROHIBITED -- it
-- speaks RSLV but has no resolver configured (names role off), so it did
-- receive the name but cannot help. Either form may arrive directly or wrapped
-- by a proxy. We skip it and try the next server; ResolverUnavailable is
-- returned only when no server can resolve. A resolver-backed server's
-- transient failure is INTERNAL (-> ResolverTransport), not handled here.
isUnsupported = \case
SMP _ (SMP.CMD SMP.UNKNOWN) -> True
SMP _ (SMP.CMD SMP.PROHIBITED) -> True
+19 -1
View File
@@ -62,6 +62,15 @@ resolveNameTests = do
-- second server must NOT be consulted: definite error means the server
-- answered, so iterating would leak the queried name.
readIORef callsRef `shouldReturn` [srv1]
it "surfaces a resolver-backend INTERNAL as ResolverTransport, not NotFound" $ do
callsRef <- newIORef []
r <- iterateResolvers [srv1, srv2] (recording callsRef (\_ -> pure $ Left backendErr))
case r of
Left (ResolverTransport e) -> e `shouldBe` backendErr
other -> expectationFailure $ "expected ResolverTransport, got " <> show other
-- a backend failure on srv1 stops iteration (the name reached a capable
-- relay); it must not be reported as the authoritative NameNotRegistered.
readIORef callsRef `shouldReturn` [srv1]
it "iterates on transport-level errors and uses the next server's success" $ do
callsRef <- newIORef []
r <- iterateResolvers [srv1, srv2] (recording callsRef stubTransportThenHit)
@@ -230,7 +239,9 @@ sampleRecord =
authErr :: AgentErrorType
authErr = SMP "smp1.example" SMP.AUTH
-- CMD PROHIBITED on the PFWD path: surfaces as PROXY ... (ProxyProtocolError ...).
-- A relay with no resolver configured (names role off) answers CMD PROHIBITED
-- (Server.hs). A relay error is transparent over the proxy (SMP host ...); the
-- PROXY-wrapped form here exercises a proxy-level rejection. Both -> skip.
prohibitedErr :: AgentErrorType
prohibitedErr = PROXY "proxy.example" "smp1.example" (ProxyProtocolError (SMP.CMD SMP.PROHIBITED))
@@ -245,3 +256,10 @@ networkErr = BROKER "smp1.example" (NETWORK (NEConnectError "simulated network f
-- as ResolverTransport without iterating, to avoid broadcasting the name.
otherDefiniteErr :: AgentErrorType
otherDefiniteErr = INTERNAL "simulated definite error"
-- A resolver-backed relay whose backing store failed (resolver 5xx, timeout,
-- decode error) answers ERR INTERNAL (Server.hs), surfacing as SMP host INTERNAL.
-- This is transient -- it must surface as ResolverTransport, NOT collapse to the
-- authoritative NameNotRegistered the way the old ERR-AUTH-for-everything did.
backendErr :: AgentErrorType
backendErr = SMP "smp1.example" SMP.INTERNAL