From 45d314cfe2786d54b4b3286ef2f9bd7fe523b87f Mon Sep 17 00:00:00 2001 From: Alain Brenzikofer Date: Sat, 5 Sep 2026 22:52:42 +0200 Subject: [PATCH] docs shortening and other review fixes --- protocol/simplex-messaging.md | 101 +++++++----------- scripts/resolver/README.md | 83 ++++++-------- scripts/resolver/service/snrc-resolve.py | 35 +++--- scripts/resolver/service/test_snrc_resolve.py | 35 +++--- src/Simplex/Messaging/Agent/Client.hs | 3 +- src/Simplex/Messaging/Client.hs | 35 +++--- src/Simplex/Messaging/Protocol.hs | 18 ++-- src/Simplex/Messaging/Server/Names.hs | 46 ++++---- .../Messaging/Server/Names/HttpResolver.hs | 48 ++++----- src/Simplex/Messaging/SimplexName.hs | 18 ++-- src/Simplex/Messaging/Transport.hs | 3 +- tests/RSLVTests.hs | 13 ++- tests/SMPNamesTests.hs | 37 +++---- 13 files changed, 195 insertions(+), 280 deletions(-) diff --git a/protocol/simplex-messaging.md b/protocol/simplex-messaging.md index 7f01b82fb..d87d52210 100644 --- a/protocol/simplex-messaging.md +++ b/protocol/simplex-messaging.md @@ -1469,31 +1469,24 @@ rslv = %s"RSLV" SP domain ; domain = canonical name as non-space bytes, consum explicit (e.g. `privacy.simplex`, `test.testing`, `example.com`), bounded to 253 bytes. -**Hashed labels.** The second-level label MAY instead be given as `[` followed -by 64 lowercase hex characters and `]` — the keccak-256 hash of that label — so -a router can answer about a name without being told it. This is ENS's encoding -for a label whose preimage is unknown; the brackets are outside the name -character set, so the form cannot collide with a registrable name, and the -backing resolver uses the hash as the registry key rather than hashing the label -again. A hashed label is 66 characters and is therefore exempt from the 63-byte -DNS label limit: it is a key into the registry, not a DNS label. +**Hashed labels.** The second-level label MAY be given as `[` + 64 lowercase hex ++ `]`, the keccak-256 hash of that label, so a router can answer without being +told the name. This is ENS's encoding for an unknown preimage; brackets are +outside the name character set, so it cannot collide with a real name. A hashed +label is 66 characters and is exempt from the 63-byte label limit — it is a +registry key, not a DNS label. A bare `0x` hex string is an ordinary label, and +would be hashed again, keying a different name. -**Only the second-level label.** It is the only label the registry is keyed on; -subname labels are needed as text to reach the record, so they are never hashed. -`[].simplex` and `sub.[].simplex` both reach the node their plain -names would, and a bracket label in any other position is an ordinary label, -hashed as written. Routers MUST reject a name whose hashed label is not the -second-level one, so that client and resolver cannot disagree about which node -was asked about. A bare `0x` hex string is likewise NOT a hashed label — it is an -ordinary label, and would be hashed again, keying a different name. +Only the second-level label may be hashed; subname labels are needed as text to +reach the record. `[].simplex` and `sub.[].simplex` reach the nodes +their plain names do; a bracket label anywhere else is an ordinary label. Routers +MUST reject a name whose hashed label is not the second-level one. -**Clients send the hash.** From v22 a client MUST hash the second-level label of -every `RSLV` and `NAVL` it sends, so a registrable name never reaches a router in -the clear. Routers below v22 cannot parse the form, so a client on an older -session sends the name itself. The record returned for a hashed query names the -hash, because that is what was asked; the client restores the name it used. -A router answering a hashed query cannot know the name's length, and so cannot -know its price or whether it meets a minimum-length policy. +From v22 a client MUST hash the second-level label of every `RSLV` and `NAVL`. +Older routers cannot parse the form, so a client on an older session sends the +name. A hashed query's record names the hash; the client restores the name it +used. A router answering a hashed query does not know the name's length, so it +cannot know its price or whether it meets a minimum-length policy. **Server-side validation.** The names router parses `domain` as a fully-qualified name (TLD required — bare labels are rejected) and forwards it @@ -1572,12 +1565,10 @@ arrays are bounded by this overall budget rather than a fixed per-field count. #### Name availability command -`RSLV` answers with a record or `NOT_FOUND`, which conflates situations a client -offering a name to register must tell apart: a name nobody has registered, a -lapsed registration its previous owner may still renew, a name the registry -holds back, and a name registrable right now but not at the ordinary price. -`NAVL` asks that question directly, and takes the same `domain` payload as -`RSLV`, hashed labels included: +`RSLV` answers `NOT_FOUND` for several different cases: never registered, lapsed +but still renewable, held back, and registrable but not at the ordinary price. A +client offering a name to register needs them apart. `NAVL` asks directly, and +takes the same `domain` as `RSLV`, hashed labels included: ```abnf navl = %s"NAVL" SP domain @@ -1609,42 +1600,30 @@ reason = %s"UNSPECIFIED" / %s"TRADEMARK" / %s"PUBLIC_INTEREST" | `AUCTION` | registrable by anyone, at `premium` above the ordinary price, decaying to nothing by `auction-ends` | offer it only with the premium shown | | `RESERVED` | held back by the registry for `reason` | do not offer it; explain `reason` | -`premium` is a decimal string rather than a wire integer because registry prices -are 256-bit values that fit no fixed-width integer. It is the surcharge alone, -not the total: a router answering a hashed query cannot know the label's length -and so cannot know its ordinary price. The client, which knows the name it -hashed, adds the base price itself. +`premium` is a decimal string because prices are 256-bit integers. It is the +surcharge only: a router answering a hashed query does not know the label's +length, so it cannot know the base price. The client adds that. -All three times are absolute rather than remaining durations, so a client can -render a countdown without re-querying. A client whose clock is wrong renders a wrong -countdown; it MUST NOT treat either deadline as authorisation to register, which -only the registry grants. +Times are absolute, not durations, so a client can count down without +re-querying. A deadline is not permission to register; only the registry grants +that. -A router that cannot obtain the payload for `GRACE` or `AUCTION` MUST answer -`TAKEN` with no `expires`, rather than `AVAILABLE`. Quoting the ordinary price -for a name that carries a premium is the one materially harmful answer here, and -withholding a name the user could have had is the smaller error. +A router that cannot read the payload for `GRACE` or `AUCTION` MUST answer +`TAKEN` with no `expires`, never `AVAILABLE`. Quoting the ordinary price for a +name that carries a premium is the harmful answer. -A router that cannot read the name's status at all MUST answer `ERR NAME -RESOLVER ` and MUST NOT answer `TAKEN`, which would assert a -registration nobody read, or `NOT_FOUND`, which a client may read as "no such -name, therefore free". This covers an unreachable chain, a TLD the backing -resolver has no registry for, and any status the router does not recognise. +A router that cannot read the status at all MUST answer `ERR NAME RESOLVER +`. Not `TAKEN`, which asserts a registration it never read, and not +`NOT_FOUND`, which reads as "free". This covers an unreachable chain, an +unconfigured TLD, and any status the router does not recognise. A client MUST +treat an unknown `reason` as `UNSPECIFIED`, not as "not reserved". -`RESERVED` carries a reason code rather than a sentence so the client can word -it in the user's language. A client MUST treat a reason it does not recognise as -`UNSPECIFIED` rather than as "not reserved". - -`NAVL` fails the way `RSLV` does — `ERR NAME NO_RESOLVER` when the router has no -resolver, `ERR NAME RESOLVER ` on a transient backing failure. It is -gated on SMP v22 and MUST NOT be sent to a router that negotiated a lower -version. Like `RSLV` it is unauthenticated and accepted directly or inside a -`PFWD` block, and clients SHOULD prefer the forwarded path: a hashed label keeps -the name from the router, but only the proxy keeps the client's IP from it. A -client whose proxy cannot carry `NAVL` — every proxy below v22, since the proxy -caps the relay version at `proxiedSMPRelayVersion` — falls back to a direct send -if its network configuration allows one, so during rollout the names router sees -the client's IP alongside the hash, and never the name. +`NAVL` fails as `RSLV` does: `ERR NAME NO_RESOLVER`, or `ERR NAME RESOLVER +`. It is gated on v22 and MUST NOT be sent to a lower version. Like +`RSLV` it is unauthenticated and works directly or in a `PFWD` block; clients +SHOULD use the proxy, because the hash hides the name but only the proxy hides +the IP. Proxies below v22 cannot carry `NAVL`, so during rollout a client that +allows direct fallback reaches the router itself — with the hash, never the name. ## Transport connection with the SMP router diff --git a/scripts/resolver/README.md b/scripts/resolver/README.md index 87143a6f3..83eee825a 100644 --- a/scripts/resolver/README.md +++ b/scripts/resolver/README.md @@ -172,48 +172,33 @@ name it sits under. ### The post-grace auction -When grace ends the registrar will sell the name to anyone, but the price -oracle adds a premium that halves each day until it reaches zero. A name in -that window reports `auction` rather than `expired`, with `premium` (a decimal -string of attoUSD, because the value is a 256-bit integer that no JSON number -can hold) and `auctionEnds`. +When grace ends anyone may register the name, but the price oracle adds a +premium that halves each day until it reaches zero. A name in that window +reports `auction` instead of `expired`, with `premium` (attoUSD as a decimal +string, since no JSON number holds a 256-bit integer) and `auctionEnds`. -The premium depends only on when the registration lapsed, never on the label, -so it is answerable for a labelhash query too. The base price is not: it depends -on the label's length, which a hashed query does not carry. `premium` is -therefore the surcharge alone, and a client that knows its own name adds the -base price itself. +The premium depends only on when the registration lapsed, not on the label, so a +labelhash query gets it too. The base price does depend on the label's length, +which a hashed query does not carry, so `premium` is the surcharge alone and the +client adds the base price. -The oracle is found through the controller's `prices()`, so no extra -configuration is needed. Its window is read from the chain rather than assumed, -because the owner can retune it; a window of zero days switches the auction off, -and every lapsed name then reports `expired` directly. The curve -(`startPremium`, `totalDays`, `endValue`) is cached for `AUCTION_PARAMS_TTL` -seconds, 5 minutes by default, since it changes only when the owner calls -`setPremium`; the decaying premium itself is read from the oracle on every -query. A retune is therefore visible within the TTL, not immediately. +The oracle comes from the controller's `prices()`, so no extra configuration is +needed. Its window is read from the chain; zero days switches the auction off. +The curve is cached for `AUCTION_PARAMS_TTL` (5 minutes), so a `setPremium` +retune shows up within that; the decaying premium is read on every query. -**Known gap.** When the auction cannot be read at all — no controller -configured, or the oracle unreachable — the name reports `expired`, which routers -map to "available at the ordinary price". A name still inside its auction would -then be quoted at list price while the registrar charges the premium. Configure -`SNRC_CONTROLLER_` wherever `SNRC_REGISTRAR_` is set, and upgrade this -service before the routers that query it. - -**Upgrade this service before the routers that query it.** Routers from v22 hash -the 2LD of every query, and two things only this version does are needed to -answer them: decoding a bracket label that sits under a subname -(`sub.[].tld`, which an older resolver hashes as literal text and so -answers about a node nobody asked about), and reporting `auction` at all — an -older resolver calls a name in its auction plain `expired`, which reads as "free -at the ordinary price" while the registrar charges the premium. +**Upgrade this service before the routers that query it.** An older resolver +reports a name in its auction as plain `expired`, which routers read as +"available at the ordinary price" while the registrar charges the premium. It +also fails to decode a bracket label under a subname (`sub.[].tld`), which +routers from v22 send. The same wrong quote happens when the auction cannot be +read at all, so set `SNRC_CONTROLLER_` wherever `SNRC_REGISTRAR_` is. ### Why a name is reserved -`reserved` carries both `reasonCode`, the controller's own reservation reason, -and `reason`, an English sentence for a human reading the REST API. Clients -should branch on `reasonCode` and word it themselves, so the wording follows the -user's language rather than the server's. +`reserved` carries `reasonCode`, the controller's reason, and `reason`, an +English sentence for a human reading this API. Clients should branch on +`reasonCode` and word it themselves, in the user's language. | `reasonCode` | Meaning | |---|---| @@ -224,10 +209,9 @@ user's language rather than the server's. | `internal` | reserved for SimpleX | | `premium` | reserved as a premium name | -A controller deployed before reservation reasons existed stores a plain boolean, -whose `true` reads back as `unspecified`, so nothing needs migrating. A code -this resolver does not know also reads as `unspecified` — the name stays -reserved either way. +A controller from before reasons existed stores a boolean; its `true` reads as +`unspecified`, so nothing needs migrating. An unknown code also reads as +`unspecified` — the name stays reserved either way. ### Querying by labelhash @@ -245,18 +229,15 @@ returns the same record. The registrar keys `nameExpires` and `reservedNames` on the labelhash too, so the status fields do not need the label either. The resolver learns the name only by guessing the label and hashing it. -Only the second-level label is a registry key, so only it is decoded — but it is -decoded wherever it sits, so `sub.[].testing` reaches the node -`sub.name.testing` does. Subname labels are needed as text to walk down to the -record and are never hashed; a bracket label to the left of the 2LD is an -ordinary label and is hashed as written. SMP routers from v22 send every 2LD -this way, so in normal operation a registrable name never reaches this service. +Only the second-level label is a registry key, and it is decoded wherever it +sits: `sub.[].testing` reaches the node `sub.name.testing` does. Subname +labels stay text; a bracket label left of the 2LD is an ordinary label. Routers +from v22 send every 2LD this way, so a registrable name normally never reaches +this service. -Read the answer from `status`. A name is free when the body says -`unregistered` (a 404), and also when it says `expired` or `auction` (a 410) — -though `auction` costs a premium on top. Every other status means somebody holds -the name or the registry holds it back. Watch out for `noResolver`: it is also a -404, but the name is taken. +Read the answer from `status`. A name is free on `unregistered` (404), and on +`expired` or `auction` (410) — `auction` costs a premium on top. Every other +status means somebody holds the name. Watch `noResolver`: also a 404, but taken. The hash must be keccak-256. `openssl dgst -sha3-256` and `sha3sum` compute SHA3-256, a different function that returns 64 valid-looking hex characters diff --git a/scripts/resolver/service/snrc-resolve.py b/scripts/resolver/service/snrc-resolve.py index 7ada4d98a..12917c0ae 100755 --- a/scripts/resolver/service/snrc-resolve.py +++ b/scripts/resolver/service/snrc-resolve.py @@ -105,9 +105,8 @@ CONTROLLERS = { "simplex": os.environ.get("SNRC_CONTROLLER_SIMPLEX", ""), # not deployed yet } -# `reservedNames` maps a name to SimplexController.Reason; 0 (None) means the -# name is not reserved. A controller predating the enum stores a bool, whose -# `true` decodes as 1 - the same "unspecified" this table already describes. +# `reservedNames` holds a SimplexController.Reason; 0 means not reserved. A +# controller from before the enum stores a bool, whose `true` decodes as 1. RESERVED_REASONS = { 1: ("unspecified", "reserved for a brand or public interest"), 2: ("trademark", "reserved to protect a trademark"), @@ -171,10 +170,9 @@ def is_encoded_labelhash(label: str) -> bool: def node_of(name: str) -> bytes: - """namehash, accepting the 2LD's label as an encoded labelhash at any depth, - so `[hash].tld` and `sub.[hash].tld` both reach the node the name itself - would. Only that label is a registry key: a bracket label anywhere else is - hashed as written, which is what the routers also enforce.""" + """namehash, decoding the 2LD's label as a labelhash wherever it sits, so + `[hash].tld` and `sub.[hash].tld` reach the nodes their names do. A bracket + label anywhere else is hashed as written.""" labels = name.split(".") if len(labels) < 2 or not is_encoded_labelhash(labels[-2]): return namehash(name) @@ -219,10 +217,8 @@ def reservation_reason(tld: str, token: int) -> int: return decode_uint(raw) -# The oracle address and its curve change only when the owner retunes the -# auction, so they are read at most once per AUCTION_PARAMS_TTL seconds instead -# of on every lapsed-name query. The premium itself is never cached: it decays -# continuously and is read from the oracle each time. +# The oracle and its curve change only on a retune, so they are read once per +# TTL rather than per query. The premium itself decays, so it is never cached. AUCTION_PARAMS_TTL = 300 _auction_params: dict = {} @@ -249,19 +245,18 @@ def auction_params(tld: str): def auction(tld: str, grace_ends: int, now: int): - """Past its grace period a name is registrable again, but at a premium that - decays to zero over the price oracle's auction window. Returns when the - premium reaches zero and what it is now, in attoUSD, or (None, None) once - prices are back to normal - which includes an auction switched off by - setting totalDays to 0.""" + """Past grace a name is registrable again, but at a premium decaying to zero + over the oracle's window. Returns when the premium reaches zero and what it + is now, in attoUSD, or (None, None) once prices are normal - which includes + an auction switched off with totalDays 0.""" oracle, start, total_days, floor = auction_params(tld) if oracle == ZERO_ADDR: return None, None ends = grace_ends + total_days * 86400 if now >= ends: return None, None - # decayedPremium is `pure`, so the premium quoted here is the oracle's own - # arithmetic rather than a reimplementation of its decay curve. + # decayedPremium is `pure`, so this is the oracle's own arithmetic rather + # than a second copy of its decay curve. decayed = decode_uint( eth_call( oracle, @@ -289,8 +284,8 @@ def name_status(name: str): } # nameExpires and reservedNames are keyed on uint256(keccak(label)). - # Only the 2LD's label is a registry key, wherever in the name it sits, so it - # is the only one decoded - the same rule node_of applies to the node. + # Only the 2LD's label is a registry key, wherever it sits - the same rule + # node_of applies to the node. label = labels[-2] if is_encoded_labelhash(label): token = int(label[1:-1], 16) diff --git a/scripts/resolver/service/test_snrc_resolve.py b/scripts/resolver/service/test_snrc_resolve.py index d2a2296e4..7bc7f9ad0 100644 --- a/scripts/resolver/service/test_snrc_resolve.py +++ b/scripts/resolver/service/test_snrc_resolve.py @@ -137,8 +137,8 @@ class EncodedLabelhashTests(unittest.TestCase): self.assertEqual(snrc.node_of("alice.testing"), snrc.namehash("alice.testing")) def test_a_bracket_subname_label_stays_literal(self): - """Only the 2LD is a registry key, so a bracket label to the left of it - is a name in its own right and is hashed as written.""" + """Only the 2LD is a key, so a bracket label left of it is hashed as + written.""" self.assertNotEqual( snrc.node_of( "[9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501]" @@ -148,8 +148,7 @@ class EncodedLabelhashTests(unittest.TestCase): ) def test_a_hashed_2ld_under_a_subname_reaches_the_same_node(self): - """Clients hash the 2LD and leave subname labels as text, so - `sub.[hash].tld` must reach the node `sub.name.tld` does.""" + """`sub.[hash].tld` must reach the node `sub.name.tld` does.""" self.assertEqual( snrc.node_of( "sub." @@ -213,7 +212,7 @@ class NameStatusTests(unittest.TestCase): return eth_call def _keys(self, status, expires, grace_ends): - """Every branch answers with the same keys; only some carry a value.""" + """Every branch answers with the same keys; only some carry values.""" return { "status": status, "expires": expires, @@ -314,8 +313,7 @@ class NameStatusTests(unittest.TestCase): self.assertTrue(seen[0].endswith(snrc.keccak(b"alice").hex())) def test_a_hashed_2ld_is_queried_by_its_hash_at_any_depth(self): - """Clients hash the 2LD and leave subname labels as text, so the token - must come from the hash, not from hashing the bracket text again.""" + """The token must come from the hash, not from hashing the brackets.""" seen = [] def eth_call(to, data): @@ -480,16 +478,14 @@ class ReservedReasonTests(unittest.TestCase): self.assertEqual(body["reasonCode"], "trademark") def test_a_controller_storing_a_bool_reads_as_unspecified(self): - """Before the enum, `reservedNames` was a bool; its `true` decodes as 1, - which is the value this table already describes as unspecified.""" + """Before the enum `reservedNames` was a bool; its `true` decodes as 1.""" snrc.eth_call = self._reserved_as(1) reg = snrc.name_status("acme.testing") self.assertEqual(reg["reasonCode"], "unspecified") self.assertEqual(reg["reason"], "reserved for a brand or public interest") def test_an_enum_value_this_resolver_predates_is_not_dropped(self): - """A controller upgraded with a new Reason still reports the name as - reserved; only the wording falls back.""" + """A new Reason still reads as reserved; only the wording falls back.""" snrc.eth_call = self._reserved_as(99) reg = snrc.name_status("acme.testing") self.assertEqual(reg["status"], "reserved") @@ -530,9 +526,8 @@ class ReservedReasonTests(unittest.TestCase): class AuctionTests(unittest.TestCase): - """Once grace ends the registrar will sell the name to anyone, but the price - oracle adds a premium that halves each day until it reaches zero. Reporting - such a name as plainly available would quote the normal price for it.""" + """Past grace anyone may register the name, but at a premium that halves + each day. Reporting it as plainly available would quote the normal price.""" REGISTRY = "0x58fc46996d975c57883564648bda5206d1a0102b" REGISTRAR = "0xef47eb4384b46c89e4482a677c2cbcbd2a6fd85a" @@ -569,9 +564,8 @@ class AuctionTests(unittest.TestCase): ) = self._saved def _chain(self, expires, total_days=TOTAL_DAYS, oracle=None, reserved=0): - """Answers as SimplexController and SimplexPriceOracle do, including the - oracle's own `decayedPremium` shift, so the arithmetic under test is the - resolver's and not a second copy of the decay curve.""" + """Answers as the controller and oracle do, including the oracle's own + `decayedPremium` shift, so the decay curve is not copied here.""" oracle = self.ORACLE if oracle is None else oracle self.oracle_calls = [] @@ -603,8 +597,7 @@ class AuctionTests(unittest.TestCase): def _lapsed(self, days_into_auction): """An expiry whose grace ended `days_into_auction` days ago. The extra - second clears the boundary, which the registrar counts as still in - grace.""" + second clears the boundary, which counts as still in grace.""" return self.now - self.GRACE - 1 - days_into_auction * 86400 def test_a_name_just_past_grace_is_in_auction_not_merely_expired(self): @@ -651,8 +644,8 @@ class AuctionTests(unittest.TestCase): self.assertEqual(self.oracle_calls, []) def test_the_oracle_curve_is_read_once_not_per_query(self): - """The curve changes only when the owner retunes the auction, so only the - decaying premium is re-read; the rest would be four RPC calls per query.""" + """The curve changes only on a retune, so only the decaying premium is + re-read; the rest would be four RPC calls per query.""" snrc.eth_call = self._chain(self._lapsed(1)) snrc.name_status("acme.testing") seen_first = len(self.oracle_calls) diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 4557541af..011ac5899 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -2002,8 +2002,7 @@ resolveName c nm userId server domain = resolveViaProxy smp proxySess = proxyResolveName smp nm proxySess domain resolveDirectly smp = directResolveName smp nm domain --- | Ask whether a name can be registered, by the same proxy-preferred path as --- `resolveName`. +-- | Ask whether a name can be registered. Same path as `resolveName`. getNameAvailability :: AgentClient -> NetworkRequestMode -> UserId -> SMPServer -> SimplexDomain -> AM NameAvailability getNameAvailability c nm userId server domain = snd <$> sendOrProxySMPCommand c nm userId server "" "NAVL" NoEntity availViaProxy availDirectly diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index 5b553dacf..8fc26a6aa 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -1056,14 +1056,22 @@ 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. +-- | How a name goes on the wire. From v22 the second-level label is sent as its +-- hash; older routers can only parse the name. A hashed query's record names the +-- hash, so callers put back the name they asked for. +queryDomain :: VersionSMP -> SimplexDomain -> SimplexDomain +queryDomain v d = if v >= nameAvailSMPVersion then hashedDomain d else d + proxyResolveName :: SMPClient -> NetworkRequestMode -> ProxiedRelay -> SimplexDomain -> ExceptT SMPClientError IO (Either ProxyClientError NameRecord) proxyResolveName c nm proxiedRelay name - | prVersion proxiedRelay >= namesSMPVersion = - proxySMPCommand c nm proxiedRelay Nothing NoEntity (RSLV (queryDomain (prVersion proxiedRelay) name)) >>= \case - Right (RNAME nr) -> pure $ Right (namedFor name nr) + | v >= namesSMPVersion = + proxySMPCommand c nm proxiedRelay Nothing NoEntity (RSLV (queryDomain v name)) >>= \case + Right (RNAME nr) -> pure $ Right nr {nrName = fullDomainName name} Right r -> throwE $ unexpectedResponse r Left e -> pure $ Left e | otherwise = throwE $ PCETransportError TEVersion + where + v = prVersion proxiedRelay -- | Direct (non-PFWD) name resolution. Exposes the client IP to the resolver; -- callers that want anonymity should use `proxyResolveName` via the standard @@ -1074,26 +1082,13 @@ directResolveName :: SMPClient -> NetworkRequestMode -> SimplexDomain -> ExceptT directResolveName c nm name | v >= namesSMPVersion = sendProtocolCommand c nm Nothing NoEntity (Cmd SResolver (RSLV (queryDomain v name))) >>= \case - RNAME nr -> pure (namedFor name nr) + RNAME nr -> pure nr {nrName = fullDomainName name} r -> throwE $ unexpectedResponse r | otherwise = throwE $ PCETransportError TEVersion where v = thVersion (thParams c) --- | How a name travels to the router. From `nameAvailSMPVersion` the --- second-level label is replaced by its hash, so the router answers about the --- name without being told it; an older router can only parse the name itself. -queryDomain :: VersionSMP -> SimplexDomain -> SimplexDomain -queryDomain v d = if v >= nameAvailSMPVersion then hashedDomain d else d - --- | The record names whatever was asked for, which for a hashed query is the --- hash, so the name the caller used is put back. -namedFor :: SimplexDomain -> NameRecord -> NameRecord -namedFor d nr = nr {nrName = fullDomainName d} - --- | Ask whether a name can be registered, over PFWD. Availability is a second --- question about the same name rather than a variant of resolution, so it has --- its own command and its own version gate. +-- | Ask whether a name can be registered, over PFWD. proxyNameAvailability :: SMPClient -> NetworkRequestMode -> ProxiedRelay -> SimplexDomain -> ExceptT SMPClientError IO (Either ProxyClientError NameAvailability) proxyNameAvailability c nm proxiedRelay name | prVersion proxiedRelay >= nameAvailSMPVersion = @@ -1103,8 +1098,8 @@ proxyNameAvailability c nm proxiedRelay name Left e -> pure $ Left e | otherwise = throwE $ PCETransportError TEVersion --- | Direct (non-PFWD) availability query, exposing the client IP to the --- resolver exactly as `directResolveName` does. +-- | Direct (non-PFWD) availability query. Exposes the client IP, as +-- `directResolveName` does. directNameAvailability :: SMPClient -> NetworkRequestMode -> SimplexDomain -> ExceptT SMPClientError IO NameAvailability directNameAvailability c nm name | thVersion (thParams c) >= nameAvailSMPVersion = diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 97632e71d..98ef27f3d 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -606,8 +606,7 @@ data Command (p :: Party) where RFWD :: EncFwdTransmission -> Command ProxyService -- use CorrId as CbNonce, proxy to relay -- Resolve SimpleX name. RSLV :: SimplexDomain -> Command Resolver - -- Whether a SimpleX name can be registered. Asked of a labelhash when the - -- client does not want to say which name it is about. + -- Whether a SimpleX name can be registered. NAVL :: SimplexDomain -> Command Resolver deriving instance Show (Command p) @@ -1601,9 +1600,8 @@ data ErrorType DUPLICATE_ -- not part of SMP protocol, used internally deriving (Eq, Show) --- | Whether a name can be registered, and when it cannot, what stands in the --- way. A lapsed registration past its grace period is available again, which is --- the distinction a caller cannot draw from resolution alone. +-- | Whether a name can be registered, and if not, why. Resolution alone cannot +-- tell a lapsed name from a live one. data NameAvailability = NAVailable | -- | registered to someone until this time, absent when the router could not @@ -1611,8 +1609,8 @@ data NameAvailability NATaken {naExpires :: Maybe Int64} | -- | lapsed, and renewable by its previous owner until this time NAInGrace {naGraceEnds :: Int64} - | -- | registrable by anyone, but at a premium, in attoUSD, that decays to - -- nothing by this time - quoting the usual price would understate it + | -- | registrable by anyone, but at a premium in attoUSD that decays to + -- nothing by this time NAAuction {naPremium :: Text, naAuctionEnds :: Int64} | NAReserved {naReason :: NameReservedReason} deriving (Eq, Show) @@ -1633,10 +1631,8 @@ instance Encoding NameAvailability where "RESERVED" -> NAReserved <$> _smpP _ -> fail "bad NameAvailability" --- | Why a name is held back, so the app can word it in the user's language --- instead of showing a sentence chosen by the server. Mirrors the reservation --- reasons the registry controller stores; "not reserved" has no constructor --- here, as it is not an answer this type is used to give. +-- | Why a name is held back, as a code so the app can word it in the user's +-- language. Mirrors the reasons the registry controller stores. data NameReservedReason = NRUnspecified | NRTrademark diff --git a/src/Simplex/Messaging/Server/Names.hs b/src/Simplex/Messaging/Server/Names.hs index 50d106d17..d87e9dcfd 100644 --- a/src/Simplex/Messaging/Server/Names.hs +++ b/src/Simplex/Messaging/Server/Names.hs @@ -73,8 +73,7 @@ resolveName env d = do logError $ "[NAMES] resolver fetch raised " <> T.pack (E.displayException e) pure (Left (RESOLVER "resolver error")) --- | Whether a name can be registered. Same timeout and failure handling as --- 'resolveName', which is the other question this server asks the resolver. +-- | Whether a name can be registered. Same timeout handling as 'resolveName'. getNameAvailability :: NamesEnv -> SimplexDomain -> IO (Either NameErrorType NameAvailability) getNameAvailability env d = do r <- E.try (timeout (resolverTimeoutMs (config env) * 1000) (fetchAvail env d)) @@ -90,37 +89,32 @@ fetchAvail :: NamesEnv -> SimplexDomain -> IO (Either NameErrorType NameAvailabi fetchAvail NamesEnv {resolverEnv} d = either (Left . mapAvailError) mapAvailability <$> availabilityHttp resolverEnv (fullDomainName d) --- | NAVL answers whether a name can be registered, so a resolver failure must --- never look like an answer about the name: NOT_FOUND, which 'mapResolverError' --- returns for 404/410/400, would read as "no such name, therefore free". +-- | NAVL must not fail as NOT_FOUND: a client reads that as "no such name, so +-- it is free". 'mapResolverError' returns it for 404/410/400. mapAvailError :: ResolverError -> NameErrorType mapAvailError = \case HttpStatusErr code -> RESOLVER ("HTTP " <> T.pack (show code)) e -> mapResolverError e --- | The resolver's own vocabulary. A lapsed registration past its grace period --- is available again; one still in grace belongs to its previous owner; one in --- the auction that follows grace is registrable, but not at the usual price. --- Only the statuses that describe the name are answers - anything else means the --- resolver could not answer, and saying "taken" to that would assert a --- registration that was never read. +-- | The resolver's vocabulary. Only the statuses that describe the name are +-- answers; anything else means it could not answer, and "taken" would assert a +-- registration nobody read. mapAvailability :: NameStatusResp -> Either NameErrorType NameAvailability -mapAvailability NameStatusResp {nsStatus, nsExpires, nsGraceEnds, nsAuctionEnds, nsPremium, nsReasonCode} = case nsStatus of - "unregistered" -> Right NAVailable - "expired" -> Right NAVailable - "grace" -> Right $ maybe lapsed NAInGrace nsGraceEnds - "auction" -> Right $ fromMaybe lapsed (NAAuction <$> nsPremium <*> nsAuctionEnds) - "reserved" -> Right $ NAReserved (maybe NRUnspecified mapReason nsReasonCode) - "registered" -> Right $ NATaken nsExpires - -- registered, but its records point nowhere - "noResolver" -> Right $ NATaken nsExpires - -- the resolver's own word for what it could not do, bounded because it is - -- its text, not ours, and it travels to the client inside ERR - s -> Left (RESOLVER (T.take 32 s)) +mapAvailability NameStatusResp {nsStatus, nsExpires, nsGraceEnds, nsAuctionEnds, nsPremium, nsReasonCode} = + case nsStatus of + "unregistered" -> Right NAVailable + "expired" -> Right NAVailable + "grace" -> Right $ maybe lapsed NAInGrace nsGraceEnds + "auction" -> Right $ fromMaybe lapsed (NAAuction <$> nsPremium <*> nsAuctionEnds) + "reserved" -> Right $ NAReserved (maybe NRUnspecified mapReason nsReasonCode) + "registered" -> Right $ NATaken nsExpires + -- registered, but its records point nowhere + "noResolver" -> Right $ NATaken nsExpires + -- the resolver's own words, bounded: they reach the client inside ERR + s -> Left (RESOLVER (T.take 32 s)) where - -- A lapsed name missing the deadline or price that its status carries: - -- withholding it is safer than quoting the ordinary price, but its expiry is - -- in the past, so it is not "registered until" anything. + -- lapsed, but missing the deadline or price its status carries. Withhold it + -- rather than quote the ordinary price; its expiry is already past. lapsed = NATaken Nothing -- | The controller's reservation reasons, as the resolver spells them. diff --git a/src/Simplex/Messaging/Server/Names/HttpResolver.hs b/src/Simplex/Messaging/Server/Names/HttpResolver.hs index ccf1933a4..0a54f809f 100644 --- a/src/Simplex/Messaging/Server/Names/HttpResolver.hs +++ b/src/Simplex/Messaging/Server/Names/HttpResolver.hs @@ -36,6 +36,7 @@ where import qualified Control.Exception as E import qualified Data.Aeson as J import Data.Aeson.Key (Key) +import qualified Data.Aeson.Types as JT import qualified Data.Aeson.KeyMap as JKM import Data.Bifunctor (first) import qualified Data.ByteArray.Encoding as BAE @@ -82,8 +83,8 @@ data ResolverEnv = ResolverEnv maxResponseBytes :: Int } --- | What the resolver says about a name's registrability. Only some statuses --- carry the fields below the status, so each is read as optional. +-- | What the resolver says about a name. Only some statuses carry the fields +-- below the status. data NameStatusResp = NameStatusResp { nsStatus :: Text, nsExpires :: Maybe Int64, @@ -137,11 +138,9 @@ resolveHttp env name = <$> httpGet env ("/resolve/" <> B.unpack (urlEncode True (encodeUtf8 name))) -- | GET /resolve/, reading what the resolver says about the name --- rather than only whether it answered. The status code alone cannot separate a --- name nobody has taken from one held back, nor a lapsed name still renewable by --- its owner from one anyone may take - that is in the body, under "status" on a --- 200 and "error" otherwise, alongside the deadline or price that status --- carries. +-- rather than only whether it answered. The status code cannot tell an +-- unregistered name from a reserved or lapsed one; that is in the body, under +-- "status" on a 200 and "error" otherwise. availabilityHttp :: ResolverEnv -> Text -> IO (Either ResolverError NameStatusResp) availabilityHttp ResolverEnv {manager, baseUrl, authHdr, timeoutMicro, maxResponseBytes} name = do req0 <- parseRequest (baseUrl <> "/resolve/" <> B.unpack (urlEncode True (encodeUtf8 name))) @@ -159,35 +158,30 @@ availabilityHttp ResolverEnv {manager, baseUrl, authHdr, timeoutMicro, maxRespon if BL.length bs > fromIntegral maxResponseBytes then Left BodyTooLarge else case J.decode bs of - Just (J.Object o) - | Just (J.String t) <- JKM.lookup field o -> - Right - NameStatusResp - { nsStatus = t, - nsExpires = jsonField o "expires", - nsGraceEnds = jsonField o "graceEnds", - nsAuctionEnds = jsonField o "auctionEnds", - nsPremium = jsonField o "premium" >>= decimalPrice, - nsReasonCode = jsonField o "reasonCode" - } + Just (J.Object o) | Just (J.String t) <- JKM.lookup field o -> Right (statusResp t o) _ -> Left (HttpStatusErr status) pure (either (Left . HttpFailure) id result) + where + statusResp t o = + NameStatusResp + { nsStatus = t, + nsExpires = jsonField o "expires", + nsGraceEnds = jsonField o "graceEnds", + nsAuctionEnds = jsonField o "auctionEnds", + nsPremium = jsonField o "premium" >>= decimalPrice, + nsReasonCode = jsonField o "reasonCode" + } --- | A price is a 256-bit integer written in decimal, so at most 78 digits. The --- wire format prefixes it with a single length byte, which would wrap silently --- on a longer string and leave the whole response unparseable, so anything else --- is dropped rather than re-encoded. +-- | A price is at most 78 decimal digits. The wire format length-prefixes it +-- with one byte, which would wrap on anything longer, so drop it instead. decimalPrice :: Text -> Maybe Text decimalPrice t | not (T.null t) && T.length t <= 78 && T.all isDigit t = Just t | otherwise = Nothing --- | A field the resolver omits, or sends as null, for the statuses that do not --- carry it. +-- | A field the resolver omits or nulls for statuses that do not carry it. jsonField :: J.FromJSON a => J.Object -> Key -> Maybe a -jsonField o k = case J.fromJSON <$> JKM.lookup k o of - Just (J.Success v) -> Just v - _ -> Nothing +jsonField o k = JT.parseMaybe J.parseJSON =<< JKM.lookup k o -- | GET /health; success = reachable with status < 400. The body is -- size-capped but NOT decoded — the probe only checks reachability. diff --git a/src/Simplex/Messaging/SimplexName.hs b/src/Simplex/Messaging/SimplexName.hs index b309b661c..ee0d4a68f 100644 --- a/src/Simplex/Messaging/SimplexName.hs +++ b/src/Simplex/Messaging/SimplexName.hs @@ -74,12 +74,9 @@ nameLabelP = do -- (Cyrillic а vs ASCII a hash to different on-chain records). isNameLetter c = c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' --- | A second-level label given as its own keccak256 hash, so a router never --- learns the name it is asked about. ENS's encoding for a label whose preimage --- is unknown: the brackets are outside the name character set, so the form --- cannot collide with a registrable name, and the resolver reads the hash as the --- registry key instead of hashing the label again. 66 characters, so it is --- exempt from the DNS label limit: it is a key into the registry, not a label. +-- | A second-level label sent as its keccak256 hash, so a router never learns +-- the name. ENS's bracket form: brackets are outside the name character set, so +-- it cannot collide with a real name. 66 chars, so exempt from the label limit. labelHashP :: AT.Parser Text labelHashP = do hex <- AT.char '[' *> AT.takeWhile1 (\c -> isDigit c || c >= 'a' && c <= 'f') <* AT.char ']' @@ -88,16 +85,15 @@ labelHashP = do isLabelHash :: Text -> Bool isLabelHash t = T.length t == 66 && T.head t == '[' && T.last t == ']' --- | The name with its second-level label replaced by that label's keccak256 --- hash, which is what the registry is keyed on - so a router can answer about --- the name without being told it. Subname labels are left as text, as reaching --- the record needs them, and a web TLD has no registry to key into. +-- | Replace the second-level label with its keccak256 hash, the registry key. +-- Subname labels stay text; a web TLD has no registry. hashedDomain :: SimplexDomain -> SimplexDomain hashedDomain d@SimplexDomain {nameTLD, domain} | nameTLD == TLDWeb || isLabelHash domain = d | otherwise = d {domain = "[" <> labelHash <> "]"} where - labelHash = decodeLatin1 $ BAE.convertToBase BAE.Base16 (hash (encodeUtf8 (T.toLower domain)) :: Digest Keccak_256) + keccak = hash (encodeUtf8 (T.toLower domain)) :: Digest Keccak_256 + labelHash = decodeLatin1 (BAE.convertToBase BAE.Base16 keccak) -- | Cap the name at 253 bytes (DNS full-domain limit) boundedNonSpace :: A.Parser ByteString diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 21edf16c1..4c905094f 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -209,8 +209,7 @@ namesSMPVersion = VersionSMP 20 serverInfoSMPVersion :: VersionSMP serverInfoSMPVersion = VersionSMP 21 --- | NAVL: whether a name can be registered. A server below this does not know --- the command, so a client must not send it. +-- | NAVL. A server below this does not know the command. nameAvailSMPVersion :: VersionSMP nameAvailSMPVersion = VersionSMP 22 diff --git a/tests/RSLVTests.hs b/tests/RSLVTests.hs index 83681cf19..73983421e 100644 --- a/tests/RSLVTests.hs +++ b/tests/RSLVTests.hs @@ -224,8 +224,8 @@ testNavlVersion = g <- C.newRandom ts <- getCurrentTime let srv = SMPServer testHost testPort testKeyHash - -- the version immediately below the gate: a range ending lower would - -- also pass for a gate at 20 or 21 and prove nothing about v22 + -- the version just below the gate: a lower ceiling would also pass for + -- a gate at 20 or 21 and prove nothing about v22 oldCfg = defaultSMPClientConfig {serverVRange = mkVersionRange minServerSMPRelayVersion serverInfoSMPVersion} pcE <- getProtocolClient g NRMInteractive (1, srv, Nothing) oldCfg [] Nothing ts (\_ -> pure ()) pc <- either (fail . show) pure pcE @@ -254,12 +254,11 @@ testNavlForwarded = auctionBody :: LB.ByteString auctionBody = "{\"error\":\"auction\",\"premium\":\"99999952316384526016153087\",\"auctionEnds\":1798191621}" --- keccak-256("alice"), the key the registry is keyed on +-- keccak-256("alice"), the registry key aliceHash :: Text aliceHash = "[9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501]" --- | A client on a current session must never put a registrable name on the --- wire: the router answers about the hash and learns only that. +-- | A current client must never put a registrable name on the wire. resolvePaths :: IORef [[Text]] -> IO [[Text]] resolvePaths reqs = filter isResolve <$> readIORef reqs where @@ -279,10 +278,10 @@ testRslvSendsTheHash = pc <- currentClient nr <- runExceptT' (directResolveName pc NRMInteractive (domain "alice.simplex")) resolvePaths reqs `shouldReturn` [["resolve", aliceHash <> ".simplex"]] - -- the record names what the caller asked for, not what went on the wire + -- the record names what the caller asked for SMP.nrName nr `shouldBe` "alice.simplex" where - -- the resolver echoes the name it was asked about, which is the hash + -- the resolver echoes what it was asked about, which is the hash echoed = testNameRecord {SMP.nrName = aliceHash <> ".simplex"} testNavlSendsTheHash :: IO () diff --git a/tests/SMPNamesTests.hs b/tests/SMPNamesTests.hs index 13eb3a294..6536bd290 100644 --- a/tests/SMPNamesTests.hs +++ b/tests/SMPNamesTests.hs @@ -120,36 +120,35 @@ availabilitySpec = do answers status404 "{\"error\":\"reserved\",\"reasonCode\":\"trademark\"}" (NAReserved NRTrademark) it "a reserved name with no reason recorded is still reserved" $ answers status404 "{\"error\":\"reserved\"}" (NAReserved NRUnspecified) - it "a reason this server does not know does not lose the reservation" $ + it "an unknown reason still reads as reserved" $ answers status404 "{\"error\":\"reserved\",\"reasonCode\":\"astrology\"}" (NAReserved NRUnspecified) it "a live registration is taken, and says until when" $ answers status200 "{\"status\":\"registered\",\"expires\":1811232000}" (NATaken (Just 1811232000)) it "a registration whose expiry could not be read is still taken" $ answers status200 "{\"status\":\"registered\",\"expires\":null}" (NATaken Nothing) - -- quoting the usual price for a name that costs a premium is the one wrong - -- answer here, so an answer missing its payload withholds the name instead + -- an answer missing its payload withholds the name: quoting the usual price + -- for one that costs a premium is the wrong answer it "grace without its deadline is reported as taken" $ answers status410 "{\"error\":\"grace\"}" (NATaken Nothing) it "an auction without its price is reported as taken" $ answers status410 "{\"error\":\"auction\",\"auctionEnds\":1798191621}" (NATaken Nothing) it "a registered name whose records point nowhere is still taken" $ answers status404 "{\"error\":\"noResolver\",\"expires\":1811232000}" (NATaken (Just 1811232000)) - -- a price is a 256-bit integer in decimal; the wire length-prefixes it with one - -- byte, so a longer or non-numeric string is dropped rather than re-encoded + -- the wire length-prefixes the price with one byte, so a longer or + -- non-numeric string is dropped rather than re-encoded it "a premium too long to encode is not quoted" $ answers status410 (jsonBody ("{\"error\":\"auction\",\"premium\":\"" <> replicate 300 '9' <> "\",\"auctionEnds\":1798191621}")) (NATaken Nothing) it "a premium that is not a decimal integer is not quoted" $ answers status410 "{\"error\":\"auction\",\"premium\":\"1e26\",\"auctionEnds\":1798191621}" (NATaken Nothing) - -- a resolver that could not answer must not be reported as an answer: saying - -- TAKEN would assert a registration nobody read, and NOT_FOUND would read as - -- "no such name, therefore free" + -- a resolver that could not answer must not look like an answer: TAKEN would + -- assert a registration nobody read, NOT_FOUND would read as "free" it "an upstream RPC failure is a resolver error, not a taken name" $ refuses status502 "{\"error\":\"upstreamError\"}" (RESOLVER "upstreamError") it "a TLD this resolver has no registry for is a resolver error" $ refuses status400 "{\"error\":\"tldNotConfigured\"}" (RESOLVER "tldNotConfigured") - it "a TLD with no registrar, so status could not be read, is a resolver error" $ + it "a TLD with no registrar is a resolver error" $ refuses status200 "{\"status\":\"unknown\",\"expires\":null}" (RESOLVER "unknown") - it "a status longer than the error carries is bounded, not passed through" $ + it "a long status is truncated, not passed through" $ refuses status502 (jsonBody ("{\"error\":\"" <> replicate 400 'e' <> "\"}")) (RESOLVER (T.replicate 32 "e")) it "a body that is not the resolver's JSON is never NOT_FOUND" $ refuses status404 "gateway" (RESOLVER "HTTP 404") @@ -174,9 +173,9 @@ availabilitySpec = do ] where jsonBody = LB.fromStrict . B.pack - answers st body expected = asks_ st body (Right expected) - refuses st body err = asks_ st body (Left err) - asks_ st body expected = + answers st body a = resolverSays st body (Right a) + refuses st body e = resolverSays st body (Left e) + resolverSays st body expected = withResolverServer (resolveResp st body) $ \port _ -> do env <- newNamesEnv (testNamesConfig port) getNameAvailability env navlDomain `shouldReturn` expected @@ -184,28 +183,24 @@ availabilitySpec = do parseNameSpec :: Spec parseNameSpec = do - -- asking by hash is how a client learns whether a name is taken without - -- saying which name it is asking about + -- asking by hash tells the client if a name is taken without naming it it "accepts a labelhash label" $ parseN ("[" <> T.replicate 64 "b" <> "].simplex") `shouldSatisfy` isRight it "refuses a hash of the wrong width" $ parseN ("[" <> T.replicate 63 "b" <> "].simplex") `shouldSatisfy` isLeft - -- the resolver keys the registry on the bracketed form only; a bare hex string - -- would be hashed again as if it were a name, answering about a different key + -- only the bracketed form is a key; a bare hex string would be hashed again it "refuses a bare hex string in place of a labelhash" $ parseN ("0x" <> T.replicate 64 "b" <> ".simplex") `shouldSatisfy` isLeft it "keeps the brackets, which are what the resolver reads as a hash" $ (strEncode <$> parseN ("[" <> T.replicate 64 "b" <> "].simplex")) `shouldBe` Right (encodeUtf8 ("[" <> T.replicate 64 "b" <> "].simplex")) - -- only the second-level label is a registry key, so only it may be hashed; - -- a subname label is needed as text to reach the record + -- only the 2LD is a registry key; subname labels are needed as text it "accepts a hashed second-level label under a subname" $ parseN ("x.[" <> T.replicate 64 "b" <> "].simplex") `shouldSatisfy` isRight it "refuses a hashed subname label" $ parseN ("[" <> T.replicate 64 "b" <> "].alice.simplex") `shouldSatisfy` isLeft it "refuses a labelhash under a web TLD, which has no registry" $ parseN ("[" <> T.replicate 64 "b" <> "].com") `shouldSatisfy` isLeft - -- the hash the client sends must be the one the resolver keys on: this is -- keccak-256("alice"), the same constant the resolver's own tests use it "hashes the second-level label to the registry key" $ (fullDomainName . hashedDomain <$> parseN "alice.simplex") @@ -213,7 +208,7 @@ parseNameSpec = do it "leaves subname labels as text" $ (fullDomainName . hashedDomain <$> parseN "x.alice.simplex") `shouldBe` Right "x.[9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501].simplex" - it "leaves a web name alone, as it has no registry to key into" $ + it "leaves a web name alone, it has no registry" $ (fullDomainName . hashedDomain <$> parseN "example.com") `shouldBe` Right "example.com" it "does not hash a name that is already a hash" $ (fullDomainName . hashedDomain . hashedDomain <$> parseN "alice.simplex")