extend SMP protocol to support name availability queries by labelhash (#1863)

* implement resolving 2LD names by labelhash

* tiny test addition

* simplify language

* report expiry and availability correctly

* compare block instead of wall clock

* simplify language

* simplify language

* map resolver 410 to NAME NOT_FOUND (a lapsed name is an answer, not a failure)

* split error into a machine-readable code and a human message

* resolver: reduce comments

* resolver: reduce comments, remove REVIEW.html

* extend SMP protocol to support name availability queries with accurate and meaningful replies

* review fixes

* more review fixes

* docs shortening and other review fixes

* add catch all for furture variants

* adversarial review (against simplex-chat) fix

* next iteration fixes

* adapt house style

* eth_call guard and cache constants

* revert NAVL command and wrap it all into RSLV

* doc fixes

* Update src/Simplex/Messaging/Server/Names.hs

Co-authored-by: Evgeny <evgeny@poberezkin.com>

* Update src/Simplex/Messaging/Protocol.hs

Co-authored-by: Evgeny <evgeny@poberezkin.com>

* Update src/Simplex/Messaging/Protocol.hs

Co-authored-by: Evgeny <evgeny@poberezkin.com>

* Update src/Simplex/Messaging/Protocol.hs

Co-authored-by: Evgeny <evgeny@poberezkin.com>

* Update src/Simplex/Messaging/Protocol.hs

Co-authored-by: Evgeny <evgeny@poberezkin.com>

* protocol types refactoring

* next iteration on types only

* rentPrices map

* claude answering ep review. to be continued...

* separate labelhash and plaintext names cleanly

* align implementation with latest type changes to test against client

* fix adversarial review findings

* trim diff

* align resolver with contract changes for names v2

* fix reverse compatibility with .testing mainnet

* fix more robustly

* NameQuery simplification

* revert drive-by refactoring

* implement full type change and introduce resolver endpoint versioning

* fix stale docs

* derive NameRegistration JSON the same way on every build

  sumTypeJSON switches to the _owsf form on swift builds, but this JSON is
  the RNAME payload and the resolver's HTTP contract, so a swift client and
  a Linux relay would disagree on every field. taggedObjectJSON is what it
  already resolves to everywhere else.

  The label modifier keeps the reservedReason_ collision escape out of the
  API, as AgentWorkersDetails does: both arms now say reservedReason.

* fix resolver boundary: status before body, cover /v2, drop dead field

  httpGet read the response body before checking the status, so an oversized
  error page surfaced as a transient "response too large" instead of the
  authoritative status. Reverting it to its previous shape restores that and
  removes the status test both callers had been re-deriving.

  registration() had no tests at all, though it is the endpoint SMP v22
  consumes. RegistrationV2Tests covers the three answer shapes, the error
  paths and the exact key set of each, which is the wire contract.

  auctionUntil was always None with no consumer. The spec claimed a reason
  word travels unchanged; a resolver can only send a word it has, and SNRC's
  registry records a number.

* fix review findings

* resolver errors say what went wrong, not "no such name"

  /v2/resolve answers 200, 400 or 502, and an unregistered name is
  NRAvailable, so no status means "not registered". Mapping 400/404/410 to
  NOT_FOUND made a misconfigured relay deny every name, and hid a relay
  upgraded ahead of its resolver. All three now surface as RESOLVER.

  rslvNotFound would have gone dead, so it counts what its name says: an
  availability answer the encoder downgrades for a session below v22. The
  wire is unchanged.

  A hashed query the registrar cannot name is refused with 502 rather than
  answered with a record named "unknown", which the client rejects anyway.
  NRRUnknown is capped to 32 printable characters again, as the spec says.
  A registered name that is also reserved no longer offers a date it will
  never free up on. rentPrices is registrationPrices throughout, and
  yearPriceUSD is USDCents rather than a bare Int64.

* fix regressions found reviewing the last two commits

  resolveNameMsg read the version off thParams', which on the PFWD path is
  the proxy's session, not the client's. It takes the version as an argument
  now, so each call site passes its own — the forwarded one uses fwdVersion.

  reservedReasonOf matched the reason words before capping, so "internal
  review" became NRRUnknown "internal", which encodes back as NRRInternal.
  Capping precedes the match, so what is kept encodes to what it decoded.

  Four agent tests still pinned NAME NOT_FOUND from a 404 stub, and two spec
  statements still described the old mapping. A registrar that does not
  record labels cannot answer a hashed query, which the resolver README now
  says.

* docs sweep

* simplify encoding

* drop resolver caching (#1866)

* rename

* remove trailing_ filter

* fix resolver v2 for subnames (#1867)

* fix resolver v2 for subnames

* simplify doc

* resolver should report response truth freshness (#1868)

* first shot at reporting freshness

* fix review findings

* renaming

---------

Co-authored-by: sh <github.shum@liber.li>
Co-authored-by: Evgeny <evgeny@poberezkin.com>
This commit is contained in:
brenzi
2026-09-16 09:58:44 +01:00
committed by GitHub
co-authored by Evgeny sh
parent 7f0218e1d5
commit ea43df2349
22 changed files with 2359 additions and 200 deletions
+3 -3
View File
@@ -228,7 +228,7 @@ import Simplex.Messaging.Protocol
ErrorType (AUTH),
MsgBody,
MsgFlags (..),
NameRecord,
NameResponse,
NtfServer,
ProtoServerWithAuth (..),
ProtocolServer (..),
@@ -461,7 +461,7 @@ getConnShortLink c = withAgentEnv c .:. getConnShortLink' c
-- | Resolve a SimpleX name (PFWD RSLV). The agent owns server selection: it
-- picks a names-capable server (ServerRoles.names) from the user's nameSrvs, so
-- chat clients just pass the parsed domain.
resolveSimplexName :: AgentClient -> NetworkRequestMode -> UserId -> SimplexDomain -> AE NameRecord
resolveSimplexName :: AgentClient -> NetworkRequestMode -> UserId -> SimplexDomain -> AE NameResponse
resolveSimplexName c nm userId domain = withAgentEnv c $ resolveSimplexName' c nm userId domain
{-# INLINE resolveSimplexName #-}
@@ -1270,7 +1270,7 @@ getConnShortLink' c nm userId = \case
deleteLocalInvShortLink' :: AgentClient -> ConnShortLink 'CMInvitation -> AM ()
deleteLocalInvShortLink' c (CSLInvitation _ srv linkId _) = withStore' c $ \db -> deleteInvShortLink db srv linkId
resolveSimplexName' :: AgentClient -> NetworkRequestMode -> UserId -> SimplexDomain -> AM NameRecord
resolveSimplexName' :: AgentClient -> NetworkRequestMode -> UserId -> SimplexDomain -> AM NameResponse
resolveSimplexName' c nm userId domain = do
resolverSrv <- getNextNameServer c userId
resolveName c nm userId resolverSrv domain
+2 -2
View File
@@ -272,7 +272,7 @@ import Simplex.Messaging.Protocol
NetworkError (..),
MsgFlags (..),
MsgId,
NameRecord,
NameResponse,
NtfServer,
NtfServerWithAuth,
ProtoServer,
@@ -2022,7 +2022,7 @@ getQueueLink c nm userId server lnkId =
-- resolver) and falls back to a direct send when the proxy is unavailable
-- (faster but exposes the client IP). Mode selection is delegated to
-- `sendOrProxySMPCommand`, which honours the network config (SPMNever etc.).
resolveName :: AgentClient -> NetworkRequestMode -> UserId -> SMPServer -> SimplexDomain -> AM NameRecord
resolveName :: AgentClient -> NetworkRequestMode -> UserId -> SMPServer -> SimplexDomain -> AM NameResponse
resolveName c nm userId server domain =
snd <$> sendOrProxySMPCommand c nm userId server "" "RSLV" NoEntity resolveViaProxy resolveDirectly
where
+14 -9
View File
@@ -166,7 +166,7 @@ import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON
import Simplex.Messaging.Protocol
import Simplex.Messaging.Protocol.Types
import Simplex.Messaging.Server.QueueStore.QueueInfo
import Simplex.Messaging.SimplexName (SimplexDomain)
import Simplex.Messaging.SimplexName (SimplexDomain, fullDomainName)
import Simplex.Messaging.TMap (TMap)
import qualified Simplex.Messaging.TMap as TM
import Simplex.Messaging.Transport
@@ -1054,11 +1054,11 @@ proxySMPMessage c nm proxiedRelay spKey sId flags msg = proxyOKSMPCommand c nm p
-- through `proxySMPCommand` and pattern-matches the expected RNAME response.
-- Version-gated on the destination relay (mirrors `connectSMPProxiedRelay`):
-- the client never sends RSLV to a relay that predates names support.
proxyResolveName :: SMPClient -> NetworkRequestMode -> ProxiedRelay -> SimplexDomain -> ExceptT SMPClientError IO (Either ProxyClientError NameRecord)
proxyResolveName :: SMPClient -> NetworkRequestMode -> ProxiedRelay -> SimplexDomain -> ExceptT SMPClientError IO (Either ProxyClientError NameResponse)
proxyResolveName c nm proxiedRelay name
| prVersion proxiedRelay >= namesSMPVersion =
proxySMPCommand c nm proxiedRelay Nothing NoEntity (RSLV name) >>= \case
Right (RNAME nr) -> pure $ Right nr
proxySMPCommand c nm proxiedRelay Nothing NoEntity (RSLV (NQDomain name)) >>= \case
Right (RNAME reg) | resolvedNameOrNotFound name reg -> pure $ Right reg
Right r -> throwE $ unexpectedResponse r
Left e -> pure $ Left e
| otherwise = throwE $ PCETransportError TEVersion
@@ -1066,16 +1066,21 @@ proxyResolveName c nm proxiedRelay name
-- | Direct (non-PFWD) name resolution. Exposes the client IP to the resolver;
-- callers that want anonymity should use `proxyResolveName` via the standard
-- proxy fallback in the agent. RSLV requires no entity ID or authorization
-- (see `noAuthCmd` in Protocol.hs). Version-gated on the session here, not the
-- encoder, so an old server never receives RSLV.
directResolveName :: SMPClient -> NetworkRequestMode -> SimplexDomain -> ExceptT SMPClientError IO NameRecord
-- (see `noAuthCmd` in Protocol.hs). Gated on the session version, below which
-- the server has no RSLV at all; the encoder gates the query format separately.
directResolveName :: SMPClient -> NetworkRequestMode -> SimplexDomain -> ExceptT SMPClientError IO NameResponse
directResolveName c nm name
| thVersion (thParams c) >= namesSMPVersion =
sendProtocolCommand c nm Nothing NoEntity (Cmd SResolver (RSLV name)) >>= \case
RNAME nr -> pure nr
sendProtocolCommand c nm Nothing NoEntity (Cmd SResolver (RSLV (NQDomain name))) >>= \case
RNAME reg | resolvedNameOrNotFound name reg -> pure reg
r -> throwE $ unexpectedResponse r
| otherwise = throwE $ PCETransportError TEVersion
resolvedNameOrNotFound :: SimplexDomain -> NameResponse -> Bool
resolvedNameOrNotFound d NameResponse {registration} = case registration of
NRRegistered {nameRecord} -> T.toLower (nrName nameRecord) == fullDomainName d
_ -> True
-- | Acknowledge message delivery (server deletes the message).
--
-- https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#acknowledge-message-delivery
+99 -1
View File
@@ -1,3 +1,6 @@
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
@@ -5,13 +8,24 @@
module Simplex.Messaging.Names.Record
( NameRecord (..),
NameResponse (..),
NameRegistration (..),
NamePricing (..),
USDCents (..),
NameReservedReason (..),
)
where
import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Aeson as J
import qualified Data.Aeson.TH as JQ
import Data.Int (Int64)
import Data.Map.Strict (Map)
import Data.Text (Text)
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix)
import qualified Data.Text as T
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, taggedObjectJSON)
import Simplex.Messaging.SystemTime (SystemSeconds)
-- | Resolved name record returned by the names role. JSON keys match the
-- resolver REST output; both FromJSON (resolver -> server) and ToJSON
@@ -44,3 +58,87 @@ $( JQ.deriveJSON
defaultJSON {J.omitNothingFields = False, J.fieldLabelModifier = dropPrefix "nr"}
''NameRecord
)
-- | US cents.
newtype USDCents = USDCents Int64
deriving (Eq, Ord, Show)
deriving newtype (ToJSON, FromJSON)
-- | What the registry holds for a name, and the block it was read at.
data NameResponse = NameResponse
{ -- | absent only from a v20/v21 router, which sent the record alone
lastBlockTs :: Maybe SystemSeconds,
registration :: NameRegistration
}
deriving (Eq, Show)
-- | What the registry holds for a name.
data NameRegistration
= -- | Held by someone. Always carries a record, empty where none was set.
NRRegistered
{ -- | absent only from a v20/v21 router, which sent the record alone
expires :: Maybe SystemSeconds,
-- | unix seconds, > expires: until here only the owner may renew
graceUntil :: Maybe SystemSeconds,
-- | held back as well, which is why it will not free up at expiry
reservedReason_ :: Maybe NameReservedReason,
nameRecord :: NameRecord
}
| -- | Held by nobody, and registrable now.
NRAvailable {pricing :: NamePricing}
| -- | Held back by the registry, and not for sale at its price.
NRReserved {reservedReason :: NameReservedReason}
deriving (Eq, Show)
-- | Enough to price the name locally, which the router cannot do behind a hash.
data NamePricing = NamePricing
{ -- | US cents per year, for the lengths the registry prices specially
registrationPrices :: Map Int USDCents,
-- | US cents per year for every other length
basePrice :: USDCents,
-- | characters; the registry refuses shorter labels
minLabelLength :: Int
}
deriving (Eq, Show)
-- | Why the registry holds a name back.
data NameReservedReason
= -- | held for SimpleX
NRRInternal
| NRRTrademark
| NRRCommunity
| -- | added to the registry after this version, and still reserved
NRRUnknown Text
deriving (Eq, Show)
instance TextEncoding NameReservedReason where
textEncode = \case
NRRInternal -> "internal"
NRRTrademark -> "trademark"
NRRCommunity -> "community"
NRRUnknown t -> t
textDecode = Just . reservedReasonOf
-- | An unknown reason is kept as text, capped: it reaches a client as a word.
-- Capping precedes the match, so what is kept encodes back to what it decoded.
reservedReasonOf :: Text -> NameReservedReason
reservedReasonOf t = case T.take 32 $ T.takeWhile (\c -> c > ' ' && c < '\DEL') t of
"internal" -> NRRInternal
"trademark" -> NRRTrademark
"community" -> NRRCommunity
r -> NRRUnknown r
instance ToJSON NameReservedReason where
toJSON = textToJSON
toEncoding = textToEncoding
instance FromJSON NameReservedReason where
parseJSON = textParseJSON "NameReservedReason"
$(JQ.deriveJSON defaultJSON ''NamePricing)
-- taggedObjectJSON, not sumTypeJSON: this JSON is the RNAME payload and the
-- resolver contract, so it must not vary with the swift build flag.
$(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "NR") ''NameRegistration)
$(JQ.deriveJSON defaultJSON ''NameResponse)
+43 -11
View File
@@ -80,6 +80,12 @@ module Simplex.Messaging.Protocol
ErrorType (..),
CommandError (..),
ProxyError (..),
NameQuery (..),
NameResponse (..),
NameRegistration (..),
NamePricing (..),
USDCents (..),
NameReservedReason (..),
NameErrorType (..),
BrokerErrorType (..),
NetworkError (..),
@@ -265,12 +271,12 @@ import Simplex.Messaging.Agent.Store.DB (Binary (..), FromField (..), ToField (.
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Names.Record (NameRecord (..))
import Simplex.Messaging.Names.Record
import Simplex.Messaging.Parsers
import Simplex.Messaging.Protocol.Types
import Simplex.Messaging.Server.QueueStore.QueueInfo
import Simplex.Messaging.ServiceScheme
import Simplex.Messaging.SimplexName (SimplexDomain)
import Simplex.Messaging.SimplexName (LabelHash, SimplexDomain (..), SimplexTLD (..), fullDomainName, labelHash)
import Simplex.Messaging.Transport
import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts (..))
import Simplex.Messaging.Util (bshow, eitherToMaybe, safeDecodeUtf8, (<$?>))
@@ -603,7 +609,7 @@ data Command (p :: Party) where
-- - corrId: unique correlation ID between proxy and relay, also used as a nonce to encrypt forwarded transmission
RFWD :: EncFwdTransmission -> Command ProxyService -- use CorrId as CbNonce, proxy to relay
-- Resolve SimpleX name.
RSLV :: SimplexDomain -> Command Resolver
RSLV :: NameQuery -> Command Resolver
deriving instance Show (Command p)
@@ -739,8 +745,8 @@ data BrokerMsg where
OK :: BrokerMsg
ERR :: ErrorType -> BrokerMsg
PONG :: BrokerMsg
-- Resolved SimpleX name.
RNAME :: NameRecord -> BrokerMsg
-- What the router knows about a SimpleX name.
RNAME :: NameResponse -> BrokerMsg
deriving (Eq, Show)
data RcvMessage = RcvMessage
@@ -1589,11 +1595,28 @@ data ErrorType
DUPLICATE_ -- not part of SMP protocol, used internally
deriving (Eq, Show)
-- | What RSLV asks about: a name, or the hash of a second-level label.
data NameQuery = NQDomain SimplexDomain | NQHash LabelHash SimplexTLD
deriving (Eq, Show)
instance Encoding NameQuery where
smpEncode = \case
NQDomain d -> encodeUtf8 $ fullDomainName d
NQHash h tld -> strEncode h <> strEncode tld
smpP = NQHash <$> strP <*> strP <|> NQDomain <$> strP
-- | Hashed from v22, except a name with subnames or a web TLD.
hashedQuery :: NameQuery -> NameQuery
hashedQuery q = case q of
NQDomain SimplexDomain {nameTLD, domain, subDomain}
| null subDomain && nameTLD /= TLDWeb -> NQHash (labelHash domain) nameTLD
_ -> q
-- | Name resolution error
data NameErrorType
= -- | the names role / resolver is not configured on this server
NO_RESOLVER
| -- | the name is not registered (resolver returned not-found)
| -- | the name does not resolve; sent only to a session below v22
NOT_FOUND
| -- | backing resolver/RPC failure - contains the diagnostic detail
RESOLVER {resolverErr :: Text}
@@ -1822,7 +1845,7 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where
PRXY host auth_ -> e (PRXY_, ' ', host, auth_)
PFWD fwdV pubKey (EncTransmission s) -> e (PFWD_, ' ', fwdV, pubKey, Tail s)
RFWD (EncFwdTransmission s) -> e (RFWD_, ' ', Tail s)
RSLV d -> e (RSLV_, ' ', d)
RSLV q -> e (RSLV_, ' ', if v >= nameAvailSMPVersion then hashedQuery q else q)
where
e :: Encoding a => a -> ByteString
e = smpEncode
@@ -1929,7 +1952,7 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where
CT SNotifierService NSUBS_
| v >= rcvServiceSMPVersion -> Cmd SNotifierService <$> (NSUBS <$> _smpP <*> smpP)
| otherwise -> pure $ Cmd SNotifierService $ NSUBS (-1) mempty
CT SResolver RSLV_ -> Cmd SResolver . RSLV <$> _smpP <* A.takeByteString
CT SResolver RSLV_ -> Cmd SResolver . RSLV <$> _smpP
fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg
{-# INLINE fromProtocolError #-}
@@ -1972,7 +1995,11 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
| v < clientNoticesSMPVersion -> BLOCKED info {notice = Nothing}
_ -> err
PONG -> e PONG_
RNAME rec -> e (RNAME_, ' ', Tail $ LB.toStrict $ J.encode rec)
RNAME res
| v >= nameAvailSMPVersion -> e (RNAME_, ' ', Tail $ LB.toStrict $ J.encode res)
| otherwise -> case registration res of
NRRegistered {nameRecord} -> e (RNAME_, ' ', Tail $ LB.toStrict $ J.encode nameRecord)
_ -> e (ERR_, ' ', NAME NOT_FOUND)
where
e :: Encoding a => a -> ByteString
e = smpEncode
@@ -2019,8 +2046,12 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
OK_ -> pure OK
ERR_ -> ERR <$> _smpP
PONG_ -> pure PONG
RNAME_ -> fmap RNAME . J.eitherDecodeStrict . unTail <$?> _smpP
RNAME_
| v >= nameAvailSMPVersion -> fmap RNAME . J.eitherDecodeStrict . unTail <$?> _smpP
| otherwise -> fmap (RNAME . oldResponse) . J.eitherDecodeStrict . unTail <$?> _smpP
where
oldResponse nameRecord =
NameResponse {lastBlockTs = Nothing, registration = NRRegistered {expires = Nothing, graceUntil = Nothing, reservedReason_ = Nothing, nameRecord}}
serviceRespP resp
| v >= rcvServiceSMPVersion = resp <$> _smpP <*> smpP
| otherwise = resp <$> _smpP <*> pure mempty
@@ -2042,7 +2073,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
PKEY {} -> noEntityMsg
RRES _ -> noEntityMsg
ALLS -> noEntityMsg
RNAME _ -> noEntityMsg
RNAME {} -> noEntityMsg
-- other broker responses must have queue ID
_
| B.null entId -> Left $ CMD NO_ENTITY
@@ -2412,3 +2443,4 @@ $(J.deriveJSON defaultJSON ''BlockingInfo)
-- run deriveJSON in one TH splice to allow mutual instance
$(concat <$> mapM @[] (J.deriveJSON (sumTypeJSON id)) [''ProxyError, ''NameErrorType, ''ErrorType])
+11 -8
View File
@@ -104,7 +104,6 @@ import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol
import Simplex.Messaging.SimplexName (SimplexDomain)
import Simplex.Messaging.Server.Control
import Simplex.Messaging.Server.Env.STM as Env
import Simplex.Messaging.Server.Expiration
@@ -1494,15 +1493,19 @@ client
Just nenv -> pure (Just nenv)
-- Runs on a forked thread so RSLV does not block other commands;
-- concurrency is limited by serverResolverConcurrency in forkCmd.
resolveNameMsg :: NamesEnv -> SimplexDomain -> M s BrokerMsg
resolveNameMsg nenv d = do
resolveNameMsg :: VersionSMP -> NamesEnv -> NameQuery -> M s BrokerMsg
resolveNameMsg v nenv q = do
st <- asks (rslvStats . serverStats)
(selector, msg) <-
liftIO (resolveName nenv d) <&> \case
Right rec -> (rslvSucc, RNAME rec)
Left e@NOT_FOUND -> (rslvNotFound, ERR $ NAME e)
liftIO (resolveName nenv q) <&> \case
Right res -> (if answered (registration res) then rslvSucc else rslvNotFound, RNAME res)
Left e -> (rslvResolverErrs, ERR $ NAME e)
incStat (selector st) $> msg
where
-- below v22 the encoder answers anything but a record as NAME NOT_FOUND
answered = \case
NRRegistered {} -> True
_ -> v >= nameAvailSMPVersion
transportErr :: TransportError -> ErrorType
transportErr = PROXY . BROKER . TRANSPORT
mkIncProxyStats :: MonadIO m => ProxyStats -> ProxyStats -> OwnServer -> (ProxyStats -> IORef Int) -> m ()
@@ -1519,7 +1522,7 @@ client
Cmd SProxyService (RFWD encBlock) -> (response . (corrId, NoEntity,) =<<) <$> processForwardedCommand encBlock
Cmd SResolver (RSLV d) -> rslvNamesEnv >>= \case
Nothing -> pure $ response (corrId, NoEntity, ERR (NAME NO_RESOLVER))
Just nenv -> forkCmd serverResolverConcurrency corrId NoEntity (resolveNameMsg nenv d)
Just nenv -> forkCmd serverResolverConcurrency corrId NoEntity (resolveNameMsg (thVersion thParams') nenv d)
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
@@ -2150,7 +2153,7 @@ client
Cmd SResolver (RSLV d) -> lift $ rslvNamesEnv >>= \case
Nothing -> pure $ Just (corrId', entId', ERR (NAME NO_RESOLVER))
Just nenv -> forkCmd serverResolverConcurrency corrId NoEntity $ do
msg <- resolveNameMsg nenv d
msg <- resolveNameMsg (thVersion clntTHParams) nenv d
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.
+9 -10
View File
@@ -20,7 +20,9 @@ import Control.Logger.Simple (logError)
import Data.Bifunctor (first)
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import Simplex.Messaging.Protocol (NameErrorType (..), NameRecord)
import Data.Text.Encoding (decodeLatin1)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Protocol (NameErrorType (..), NameQuery, NameResponse)
import Simplex.Messaging.Server.Names.HttpResolver
( ResolverEnv,
ResolverError (..),
@@ -30,7 +32,6 @@ import Simplex.Messaging.Server.Names.HttpResolver
newResolverEnv,
resolveHttp,
)
import Simplex.Messaging.SimplexName (SimplexDomain, fullDomainName)
import System.Timeout (timeout)
data NamesConfig = NamesConfig
@@ -58,9 +59,9 @@ pingEndpoint :: NamesEnv -> IO (Either ResolverError ())
pingEndpoint NamesEnv {resolverEnv, config} =
fromMaybe (Left ResolverTimeout) <$> timeout (resolverTimeoutMs config * 1000) (healthHttp resolverEnv)
resolveName :: NamesEnv -> SimplexDomain -> IO (Either NameErrorType NameRecord)
resolveName env d = do
r <- E.try (timeout (resolverTimeoutMs (config env) * 1000) (fetch env d))
resolveName :: NamesEnv -> NameQuery -> IO (Either NameErrorType NameResponse)
resolveName env q = do
r <- E.try (timeout (resolverTimeoutMs (config env) * 1000) (fetch env q))
case r of
Right result -> pure (fromMaybe (Left (RESOLVER "timeout")) result)
Left e
@@ -69,14 +70,12 @@ resolveName env d = do
logError $ "[NAMES] resolver fetch raised " <> T.pack (E.displayException e)
pure (Left (RESOLVER "resolver error"))
fetch :: NamesEnv -> SimplexDomain -> IO (Either NameErrorType NameRecord)
fetch NamesEnv {resolverEnv} d =
first mapResolverError <$> resolveHttp resolverEnv (fullDomainName d)
fetch :: NamesEnv -> NameQuery -> IO (Either NameErrorType NameResponse)
fetch NamesEnv {resolverEnv} q =
first mapResolverError <$> resolveHttp resolverEnv (decodeLatin1 $ smpEncode q)
mapResolverError :: ResolverError -> NameErrorType
mapResolverError = \case
HttpStatusErr 404 -> NOT_FOUND
HttpStatusErr 400 -> NOT_FOUND
HttpStatusErr code -> RESOLVER ("HTTP " <> T.pack (show code))
HttpFailure _ -> RESOLVER "transport failure"
BodyTooLarge -> RESOLVER "response too large"
@@ -8,10 +8,12 @@
--
-- The Python REST resolver (see scripts/resolver/snrc-resolve.py) exposes
--
-- GET /resolve/<name> -> 200 with a NameRecord JSON document
-- 404 / 400 for unknown names / TLDs
-- 502 for upstream RPC failures
-- GET /health -> 200 when the resolver process is ready
-- GET /v2/resolve/<query> -> 200 with a NameRegistration JSON document, for
-- all three registration shapes; 400 for unknown
-- TLDs, 502 for upstream RPC failures
-- GET /v1/resolve/<name> -> 200 with a NameRecord, what relays before SMP
-- v22 call as /resolve
-- GET /health -> 200 when the resolver process is ready
--
-- Boundary properties:
-- * Response body read with `brReadSome maxResponseBytes` — adversarial
@@ -57,7 +59,7 @@ import qualified Network.HTTP.Client as HC
import Network.HTTP.Client.TLS (tlsManagerSettings)
import qualified Network.HTTP.Types as HT
import Network.HTTP.Types.URI (urlEncode)
import Simplex.Messaging.Names.Record (NameRecord)
import Simplex.Messaging.Names.Record (NameResponse)
data RpcAuth = AuthBearer Text | AuthBasic Text Text
@@ -108,14 +110,12 @@ authHeader = \case
let encoded = BAE.convertToBase BAE.Base64 (encodeUtf8 u <> ":" <> encodeUtf8 p) :: ByteString
in ("Authorization", "Basic " <> encoded)
-- | GET <baseUrl>/resolve/<percent-encoded name>, decoding the 200 body
-- directly into a NameRecord in one pass (no intermediate Aeson Value). The
-- name is percent-encoded (every non-unreserved byte per RFC 3986): the
-- resolver expects raw labels, so slashes/punctuation must not alter the path.
resolveHttp :: ResolverEnv -> Text -> IO (Either ResolverError NameRecord)
resolveHttp env name =
-- | The query is a name or a bracketed label hash, percent-encoded (every
-- non-unreserved byte per RFC 3986) so it cannot alter the path.
resolveHttp :: ResolverEnv -> Text -> IO (Either ResolverError NameResponse)
resolveHttp env q =
(>>= first InvalidJson . J.eitherDecodeStrict . BL.toStrict)
<$> httpGet env ("/resolve/" <> B.unpack (urlEncode True (encodeUtf8 name)))
<$> httpGet env ("/v2/resolve/" <> B.unpack (urlEncode True (encodeUtf8 q)))
-- | GET <baseUrl>/health; success = reachable with status < 400. The body is
-- size-capped but NOT decoded — the probe only checks reachability.
+2 -2
View File
@@ -469,11 +469,11 @@ prometheusMetrics sm rtm ts =
\# TYPE simplex_smp_names_reqs counter\n\
\simplex_smp_names_reqs " <> mshow _rslvReqs <> "\n# rslvReqs\n\
\\n\
\# HELP simplex_smp_names_success NameRecord successfully resolved and returned.\n\
\# HELP simplex_smp_names_success NameRecord resolved, or availability answered.\n\
\# TYPE simplex_smp_names_success counter\n\
\simplex_smp_names_success " <> mshow _rslvSucc <> "\n# rslvSucc\n\
\\n\
\# HELP simplex_smp_names_not_found Name not registered (resolver returned 404 / 400).\n\
\# HELP simplex_smp_names_not_found Answers a client below v22 reads as NOT_FOUND.\n\
\# TYPE simplex_smp_names_not_found counter\n\
\simplex_smp_names_not_found " <> mshow _rslvNotFound <> "\n# rslvNotFound\n\
\\n\
+30 -6
View File
@@ -10,14 +10,21 @@ module Simplex.Messaging.SimplexName
SimplexTLD (..),
SimplexNameType (..),
fullDomainName,
LabelHash (..),
labelHash,
boundedNonSpace,
shortNameInfoStr,
)
where
import Control.Applicative (optional, (<|>))
import Crypto.Hash (Digest, hash)
import Crypto.Hash.Algorithms (Keccak_256)
import qualified Data.Aeson.TH as J
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.Attoparsec.Text as AT
import qualified Data.ByteArray as BA
import qualified Data.ByteArray.Encoding as BAE
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Char (isDigit)
@@ -70,6 +77,20 @@ nameLabelP = do
-- (Cyrillic а vs ASCII a hash to different on-chain records).
isNameLetter c = c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
-- | The registry's key for a label: 32 bytes, as labelOf takes it.
newtype LabelHash = LabelHash ByteString
deriving (Eq, Show)
-- | keccak-256 of the lowercased label, as the registry keys it.
labelHash :: Text -> LabelHash
labelHash label = LabelHash $ BA.convert (hash (encodeUtf8 (T.toLower label)) :: Digest Keccak_256)
instance StrEncoding LabelHash where
strEncode (LabelHash h) = '[' `B.cons` (BAE.convertToBase BAE.Base16 h `B.snoc` ']')
strP = do
h <- BAE.convertFromBase BAE.Base16 <$?> (A.char '[' *> A.takeWhile (/= ']') <* A.char ']')
if B.length h == 32 then pure $ LabelHash h else fail "bad LabelHash"
-- | Cap the name at 253 bytes (DNS full-domain limit)
boundedNonSpace :: A.Parser ByteString
boundedNonSpace = do
@@ -108,12 +129,15 @@ instance Encoding SimplexDomain where
smpP = strP
fullDomainName :: SimplexDomain -> Text
fullDomainName SimplexDomain {nameTLD, domain, subDomain} = T.intercalate "." (reverse subDomain ++ [domain] ++ tld')
where
tld' = case nameTLD of
TLDSimplex -> ["simplex"]
TLDTesting -> ["testing"]
TLDWeb -> []
fullDomainName SimplexDomain {nameTLD, domain, subDomain} = T.intercalate "." (reverse subDomain ++ [domain]) <> decodeLatin1 (strEncode nameTLD)
instance StrEncoding SimplexTLD where
strEncode = \case
TLDSimplex -> ".simplex"
TLDTesting -> ".testing"
TLDWeb -> ""
strP =
".simplex" $> TLDSimplex <|> ".testing" $> TLDTesting <|> pure TLDWeb
shortNameInfoStr :: SimplexNameInfo -> Text
shortNameInfoStr = \case
+14 -7
View File
@@ -53,6 +53,7 @@ module Simplex.Messaging.Transport
rcvServiceSMPVersion,
namesSMPVersion,
serverInfoSMPVersion,
nameAvailSMPVersion,
simplexMQVersion,
smpBlockSize,
TransportConfig (..),
@@ -175,6 +176,7 @@ smpBlockSize = 16384
-- 19 - service subscriptions to messages (10/20/2025)
-- 20 - public namespaces resolver, RSLV command (6/20/2026)
-- 21 - server public information in handshake (7/5/2026)
-- 22 - RNAME answers name availability as well as the record (7/25/2026)
data SMPVersion
@@ -211,6 +213,11 @@ namesSMPVersion = VersionSMP 20
serverInfoSMPVersion :: VersionSMP
serverInfoSMPVersion = VersionSMP 21
-- | RNAME carries availability. A server below this answers RSLV with the
-- record alone, and ERR NAME NOT_FOUND for a name that does not resolve.
nameAvailSMPVersion :: VersionSMP
nameAvailSMPVersion = VersionSMP 22
minClientSMPRelayVersion :: VersionSMP
minClientSMPRelayVersion = VersionSMP 14
@@ -218,20 +225,20 @@ minServerSMPRelayVersion :: VersionSMP
minServerSMPRelayVersion = VersionSMP 14
currentClientSMPRelayVersion :: VersionSMP
currentClientSMPRelayVersion = VersionSMP 21
currentClientSMPRelayVersion = VersionSMP 22
currentServerSMPRelayVersion :: VersionSMP
currentServerSMPRelayVersion = VersionSMP 21
currentServerSMPRelayVersion = VersionSMP 22
-- Max SMP protocol version to be used in e2e encrypted connection between
-- client and server, as defined by SMP proxy. Normally set below the current
-- version to prevent client version fingerprinting by the destination relays
-- when clients upgrade at different times. Pinned to the current version (20)
-- for this release because proxied name resolution is gated on namesSMPVersion
-- (20), so the one-version anti-fingerprinting buffer does not apply yet; it
-- reappears once the current version advances past 20.
-- when clients upgrade at different times. Pinned to the current version (22)
-- for this release because a proxied RSLV only carries availability from
-- nameAvailSMPVersion (22), so the one-version anti-fingerprinting buffer does
-- not apply yet; it reappears once the current version advances past 22.
proxiedSMPRelayVersion :: VersionSMP
proxiedSMPRelayVersion = VersionSMP 20
proxiedSMPRelayVersion = VersionSMP 22
-- minimal supported protocol version is 14
supportedClientSMPRelayVRange :: VersionRangeSMP