Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion scripts/resolver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>`

What routers before SMP v22 call. Its shape is unrelated to v2's: the record is
Expand Down Expand Up @@ -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

Expand Down
34 changes: 26 additions & 8 deletions scripts/resolver/service/snrc-resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down Expand Up @@ -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 <date>" 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": "",
Expand Down
43 changes: 40 additions & 3 deletions scripts/resolver/service/test_snrc_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -834,11 +834,13 @@ def _abi_bytes(value: bytes) -> str:
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)")):
Expand All @@ -858,7 +860,7 @@ def eth_call(to, data):
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
Expand Down Expand Up @@ -971,6 +973,41 @@ def test_a_hashed_query_is_answered_with_the_name_the_registrar_recorded(self):
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()
Loading