mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-11-04 08:37:49 +00:00
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:
1 parent
5dc64f1cf4
commit
f53a6f3364
12 files changed
+226
-10
No files matched your search
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, "{}") $
|
||||
|
||||
Reference in new issue
Block a user