mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-11-04 08:37:49 +00:00
resolver: answer a subname only while its name's holder holds it (#2013)
This commit is contained in:
1 parent
360f6b5530
commit
9c4a814fda
3 files changed
+89
-21
No files matched your search
@@ -173,7 +173,10 @@ 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.
|
||||
bounds its lifetime. A subname is answered only while it is held: owned in the
|
||||
registry by the 2LD's holder, or by the subname registrar that the registrar's
|
||||
`subnameHook()` names, while its `ownerOf` names a holder. Any other subname,
|
||||
created or not, reports as not registered.
|
||||
|
||||
### v1: `/resolve/<name>`
|
||||
|
||||
@@ -233,7 +236,8 @@ 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. A subname nobody created answers 404 `unregistered`.
|
||||
name it sits under. A subname that is not held, as v2 defines it, answers 404
|
||||
`unregistered`.
|
||||
|
||||
#### v1 errors
|
||||
|
||||
@@ -358,16 +362,18 @@ from before the enum stores a boolean, whose `true` decodes as 1, which is why
|
||||
|
||||
### Configuring addresses
|
||||
|
||||
The resolver reads three contracts, each configured per TLD, and the name
|
||||
policy a controller names.
|
||||
The resolver reads three contracts, each configured per TLD, the name policy a
|
||||
controller names, and the subname registrar a registrar names.
|
||||
|
||||
The **registry** answers who owns a node, and `/resolve` reads the records from
|
||||
it. The **registrar** (ERC-721) holds `nameExpires` and `GRACE_PERIOD`, which
|
||||
is where every expiry field comes from, and `labelOf`, which is how a hashed
|
||||
query is answered with a name. A name registered without recording its label
|
||||
cannot answer one, and `/v2/resolve` refuses it rather than answer with a name
|
||||
the client will reject. With no registrar for a TLD, `/resolve` still works and
|
||||
reports `"status": "unknown"`. The **controller** holds
|
||||
the client will reject. Its `subnameHook()` names the subname registrar, whose
|
||||
`ownerOf` says who holds a subname it owns. With no registrar for a TLD,
|
||||
`/resolve` still works and reports `"status": "unknown"`, and only subnames the
|
||||
2LD's holder owns in the registry are answered. The **controller** holds
|
||||
`reservedNames`, which is where `reasonCode` comes from. With no controller a
|
||||
held-back name reads as not reserved, and no name can be priced.
|
||||
|
||||
@@ -403,7 +409,8 @@ as env vars when you run the script directly.
|
||||
|
||||
A lookup reads the chain in at most three rounds: the name's status and
|
||||
registry entries, then its record from its resolver, then prices for a name
|
||||
that is free. The node runs the calls of a JSON-RPC batch one after another,
|
||||
that is free. A subname owned by the subname registrar takes one more round, for
|
||||
its holder. The node runs the calls of a JSON-RPC batch one after another,
|
||||
so the contract reads of a round go to the chain as one `eth_call` to
|
||||
[Multicall3](https://github.com/mds1/multicall). A node that is slow for a
|
||||
moment therefore delays a lookup a few times, not once per read. Connections to
|
||||
|
||||
@@ -839,6 +839,14 @@ def owner_call(registry: str, node: bytes):
|
||||
return registry, selector("owner(bytes32)") + node.hex()
|
||||
|
||||
|
||||
def subname_hook_call(registrar: str):
|
||||
return registrar, selector("subnameHook()")
|
||||
|
||||
|
||||
def subname_owner_call(subnames: str, node: bytes):
|
||||
return subnames, selector("ownerOf(uint256)") + node.hex()
|
||||
|
||||
|
||||
def text_call(resolver: str, node: bytes, key: str):
|
||||
return resolver, encode_text_call(node, key)
|
||||
|
||||
@@ -858,12 +866,16 @@ def lookup_reads(name: str):
|
||||
reads += [BLOCK_READ, eth_call_read(*expires_call(registrar, token)), eth_call_read(*grace_call(registrar))]
|
||||
if len(labels) == 2 and is_encoded_labelhash(labels[0]):
|
||||
reads.append(eth_call_read(*label_call(registrar, token)))
|
||||
if len(labels) > 2:
|
||||
reads.append(eth_call_read(*subname_hook_call(registrar)))
|
||||
if CONTROLLERS.get(tld):
|
||||
reads.append(eth_call_read(*reserved_call(NAME_POLICIES.get(tld, CONTROLLERS[tld]), token)))
|
||||
registry = REGISTRIES.get(tld)
|
||||
if registry:
|
||||
node = node_of(name)
|
||||
reads += [eth_call_read(*resolver_call(registry, node)), eth_call_read(*owner_call(registry, node))]
|
||||
if len(labels) > 2:
|
||||
reads.append(eth_call_read(*owner_call(registry, namehash(".".join(labels[-2:])))))
|
||||
return reads
|
||||
|
||||
|
||||
@@ -1132,6 +1144,25 @@ def name_record(name: str):
|
||||
return rec
|
||||
|
||||
|
||||
def subname_holder(name: str) -> str:
|
||||
labels = name.split(".")
|
||||
tld = labels[-1]
|
||||
registry, registrar = REGISTRIES[tld], REGISTRARS.get(tld)
|
||||
node = node_of(name)
|
||||
owner = decode_address(eth_call(*owner_call(registry, node)))
|
||||
holder = decode_address(eth_call(*owner_call(registry, namehash(".".join(labels[-2:])))))
|
||||
if owner in (ZERO_ADDR, holder):
|
||||
return owner
|
||||
if not registrar:
|
||||
return ZERO_ADDR
|
||||
try:
|
||||
if owner != decode_address(eth_call(*subname_hook_call(registrar))):
|
||||
return ZERO_ADDR
|
||||
return decode_address(eth_call(*subname_owner_call(owner, node)))
|
||||
except RuntimeError:
|
||||
return ZERO_ADDR
|
||||
|
||||
|
||||
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}
|
||||
@@ -1152,9 +1183,9 @@ 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"}
|
||||
# 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:
|
||||
# a subname inherits the 2LD's status, so only its holder says whether
|
||||
# it exists
|
||||
if len(name.split(".")) > 2 and subname_holder(name) == ZERO_ADDR:
|
||||
# name_status reads pricing only when the name was already
|
||||
# unregistered, so read it here
|
||||
status = "unregistered"
|
||||
@@ -1217,19 +1248,19 @@ def resolve(name: str):
|
||||
return (404 if reg["status"] == "unregistered" else 410), body
|
||||
|
||||
resolver_addr = decode_address(eth_call(*resolver_call(registry, node)))
|
||||
if len(name.split(".")) > 2 and subname_holder(name) == ZERO_ADDR:
|
||||
return 404, {
|
||||
"name": name,
|
||||
**reg,
|
||||
"status": "unregistered",
|
||||
"error": "unregistered",
|
||||
"message": "nobody holds this subname",
|
||||
}
|
||||
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. For a subname, no owner means nobody created it.
|
||||
# answerable.
|
||||
owner = decode_address(eth_call(*owner_call(registry, node)))
|
||||
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": "",
|
||||
|
||||
@@ -838,6 +838,8 @@ class RegistrationV2Tests(unittest.TestCase):
|
||||
CONTROLLER = "0x281ca41311c2aa808c917c4674639d7567b75714"
|
||||
ORACLE = "0x1e0c9a2b9d1a4c8f7b3e5d6a9c2f4b8e1d7a3c50"
|
||||
OWNER = "0xd83bd7e0e6b8a4c1f2593a7b0c4e8d1a6f9b2c37"
|
||||
FORMER_OWNER = "0x5a0e1c2b3d4f5e6a7b8c9d0e1f2a3b4c5d6e7f80"
|
||||
SUBNAMES = "0x3d749792ea31a74fca465f361659dc112a31fb70"
|
||||
|
||||
GRACE = 90 * 86400
|
||||
BASE = 200
|
||||
@@ -881,13 +883,16 @@ 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", owner=None):
|
||||
def _chain(self, expires, reserved=0, oracle=None, label=b"acme", owner=None, holder=None, subname_holder=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. `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
|
||||
holder = owner if holder is None else holder
|
||||
subname_holder = snrc.ZERO_ADDR if subname_holder is None else subname_holder
|
||||
parent = snrc.namehash("acme.testing").hex()
|
||||
|
||||
def eth_call(to, data):
|
||||
if data.startswith(snrc.selector("labelOf(uint256)")):
|
||||
@@ -907,7 +912,11 @@ 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(owner, 16))
|
||||
return "0x" + snrc.encode_uint(int(holder if data.endswith(parent) else owner, 16))
|
||||
if data.startswith(snrc.selector("subnameHook()")):
|
||||
return "0x" + snrc.encode_uint(int(self.SUBNAMES, 16))
|
||||
if data.startswith(snrc.selector("ownerOf(uint256)")):
|
||||
return "0x" + snrc.encode_uint(int(subname_holder, 16))
|
||||
return self.fail("unexpected call " + data[:10])
|
||||
|
||||
return eth_call
|
||||
@@ -1037,6 +1046,27 @@ class RegistrationV2Tests(unittest.TestCase):
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(body["type"], "available")
|
||||
|
||||
def test_a_subname_the_subname_registrar_holds_for_the_holder_is_registered(self):
|
||||
snrc.eth_call = self._chain(self.now + 3600, owner=self.SUBNAMES, holder=self.OWNER, subname_holder=self.OWNER)
|
||||
_, body = registration("sub.acme.testing")
|
||||
self.assertEqual(body["type"], "registered")
|
||||
|
||||
def test_a_subname_the_subname_registrar_no_longer_holds_is_not_registered(self):
|
||||
snrc.eth_call = self._chain(self.now + 3600, owner=self.SUBNAMES, holder=self.OWNER)
|
||||
_, body = registration("sub.acme.testing")
|
||||
self.assertEqual(body["type"], "available")
|
||||
|
||||
def test_a_subnode_a_former_holder_owns_is_not_registered(self):
|
||||
snrc.eth_call = self._chain(self.now + 3600, owner=self.FORMER_OWNER, holder=self.OWNER)
|
||||
_, body = registration("sub.acme.testing")
|
||||
self.assertEqual(body["type"], "available")
|
||||
|
||||
def test_v1_does_not_resolve_a_subnode_a_former_holder_owns(self):
|
||||
snrc.eth_call = self._chain(self.now + 3600, owner=self.FORMER_OWNER, holder=self.OWNER)
|
||||
status, body = snrc.resolve("sub.acme.testing")
|
||||
self.assertEqual(status, 404)
|
||||
self.assertEqual(body["error"], "unregistered")
|
||||
|
||||
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)
|
||||
|
||||
Reference in new issue
Block a user