diff --git a/scripts/resolver/service/snrc-resolve.py b/scripts/resolver/service/snrc-resolve.py index c332d4090..4d33d3d73 100755 --- a/scripts/resolver/service/snrc-resolve.py +++ b/scripts/resolver/service/snrc-resolve.py @@ -890,24 +890,11 @@ def owned_by(address: str, offset: int = 0): """Every name an address holds, across every configured TLD, with the account's on-chain footprint. - Read off the ERC-721 registrar rather than from logs: the token is the - name, so `balanceOf` / `tokenOfOwnerByIndex` is the current answer and it - includes names acquired by transfer, which a scan of registration events - would miss. `labelOf` returns the plaintext label, recorded write-once at - registration, so no off-chain index is needed to turn a token id back into - a name. - - Enumeration is deliberately not maintained on expiry, so a lapsed name - stays in the list until someone re-registers it. That is reported rather - than filtered: every entry carries `status`, using the same vocabulary as - /resolve, and a caller scanning a recovered key is exactly the caller who - needs to be told one of its names can still be renewed. - - `inUse` answers the question a recovery scan actually asks - has this - account ever been used - which holding a name is only one way to be. An - account that was funded or ever sent a transaction is in use even with no - name, so the nonce and balance it is derived from are reported too: a scan - that gets this wrong hands out an account its owner is already using. + Enumeration is read off the ERC-721 registrar, so a name acquired by + transfer counts, and a lapsed one stays listed until re-registered - it is + reported with its `status`, not filtered. `inUse` is what a recovery scan + asks; holding a name is only one way to be in use, so the nonce and balance + it is derived from are reported too. """ if not is_address(address): return 400, { diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index ac6f1806e..c9f35995b 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -1281,12 +1281,12 @@ deleteLocalInvShortLink' c (CSLInvitation _ srv linkId _) = withStore' c $ \db - ownedSimplexNames' :: AgentClient -> NetworkRequestMode -> UserId -> [SMPServer] -> Address -> Word32 -> AM (SMPServer, OwnedNames) ownedSimplexNames' c nm userId used addr offset = do - srv <- getNextNameServerAvoiding c userId used + srv <- getNextNameServer 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 + resolverSrv <- getNextNameServer c userId [] resolveName c nm userId resolverSrv domain changeConnectionUser' :: AgentClient -> UserId -> ConnId -> UserId -> AM () diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 3786bf811..60776b53d 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -71,7 +71,6 @@ module Simplex.Messaging.Agent.Client getQueueLink, resolveName, ownedNames, - getNextNameServerAvoiding, getNextNameServer, enableQueueNotifications, EnableQueueNtfReq (..), @@ -260,9 +259,9 @@ import Simplex.Messaging.Crypto.BBS (BBSPresHeader (..), BBSPublicKey) import Simplex.Messaging.Crypto.Entitlement (EntitlementCredential, EntitlementProof, generateEntitlementProof) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Notifications.Client import Simplex.Messaging.Eth.Address (Address) import Simplex.Messaging.Names.Record (OwnedNames) +import Simplex.Messaging.Notifications.Client import Simplex.Messaging.Notifications.Protocol import Simplex.Messaging.Notifications.Transport (NTFVersion) import Simplex.Messaging.Notifications.Types @@ -2042,32 +2041,20 @@ ownedNames c nm userId server addr offset = 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. -getNextNameServer :: AgentClient -> UserId -> AM SMPServer -getNextNameServer c userId = +-- Servers already used are avoided where the set allows: one relay asked about +-- every account of a scan would learn they belong to one wallet. +getNextNameServer :: AgentClient -> UserId -> [SMPServer] -> AM SMPServer +getNextNameServer c userId usedSrvs = liftIO (TM.lookupIO userId (userServers c :: TMap UserId (UserServers 'PSMP))) >>= \case Just UserServers {nameSrvs} -> case L.nonEmpty nameSrvs of - Just srvs -> protoServer <$> pickServer srvs + Just srvs -> protoServer <$> pickServer (fromMaybe srvs $ L.nonEmpty $ L.filter (isUnusedServer usedHosts) srvs) Nothing -> throwE NO_NAME_SERVERS Nothing -> throwE $ INTERNAL "unknown userId - no user servers" + where + usedHosts = S.unions $ map serverHosts usedSrvs enableQueueNotifications :: AgentClient -> RcvQueue -> SMP.NtfPublicAuthKey -> SMP.RcvNtfPublicDhKey -> AM (SMP.NotifierId, SMP.RcvNtfPublicDhKey) enableQueueNotifications c rq@RcvQueue {rcvId, rcvPrivateKey} notifierKey rcvNtfPublicDhKey = diff --git a/src/Simplex/Messaging/Server/Names.hs b/src/Simplex/Messaging/Server/Names.hs index 703294723..06e59098d 100644 --- a/src/Simplex/Messaging/Server/Names.hs +++ b/src/Simplex/Messaging/Server/Names.hs @@ -68,6 +68,17 @@ pingEndpoint NamesEnv {resolverEnv, config} = resolveName :: NamesEnv -> NameQuery -> IO (Either NameErrorType NameResponse) resolveName env q = resolverCall env $ fetch env q +-- | Bound the call by the configured timeout, and report a raised exception as an error, leaving async exceptions alone. +resolverCall :: NamesEnv -> IO (Either NameErrorType a) -> IO (Either NameErrorType a) +resolverCall env a = + E.try (timeout (resolverTimeoutMs (config env) * 1000) a) >>= \case + 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")) + fetch :: NamesEnv -> NameQuery -> IO (Either NameErrorType NameResponse) fetch NamesEnv {resolverEnv} q = first mapResolverError <$> resolveHttp resolverEnv (decodeLatin1 $ smpEncode q) @@ -80,17 +91,6 @@ fetchOwned :: NamesEnv -> Address -> Word32 -> IO (Either NameErrorType OwnedNam fetchOwned NamesEnv {resolverEnv} addr offset = first mapResolverError <$> ownedByHttp resolverEnv (decodeLatin1 $ strEncode addr) offset --- | Bound the call by the configured timeout, and report a raised exception as an error, leaving async exceptions alone. -resolverCall :: NamesEnv -> IO (Either NameErrorType a) -> IO (Either NameErrorType a) -resolverCall env a = - E.try (timeout (resolverTimeoutMs (config env) * 1000) a) >>= \case - 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")) - mapResolverError :: ResolverError -> NameErrorType mapResolverError = \case HttpStatusErr code -> RESOLVER ("HTTP " <> T.pack (show code))