mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-09-14 04:27:21 +00:00
resolver errors say what went wrong, not "no such name"
/v2/resolve answers 200, 400 or 502, and an unregistered name is NRAvailable, so no status means "not registered". Mapping 400/404/410 to NOT_FOUND made a misconfigured relay deny every name, and hid a relay upgraded ahead of its resolver. All three now surface as RESOLVER. rslvNotFound would have gone dead, so it counts what its name says: an availability answer the encoder downgrades for a session below v22. The wire is unchanged. A hashed query the registrar cannot name is refused with 502 rather than answered with a record named "unknown", which the client rejects anyway. NRRUnknown is capped to 32 printable characters again, as the spec says. A registered name that is also reserved no longer offers a date it will never free up on. rentPrices is registrationPrices throughout, and yearPriceUSD is USDCents rather than a bare Int64.
This commit is contained in:
@@ -182,6 +182,11 @@ every query.
|
||||
**Set `SNRC_CONTROLLER_<TLD>` wherever `SNRC_REGISTRAR_<TLD>` is.** Without a
|
||||
controller there is no oracle, so no name can be priced.
|
||||
|
||||
**Upgrade this service before the routers that query it.** Routers from SMP v22
|
||||
call `/v2/resolve`, which an older resolver does not serve. Every name then
|
||||
answers `ERR NAME RESOLVER "HTTP 404"` until this service is upgraded, while
|
||||
`/health` still reports it as ready.
|
||||
|
||||
### Why a name is reserved
|
||||
|
||||
A held-back name carries `reasonCode`, the controller's reason, and `reason`, an
|
||||
|
||||
@@ -272,7 +272,7 @@ def read_oracle_prices(controller: str, oracle: str):
|
||||
min_len = decode_uint(eth_call(controller, selector("minCharLength()")))
|
||||
return {
|
||||
# lengths the registry refuses are left out rather than priced at zero
|
||||
"rentPrices": {n: c for n, c in tiers.items() if n >= min_len},
|
||||
"registrationPrices": {n: c for n, c in tiers.items() if n >= min_len},
|
||||
"basePrice": base,
|
||||
"minLabelLength": min_len,
|
||||
"_premiumUnknown": premium_unknown,
|
||||
@@ -388,22 +388,23 @@ def decode_bytes(hex_data: str) -> bytes:
|
||||
return raw[64:64 + length]
|
||||
|
||||
|
||||
def registered_label(registrar: str, token: int) -> str:
|
||||
def registered_label(registrar: str, token: int):
|
||||
"""The plaintext label the registrar recorded at registration, keyed by the
|
||||
hash of that label. A name registered without registerWithLabel has none,
|
||||
and answers "unknown" instead."""
|
||||
hash of that label. None when the name was registered without
|
||||
registerWithLabel, so the registrar cannot name it."""
|
||||
raw = decode_bytes(eth_call(registrar, selector("labelOf(uint256)") + encode_uint(token)))
|
||||
return raw.decode("utf-8", errors="replace") if raw else "unknown"
|
||||
return raw.decode("utf-8", errors="replace") if raw else None
|
||||
|
||||
|
||||
def canonical_name(name: str) -> str:
|
||||
def canonical_name(name: str):
|
||||
"""The name to answer with: a hashed query does not carry one, so the
|
||||
registrar's record of the label fills it in."""
|
||||
registrar's record of the label fills it in. None when it recorded none."""
|
||||
labels = name.split(".")
|
||||
registrar = REGISTRARS.get(labels[-1])
|
||||
if not registrar or len(labels) != 2 or not is_encoded_labelhash(labels[0]):
|
||||
return name
|
||||
return registered_label(registrar, label_token(labels[0])) + "." + labels[1]
|
||||
label = registered_label(registrar, label_token(labels[0]))
|
||||
return label + "." + labels[1] if label else None
|
||||
|
||||
|
||||
def label_token(label: str) -> int:
|
||||
@@ -738,12 +739,17 @@ def registration(name: str):
|
||||
reg = name_status(name)
|
||||
status = reg["status"]
|
||||
if status in ("registered", "grace"):
|
||||
rec = name_record(name)
|
||||
# the client checks that the record names what it asked about, so a
|
||||
# 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": name_record(name),
|
||||
"nameRecord": rec,
|
||||
}
|
||||
if reg["reasonCode"]:
|
||||
return 200, {"type": "reserved", "reservedReason": reg["reasonCode"]}
|
||||
@@ -753,7 +759,7 @@ def registration(name: str):
|
||||
return 200, {
|
||||
"type": "available",
|
||||
"pricing": {
|
||||
"registrationPrices": reg["rentPrices"],
|
||||
"registrationPrices": reg["registrationPrices"],
|
||||
"basePrice": reg["basePrice"],
|
||||
"minLabelLength": reg["minLabelLength"],
|
||||
},
|
||||
@@ -800,7 +806,7 @@ def resolve(name: str):
|
||||
# answerable.
|
||||
owner = decode_address(eth_call(registry, selector("owner(bytes32)") + node_hex))
|
||||
return 200, {
|
||||
"name": canonical_name(name),
|
||||
"name": canonical_name(name) or name,
|
||||
"nickname": "",
|
||||
"website": "",
|
||||
"location": "",
|
||||
@@ -837,7 +843,7 @@ def resolve(name: str):
|
||||
# use the ENSIP-5 dot convention (e.g. "simplex.contact") — only the
|
||||
# resolver's JSON surface camelCases them.
|
||||
return 200, {
|
||||
"name": canonical_name(name),
|
||||
"name": canonical_name(name) or name,
|
||||
"nickname": nickname,
|
||||
"website": texts.get("url", ""),
|
||||
"location": texts.get("location", ""),
|
||||
|
||||
@@ -606,7 +606,7 @@ class PricingTests(unittest.TestCase):
|
||||
snrc.eth_call = self._chain(self._lapsed(0))
|
||||
reg = snrc.name_status("acme.testing")
|
||||
# 1 and 2 are below minCharLength
|
||||
self.assertEqual(reg["rentPrices"], {3: 1600, 4: 800, 5: 500})
|
||||
self.assertEqual(reg["registrationPrices"], {3: 1600, 4: 800, 5: 500})
|
||||
self.assertEqual(reg["basePrice"], self.BASE)
|
||||
self.assertEqual(reg["minLabelLength"], self.MIN_LENGTH)
|
||||
|
||||
@@ -856,12 +856,22 @@ class RegistrationV2Tests(unittest.TestCase):
|
||||
words += [snrc.encode_uint(length), snrc.encode_uint(cents)]
|
||||
return "0x" + "".join(words)
|
||||
|
||||
def _chain(self, expires, reserved=0, oracle=None):
|
||||
@staticmethod
|
||||
def _abi_bytes(value: bytes) -> str:
|
||||
"""head offset, length, then the payload padded to a 32-byte word."""
|
||||
pad = (-len(value)) % 32
|
||||
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"):
|
||||
"""The registry answers a zero resolver, so name_record returns the
|
||||
empty record a registered name still has."""
|
||||
empty record a registered name still has. `label` is what the registrar
|
||||
recorded for the 2LD; b"" means it recorded none."""
|
||||
oracle = self.ORACLE if oracle is None else oracle
|
||||
|
||||
def eth_call(to, data):
|
||||
if data.startswith(snrc.selector("labelOf(uint256)")):
|
||||
return self._abi_bytes(label)
|
||||
if data.startswith(snrc.selector("nameExpires(uint256)")):
|
||||
return "0x" + snrc.encode_uint(expires)
|
||||
if data.startswith(snrc.selector("GRACE_PERIOD()")):
|
||||
@@ -976,6 +986,21 @@ class RegistrationV2Tests(unittest.TestCase):
|
||||
_, body = snrc.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):
|
||||
"""The client checks the record names what it asked about, so answering
|
||||
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")
|
||||
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")
|
||||
self.assertEqual(status, 200)
|
||||
self.assertEqual(body["nameRecord"]["name"], "acme.testing")
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user