names: ROWN, the names an address owns

A recovered key has to be scanned for the accounts it already uses, which
needs a question the protocol could not ask: what does this address own.

ROWN carries one address and a page offset and is answered with ROWND,
from nameOwnedSMPVersion. The agent returns the relay it used so a scan
can ask the next account elsewhere: sending every account to one relay
would tell it they belong to one wallet.

inUse is the answer the scan acts on. Holding a name is only one way for
an account to be in use, so the resolver reports the nonce and balance it
is derived from; a scan that reads names alone hands out an account its
owner is already using.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WHaVjENni2mwBUL77SwFvr
This commit is contained in:
Alain BrenzikoferandClaude Opus 5 committed 2026-09-22 09:19:47 +00:00
1 parent 5dc64f1cf4
commit f53a6f3364
12 files changed
+226 -10

No files matched your search

+14
View File
@@ -67,6 +67,7 @@ module Simplex.Messaging.Agent
deleteConnShortLink,
getConnShortLink,
resolveSimplexName,
ownedSimplexNames,
getConnLinkPrivKey,
deleteLocalInvShortLink,
changeConnectionUser,
@@ -222,6 +223,8 @@ import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Notifications.Protocol (DeviceToken, NtfRegCode (NtfRegCode), NtfTknStatus (..), NtfTokenId, PNMessageData (..), pnMessagesP)
import Simplex.Messaging.Notifications.Types
import Simplex.Messaging.Parsers (defaultJSON, parse)
import Simplex.Messaging.Eth.Address (Address)
import Simplex.Messaging.Names.Record (OwnedNames)
import Simplex.Messaging.Protocol
( BrokerMsg,
Cmd (..),
@@ -465,6 +468,12 @@ resolveSimplexName :: AgentClient -> NetworkRequestMode -> UserId -> SimplexDoma
resolveSimplexName c nm userId domain = withAgentEnv c $ resolveSimplexName' c nm userId domain
{-# INLINE resolveSimplexName #-}
-- | Names an address owns. The relay used is returned so a scan can pass it
-- back as used and ask the next account elsewhere.
ownedSimplexNames :: AgentClient -> NetworkRequestMode -> UserId -> [SMPServer] -> Address -> Word32 -> AE (SMPServer, OwnedNames)
ownedSimplexNames c nm userId used addr offset = withAgentEnv c $ ownedSimplexNames' c nm userId used addr offset
{-# INLINE ownedSimplexNames #-}
getConnLinkPrivKey :: AgentClient -> ConnId -> AE (Maybe C.PrivateKeyEd25519)
getConnLinkPrivKey c = withAgentEnv c . getConnLinkPrivKey' c
{-# INLINE getConnLinkPrivKey #-}
@@ -1270,6 +1279,11 @@ getConnShortLink' c nm userId = \case
deleteLocalInvShortLink' :: AgentClient -> ConnShortLink 'CMInvitation -> AM ()
deleteLocalInvShortLink' c (CSLInvitation _ srv linkId _) = withStore' c $ \db -> deleteInvShortLink db srv linkId
ownedSimplexNames' :: AgentClient -> NetworkRequestMode -> UserId -> [SMPServer] -> Address -> Word32 -> AM (SMPServer, OwnedNames)
ownedSimplexNames' c nm userId used addr offset = do
srv <- getNextNameServerAvoiding c userId used
(srv,) <$> ownedNames c nm userId srv addr offset
resolveSimplexName' :: AgentClient -> NetworkRequestMode -> UserId -> SimplexDomain -> AM NameResponse
resolveSimplexName' c nm userId domain = do
resolverSrv <- getNextNameServer c userId
+29
View File
@@ -70,6 +70,8 @@ module Simplex.Messaging.Agent.Client
secureGetQueueLink,
getQueueLink,
resolveName,
ownedNames,
getNextNameServerAvoiding,
getNextNameServer,
enableQueueNotifications,
EnableQueueNtfReq (..),
@@ -263,6 +265,8 @@ import Simplex.Messaging.Notifications.Protocol
import Simplex.Messaging.Notifications.Transport (NTFVersion)
import Simplex.Messaging.Notifications.Types
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parse, sumTypeJSON)
import Simplex.Messaging.Eth.Address (Address)
import Simplex.Messaging.Names.Record (OwnedNames)
import Simplex.Messaging.Protocol
( AProtocolType (..),
BrokerMsg,
@@ -2029,6 +2033,31 @@ resolveName c nm userId server domain =
resolveViaProxy smp proxySess = proxyResolveName smp nm proxySess domain
resolveDirectly smp = directResolveName smp nm domain
-- | Names an address owns, from one names-capable relay. Mirrors resolveName:
-- proxied where the network config allows it, direct otherwise.
ownedNames :: AgentClient -> NetworkRequestMode -> UserId -> SMPServer -> Address -> Word32 -> AM OwnedNames
ownedNames c nm userId server addr offset =
snd <$> sendOrProxySMPCommand c nm userId server "" "ROWN" NoEntity ownedViaProxy ownedDirectly
where
ownedViaProxy smp proxySess = proxyOwnedNames smp nm proxySess addr offset
ownedDirectly smp = directOwnedNames smp nm addr offset
-- | A names-capable server the caller has not used yet. A scan asks about one
-- account per relay, because sending the whole list to one relay would tell it
-- those accounts belong to one wallet. With fewer relays than accounts the
-- choice wraps onto used ones, which is the best the configured set allows.
getNextNameServerAvoiding :: AgentClient -> UserId -> [SMPServer] -> AM SMPServer
getNextNameServerAvoiding c userId used =
liftIO (TM.lookupIO userId (userServers c :: TMap UserId (UserServers 'PSMP))) >>= \case
Just UserServers {nameSrvs} ->
let unused = filter ((`notElem` used) . protoServer . snd) nameSrvs
in case L.nonEmpty unused of
Just srvs -> protoServer <$> pickServer srvs
Nothing -> case L.nonEmpty nameSrvs of
Just srvs -> protoServer <$> pickServer srvs
Nothing -> throwE NO_NAME_SERVERS
Nothing -> throwE $ INTERNAL "unknown userId - no user servers"
-- | 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.
+25
View File
@@ -75,6 +75,8 @@ module Simplex.Messaging.Client
proxySMPMessage,
proxyResolveName,
directResolveName,
proxyOwnedNames,
directOwnedNames,
forwardSMPTransmission,
getSMPQueueInfo,
sendProtocolCommand,
@@ -163,6 +165,9 @@ import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON)
import Data.Word (Word32)
import Simplex.Messaging.Eth.Address (Address)
import Simplex.Messaging.Names.Record (OwnedNames)
import Simplex.Messaging.Protocol
import Simplex.Messaging.Protocol.Types
import Simplex.Messaging.Server.QueueStore.QueueInfo
@@ -1076,6 +1081,26 @@ 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.
proxyOwnedNames :: SMPClient -> NetworkRequestMode -> ProxiedRelay -> Address -> Word32 -> ExceptT SMPClientError IO (Either ProxyClientError OwnedNames)
proxyOwnedNames c nm proxiedRelay addr offset
| prVersion proxiedRelay >= nameOwnedSMPVersion =
proxySMPCommand c nm proxiedRelay Nothing NoEntity (ROWN addr offset) >>= \case
Right (ROWND owned) -> pure $ Right owned
Right r -> throwE $ unexpectedResponse r
Left e -> pure $ Left e
| otherwise = throwE $ PCETransportError TEVersion
-- | Direct (non-PFWD) owned-names lookup, exposing the client IP.
directOwnedNames :: SMPClient -> NetworkRequestMode -> Address -> Word32 -> ExceptT SMPClientError IO OwnedNames
directOwnedNames c nm addr offset
| thVersion (thParams c) >= nameOwnedSMPVersion =
sendProtocolCommand c nm Nothing NoEntity (Cmd SResolver (ROWN addr offset)) >>= \case
ROWND owned -> pure owned
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
+8
View File
@@ -19,8 +19,10 @@ import Data.Char (isDigit, isHexDigit, isLower, isUpper, toLower)
import Data.Word (Word32, Word8)
import Simplex.Messaging.Crypto.BIP32 (hardened)
import qualified Simplex.Messaging.Crypto.Secp256k1 as S
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Eth.Keccak (keccak256)
import Simplex.Messaging.Util ((<$?>))
-- | A 20-byte Ethereum address. 'Show' renders the EIP-55 checksummed form, as pasted into a block explorer.
newtype Address = Address ByteString
@@ -30,6 +32,12 @@ instance Show Address where
show = BC.unpack . checksumAddress
-- | EIP-55 checksummed hex. Parsing accepts bare or @0x@-prefixed hex and verifies a mixed-case checksum.
-- | On the wire the address is its EIP-55 hex, length-prefixed, so a field
-- following it is not swallowed by the hex parser.
instance Encoding Address where
smpEncode = smpEncode . strEncode
smpP = strDecode <$?> smpP
instance StrEncoding Address where
strEncode = checksumAddress
strP = do
+29
View File
@@ -13,6 +13,8 @@ module Simplex.Messaging.Names.Record
NamePricing (..),
USDCents (..),
NameReservedReason (..),
OwnedNames (..),
OwnedName (..),
)
where
@@ -90,6 +92,29 @@ data NameRegistration
NRReserved {reservedReason :: NameReservedReason}
deriving (Eq, Show)
-- | What the registry holds for an address: the names it owns, and whether
-- the account has been used at all. Holding a name is only one way to be in
-- use, so a recovery scan that reads only `ownNames` hands out an account its
-- owner is already using.
data OwnedNames = OwnedNames
{ ownNames :: [OwnedName],
ownInUse :: Bool,
-- | the cursor to resume from, absent when the listing is complete
ownNextOffset :: Maybe Int
}
deriving (Eq, Show)
-- | One name an address holds. Enumeration is not maintained on expiry, so a
-- lapsed name stays listed; `onStatus` is how a caller tells it apart.
data OwnedName = OwnedName
{ -- | absent when the registrar never recorded the label
onName :: Maybe Text,
onLabelhash :: Text,
onExpires :: SystemSeconds,
onStatus :: Text
}
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
@@ -135,6 +160,10 @@ instance ToJSON NameReservedReason where
instance FromJSON NameReservedReason where
parseJSON = textParseJSON "NameReservedReason"
$(JQ.deriveJSON defaultJSON {J.fieldLabelModifier = dropPrefix "on"} ''OwnedName)
$(JQ.deriveJSON defaultJSON {J.fieldLabelModifier = dropPrefix "own"} ''OwnedNames)
$(JQ.deriveJSON defaultJSON ''NamePricing)
-- taggedObjectJSON, not sumTypeJSON: this JSON is the RNAME payload and the
+18 -1
View File
@@ -261,7 +261,7 @@ import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
import Data.Time.Clock.System (SystemTime (..), systemToUTCTime)
import Data.Type.Equality
import Data.Word (Word8, Word16)
import Data.Word (Word8, Word16, Word32)
import GHC.TypeLits (ErrorMessage (..), TypeError, type (+))
import qualified GHC.TypeLits as TE
import qualified GHC.TypeLits as Type
@@ -271,6 +271,7 @@ 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.Eth.Address (Address)
import Simplex.Messaging.Names.Record
import Simplex.Messaging.Parsers
import Simplex.Messaging.Protocol.Types
@@ -610,6 +611,8 @@ data Command (p :: Party) where
RFWD :: EncFwdTransmission -> Command ProxyService -- use CorrId as CbNonce, proxy to relay
-- Resolve SimpleX name.
RSLV :: NameQuery -> Command Resolver
-- Names an address owns, and whether the account has been used at all.
ROWN :: Address -> Word32 -> Command Resolver
deriving instance Show (Command p)
@@ -747,6 +750,8 @@ data BrokerMsg where
PONG :: BrokerMsg
-- What the router knows about a SimpleX name.
RNAME :: NameResponse -> BrokerMsg
-- What the router knows about the names an address owns.
ROWND :: OwnedNames -> BrokerMsg
deriving (Eq, Show)
data RcvMessage = RcvMessage
@@ -958,6 +963,7 @@ data CommandTag (p :: Party) where
NSUB_ :: CommandTag Notifier
NSUBS_ :: CommandTag NotifierService
RSLV_ :: CommandTag Resolver
ROWN_ :: CommandTag Resolver
data CmdTag = forall p. PartyI p => CT (SParty p) (CommandTag p)
@@ -985,6 +991,7 @@ data BrokerMsgTag
| ERR_
| PONG_
| RNAME_
| ROWND_
deriving (Show)
class ProtocolMsgTag t where
@@ -1022,6 +1029,7 @@ instance PartyI p => Encoding (CommandTag p) where
NSUB_ -> "NSUB"
NSUBS_ -> "NSUBS"
RSLV_ -> "RSLV"
ROWN_ -> "ROWN"
smpP = messageTagP
instance ProtocolMsgTag CmdTag where
@@ -1051,6 +1059,7 @@ instance ProtocolMsgTag CmdTag where
"NSUB" -> Just $ CT SNotifier NSUB_
"NSUBS" -> Just $ CT SNotifierService NSUBS_
"RSLV" -> Just $ CT SResolver RSLV_
"ROWN" -> Just $ CT SResolver ROWN_
_ -> Nothing
instance Encoding CmdTag where
@@ -1081,6 +1090,7 @@ instance Encoding BrokerMsgTag where
ERR_ -> "ERR"
PONG_ -> "PONG"
RNAME_ -> "RNAME"
ROWND_ -> "ROWND"
smpP = messageTagP
instance ProtocolMsgTag BrokerMsgTag where
@@ -1104,6 +1114,7 @@ instance ProtocolMsgTag BrokerMsgTag where
"ERR" -> Just ERR_
"PONG" -> Just PONG_
"RNAME" -> Just RNAME_
"ROWND" -> Just ROWND_
_ -> Nothing
-- | SMP message body format
@@ -1846,6 +1857,7 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where
PFWD fwdV pubKey (EncTransmission s) -> e (PFWD_, ' ', fwdV, pubKey, Tail s)
RFWD (EncFwdTransmission s) -> e (RFWD_, ' ', Tail s)
RSLV q -> e (RSLV_, ' ', if v >= nameAvailSMPVersion then hashedQuery q else q)
ROWN addr offset -> e (ROWN_, ' ', addr, offset)
where
e :: Encoding a => a -> ByteString
e = smpEncode
@@ -1871,6 +1883,7 @@ instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where
PFWD {} -> entityCmd
RFWD _ -> noAuthCmd
RSLV _ -> noAuthCmd
ROWN _ _ -> noAuthCmd
SUB -> serviceCmd
NSUB -> serviceCmd
-- other client commands must have both signature and queue ID
@@ -1953,6 +1966,7 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where
| v >= rcvServiceSMPVersion -> Cmd SNotifierService <$> (NSUBS <$> _smpP <*> smpP)
| otherwise -> pure $ Cmd SNotifierService $ NSUBS (-1) mempty
CT SResolver RSLV_ -> Cmd SResolver . RSLV <$> _smpP
CT SResolver ROWN_ -> Cmd SResolver <$> (ROWN <$> _smpP <*> smpP)
fromProtocolError = fromProtocolError @SMPVersion @ErrorType @BrokerMsg
{-# INLINE fromProtocolError #-}
@@ -1995,6 +2009,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
| v < clientNoticesSMPVersion -> BLOCKED info {notice = Nothing}
_ -> err
PONG -> e PONG_
ROWND owned -> e (ROWND_, ' ', Tail $ LB.toStrict $ J.encode owned)
RNAME res
| v >= nameAvailSMPVersion -> e (RNAME_, ' ', Tail $ LB.toStrict $ J.encode res)
| otherwise -> case registration res of
@@ -2046,6 +2061,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
OK_ -> pure OK
ERR_ -> ERR <$> _smpP
PONG_ -> pure PONG
ROWND_ -> fmap ROWND . J.eitherDecodeStrict . unTail <$?> _smpP
RNAME_
| v >= nameAvailSMPVersion -> fmap RNAME . J.eitherDecodeStrict . unTail <$?> _smpP
| otherwise -> fmap (RNAME . oldResponse) . J.eitherDecodeStrict . unTail <$?> _smpP
@@ -2074,6 +2090,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
RRES _ -> noEntityMsg
ALLS -> noEntityMsg
RNAME {} -> noEntityMsg
ROWND {} -> noEntityMsg
-- other broker responses must have queue ID
_
| B.null entId -> Left $ CMD NO_ENTITY
+16 -1
View File
@@ -109,7 +109,9 @@ import Simplex.Messaging.Server.Env.STM as Env
import Simplex.Messaging.Server.Expiration
import Simplex.Messaging.Server.MsgStore
import Simplex.Messaging.Server.MsgStore.Journal (JournalMsgStore, JournalQueue (..), getJournalQueueMessages)
import Simplex.Messaging.Server.Names (NamesEnv, closeNamesEnv, resolveName)
import Data.Word (Word32)
import Simplex.Messaging.Eth.Address (Address)
import Simplex.Messaging.Server.Names (NamesEnv, closeNamesEnv, ownedNames, resolveName)
import Simplex.Messaging.Server.MsgStore.STM
import Simplex.Messaging.Server.MsgStore.Types
import Simplex.Messaging.Server.NtfStore
@@ -1276,6 +1278,7 @@ verifyQueueTransmission service thAuth (tAuth, authorized, (corrId, entId, comma
vc SProxiedClient _ = VRVerified Nothing
vc SProxyService (RFWD _) = VRVerified Nothing
vc SResolver (RSLV _) = VRVerified Nothing
vc SResolver (ROWN _ _) = VRVerified Nothing
checkRole = case (service, partyClientRole p) of
(Just THClientService {serviceRole}, Just role) -> serviceRole == role
_ -> True
@@ -1506,6 +1509,15 @@ client
answered = \case
NRRegistered {} -> True
_ -> v >= nameAvailSMPVersion
-- 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
st <- asks (rslvStats . serverStats)
(selector, msg) <-
liftIO (ownedNames nenv addr offset) <&> \case
Right owned -> (rslvSucc, ROWND owned)
Left e -> (rslvResolverErrs, ERR $ NAME e)
incStat (selector st) $> msg
transportErr :: TransportError -> ErrorType
transportErr = PROXY . BROKER . TRANSPORT
mkIncProxyStats :: MonadIO m => ProxyStats -> ProxyStats -> OwnServer -> (ProxyStats -> IORef Int) -> m ()
@@ -1523,6 +1535,9 @@ client
Cmd SResolver (RSLV d) -> 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)
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
+24
View File
@@ -12,6 +12,7 @@ module Simplex.Messaging.Server.Names
closeNamesEnv,
pingEndpoint,
resolveName,
ownedNames,
)
where
@@ -19,10 +20,14 @@ import qualified Control.Exception as E
import Control.Logger.Simple (logError)
import Data.Bifunctor (first)
import Data.Maybe (fromMaybe)
import Data.Word (Word32)
import qualified Data.Text as T
import Data.Text.Encoding (decodeLatin1)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String (strEncode)
import Simplex.Messaging.Protocol (NameErrorType (..), NameQuery, NameResponse)
import Simplex.Messaging.Eth.Address (Address)
import Simplex.Messaging.Names.Record (OwnedNames)
import Simplex.Messaging.Server.Names.HttpResolver
( ResolverEnv,
ResolverError (..),
@@ -30,6 +35,7 @@ import Simplex.Messaging.Server.Names.HttpResolver
closeResolverEnv,
healthHttp,
newResolverEnv,
ownedByHttp,
resolveHttp,
)
import System.Timeout (timeout)
@@ -74,6 +80,24 @@ fetch :: NamesEnv -> NameQuery -> IO (Either NameErrorType NameResponse)
fetch NamesEnv {resolverEnv} q =
first mapResolverError <$> resolveHttp resolverEnv (decodeLatin1 $ smpEncode q)
-- | The names an address owns. A resolver without the endpoint answers 404,
-- which mapResolverError reports as RESOLVER, so a scan cannot read "this
-- resolver cannot say" as "this address owns nothing".
ownedNames :: NamesEnv -> Address -> Word32 -> IO (Either NameErrorType OwnedNames)
ownedNames env addr offset = do
r <- E.try (timeout (resolverTimeoutMs (config env) * 1000) (fetchOwned env addr offset))
case r of
Right result -> pure (fromMaybe (Left (RESOLVER "timeout")) result)
Left e
| Just (_ :: E.SomeAsyncException) <- E.fromException e -> E.throwIO e
| otherwise -> do
logError $ "[NAMES] resolver fetch raised " <> T.pack (E.displayException e)
pure (Left (RESOLVER "resolver error"))
fetchOwned :: NamesEnv -> Address -> Word32 -> IO (Either NameErrorType OwnedNames)
fetchOwned NamesEnv {resolverEnv} addr offset =
first mapResolverError <$> ownedByHttp resolverEnv (decodeLatin1 $ strEncode addr) (fromIntegral offset)
mapResolverError :: ResolverError -> NameErrorType
mapResolverError = \case
HttpStatusErr code -> RESOLVER ("HTTP " <> T.pack (show code))
@@ -29,6 +29,7 @@ module Simplex.Messaging.Server.Names.HttpResolver
newResolverEnv,
closeResolverEnv,
resolveHttp,
ownedByHttp,
healthHttp,
)
where
@@ -59,7 +60,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 (NameResponse)
import Simplex.Messaging.Names.Record (NameResponse, OwnedNames)
data RpcAuth = AuthBearer Text | AuthBasic Text Text
@@ -117,6 +118,13 @@ resolveHttp env q =
(>>= first InvalidJson . J.eitherDecodeStrict . BL.toStrict)
<$> httpGet env ("/v2/resolve/" <> B.unpack (urlEncode True (encodeUtf8 q)))
-- | The address is EIP-55 hex, which is already URL-safe, but it is encoded
-- for the same reason the name is: nothing from a client reaches the path raw.
ownedByHttp :: ResolverEnv -> Text -> Int -> IO (Either ResolverError OwnedNames)
ownedByHttp env addr offset =
(>>= first InvalidJson . J.eitherDecodeStrict . BL.toStrict)
<$> httpGet env ("/v2/owned-by/" <> B.unpack (urlEncode True (encodeUtf8 addr)) <> "?offset=" <> show offset)
-- | GET <baseUrl>/health; success = reachable with status < 400. The body is
-- size-capped but NOT decoded — the probe only checks reachability.
healthHttp :: ResolverEnv -> IO (Either ResolverError ())
+13 -7
View File
@@ -54,6 +54,7 @@ module Simplex.Messaging.Transport
namesSMPVersion,
serverInfoSMPVersion,
nameAvailSMPVersion,
nameOwnedSMPVersion,
simplexMQVersion,
smpBlockSize,
TransportConfig (..),
@@ -218,6 +219,11 @@ serverInfoSMPVersion = VersionSMP 21
nameAvailSMPVersion :: VersionSMP
nameAvailSMPVersion = VersionSMP 22
-- | ROWN lists the names an address owns. A server below this does not answer
-- it, which a recovery scan must not read as the address owning nothing.
nameOwnedSMPVersion :: VersionSMP
nameOwnedSMPVersion = VersionSMP 23
minClientSMPRelayVersion :: VersionSMP
minClientSMPRelayVersion = VersionSMP 14
@@ -225,20 +231,20 @@ minServerSMPRelayVersion :: VersionSMP
minServerSMPRelayVersion = VersionSMP 14
currentClientSMPRelayVersion :: VersionSMP
currentClientSMPRelayVersion = VersionSMP 22
currentClientSMPRelayVersion = VersionSMP 23
currentServerSMPRelayVersion :: VersionSMP
currentServerSMPRelayVersion = VersionSMP 22
currentServerSMPRelayVersion = VersionSMP 23
-- 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 (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.
-- when clients upgrade at different times. Pinned to the current version (23)
-- for this release because a proxied ROWN is only answered from
-- nameOwnedSMPVersion (23), and a scan that cannot proxy exposes its address
-- to the relay; the buffer reappears once the current version advances past 23.
proxiedSMPRelayVersion :: VersionSMP
proxiedSMPRelayVersion = VersionSMP 22
proxiedSMPRelayVersion = VersionSMP 23
-- minimal supported protocol version is 14
supportedClientSMPRelayVRange :: VersionRangeSMP
+1
View File
@@ -53,6 +53,7 @@ resolveResp :: Status -> LB.ByteString -> [Text] -> (Status, LB.ByteString)
resolveResp st body = \case
["health"] -> (ok200, "{}")
("v2" : "resolve" : _) -> (st, body)
("v2" : "owned-by" : _) -> (st, body)
_ -> (notFound404, "{}")
testNamesConfig :: Int -> NamesConfig
+40
View File
@@ -27,6 +27,8 @@ import Simplex.Messaging.Client
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Encoding.String (strDecode)
import SMPNamesTests (availableBody, registeredBody, reservedBody, resolved, testNameRecord, testPricing)
import Simplex.Messaging.Eth.Address (Address)
import Simplex.Messaging.Names.Record (OwnedName (..), OwnedNames (..))
import Simplex.Messaging.Protocol
( BrokerMsg (..),
Cmd (..),
@@ -72,6 +74,16 @@ withProxyAndResolver (st, body) runTest =
withSmpServerConfigOn (transport @TLS) memProxyCfg testPort $ \_ ->
withSmpServerConfigOn (transport @TLS) (withNames port memCfg2) testPort2 (const runTest)
sendRown :: Transport c => THandleSMP c 'TClient -> B.ByteString -> Address -> IO (Transmission (Either ErrorType BrokerMsg))
sendRown h@THandle {params} corrId addr = do
let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId corrId, NoEntity, Cmd SResolver (ROWN addr 0))
[Right ()] <- tPut h (Right (Nothing, tToSend) :| [])
r :| _ <- tGetClient h
pure r
testAddr :: Address
testAddr = either error id $ strDecode "0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
sendRslv :: Transport c => THandleSMP c 'TClient -> B.ByteString -> SimplexDomain -> IO (Transmission (Either ErrorType BrokerMsg))
sendRslv h@THandle {params} corrId d = do
let TransmissionForAuth {tToSend} = encodeTransmissionForAuth params (CorrId corrId, NoEntity, Cmd SResolver (RSLV (NQDomain d)))
@@ -84,6 +96,8 @@ rslvTests = do
describe "RSLV direct (non-forwarded)" $ do
it "resolver without the v2 route (404) -> NAME RESOLVER, not NOT_FOUND" testRslvBackendNotFound
it "resolver replies 502 -> NAME (RESOLVER ..)" testRslvBackendHttpErr
it "ROWN returns the names an address owns" testRownOwned
it "ROWN on a resolver without the endpoint is a resolver error, not an empty answer" testRownUnsupported
it "no names config -> NAME NO_RESOLVER" testRslvDisabled
it "refuses to send RSLV on a session below namesSMPVersion" testRslvVersion
describe "RSLV forwarded (PFWD)" $ do
@@ -105,6 +119,32 @@ rslvTests = do
-- | /v2/resolve answers 200, 400 or 502, so a 404 is a resolver that predates
-- the route, not a name that does not exist.
-- The scan reads inUse, so an account in use with no names must not arrive
-- looking the same as one that owns nothing.
testRownOwned :: IO ()
testRownOwned =
withResolverServer (status200, ownedBody) $
testSMPClient @TLS $ \h -> do
(corrId, _entId, resp) <- sendRown h "ro01" testAddr
corrId `shouldBe` CorrId "ro01"
case resp of
Right (ROWND owned) -> do
map onName (ownNames owned) `shouldBe` [Just "alice.testing"]
ownInUse owned `shouldBe` True
r -> expectationFailure $ "unexpected " <> show r
-- A resolver that does not serve owned-by answers 404, which must reach the
-- client as a resolver error: read as "owns nothing" it would end a scan early.
testRownUnsupported :: IO ()
testRownUnsupported =
withResolverServer (status404, "{}") $
testSMPClient @TLS $ \h -> do
(_, _, resp) <- sendRown h "ro02" testAddr
resp `shouldBe` Right (ERR (NAME (RESOLVER "HTTP 404")))
ownedBody :: LB.ByteString
ownedBody = "{\"address\":\"0x70997970c51812dc3a010c7d01b50e0d17dc79c8\",\"names\":[{\"name\":\"alice.testing\",\"labelhash\":\"0x9c02\",\"expires\":1821603121,\"status\":\"registered\"}],\"inUse\":true,\"nextOffset\":null}"
testRslvBackendNotFound :: IO ()
testRslvBackendNotFound =
withResolverServer (status404, "{}") $