diff --git a/scripts/resolver/README.md b/scripts/resolver/README.md index f667f536d..e1570d60a 100644 --- a/scripts/resolver/README.md +++ b/scripts/resolver/README.md @@ -155,6 +155,9 @@ Error bodies carry `name` and a fixed `error` code to branch on. Only label, so a hashed query cannot be answered with a name. See [Querying by labelhash](#querying-by-labelhash). +A subname reports the expiry and grace of the 2LD above it, since that is what +bounds its lifetime. A subname nobody created reports as not registered. + ### v1: `/resolve/` What routers before SMP v22 call. Its shape is unrelated to v2's: the record is @@ -213,7 +216,7 @@ expiry is what separates *never registered* from *registered and since released*. A subname reports the status of the 2LD above it, which is only as good as the -name it sits under. +name it sits under. A subname nobody created answers 404 `unregistered`. #### v1 errors diff --git a/scripts/resolver/service/snrc-resolve.py b/scripts/resolver/service/snrc-resolve.py index dcf673642..7925c2ff3 100755 --- a/scripts/resolver/service/snrc-resolve.py +++ b/scripts/resolver/service/snrc-resolve.py @@ -722,13 +722,23 @@ 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, { - "type": "registered", - "expires": reg["expires"], - "graceUntil": reg["graceEnds"], - "reservedReason_": reg["reasonCode"], - "nameRecord": rec, - } + # a subname inherits the 2LD's status, so only its node's owner says + # whether anyone created it + if len(name.split(".")) > 2 and rec["owner"] == ZERO_ADDR: + # name_status reads pricing only when the name was already + # unregistered, so read it here + status = "unregistered" + pricing = pricing_params(tld) + if pricing: + reg.update({k: v for k, v in pricing.items() if not k.startswith("_")}) + else: + return 200, { + "type": "registered", + "expires": reg["expires"], + "graceUntil": reg["graceEnds"], + "reservedReason_": reg["reasonCode"], + "nameRecord": rec, + } if reg["reasonCode"]: return 200, {"type": "reserved", "reservedReason": reg["reasonCode"]} if status in ("unregistered", "expired"): @@ -781,8 +791,16 @@ def resolve(name: str): if resolver_addr == ZERO_ADDR: # A registered name always resolves: with no resolver set the record is # still returned with every field unset, so "taken until " stays - # answerable. + # answerable. For a subname, no owner means nobody created it. owner = decode_address(eth_call(registry, selector("owner(bytes32)") + node_hex)) + if len(name.split(".")) > 2 and owner == ZERO_ADDR: + return 404, { + "name": name, + **reg, + "status": "unregistered", + "error": "unregistered", + "message": "this subname has never been created", + } return 200, { "name": canonical_name(name) or name, "nickname": "", diff --git a/scripts/resolver/service/test_snrc_resolve.py b/scripts/resolver/service/test_snrc_resolve.py index 84520a49a..ac142e266 100644 --- a/scripts/resolver/service/test_snrc_resolve.py +++ b/scripts/resolver/service/test_snrc_resolve.py @@ -834,11 +834,13 @@ class RegistrationV2Tests(unittest.TestCase): return ("0x" + snrc.encode_uint(0x20) + snrc.encode_uint(len(value)) + (value + b"\x00" * pad).hex()) - def _chain(self, expires, reserved=0, oracle=None, label=b"acme"): + def _chain(self, expires, reserved=0, oracle=None, label=b"acme", owner=None): """The registry answers a zero resolver, so name_record returns the empty record a registered name still has. `label` is what the registrar - recorded for the 2LD; b"" means it recorded none.""" + recorded for the 2LD; b"" means it recorded none. `owner` is the owner of + the queried node; ZERO_ADDR means that node was never created.""" oracle = self.ORACLE if oracle is None else oracle + owner = self.OWNER if owner is None else owner def eth_call(to, data): if data.startswith(snrc.selector("labelOf(uint256)")): @@ -858,7 +860,7 @@ class RegistrationV2Tests(unittest.TestCase): if data.startswith(snrc.selector("resolver(bytes32)")): return "0x" + snrc.encode_uint(0) if data.startswith(snrc.selector("owner(bytes32)")): - return "0x" + snrc.encode_uint(int(self.OWNER, 16)) + return "0x" + snrc.encode_uint(int(owner, 16)) return self.fail("unexpected call " + data[:10]) return eth_call @@ -971,6 +973,41 @@ class RegistrationV2Tests(unittest.TestCase): status, body = snrc.registration(hashed + ".testing") self.assertEqual(status, 200) self.assertEqual(body["nameRecord"]["name"], "acme.testing") + def test_a_subname_that_exists_is_registered_with_its_parents_dates(self): + expires = self.now + 3600 + snrc.eth_call = self._chain(expires) + status, body = snrc.registration("sub.acme.testing") + self.assertEqual(status, 200) + self.assertEqual(body["type"], "registered") + self.assertEqual(body["expires"], expires) + self.assertEqual(body["nameRecord"]["name"], "sub.acme.testing") + + def test_a_subname_nobody_created_is_not_registered(self): + """The registrar only tracks 2LDs, so the parent's registration says + nothing about a child that was never created: its node has no owner.""" + snrc.eth_call = self._chain(self.now + 3600, owner=snrc.ZERO_ADDR) + status, body = snrc.registration("sub.acme.testing") + self.assertEqual(status, 200) + self.assertEqual(body["type"], "available") + + def test_a_2ld_is_not_subject_to_the_owner_check(self): + """Only a subname can be absent under a registered parent.""" + snrc.eth_call = self._chain(self.now + 3600, owner=snrc.ZERO_ADDR) + _, body = snrc.registration("acme.testing") + self.assertEqual(body["type"], "registered") + def test_v1_does_not_report_an_uncreated_subname_as_registered(self): + """v1 has no availability, so the only honest answer is not-found. The + 2LD case is untouched: a registered name with no resolver still resolves.""" + snrc.eth_call = self._chain(self.now + 3600, owner=snrc.ZERO_ADDR) + status, body = snrc.resolve("sub.acme.testing") + self.assertEqual(status, 404) + self.assertEqual(body["error"], "unregistered") + + def test_v1_still_resolves_a_2ld_with_no_resolver_set(self): + snrc.eth_call = self._chain(self.now + 3600, owner=snrc.ZERO_ADDR) + status, body = snrc.resolve("acme.testing") + self.assertEqual(status, 200) + self.assertEqual(body["resolver"], snrc.ZERO_ADDR) if __name__ == "__main__": unittest.main()