From a921d7cc23c2cd99a226d2527b48c74527cc86e8 Mon Sep 17 00:00:00 2001 From: Alain Brenzikofer Date: Fri, 11 Sep 2026 17:36:48 +0200 Subject: [PATCH 1/3] first shot at reporting freshness --- protocol/simplex-messaging.md | 21 +++++-- scripts/resolver/README.md | 13 ++++- scripts/resolver/service/snrc-resolve.py | 41 +++++++++---- scripts/resolver/service/test_snrc_resolve.py | 58 +++++++++++++------ src/Simplex/Messaging/Agent.hs | 6 +- src/Simplex/Messaging/Agent/Client.hs | 4 +- src/Simplex/Messaging/Client.hs | 8 +-- src/Simplex/Messaging/Names/Record.hs | 14 +++++ src/Simplex/Messaging/Protocol.hs | 15 +++-- src/Simplex/Messaging/Server.hs | 2 +- src/Simplex/Messaging/Server/Names.hs | 6 +- .../Messaging/Server/Names/HttpResolver.hs | 4 +- tests/AgentTests/ResolveNameTests.hs | 4 +- tests/RSLVTests.hs | 19 +++--- tests/SMPNamesTests.hs | 34 +++++++---- 15 files changed, 169 insertions(+), 80 deletions(-) diff --git a/protocol/simplex-messaging.md b/protocol/simplex-messaging.md index 52a3e90a0..6de1f2cc4 100644 --- a/protocol/simplex-messaging.md +++ b/protocol/simplex-messaging.md @@ -1526,12 +1526,25 @@ fact that this router cannot resolve, so iterating past it is safe. `RNAME` answers both what a name resolves to and whether it can be registered. ```abnf -rname = %s"RNAME" SP registration +rname = %s"RNAME" SP resolution ``` -`registration` is a UTF-8 JSON object consuming the remainder of the -transmission. Its `type` selects which of the three answers it is. Money is US -cents, times are seconds since the Unix epoch, and lengths are characters. +`resolution` is a UTF-8 JSON object consuming the remainder of the transmission: +the `registration` below, and `readAt`, the block timestamp the registry was read +at. Money is US cents, times are seconds since the Unix epoch, and lengths are +characters. + +| Field | JSON type | Constraints | +|---|---|---| +| `readAt` | number | block timestamp the answer was read at. Absent only from a v20/v21 router, which sent the record alone | +| `registration` | object | below | + +A router reads the registry through a node of its own, so an answer can predate +a client's own transaction. `readAt` is what lets the client tell that apart +from a current answer: a name registered after `readAt` still reads as +`available`. A client MUST NOT treat an answer as current without checking it. + +`registration`'s `type` selects which of the three answers it is. | `type` | Meaning | |---|---| diff --git a/scripts/resolver/README.md b/scripts/resolver/README.md index f667f536d..25ce5ca27 100644 --- a/scripts/resolver/README.md +++ b/scripts/resolver/README.md @@ -62,9 +62,14 @@ curl -s -X POST http://127.0.0.1:8545 \ **2. resolver is healthy:** ```sh curl -s http://127.0.0.1:8000/health | jq -# → {"ok": true, "rpc": "http://reth:8545", "registries": {"testing": "0x…", "simplex": ""}} +# → {"ok": true, "rpc": "http://reth:8545", "registries": {"testing": "0x…", "simplex": ""}, +# "blockNumber": 23400000, "readAt": 1780000000, "lagSeconds": 12} ``` +`lagSeconds` is the node's latest block against the wall clock. A resolver that +is reachable and answering can still be hours behind, and every name it reports +is that stale; `null` means the node could not be reached at all. + **3. resolver resolves a live name** (`foobar.testing` is a populated test name): ```sh curl -s http://127.0.0.1:8000/resolve/foobar.testing | jq @@ -129,8 +134,10 @@ its own shape does: ### v2: `/v2/resolve/` -The body is the SMP protocol's `NameRegistration`, which the router decodes as -is and forwards; translating the registry's model to it is this resolver's job. +The body is the SMP protocol's `NameResolution`, which the router decodes as is +and forwards; translating the registry's model to it is this resolver's job. It +is the registration below, plus `readAt`, the timestamp of the block it was read +at: a node that lags answers with names it has not seen registered yet. Its `type` is `registered`, `available` or `reserved`, and the fields each one carries are specified once, in the **Name response** section of [`protocol/simplex-messaging.md`](../../protocol/simplex-messaging.md). It is diff --git a/scripts/resolver/service/snrc-resolve.py b/scripts/resolver/service/snrc-resolve.py index dcf673642..c74c803ba 100755 --- a/scripts/resolver/service/snrc-resolve.py +++ b/scripts/resolver/service/snrc-resolve.py @@ -67,6 +67,7 @@ import json import os import sys +import time from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import unquote, urlparse from urllib.request import Request, urlopen @@ -302,6 +303,8 @@ def name_status(name: str): if not registrar or len(labels) < 2: return { "status": "unknown", + # nothing was read, so there is no block to report + "readAt": None, "expires": None, "graceEnds": None, "reasonCode": None, @@ -315,12 +318,9 @@ def name_status(name: str): expires = decode_uint( eth_call(registrar, selector("nameExpires(uint256)") + encode_uint(token)) ) - if expires == 0: - status, grace, now = "unregistered", 0, 0 - else: - grace = grace_period(registrar) - now = chain_now() - status = expiry_status(expires, grace, now) + grace = grace_period(registrar) if expires else 0 + now = chain_now() + status = expiry_status(expires, grace, now) # A reservation is orthogonal to the registration: a registered name can be # held back too. @@ -329,6 +329,8 @@ def name_status(name: str): out = { "status": status, + # the block this was read at: the resolver is only as current as its node + "readAt": now, "expires": expires or None, "graceEnds": (expires + grace) if expires else None, "reasonCode": reason[0] if reason else None, @@ -708,6 +710,12 @@ def name_record(name: str): return rec +def resolution(reg, registration_body): + """The SMP protocol's NameResolution: the registration and the block it was + read at, so a client can tell an answer that predates its own transaction.""" + return 200, {"readAt": reg["readAt"], "registration": registration_body} + + def registration(name: str): """The SMP protocol's NameRegistration, which the relay decodes as is. Translating the contract's model to it is this resolver's job.""" @@ -722,26 +730,26 @@ def registration(name: str): # hashed query the registrar cannot name is refused rather than answered if rec["name"] is None: return 502, {"name": name, "error": "labelNotRecorded"} - return 200, { + return resolution(reg, { "type": "registered", "expires": reg["expires"], "graceUntil": reg["graceEnds"], "reservedReason_": reg["reasonCode"], "nameRecord": rec, - } + }) if reg["reasonCode"]: - return 200, {"type": "reserved", "reservedReason": reg["reasonCode"]} + return resolution(reg, {"type": "reserved", "reservedReason": reg["reasonCode"]}) if status in ("unregistered", "expired"): if "basePrice" not in reg: return 502, {"name": name, "error": "noPriceOracle"} - return 200, { + return resolution(reg, { "type": "available", "pricing": { "registrationPrices": reg["registrationPrices"], "basePrice": reg["basePrice"], "minLabelLength": reg["minLabelLength"], }, - } + }) return 502, {"name": name, "error": status} @@ -845,9 +853,18 @@ def do_GET(self): # noqa: N802 - http.server contract parts = [unquote(p) for p in path.split("/") if p] if parts == ["health"]: + try: + block = rpc("eth_getBlockByNumber", ["latest", False]) + head = { + "blockNumber": decode_uint(block["number"]), + "readAt": decode_uint(block["timestamp"]), + "lagSeconds": int(time.time()) - decode_uint(block["timestamp"]), + } + except Exception: # unreachable node: say so rather than omit it + head = {"blockNumber": None, "readAt": None, "lagSeconds": None} self._respond( 200, - {"ok": True, "rpc": RPC, "registries": REGISTRIES}, + {"ok": True, "rpc": RPC, "registries": REGISTRIES, **head}, ) return diff --git a/scripts/resolver/service/test_snrc_resolve.py b/scripts/resolver/service/test_snrc_resolve.py index 84520a49a..c9261e678 100644 --- a/scripts/resolver/service/test_snrc_resolve.py +++ b/scripts/resolver/service/test_snrc_resolve.py @@ -20,6 +20,12 @@ _SPEC.loader.exec_module(snrc) +def registration(name): + """registration() answers a NameResolution; most tests assert what is in it.""" + status, body = snrc.registration(name) + return status, (body["registration"] if status == 200 else body) + + class SplitLinksTests(unittest.TestCase): """`split_links` decodes the multi-URL convention for simplex.contact / simplex.channel text records. Reuses the same rule the dApp's @@ -191,10 +197,11 @@ def eth_call(to, data): return eth_call - def _keys(self, status, expires, grace_ends): + def _keys(self, status, expires, grace_ends, read_at=-1): """Every branch answers with the same keys; only some carry values.""" return { "status": status, + "readAt": self.now if read_at == -1 else read_at, "expires": expires, "graceEnds": grace_ends, "reasonCode": None, @@ -212,7 +219,8 @@ def setUp(self): snrc.REGISTRARS = {"testing": self.REGISTRAR} # Expiry alone; ReservedTests covers a configured controller. snrc.CONTROLLERS = {"testing": ""} - snrc.chain_now = lambda: int(time.time()) + self.now = int(time.time()) + snrc.chain_now = lambda: self.now def tearDown(self): ( @@ -315,12 +323,13 @@ def test_unconfigured_tld_is_unknown_rather_than_unregistered(self): snrc.eth_call = lambda *a: self.fail("must not reach the chain") self.assertEqual( snrc.name_status("alice.testing"), - self._keys("unknown", None, None), + self._keys("unknown", None, None, read_at=None), ) def test_every_branch_returns_the_same_keys(self): keys = { "status", + "readAt", "expires", "graceEnds", "reasonCode", @@ -869,7 +878,7 @@ def _lapsed(self, days_past_grace): def test_a_live_name_is_registered_and_carries_its_record(self): expires = self.now + 3600 snrc.eth_call = self._chain(expires) - status, body = snrc.registration("acme.testing") + status, body = registration("acme.testing") self.assertEqual(status, 200) self.assertEqual(body["type"], "registered") self.assertEqual(body["expires"], expires) @@ -880,19 +889,19 @@ def test_a_live_name_is_registered_and_carries_its_record(self): def test_a_name_in_grace_is_still_registered(self): expires = self.now - 3600 snrc.eth_call = self._chain(expires) - _, body = snrc.registration("acme.testing") + _, body = registration("acme.testing") self.assertEqual(body["type"], "registered") self.assertGreater(body["graceUntil"], self.now) def test_a_registered_name_that_is_held_back_says_so(self): snrc.eth_call = self._chain(self.now + 3600, reserved=1) - _, body = snrc.registration("acme.testing") + _, body = registration("acme.testing") self.assertEqual(body["type"], "registered") self.assertEqual(body["reservedReason_"], "internal") def test_an_unregistered_name_is_available_with_its_pricing(self): snrc.eth_call = self._chain(0) - status, body = snrc.registration("acme.testing") + status, body = registration("acme.testing") self.assertEqual(status, 200) self.assertEqual(body["type"], "available") # lengths below minCharLength are unregistrable, so they are not priced @@ -902,13 +911,13 @@ def test_an_unregistered_name_is_available_with_its_pricing(self): def test_a_lapsed_name_is_available_at_the_ordinary_price(self): snrc.eth_call = self._chain(self._lapsed(1)) - _, body = snrc.registration("acme.testing") + _, body = registration("acme.testing") self.assertEqual(body["type"], "available") self.assertEqual(body["pricing"]["basePrice"], self.BASE) def test_a_held_back_name_is_reserved_and_is_never_priced(self): snrc.eth_call = self._chain(0, reserved=2) - status, body = snrc.registration("acme.testing") + status, body = registration("acme.testing") self.assertEqual(status, 200) self.assertEqual(body["type"], "reserved") self.assertEqual(body["reservedReason"], "trademark") @@ -918,27 +927,27 @@ def test_a_hashed_query_answers_the_same_as_the_name(self): # keccak-256("acme") hashed = "[e29dae06ef4c3e336b7538b6d4f52ca1ecec009b1df6fb501320e11b223aeeaf]" snrc.eth_call = self._chain(0) - _, by_name = snrc.registration("acme.testing") - _, by_hash = snrc.registration(hashed + ".testing") + _, by_name = registration("acme.testing") + _, by_hash = registration(hashed + ".testing") self.assertEqual(by_name, by_hash) def test_an_unconfigured_tld_is_refused_not_answered(self): snrc.REGISTRIES = {"testing": ""} snrc.eth_call = lambda *a: self.fail("must not reach the chain") - status, body = snrc.registration("acme.testing") + status, body = registration("acme.testing") self.assertEqual(status, 400) self.assertEqual(body["error"], "tldNotConfigured") def test_no_price_oracle_is_an_error_not_a_free_name(self): snrc.eth_call = self._chain(0, oracle=snrc.ZERO_ADDR) - status, body = snrc.registration("acme.testing") + status, body = registration("acme.testing") self.assertEqual(status, 502) self.assertEqual(body["error"], "noPriceOracle") def test_a_status_it_cannot_read_is_an_error_not_a_registration(self): snrc.REGISTRARS = {"testing": ""} snrc.eth_call = self._chain(0) - status, body = snrc.registration("acme.testing") + status, body = registration("acme.testing") self.assertEqual(status, 502) self.assertEqual(body["error"], "unknown") @@ -953,7 +962,7 @@ def test_each_answer_carries_exactly_its_own_fields(self): for expected_type, (chain, keys) in cases.items(): with self.subTest(type=expected_type): snrc.eth_call = chain - _, body = snrc.registration("acme.testing") + _, body = registration("acme.testing") self.assertEqual(body["type"], expected_type) self.assertEqual(set(body), keys) def test_a_hashed_query_the_registrar_cannot_name_is_refused(self): @@ -961,16 +970,31 @@ def test_a_hashed_query_the_registrar_cannot_name_is_refused(self): with a record the registrar could not name would only fail there.""" hashed = "[e29dae06ef4c3e336b7538b6d4f52ca1ecec009b1df6fb501320e11b223aeeaf]" snrc.eth_call = self._chain(self.now + 3600, label=b"") - status, body = snrc.registration(hashed + ".testing") + status, body = registration(hashed + ".testing") self.assertEqual(status, 502) self.assertEqual(body["error"], "labelNotRecorded") def test_a_hashed_query_is_answered_with_the_name_the_registrar_recorded(self): hashed = "[e29dae06ef4c3e336b7538b6d4f52ca1ecec009b1df6fb501320e11b223aeeaf]" snrc.eth_call = self._chain(self.now + 3600) - status, body = snrc.registration(hashed + ".testing") + status, body = registration(hashed + ".testing") self.assertEqual(status, 200) self.assertEqual(body["nameRecord"]["name"], "acme.testing") + def test_the_answer_says_which_block_it_was_read_at(self): + """The resolver is only as current as its node. Without this a client + cannot tell an answer that predates its own registration.""" + snrc.eth_call = self._chain(self.now + 3600) + _, res = snrc.registration("acme.testing") + self.assertEqual(res["readAt"], self.now) + self.assertEqual(res["registration"]["type"], "registered") + + def test_an_available_name_says_so_too(self): + """This is the path that reads no block otherwise, and the one where + staleness matters most: the name may already be taken.""" + snrc.eth_call = self._chain(0) + _, res = snrc.registration("acme.testing") + self.assertEqual(res["readAt"], self.now) + self.assertEqual(res["registration"]["type"], "available") if __name__ == "__main__": unittest.main() diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index b1e791d6d..7e2f6ed8b 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -228,7 +228,7 @@ import Simplex.Messaging.Protocol ErrorType (AUTH), MsgBody, MsgFlags (..), - NameRegistration, + NameResolution, 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 NameRegistration +resolveSimplexName :: AgentClient -> NetworkRequestMode -> UserId -> SimplexDomain -> AE NameResolution 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 NameRegistration +resolveSimplexName' :: AgentClient -> NetworkRequestMode -> UserId -> SimplexDomain -> AM NameResolution resolveSimplexName' c nm userId domain = do resolverSrv <- getNextNameServer c userId resolveName c nm userId resolverSrv domain diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index d34fb55b9..e687f2dd3 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -272,7 +272,7 @@ import Simplex.Messaging.Protocol NetworkError (..), MsgFlags (..), MsgId, - NameRegistration, + NameResolution, 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 NameRegistration +resolveName :: AgentClient -> NetworkRequestMode -> UserId -> SMPServer -> SimplexDomain -> AM NameResolution resolveName c nm userId server domain = snd <$> sendOrProxySMPCommand c nm userId server "" "RSLV" NoEntity resolveViaProxy resolveDirectly where diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 95bad33a2..7c7d23ebd 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -1054,7 +1054,7 @@ 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 NameRegistration) +proxyResolveName :: SMPClient -> NetworkRequestMode -> ProxiedRelay -> SimplexDomain -> ExceptT SMPClientError IO (Either ProxyClientError NameResolution) proxyResolveName c nm proxiedRelay name | prVersion proxiedRelay >= namesSMPVersion = proxySMPCommand c nm proxiedRelay Nothing NoEntity (RSLV (NQDomain name)) >>= \case @@ -1068,7 +1068,7 @@ proxyResolveName c nm proxiedRelay name -- proxy fallback in the agent. RSLV requires no entity ID or authorization -- (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 NameRegistration +directResolveName :: SMPClient -> NetworkRequestMode -> SimplexDomain -> ExceptT SMPClientError IO NameResolution directResolveName c nm name | thVersion (thParams c) >= namesSMPVersion = sendProtocolCommand c nm Nothing NoEntity (Cmd SResolver (RSLV (NQDomain name))) >>= \case @@ -1076,8 +1076,8 @@ directResolveName c nm name r -> throwE $ unexpectedResponse r | otherwise = throwE $ PCETransportError TEVersion -resolvedNameOrNotFound :: SimplexDomain -> NameRegistration -> Bool -resolvedNameOrNotFound d = \case +resolvedNameOrNotFound :: SimplexDomain -> NameResolution -> Bool +resolvedNameOrNotFound d NameResolution {registration} = case registration of NRRegistered {nameRecord} -> T.toLower (nrName nameRecord) == fullDomainName d _ -> True diff --git a/src/Simplex/Messaging/Names/Record.hs b/src/Simplex/Messaging/Names/Record.hs index 3d8d1e7c9..0b4f63e70 100644 --- a/src/Simplex/Messaging/Names/Record.hs +++ b/src/Simplex/Messaging/Names/Record.hs @@ -8,6 +8,7 @@ module Simplex.Messaging.Names.Record ( NameRecord (..), + NameResolution (..), NameRegistration (..), NamePricing (..), USDCents (..), @@ -63,6 +64,17 @@ newtype USDCents = USDCents Int64 deriving (Eq, Ord, Show) deriving newtype (ToJSON, FromJSON) +-- | What the registry holds for a name, and the chain state it was read at, so +-- a client can tell an answer that predates its own transaction from a current +-- one. The resolver is only as current as the node behind it. +data NameResolution = NameResolution + { -- | block timestamp the registry was read at; absent only from a v20/v21 + -- router, which sent the record alone + readAt :: 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. @@ -131,3 +143,5 @@ $(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 ''NameResolution) diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index afce76d8c..6fb3efff2 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -81,6 +81,7 @@ module Simplex.Messaging.Protocol CommandError (..), ProxyError (..), NameQuery (..), + NameResolution (..), NameRegistration (..), NamePricing (..), USDCents (..), @@ -745,7 +746,7 @@ data BrokerMsg where ERR :: ErrorType -> BrokerMsg PONG :: BrokerMsg -- What the router knows about a SimpleX name. - RNAME :: NameRegistration -> BrokerMsg + RNAME :: NameResolution -> BrokerMsg deriving (Eq, Show) data RcvMessage = RcvMessage @@ -1994,9 +1995,9 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where | v < clientNoticesSMPVersion -> BLOCKED info {notice = Nothing} _ -> err PONG -> e PONG_ - RNAME reg - | v >= nameAvailSMPVersion -> e (RNAME_, ' ', Tail $ LB.toStrict $ J.encode reg) - | otherwise -> case reg of + 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 @@ -2047,9 +2048,11 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where PONG_ -> pure PONG RNAME_ | v >= nameAvailSMPVersion -> fmap RNAME . J.eitherDecodeStrict . unTail <$?> _smpP - | otherwise -> fmap (RNAME . oldRegistration) . J.eitherDecodeStrict . unTail <$?> _smpP + | otherwise -> fmap (RNAME . oldResolution) . J.eitherDecodeStrict . unTail <$?> _smpP where - oldRegistration nameRecord = NRRegistered {expires = Nothing, graceUntil = Nothing, reservedReason_ = Nothing, nameRecord} + -- a v20/v21 router sent the record alone: no dates, no reservation, no block + oldResolution nameRecord = + NameResolution {readAt = Nothing, registration = NRRegistered {expires = Nothing, graceUntil = Nothing, reservedReason_ = Nothing, nameRecord}} serviceRespP resp | v >= rcvServiceSMPVersion = resp <$> _smpP <*> smpP | otherwise = resp <$> _smpP <*> pure mempty diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index b6e73d48c..097cabbbe 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -1498,7 +1498,7 @@ client st <- asks (rslvStats . serverStats) (selector, msg) <- liftIO (resolveName nenv q) <&> \case - Right reg -> (if answered reg then rslvSucc else rslvNotFound, RNAME reg) + Right res -> (if answered (registration res) then rslvSucc else rslvNotFound, RNAME res) Left e -> (rslvResolverErrs, ERR $ NAME e) incStat (selector st) $> msg where diff --git a/src/Simplex/Messaging/Server/Names.hs b/src/Simplex/Messaging/Server/Names.hs index 3ec1d7b5c..33d038cb6 100644 --- a/src/Simplex/Messaging/Server/Names.hs +++ b/src/Simplex/Messaging/Server/Names.hs @@ -22,7 +22,7 @@ import Data.Maybe (fromMaybe) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) import Simplex.Messaging.Encoding -import Simplex.Messaging.Protocol (NameErrorType (..), NameQuery, NameRegistration) +import Simplex.Messaging.Protocol (NameErrorType (..), NameQuery, NameResolution) import Simplex.Messaging.Server.Names.HttpResolver ( ResolverEnv, ResolverError (..), @@ -59,7 +59,7 @@ pingEndpoint :: NamesEnv -> IO (Either ResolverError ()) pingEndpoint NamesEnv {resolverEnv, config} = fromMaybe (Left ResolverTimeout) <$> timeout (resolverTimeoutMs config * 1000) (healthHttp resolverEnv) -resolveName :: NamesEnv -> NameQuery -> IO (Either NameErrorType NameRegistration) +resolveName :: NamesEnv -> NameQuery -> IO (Either NameErrorType NameResolution) resolveName env q = do r <- E.try (timeout (resolverTimeoutMs (config env) * 1000) (fetch env q)) case r of @@ -70,7 +70,7 @@ resolveName env q = do logError $ "[NAMES] resolver fetch raised " <> T.pack (E.displayException e) pure (Left (RESOLVER "resolver error")) -fetch :: NamesEnv -> NameQuery -> IO (Either NameErrorType NameRegistration) +fetch :: NamesEnv -> NameQuery -> IO (Either NameErrorType NameResolution) fetch NamesEnv {resolverEnv} q = first mapResolverError <$> resolveHttp resolverEnv (decodeLatin1 $ smpEncode q) diff --git a/src/Simplex/Messaging/Server/Names/HttpResolver.hs b/src/Simplex/Messaging/Server/Names/HttpResolver.hs index 881fe664b..2ec96e875 100644 --- a/src/Simplex/Messaging/Server/Names/HttpResolver.hs +++ b/src/Simplex/Messaging/Server/Names/HttpResolver.hs @@ -59,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 (NameRegistration) +import Simplex.Messaging.Names.Record (NameResolution) data RpcAuth = AuthBearer Text | AuthBasic Text Text @@ -112,7 +112,7 @@ authHeader = \case -- | 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 NameRegistration) +resolveHttp :: ResolverEnv -> Text -> IO (Either ResolverError NameResolution) resolveHttp env q = (>>= first InvalidJson . J.eitherDecodeStrict . BL.toStrict) <$> httpGet env ("/v2/resolve/" <> B.unpack (urlEncode True (encodeUtf8 q))) diff --git a/tests/AgentTests/ResolveNameTests.hs b/tests/AgentTests/ResolveNameTests.hs index b6f81a647..27d402b14 100644 --- a/tests/AgentTests/ResolveNameTests.hs +++ b/tests/AgentTests/ResolveNameTests.hs @@ -93,7 +93,7 @@ testAvailSuccess = withDirectResolver (status200, availableBody) $ \c -> do r <- runExceptT $ resolveSimplexName c NRMInteractive 1 (SimplexDomain TLDSimplex "alice" []) case r of - Right (SMP.NRAvailable {}) -> pure () + Right SMP.NameResolution {registration = SMP.NRAvailable {}} -> pure () _ -> expectationFailure $ "expected Right NRAvailable, got: " <> show r -- | 404 is a resolver that predates /v2/resolve: no status from that endpoint @@ -159,5 +159,5 @@ testDirectSuccess = withDirectResolver (status200, registeredBody testNameRecord) $ \c -> do r <- runExceptT $ resolveSimplexName c NRMInteractive 1 (SimplexDomain TLDSimplex "alice" []) case r of - Right (SMP.NRRegistered {nameRecord}) -> nameRecord `shouldBe` testNameRecord + Right SMP.NameResolution {registration = SMP.NRRegistered {nameRecord}} -> nameRecord `shouldBe` testNameRecord _ -> expectationFailure $ "expected Right NRRegistered, got: " <> show r diff --git a/tests/RSLVTests.hs b/tests/RSLVTests.hs index dbe188811..063bc23d6 100644 --- a/tests/RSLVTests.hs +++ b/tests/RSLVTests.hs @@ -26,7 +26,7 @@ import SMPClient import Simplex.Messaging.Client import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String (strDecode) -import SMPNamesTests (availableBody, registeredBody, reservedBody, testNameRecord, testPricing) +import SMPNamesTests (availableBody, registeredBody, reservedBody, resolved, testNameRecord, testPricing) import Simplex.Messaging.Protocol ( BrokerMsg (..), Cmd (..), @@ -35,6 +35,7 @@ import Simplex.Messaging.Protocol ErrorType (..), NameQuery (..), NameRegistration (..), + NameResolution (..), NameErrorType (..), NameReservedReason (..), SParty (..), @@ -140,7 +141,7 @@ testRslvVersion = Left (PCETransportError TEVersion) -> pure () _ -> expectationFailure $ "expected Left (PCETransportError TEVersion), got: " <> show r -forwardedResolveAlice :: IO (Either SMPClientError (Either ProxyClientError SMP.NameRegistration)) +forwardedResolveAlice :: IO (Either SMPClientError (Either ProxyClientError SMP.NameResolution)) forwardedResolveAlice = do g <- C.newRandom ts <- getCurrentTime @@ -163,7 +164,7 @@ testRslvForwardedSuccess :: IO () testRslvForwardedSuccess = withProxyAndResolver (status200, registeredBody testNameRecord) $ forwardedResolveAlice >>= \r -> case r of - Right (Right NRRegistered {nameRecord}) -> nameRecord `shouldBe` testNameRecord + Right (Right NameResolution {registration = NRRegistered {nameRecord}}) -> nameRecord `shouldBe` testNameRecord _ -> expectationFailure $ "expected Right (Right NRRegistered), got: " <> show r testRslvSuccess :: IO () @@ -173,7 +174,7 @@ testRslvSuccess = (corrId, _entId, resp) <- sendRslv h "rs07" (domain "alice.simplex") corrId `shouldBe` CorrId "rs07" case resp of - Right (RNAME NRRegistered {nameRecord}) -> nameRecord `shouldBe` testNameRecord + Right (RNAME NameResolution {registration = NRRegistered {nameRecord}}) -> nameRecord `shouldBe` testNameRecord _ -> expectationFailure $ "expected Right (RNAME NRRegistered), got: " <> show resp testRslvAvailable :: IO () @@ -182,14 +183,14 @@ testRslvAvailable = testSMPClient @TLS $ \h -> do (corrId, _entId, resp) <- sendRslv h "na01" (domain "ghost.simplex") corrId `shouldBe` CorrId "na01" - resp `shouldBe` Right (RNAME (NRAvailable testPricing)) + resp `shouldBe` Right (RNAME (resolved (NRAvailable testPricing))) testRslvReserved :: IO () testRslvReserved = withResolverServer (status200, reservedBody) $ testSMPClient @TLS $ \h -> do (_, _, resp) <- sendRslv h "na03" (domain "acme.simplex") - resp `shouldBe` Right (RNAME (NRReserved NRRTrademark)) + resp `shouldBe` Right (RNAME (resolved (NRReserved NRRTrademark))) -- | A client that predates v22 must see exactly what it saw before: the record -- for a name that resolves, and NOT_FOUND for one that does not. @@ -209,7 +210,7 @@ testRslvOldClientRecord = withResolverServer (status200, registeredBody testNameRecord) $ do pc <- oldClient r <- runExceptT' (directResolveName pc NRMInteractive (domain "alice.simplex")) - r `shouldBe` NRRegistered Nothing Nothing Nothing testNameRecord + r `shouldBe` NameResolution Nothing (NRRegistered Nothing Nothing Nothing testNameRecord) testRslvOldClientNotFound :: IO () testRslvOldClientNotFound = @@ -224,7 +225,7 @@ testRslvForwardedAvailable :: IO () testRslvForwardedAvailable = withProxyAndResolver (status200, availableBody) $ forwardedResolveAlice >>= \r -> case r of - Right (Right (NRAvailable pricing)) -> pricing `shouldBe` testPricing + Right (Right NameResolution {registration = NRAvailable {pricing}}) -> pricing `shouldBe` testPricing _ -> expectationFailure $ "expected Right (Right NRAvailable), got: " <> show r -- keccak-256("alice"), the registry key @@ -253,7 +254,7 @@ testRslvSendsTheHash = resolvePaths reqs `shouldReturn` [["v2", "resolve", aliceHash <> ".simplex"]] -- the client never sent the name, and the record still names it case r of - NRRegistered {nameRecord} -> SMP.nrName nameRecord `shouldBe` "alice.simplex" + NameResolution {registration = NRRegistered {nameRecord}} -> SMP.nrName nameRecord `shouldBe` "alice.simplex" _ -> expectationFailure $ "expected NRRegistered, got: " <> show r testSubnameKeepsItsLabels :: IO () diff --git a/tests/SMPNamesTests.hs b/tests/SMPNamesTests.hs index d2d2a2c5d..b33d60eb1 100644 --- a/tests/SMPNamesTests.hs +++ b/tests/SMPNamesTests.hs @@ -3,7 +3,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -module SMPNamesTests (smpNamesTests, testNameRecord, testPricing, registeredBody, availableBody, reservedBody) where +module SMPNamesTests (smpNamesTests, testNameRecord, testPricing, registeredBody, availableBody, reservedBody, resolutionBody, resolved) where import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B @@ -18,7 +18,7 @@ import Network.HTTP.Types (status200, status400, status404, status500, status502 import NamesResolverServer (resolveResp, testNamesConfig, withResolverServer, withResolverServerDelayed) import Simplex.Messaging.Encoding (smpDecode, smpEncode) import Simplex.Messaging.Encoding.String (strDecode) -import Simplex.Messaging.Protocol (Command (..), ErrorType (..), NameErrorType (..), NamePricing (..), NameQuery (..), NameRecord (..), NameRegistration (..), NameReservedReason (..), ProtocolEncoding (..), USDCents (..)) +import Simplex.Messaging.Protocol (Command (..), ErrorType (..), NameErrorType (..), NamePricing (..), NameQuery (..), NameRecord (..), NameRegistration (..), NameResolution (..), NameReservedReason (..), ProtocolEncoding (..), USDCents (..)) import Simplex.Messaging.Server.Main (validateUrl) import Simplex.Messaging.Server.Names ( NamesConfig (..), @@ -54,13 +54,23 @@ testNameRecord = -- from the Haskell value: the literal JSON is the contract with the resolver. registeredBody :: NameRecord -> LB.ByteString registeredBody nameRec = - "{\"type\":\"registered\",\"expires\":1813853483,\"graceUntil\":1821629483,\"reservedReason_\":null,\"nameRecord\":" <> J.encode nameRec <> "}" + resolutionBody $ "{\"type\":\"registered\",\"expires\":1813853483,\"graceUntil\":1821629483,\"reservedReason_\":null,\"nameRecord\":" <> J.encode nameRec <> "}" availableBody :: LB.ByteString -availableBody = "{\"type\":\"available\",\"pricing\":{\"registrationPrices\":{\"3\":12793,\"4\":3198},\"basePrice\":100,\"minLabelLength\":3}}" +availableBody = resolutionBody "{\"type\":\"available\",\"pricing\":{\"registrationPrices\":{\"3\":12793,\"4\":3198},\"basePrice\":100,\"minLabelLength\":3}}" reservedBody :: LB.ByteString -reservedBody = "{\"type\":\"reserved\",\"reservedReason\":\"trademark\"}" +reservedBody = resolutionBody "{\"type\":\"reserved\",\"reservedReason\":\"trademark\"}" + +-- | The registration, and the block the resolver read it at. +resolutionBody :: LB.ByteString -> LB.ByteString +resolutionBody reg = "{\"readAt\":" <> testReadAt <> ",\"registration\":" <> reg <> "}" + +testReadAt :: LB.ByteString +testReadAt = "1813000000" + +resolved :: NameRegistration -> NameResolution +resolved registration = NameResolution {readAt = Just (RoundedSystemTime 1813000000), registration} -- | What `registeredBody testNameRecord` resolves to. registeredAlice :: NameRegistration @@ -148,17 +158,17 @@ availabilitySpec = do -- one lookup answers what the name points to, whether it can be taken, and -- whether it is held back it "a registered name answers with its record and dates" $ - answers (registeredBody testNameRecord) registeredAlice + answers (registeredBody testNameRecord) (resolved registeredAlice) it "a registered name can be held back too" $ - answers heldBackBody $ + answers heldBackBody . resolved $ NRRegistered {expires = Just (RoundedSystemTime 1813853483), graceUntil = Just (RoundedSystemTime 1821629483), reservedReason_ = Just NRRInternal, nameRecord = testNameRecord} it "an unregistered name answers with the price" $ - answers availableBody NRAvailable {pricing = testPricing} + answers availableBody (resolved NRAvailable {pricing = testPricing}) it "reserved carries the reason and no price" $ - answers reservedBody (NRReserved NRRTrademark) + answers reservedBody (resolved $ NRReserved NRRTrademark) -- losing the reservation would offer a name that cannot be registered it "a reason from a later version still reserves the name" $ - answers "{\"type\":\"reserved\",\"reservedReason\":\"seasonal\"}" (NRReserved (NRRUnknown "seasonal")) + answers (resolutionBody "{\"type\":\"reserved\",\"reservedReason\":\"seasonal\"}") (resolved $ NRReserved (NRRUnknown "seasonal")) -- RNAME carries the registration as JSON, so that is the encoding to hold it "every registration survives the wire" $ mapM_ @@ -177,7 +187,7 @@ availabilitySpec = do J.encode NRRTrademark `shouldBe` "\"trademark\"" where heldBackBody = - "{\"type\":\"registered\",\"expires\":1813853483,\"graceUntil\":1821629483,\"reservedReason_\":\"internal\",\"nameRecord\":" <> J.encode testNameRecord <> "}" + resolutionBody $ "{\"type\":\"registered\",\"expires\":1813853483,\"graceUntil\":1821629483,\"reservedReason_\":\"internal\",\"nameRecord\":" <> J.encode testNameRecord <> "}" answers body a = withResolverServer (resolveResp status200 body) $ \port _ -> do env <- newNamesEnv (testNamesConfig port) @@ -246,7 +256,7 @@ resolverSpec = do it "returns the registration on 200 OK" $ withResolverServer (resolveResp status200 (registeredBody testNameRecord)) $ \port _ -> do env <- newNamesEnv (testNamesConfig port) - resolveName env aliceDomain `shouldReturn` Right registeredAlice + resolveName env aliceDomain `shouldReturn` Right (resolved registeredAlice) -- /v2/resolve answers 200, 400 or 502 and never says "no such name": an -- unregistered name is NRAvailable. So no status maps to NOT_FOUND. From 908c6c772821a988bb81387341333f78f3c98740 Mon Sep 17 00:00:00 2001 From: Alain Brenzikofer Date: Fri, 11 Sep 2026 18:00:46 +0200 Subject: [PATCH 2/3] fix review findings --- protocol/simplex-messaging.md | 7 +++--- scripts/resolver/README.md | 6 ++--- scripts/resolver/service/snrc-resolve.py | 32 ++++++++++++------------ src/Simplex/Messaging/Names/Record.hs | 7 ++---- src/Simplex/Messaging/Protocol.hs | 1 - 5 files changed, 24 insertions(+), 29 deletions(-) diff --git a/protocol/simplex-messaging.md b/protocol/simplex-messaging.md index 6de1f2cc4..69b8e0ea9 100644 --- a/protocol/simplex-messaging.md +++ b/protocol/simplex-messaging.md @@ -1539,10 +1539,9 @@ characters. | `readAt` | number | block timestamp the answer was read at. Absent only from a v20/v21 router, which sent the record alone | | `registration` | object | below | -A router reads the registry through a node of its own, so an answer can predate -a client's own transaction. `readAt` is what lets the client tell that apart -from a current answer: a name registered after `readAt` still reads as -`available`. A client MUST NOT treat an answer as current without checking it. +A router reads the registry through a node of its own, which can lag. A name +registered or changed after `readAt` still reads as it did before, so a client +MUST NOT treat an answer as current without checking it. `registration`'s `type` selects which of the three answers it is. diff --git a/scripts/resolver/README.md b/scripts/resolver/README.md index 25ce5ca27..b949a252e 100644 --- a/scripts/resolver/README.md +++ b/scripts/resolver/README.md @@ -63,12 +63,12 @@ curl -s -X POST http://127.0.0.1:8545 \ ```sh curl -s http://127.0.0.1:8000/health | jq # → {"ok": true, "rpc": "http://reth:8545", "registries": {"testing": "0x…", "simplex": ""}, -# "blockNumber": 23400000, "readAt": 1780000000, "lagSeconds": 12} +# "blockNumber": 23400000, "chainLagSeconds": 12} ``` -`lagSeconds` is the node's latest block against the wall clock. A resolver that +`chainLagSeconds` is how far the node is behind the wall clock. A resolver that is reachable and answering can still be hours behind, and every name it reports -is that stale; `null` means the node could not be reached at all. +is that stale. `null` means the node could not be reached. **3. resolver resolves a live name** (`foobar.testing` is a populated test name): ```sh diff --git a/scripts/resolver/service/snrc-resolve.py b/scripts/resolver/service/snrc-resolve.py index c74c803ba..eadfb0b9e 100755 --- a/scripts/resolver/service/snrc-resolve.py +++ b/scripts/resolver/service/snrc-resolve.py @@ -184,6 +184,19 @@ def node_of(name: str) -> bytes: # ---------- Registration status ---------- +def head_block(): + """How far behind the node is. Unlike expiry, this is the one thing that has + to be measured against the host clock: a node that stops still has a block.""" + try: + block = rpc("eth_getBlockByNumber", ["latest", False]) + return { + "blockNumber": decode_uint(block["number"]), + "chainLagSeconds": int(time.time()) - decode_uint(block["timestamp"]), + } + except Exception: + return {"blockNumber": None, "chainLagSeconds": None} + + def chain_now() -> int: """Expiry is compared against the block timestamp, never the host clock.""" block = rpc("eth_getBlockByNumber", ["latest", False]) @@ -329,7 +342,7 @@ def name_status(name: str): out = { "status": status, - # the block this was read at: the resolver is only as current as its node + # the block this was read at "readAt": now, "expires": expires or None, "graceEnds": (expires + grace) if expires else None, @@ -711,8 +724,7 @@ def name_record(name: str): def resolution(reg, registration_body): - """The SMP protocol's NameResolution: the registration and the block it was - read at, so a client can tell an answer that predates its own transaction.""" + """The SMP protocol's NameResolution: the registration and the block read at.""" return 200, {"readAt": reg["readAt"], "registration": registration_body} @@ -853,19 +865,7 @@ def do_GET(self): # noqa: N802 - http.server contract parts = [unquote(p) for p in path.split("/") if p] if parts == ["health"]: - try: - block = rpc("eth_getBlockByNumber", ["latest", False]) - head = { - "blockNumber": decode_uint(block["number"]), - "readAt": decode_uint(block["timestamp"]), - "lagSeconds": int(time.time()) - decode_uint(block["timestamp"]), - } - except Exception: # unreachable node: say so rather than omit it - head = {"blockNumber": None, "readAt": None, "lagSeconds": None} - self._respond( - 200, - {"ok": True, "rpc": RPC, "registries": REGISTRIES, **head}, - ) + self._respond(200, {"ok": True, "rpc": RPC, "registries": REGISTRIES, **head_block()}) return if len(parts) == 3 and parts[0] == "v2" and parts[1] == "resolve": diff --git a/src/Simplex/Messaging/Names/Record.hs b/src/Simplex/Messaging/Names/Record.hs index 0b4f63e70..6e400a452 100644 --- a/src/Simplex/Messaging/Names/Record.hs +++ b/src/Simplex/Messaging/Names/Record.hs @@ -64,12 +64,9 @@ newtype USDCents = USDCents Int64 deriving (Eq, Ord, Show) deriving newtype (ToJSON, FromJSON) --- | What the registry holds for a name, and the chain state it was read at, so --- a client can tell an answer that predates its own transaction from a current --- one. The resolver is only as current as the node behind it. +-- | What the registry holds for a name, and the block it was read at. data NameResolution = NameResolution - { -- | block timestamp the registry was read at; absent only from a v20/v21 - -- router, which sent the record alone + { -- | absent only from a v20/v21 router, which sent the record alone readAt :: Maybe SystemSeconds, registration :: NameRegistration } diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 6fb3efff2..e8e554a89 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -2050,7 +2050,6 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where | v >= nameAvailSMPVersion -> fmap RNAME . J.eitherDecodeStrict . unTail <$?> _smpP | otherwise -> fmap (RNAME . oldResolution) . J.eitherDecodeStrict . unTail <$?> _smpP where - -- a v20/v21 router sent the record alone: no dates, no reservation, no block oldResolution nameRecord = NameResolution {readAt = Nothing, registration = NRRegistered {expires = Nothing, graceUntil = Nothing, reservedReason_ = Nothing, nameRecord}} serviceRespP resp From ad9fc367dc3cdfa0682c7e577b5659b518e6a0b3 Mon Sep 17 00:00:00 2001 From: Alain Brenzikofer Date: Fri, 11 Sep 2026 19:04:51 +0200 Subject: [PATCH 3/3] renaming --- protocol/simplex-messaging.md | 12 +++++----- scripts/resolver/README.md | 4 ++-- scripts/resolver/service/snrc-resolve.py | 16 +++++++------- scripts/resolver/service/test_snrc_resolve.py | 10 ++++----- src/Simplex/Messaging/Agent.hs | 6 ++--- src/Simplex/Messaging/Agent/Client.hs | 4 ++-- src/Simplex/Messaging/Client.hs | 8 +++---- src/Simplex/Messaging/Names/Record.hs | 8 +++---- src/Simplex/Messaging/Protocol.hs | 10 ++++----- src/Simplex/Messaging/Server/Names.hs | 6 ++--- .../Messaging/Server/Names/HttpResolver.hs | 4 ++-- tests/AgentTests/ResolveNameTests.hs | 4 ++-- tests/RSLVTests.hs | 14 ++++++------ tests/SMPNamesTests.hs | 22 +++++++++---------- 14 files changed, 64 insertions(+), 64 deletions(-) diff --git a/protocol/simplex-messaging.md b/protocol/simplex-messaging.md index 69b8e0ea9..a80d211b2 100644 --- a/protocol/simplex-messaging.md +++ b/protocol/simplex-messaging.md @@ -1526,21 +1526,21 @@ fact that this router cannot resolve, so iterating past it is safe. `RNAME` answers both what a name resolves to and whether it can be registered. ```abnf -rname = %s"RNAME" SP resolution +rname = %s"RNAME" SP nameResponse ``` -`resolution` is a UTF-8 JSON object consuming the remainder of the transmission: -the `registration` below, and `readAt`, the block timestamp the registry was read -at. Money is US cents, times are seconds since the Unix epoch, and lengths are +`nameResponse` is a UTF-8 JSON object consuming the remainder of the +transmission: the `registration` below, and `lastBlockTs`, the block timestamp the +registry was read at. Money is US cents, times are seconds since the Unix epoch, and lengths are characters. | Field | JSON type | Constraints | |---|---|---| -| `readAt` | number | block timestamp the answer was read at. Absent only from a v20/v21 router, which sent the record alone | +| `lastBlockTs` | number | block timestamp the answer was read at. Absent only from a v20/v21 router, which sent the record alone | | `registration` | object | below | A router reads the registry through a node of its own, which can lag. A name -registered or changed after `readAt` still reads as it did before, so a client +registered or changed after `lastBlockTs` still reads as it did before, so a client MUST NOT treat an answer as current without checking it. `registration`'s `type` selects which of the three answers it is. diff --git a/scripts/resolver/README.md b/scripts/resolver/README.md index 97f72090f..f8f5dde8b 100644 --- a/scripts/resolver/README.md +++ b/scripts/resolver/README.md @@ -134,9 +134,9 @@ its own shape does: ### v2: `/v2/resolve/` -The body is the SMP protocol's `NameResolution`, which the router decodes as is +The body is the SMP protocol's `NameResponse`, which the router decodes as is and forwards; translating the registry's model to it is this resolver's job. It -is the registration below, plus `readAt`, the timestamp of the block it was read +is the registration below, plus `lastBlockTs`, the timestamp of the block it was read at: a node that lags answers with names it has not seen registered yet. Its `type` is `registered`, `available` or `reserved`, and the fields each one carries are specified once, in the **Name response** section of diff --git a/scripts/resolver/service/snrc-resolve.py b/scripts/resolver/service/snrc-resolve.py index fe2a1355c..42881a2a8 100755 --- a/scripts/resolver/service/snrc-resolve.py +++ b/scripts/resolver/service/snrc-resolve.py @@ -317,7 +317,7 @@ def name_status(name: str): return { "status": "unknown", # nothing was read, so there is no block to report - "readAt": None, + "lastBlockTs": None, "expires": None, "graceEnds": None, "reasonCode": None, @@ -343,7 +343,7 @@ def name_status(name: str): out = { "status": status, # the block this was read at - "readAt": now, + "lastBlockTs": now, "expires": expires or None, "graceEnds": (expires + grace) if expires else None, "reasonCode": reason[0] if reason else None, @@ -723,9 +723,9 @@ def name_record(name: str): return rec -def resolution(reg, registration_body): - """The SMP protocol's NameResolution: the registration and the block read at.""" - return 200, {"readAt": reg["readAt"], "registration": registration_body} +def name_response(reg, registration_body): + """The SMP protocol's NameResponse: the registration and the block read at.""" + return 200, {"lastBlockTs": reg["lastBlockTs"], "registration": registration_body} def registration(name: str): @@ -752,7 +752,7 @@ def registration(name: str): if pricing: reg.update({k: v for k, v in pricing.items() if not k.startswith("_")}) else: - return resolution(reg, { + return name_response(reg, { "type": "registered", "expires": reg["expires"], "graceUntil": reg["graceEnds"], @@ -760,11 +760,11 @@ def registration(name: str): "nameRecord": rec, }) if reg["reasonCode"]: - return resolution(reg, {"type": "reserved", "reservedReason": reg["reasonCode"]}) + return name_response(reg, {"type": "reserved", "reservedReason": reg["reasonCode"]}) if status in ("unregistered", "expired"): if "basePrice" not in reg: return 502, {"name": name, "error": "noPriceOracle"} - return resolution(reg, { + return name_response(reg, { "type": "available", "pricing": { "registrationPrices": reg["registrationPrices"], diff --git a/scripts/resolver/service/test_snrc_resolve.py b/scripts/resolver/service/test_snrc_resolve.py index 7bf6dfa45..4b0555ed1 100644 --- a/scripts/resolver/service/test_snrc_resolve.py +++ b/scripts/resolver/service/test_snrc_resolve.py @@ -21,7 +21,7 @@ def registration(name): - """registration() answers a NameResolution; most tests assert what is in it.""" + """registration() answers a NameResponse; most tests assert what is in it.""" status, body = snrc.registration(name) return status, (body["registration"] if status == 200 else body) @@ -201,7 +201,7 @@ def _keys(self, status, expires, grace_ends, read_at=-1): """Every branch answers with the same keys; only some carry values.""" return { "status": status, - "readAt": self.now if read_at == -1 else read_at, + "lastBlockTs": self.now if read_at == -1 else read_at, "expires": expires, "graceEnds": grace_ends, "reasonCode": None, @@ -329,7 +329,7 @@ def test_unconfigured_tld_is_unknown_rather_than_unregistered(self): def test_every_branch_returns_the_same_keys(self): keys = { "status", - "readAt", + "lastBlockTs", "expires", "graceEnds", "reasonCode", @@ -1024,7 +1024,7 @@ def test_the_answer_says_which_block_it_was_read_at(self): cannot tell an answer that predates its own registration.""" snrc.eth_call = self._chain(self.now + 3600) _, res = snrc.registration("acme.testing") - self.assertEqual(res["readAt"], self.now) + self.assertEqual(res["lastBlockTs"], self.now) self.assertEqual(res["registration"]["type"], "registered") def test_an_available_name_says_so_too(self): @@ -1032,7 +1032,7 @@ def test_an_available_name_says_so_too(self): staleness matters most: the name may already be taken.""" snrc.eth_call = self._chain(0) _, res = snrc.registration("acme.testing") - self.assertEqual(res["readAt"], self.now) + self.assertEqual(res["lastBlockTs"], self.now) self.assertEqual(res["registration"]["type"], "available") if __name__ == "__main__": diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 7e2f6ed8b..e9a7379b4 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -228,7 +228,7 @@ import Simplex.Messaging.Protocol ErrorType (AUTH), MsgBody, MsgFlags (..), - NameResolution, + 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 NameResolution +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 NameResolution +resolveSimplexName' :: AgentClient -> NetworkRequestMode -> UserId -> SimplexDomain -> AM NameResponse resolveSimplexName' c nm userId domain = do resolverSrv <- getNextNameServer c userId resolveName c nm userId resolverSrv domain diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index e687f2dd3..8cbb4c17b 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -272,7 +272,7 @@ import Simplex.Messaging.Protocol NetworkError (..), MsgFlags (..), MsgId, - NameResolution, + 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 NameResolution +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 diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 7c7d23ebd..f8f1a4cb9 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -1054,7 +1054,7 @@ 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 NameResolution) +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 (NQDomain name)) >>= \case @@ -1068,7 +1068,7 @@ proxyResolveName c nm proxiedRelay name -- proxy fallback in the agent. RSLV requires no entity ID or authorization -- (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 NameResolution +directResolveName :: SMPClient -> NetworkRequestMode -> SimplexDomain -> ExceptT SMPClientError IO NameResponse directResolveName c nm name | thVersion (thParams c) >= namesSMPVersion = sendProtocolCommand c nm Nothing NoEntity (Cmd SResolver (RSLV (NQDomain name))) >>= \case @@ -1076,8 +1076,8 @@ directResolveName c nm name r -> throwE $ unexpectedResponse r | otherwise = throwE $ PCETransportError TEVersion -resolvedNameOrNotFound :: SimplexDomain -> NameResolution -> Bool -resolvedNameOrNotFound d NameResolution {registration} = case registration of +resolvedNameOrNotFound :: SimplexDomain -> NameResponse -> Bool +resolvedNameOrNotFound d NameResponse {registration} = case registration of NRRegistered {nameRecord} -> T.toLower (nrName nameRecord) == fullDomainName d _ -> True diff --git a/src/Simplex/Messaging/Names/Record.hs b/src/Simplex/Messaging/Names/Record.hs index 6e400a452..461bec16e 100644 --- a/src/Simplex/Messaging/Names/Record.hs +++ b/src/Simplex/Messaging/Names/Record.hs @@ -8,7 +8,7 @@ module Simplex.Messaging.Names.Record ( NameRecord (..), - NameResolution (..), + NameResponse (..), NameRegistration (..), NamePricing (..), USDCents (..), @@ -65,9 +65,9 @@ newtype USDCents = USDCents Int64 deriving newtype (ToJSON, FromJSON) -- | What the registry holds for a name, and the block it was read at. -data NameResolution = NameResolution +data NameResponse = NameResponse { -- | absent only from a v20/v21 router, which sent the record alone - readAt :: Maybe SystemSeconds, + lastBlockTs :: Maybe SystemSeconds, registration :: NameRegistration } deriving (Eq, Show) @@ -141,4 +141,4 @@ $(JQ.deriveJSON defaultJSON ''NamePricing) -- resolver contract, so it must not vary with the swift build flag. $(JQ.deriveJSON (taggedObjectJSON $ dropPrefix "NR") ''NameRegistration) -$(JQ.deriveJSON defaultJSON ''NameResolution) +$(JQ.deriveJSON defaultJSON ''NameResponse) diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index e8e554a89..e2cbef3a4 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -81,7 +81,7 @@ module Simplex.Messaging.Protocol CommandError (..), ProxyError (..), NameQuery (..), - NameResolution (..), + NameResponse (..), NameRegistration (..), NamePricing (..), USDCents (..), @@ -746,7 +746,7 @@ data BrokerMsg where ERR :: ErrorType -> BrokerMsg PONG :: BrokerMsg -- What the router knows about a SimpleX name. - RNAME :: NameResolution -> BrokerMsg + RNAME :: NameResponse -> BrokerMsg deriving (Eq, Show) data RcvMessage = RcvMessage @@ -2048,10 +2048,10 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where PONG_ -> pure PONG RNAME_ | v >= nameAvailSMPVersion -> fmap RNAME . J.eitherDecodeStrict . unTail <$?> _smpP - | otherwise -> fmap (RNAME . oldResolution) . J.eitherDecodeStrict . unTail <$?> _smpP + | otherwise -> fmap (RNAME . oldResponse) . J.eitherDecodeStrict . unTail <$?> _smpP where - oldResolution nameRecord = - NameResolution {readAt = Nothing, registration = NRRegistered {expires = Nothing, graceUntil = Nothing, reservedReason_ = Nothing, nameRecord}} + 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 diff --git a/src/Simplex/Messaging/Server/Names.hs b/src/Simplex/Messaging/Server/Names.hs index 33d038cb6..601519c56 100644 --- a/src/Simplex/Messaging/Server/Names.hs +++ b/src/Simplex/Messaging/Server/Names.hs @@ -22,7 +22,7 @@ import Data.Maybe (fromMaybe) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1) import Simplex.Messaging.Encoding -import Simplex.Messaging.Protocol (NameErrorType (..), NameQuery, NameResolution) +import Simplex.Messaging.Protocol (NameErrorType (..), NameQuery, NameResponse) import Simplex.Messaging.Server.Names.HttpResolver ( ResolverEnv, ResolverError (..), @@ -59,7 +59,7 @@ pingEndpoint :: NamesEnv -> IO (Either ResolverError ()) pingEndpoint NamesEnv {resolverEnv, config} = fromMaybe (Left ResolverTimeout) <$> timeout (resolverTimeoutMs config * 1000) (healthHttp resolverEnv) -resolveName :: NamesEnv -> NameQuery -> IO (Either NameErrorType NameResolution) +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 @@ -70,7 +70,7 @@ resolveName env q = do logError $ "[NAMES] resolver fetch raised " <> T.pack (E.displayException e) pure (Left (RESOLVER "resolver error")) -fetch :: NamesEnv -> NameQuery -> IO (Either NameErrorType NameResolution) +fetch :: NamesEnv -> NameQuery -> IO (Either NameErrorType NameResponse) fetch NamesEnv {resolverEnv} q = first mapResolverError <$> resolveHttp resolverEnv (decodeLatin1 $ smpEncode q) diff --git a/src/Simplex/Messaging/Server/Names/HttpResolver.hs b/src/Simplex/Messaging/Server/Names/HttpResolver.hs index 2ec96e875..0f272bcf4 100644 --- a/src/Simplex/Messaging/Server/Names/HttpResolver.hs +++ b/src/Simplex/Messaging/Server/Names/HttpResolver.hs @@ -59,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 (NameResolution) +import Simplex.Messaging.Names.Record (NameResponse) data RpcAuth = AuthBearer Text | AuthBasic Text Text @@ -112,7 +112,7 @@ authHeader = \case -- | 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 NameResolution) +resolveHttp :: ResolverEnv -> Text -> IO (Either ResolverError NameResponse) resolveHttp env q = (>>= first InvalidJson . J.eitherDecodeStrict . BL.toStrict) <$> httpGet env ("/v2/resolve/" <> B.unpack (urlEncode True (encodeUtf8 q))) diff --git a/tests/AgentTests/ResolveNameTests.hs b/tests/AgentTests/ResolveNameTests.hs index 27d402b14..d8507194e 100644 --- a/tests/AgentTests/ResolveNameTests.hs +++ b/tests/AgentTests/ResolveNameTests.hs @@ -93,7 +93,7 @@ testAvailSuccess = withDirectResolver (status200, availableBody) $ \c -> do r <- runExceptT $ resolveSimplexName c NRMInteractive 1 (SimplexDomain TLDSimplex "alice" []) case r of - Right SMP.NameResolution {registration = SMP.NRAvailable {}} -> pure () + Right SMP.NameResponse {registration = SMP.NRAvailable {}} -> pure () _ -> expectationFailure $ "expected Right NRAvailable, got: " <> show r -- | 404 is a resolver that predates /v2/resolve: no status from that endpoint @@ -159,5 +159,5 @@ testDirectSuccess = withDirectResolver (status200, registeredBody testNameRecord) $ \c -> do r <- runExceptT $ resolveSimplexName c NRMInteractive 1 (SimplexDomain TLDSimplex "alice" []) case r of - Right SMP.NameResolution {registration = SMP.NRRegistered {nameRecord}} -> nameRecord `shouldBe` testNameRecord + Right SMP.NameResponse {registration = SMP.NRRegistered {nameRecord}} -> nameRecord `shouldBe` testNameRecord _ -> expectationFailure $ "expected Right NRRegistered, got: " <> show r diff --git a/tests/RSLVTests.hs b/tests/RSLVTests.hs index 063bc23d6..9263b6953 100644 --- a/tests/RSLVTests.hs +++ b/tests/RSLVTests.hs @@ -35,7 +35,7 @@ import Simplex.Messaging.Protocol ErrorType (..), NameQuery (..), NameRegistration (..), - NameResolution (..), + NameResponse (..), NameErrorType (..), NameReservedReason (..), SParty (..), @@ -141,7 +141,7 @@ testRslvVersion = Left (PCETransportError TEVersion) -> pure () _ -> expectationFailure $ "expected Left (PCETransportError TEVersion), got: " <> show r -forwardedResolveAlice :: IO (Either SMPClientError (Either ProxyClientError SMP.NameResolution)) +forwardedResolveAlice :: IO (Either SMPClientError (Either ProxyClientError SMP.NameResponse)) forwardedResolveAlice = do g <- C.newRandom ts <- getCurrentTime @@ -164,7 +164,7 @@ testRslvForwardedSuccess :: IO () testRslvForwardedSuccess = withProxyAndResolver (status200, registeredBody testNameRecord) $ forwardedResolveAlice >>= \r -> case r of - Right (Right NameResolution {registration = NRRegistered {nameRecord}}) -> nameRecord `shouldBe` testNameRecord + Right (Right NameResponse {registration = NRRegistered {nameRecord}}) -> nameRecord `shouldBe` testNameRecord _ -> expectationFailure $ "expected Right (Right NRRegistered), got: " <> show r testRslvSuccess :: IO () @@ -174,7 +174,7 @@ testRslvSuccess = (corrId, _entId, resp) <- sendRslv h "rs07" (domain "alice.simplex") corrId `shouldBe` CorrId "rs07" case resp of - Right (RNAME NameResolution {registration = NRRegistered {nameRecord}}) -> nameRecord `shouldBe` testNameRecord + Right (RNAME NameResponse {registration = NRRegistered {nameRecord}}) -> nameRecord `shouldBe` testNameRecord _ -> expectationFailure $ "expected Right (RNAME NRRegistered), got: " <> show resp testRslvAvailable :: IO () @@ -210,7 +210,7 @@ testRslvOldClientRecord = withResolverServer (status200, registeredBody testNameRecord) $ do pc <- oldClient r <- runExceptT' (directResolveName pc NRMInteractive (domain "alice.simplex")) - r `shouldBe` NameResolution Nothing (NRRegistered Nothing Nothing Nothing testNameRecord) + r `shouldBe` NameResponse Nothing (NRRegistered Nothing Nothing Nothing testNameRecord) testRslvOldClientNotFound :: IO () testRslvOldClientNotFound = @@ -225,7 +225,7 @@ testRslvForwardedAvailable :: IO () testRslvForwardedAvailable = withProxyAndResolver (status200, availableBody) $ forwardedResolveAlice >>= \r -> case r of - Right (Right NameResolution {registration = NRAvailable {pricing}}) -> pricing `shouldBe` testPricing + Right (Right NameResponse {registration = NRAvailable {pricing}}) -> pricing `shouldBe` testPricing _ -> expectationFailure $ "expected Right (Right NRAvailable), got: " <> show r -- keccak-256("alice"), the registry key @@ -254,7 +254,7 @@ testRslvSendsTheHash = resolvePaths reqs `shouldReturn` [["v2", "resolve", aliceHash <> ".simplex"]] -- the client never sent the name, and the record still names it case r of - NameResolution {registration = NRRegistered {nameRecord}} -> SMP.nrName nameRecord `shouldBe` "alice.simplex" + NameResponse {registration = NRRegistered {nameRecord}} -> SMP.nrName nameRecord `shouldBe` "alice.simplex" _ -> expectationFailure $ "expected NRRegistered, got: " <> show r testSubnameKeepsItsLabels :: IO () diff --git a/tests/SMPNamesTests.hs b/tests/SMPNamesTests.hs index b33d60eb1..7f28a0b34 100644 --- a/tests/SMPNamesTests.hs +++ b/tests/SMPNamesTests.hs @@ -3,7 +3,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -module SMPNamesTests (smpNamesTests, testNameRecord, testPricing, registeredBody, availableBody, reservedBody, resolutionBody, resolved) where +module SMPNamesTests (smpNamesTests, testNameRecord, testPricing, registeredBody, availableBody, reservedBody, responseBody, resolved) where import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B @@ -18,7 +18,7 @@ import Network.HTTP.Types (status200, status400, status404, status500, status502 import NamesResolverServer (resolveResp, testNamesConfig, withResolverServer, withResolverServerDelayed) import Simplex.Messaging.Encoding (smpDecode, smpEncode) import Simplex.Messaging.Encoding.String (strDecode) -import Simplex.Messaging.Protocol (Command (..), ErrorType (..), NameErrorType (..), NamePricing (..), NameQuery (..), NameRecord (..), NameRegistration (..), NameResolution (..), NameReservedReason (..), ProtocolEncoding (..), USDCents (..)) +import Simplex.Messaging.Protocol (Command (..), ErrorType (..), NameErrorType (..), NamePricing (..), NameQuery (..), NameRecord (..), NameRegistration (..), NameResponse (..), NameReservedReason (..), ProtocolEncoding (..), USDCents (..)) import Simplex.Messaging.Server.Main (validateUrl) import Simplex.Messaging.Server.Names ( NamesConfig (..), @@ -54,23 +54,23 @@ testNameRecord = -- from the Haskell value: the literal JSON is the contract with the resolver. registeredBody :: NameRecord -> LB.ByteString registeredBody nameRec = - resolutionBody $ "{\"type\":\"registered\",\"expires\":1813853483,\"graceUntil\":1821629483,\"reservedReason_\":null,\"nameRecord\":" <> J.encode nameRec <> "}" + responseBody $ "{\"type\":\"registered\",\"expires\":1813853483,\"graceUntil\":1821629483,\"reservedReason_\":null,\"nameRecord\":" <> J.encode nameRec <> "}" availableBody :: LB.ByteString -availableBody = resolutionBody "{\"type\":\"available\",\"pricing\":{\"registrationPrices\":{\"3\":12793,\"4\":3198},\"basePrice\":100,\"minLabelLength\":3}}" +availableBody = responseBody "{\"type\":\"available\",\"pricing\":{\"registrationPrices\":{\"3\":12793,\"4\":3198},\"basePrice\":100,\"minLabelLength\":3}}" reservedBody :: LB.ByteString -reservedBody = resolutionBody "{\"type\":\"reserved\",\"reservedReason\":\"trademark\"}" +reservedBody = responseBody "{\"type\":\"reserved\",\"reservedReason\":\"trademark\"}" -- | The registration, and the block the resolver read it at. -resolutionBody :: LB.ByteString -> LB.ByteString -resolutionBody reg = "{\"readAt\":" <> testReadAt <> ",\"registration\":" <> reg <> "}" +responseBody :: LB.ByteString -> LB.ByteString +responseBody reg = "{\"lastBlockTs\":" <> testReadAt <> ",\"registration\":" <> reg <> "}" testReadAt :: LB.ByteString testReadAt = "1813000000" -resolved :: NameRegistration -> NameResolution -resolved registration = NameResolution {readAt = Just (RoundedSystemTime 1813000000), registration} +resolved :: NameRegistration -> NameResponse +resolved registration = NameResponse {lastBlockTs = Just (RoundedSystemTime 1813000000), registration} -- | What `registeredBody testNameRecord` resolves to. registeredAlice :: NameRegistration @@ -168,7 +168,7 @@ availabilitySpec = do answers reservedBody (resolved $ NRReserved NRRTrademark) -- losing the reservation would offer a name that cannot be registered it "a reason from a later version still reserves the name" $ - answers (resolutionBody "{\"type\":\"reserved\",\"reservedReason\":\"seasonal\"}") (resolved $ NRReserved (NRRUnknown "seasonal")) + answers (responseBody "{\"type\":\"reserved\",\"reservedReason\":\"seasonal\"}") (resolved $ NRReserved (NRRUnknown "seasonal")) -- RNAME carries the registration as JSON, so that is the encoding to hold it "every registration survives the wire" $ mapM_ @@ -187,7 +187,7 @@ availabilitySpec = do J.encode NRRTrademark `shouldBe` "\"trademark\"" where heldBackBody = - resolutionBody $ "{\"type\":\"registered\",\"expires\":1813853483,\"graceUntil\":1821629483,\"reservedReason_\":\"internal\",\"nameRecord\":" <> J.encode testNameRecord <> "}" + responseBody $ "{\"type\":\"registered\",\"expires\":1813853483,\"graceUntil\":1821629483,\"reservedReason_\":\"internal\",\"nameRecord\":" <> J.encode testNameRecord <> "}" answers body a = withResolverServer (resolveResp status200 body) $ \port _ -> do env <- newNamesEnv (testNamesConfig port)