review fixes

This commit is contained in:
Alain Brenzikofer committed 2026-09-05 18:37:55 +02:00
1 parent e3f00b46f6
commit 69cce33675
15 files changed
+425 -173

No files matched your search

+19 -1
View File
@@ -187,7 +187,18 @@ base price itself.
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.
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.
**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_<TLD>` wherever `SNRC_REGISTRAR_<TLD>` is set, and upgrade this
service before the routers that query it.
Upgrade the resolver before the router that queries it. A resolver without this
status reports a name in its auction as plain `expired`, which reads as "free at
@@ -231,6 +242,13 @@ 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.[<hash>].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.
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
+43 -13
View File
@@ -66,6 +66,7 @@ Unrecognised payloads fall back to `0x`-prefixed raw hex.
import hashlib
import json
import os
import time
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import unquote, urlparse
@@ -170,12 +171,17 @@ def is_encoded_labelhash(label: str) -> bool:
def node_of(name: str) -> bytes:
"""namehash, accepting an encoded labelhash in place of a 2LD's label. In a
subname a bracket label is hashed as written, not decoded."""
"""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."""
labels = name.split(".")
if len(labels) == 2 and is_encoded_labelhash(labels[0]):
return keccak(namehash(labels[1]) + bytes.fromhex(labels[0][1:-1]))
return namehash(name)
if len(labels) < 2 or not is_encoded_labelhash(labels[-2]):
return namehash(name)
node = keccak(namehash(labels[-1]) + bytes.fromhex(labels[-2][1:-1]))
for label in reversed(labels[:-2]):
node = keccak(node + keccak(label.encode()))
return node
# ---------- Registration status ----------
@@ -213,23 +219,47 @@ 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.
AUCTION_PARAMS_TTL = 300
_auction_params: dict = {}
def auction_params(tld: str):
"""(oracle, startPremium, totalDays, endValue) for the TLD's controller, or
(ZERO_ADDR, 0, 0, 0) when no controller or no oracle is configured."""
cached = _auction_params.get(tld)
if cached and time.time() - cached[0] < AUCTION_PARAMS_TTL:
return cached[1]
params = (ZERO_ADDR, 0, 0, 0)
controller = CONTROLLERS.get(tld)
if controller:
oracle = decode_address(eth_call(controller, selector("prices()")))
if oracle != ZERO_ADDR:
params = (
oracle,
decode_uint(eth_call(oracle, selector("startPremium()"))),
decode_uint(eth_call(oracle, selector("totalDays()"))),
decode_uint(eth_call(oracle, selector("endValue()"))),
)
_auction_params[tld] = (time.time(), params)
return params
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."""
controller = CONTROLLERS.get(tld)
if not controller:
return None, None
oracle = decode_address(eth_call(controller, selector("prices()")))
oracle, start, total_days, floor = auction_params(tld)
if oracle == ZERO_ADDR:
return None, None
ends = grace_ends + decode_uint(eth_call(oracle, selector("totalDays()"))) * 86400
ends = grace_ends + total_days * 86400
if now >= ends:
return None, None
start = decode_uint(eth_call(oracle, selector("startPremium()")))
floor = decode_uint(eth_call(oracle, selector("endValue()")))
# decayedPremium is `pure`, so the premium quoted here is the oracle's own
# arithmetic rather than a reimplementation of its decay curve.
decayed = decode_uint(
@@ -261,7 +291,7 @@ def name_status(name: str):
# nameExpires and reservedNames are keyed on uint256(keccak(label)).
# Decoded for a 2LD only, the same rule node_of applies to the node.
label = labels[-2]
if len(labels) == 2 and is_encoded_labelhash(label):
if is_encoded_labelhash(label):
token = int(label[1:-1], 16)
else:
token = int.from_bytes(keccak(label.encode()), "big")
+90 -79
View File
@@ -97,6 +97,7 @@ class EncodedLabelhashTests(unittest.TestCase):
snrc.REGISTRARS = {"testing": self.REGISTRAR}
snrc.CONTROLLERS = {"testing": ""}
snrc.chain_now = lambda: int(time.time())
snrc._auction_params.clear()
def tearDown(self):
snrc.REGISTRARS, snrc.CONTROLLERS, snrc.eth_call, snrc.chain_now = self._saved
@@ -135,7 +136,9 @@ class EncodedLabelhashTests(unittest.TestCase):
def test_a_plain_name_is_unaffected(self):
self.assertEqual(snrc.node_of("alice.testing"), snrc.namehash("alice.testing"))
def test_an_encoded_subname_is_not_the_name_it_would_decode_to(self):
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."""
self.assertNotEqual(
snrc.node_of(
"[9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501]"
@@ -143,13 +146,25 @@ class EncodedLabelhashTests(unittest.TestCase):
),
snrc.namehash("alice.alice.testing"),
)
self.assertNotEqual(
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."""
self.assertEqual(
snrc.node_of(
"alice."
"sub."
"[9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501]"
".testing"
),
snrc.namehash("alice.alice.testing"),
snrc.namehash("sub.alice.testing"),
)
self.assertEqual(
snrc.node_of(
"a.b."
"[9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501]"
".testing"
),
snrc.namehash("a.b.alice.testing"),
)
def test_a_0x_prefixed_label_is_taken_literally(self):
@@ -221,6 +236,7 @@ class NameStatusTests(unittest.TestCase):
# Expiry alone; ReservedTests covers a configured controller.
snrc.CONTROLLERS = {"testing": ""}
snrc.chain_now = lambda: int(time.time())
snrc._auction_params.clear()
def tearDown(self):
(
@@ -297,6 +313,20 @@ class NameStatusTests(unittest.TestCase):
# the token asked about is keccak("alice"), not keccak("x")
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."""
seen = []
def eth_call(to, data):
seen.append(data)
return "0x" + snrc.encode_uint(0)
snrc.eth_call = eth_call
hashed = "[" + snrc.keccak(b"alice").hex() + "]"
snrc.name_status("x." + hashed + ".testing")
self.assertTrue(seen[0].endswith(snrc.keccak(b"alice").hex()))
def test_unconfigured_tld_is_unknown_rather_than_unregistered(self):
snrc.REGISTRARS = {"testing": ""}
snrc.eth_call = lambda *a: self.fail("must not reach the chain")
@@ -333,6 +363,7 @@ class ReservedTests(unittest.TestCase):
snrc.REGISTRARS = {"testing": self.REGISTRAR}
snrc.CONTROLLERS = {"testing": self.CONTROLLER}
snrc.chain_now = lambda: int(time.time())
snrc._auction_params.clear()
def tearDown(self):
snrc.REGISTRARS, snrc.CONTROLLERS, snrc.eth_call, snrc.chain_now = self._saved
@@ -400,6 +431,7 @@ class ReservedReasonTests(unittest.TestCase):
snrc.REGISTRARS = {"testing": self.REGISTRAR}
snrc.CONTROLLERS = {"testing": self.CONTROLLER}
snrc.chain_now = lambda: int(time.time())
snrc._auction_params.clear()
def tearDown(self):
(
@@ -422,6 +454,47 @@ class ReservedReasonTests(unittest.TestCase):
return eth_call
def _reserved_as(self, code):
def eth_call(to, data):
if data.startswith(snrc.selector("reservedNames(bytes32)")):
return "0x" + snrc.encode_uint(code)
if data.startswith(snrc.selector("GRACE_PERIOD()")):
return "0x" + snrc.encode_uint(90 * 86400)
if data.startswith(snrc.selector("prices()")):
return "0x" + snrc.encode_uint(0)
return "0x" + snrc.encode_uint(0)
return eth_call
def test_every_enum_value_has_a_code_and_a_sentence(self):
for code, (name, sentence) in snrc.RESERVED_REASONS.items():
snrc.eth_call = self._reserved_as(code)
reg = snrc.name_status("acme.testing")
self.assertEqual(reg["status"], "reserved", name)
self.assertEqual(reg["reasonCode"], name)
self.assertEqual(reg["reason"], sentence)
def test_a_trademark_reservation_says_so(self):
snrc.eth_call = self._reserved_as(2)
_, body = snrc.resolve("acme.testing")
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."""
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."""
snrc.eth_call = self._reserved_as(99)
reg = snrc.name_status("acme.testing")
self.assertEqual(reg["status"], "reserved")
self.assertEqual(reg["reasonCode"], "unspecified")
def test_a_reserved_name_carries_the_reason(self):
snrc.eth_call = self._chain(0, True)
status, body = snrc.resolve("acme.testing")
@@ -484,6 +557,7 @@ class AuctionTests(unittest.TestCase):
snrc.CONTROLLERS = {"testing": self.CONTROLLER}
self.now = int(time.time())
snrc.chain_now = lambda: self.now
snrc._auction_params.clear()
def tearDown(self):
(
@@ -576,10 +650,17 @@ class AuctionTests(unittest.TestCase):
self.assertEqual(snrc.name_status("acme.testing")["status"], "grace")
self.assertEqual(self.oracle_calls, [])
def test_a_live_name_never_reaches_the_oracle(self):
snrc.eth_call = self._chain(self.now + 3600)
self.assertEqual(snrc.name_status("acme.testing")["status"], "registered")
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."""
snrc.eth_call = self._chain(self._lapsed(1))
snrc.name_status("acme.testing")
seen_first = len(self.oracle_calls)
snrc.name_status("acme.testing")
self.assertEqual(
self.oracle_calls[seen_first:],
[snrc.selector("decayedPremium(uint256,uint256)")],
)
def test_a_reserved_lapsed_name_stays_reserved_rather_than_auctioned(self):
snrc.eth_call = self._chain(self._lapsed(0), reserved=2)
@@ -616,77 +697,6 @@ class AuctionTests(unittest.TestCase):
self.assertIsNotNone(body["premium"])
class ReasonCodeTests(unittest.TestCase):
"""The reason a name is held back is the controller's `Reason` enum, so the
app can word it in the user's language instead of showing a server string."""
REGISTRY = "0x58fc46996d975c57883564648bda5206d1a0102b"
REGISTRAR = "0xef47eb4384b46c89e4482a677c2cbcbd2a6fd85a"
CONTROLLER = "0x281ca41311c2aa808c917c4674639d7567b75714"
def setUp(self):
self._saved = (
snrc.REGISTRIES,
snrc.REGISTRARS,
snrc.CONTROLLERS,
snrc.eth_call,
snrc.chain_now,
)
snrc.REGISTRIES = {"testing": self.REGISTRY}
snrc.REGISTRARS = {"testing": self.REGISTRAR}
snrc.CONTROLLERS = {"testing": self.CONTROLLER}
snrc.chain_now = lambda: int(time.time())
def tearDown(self):
(
snrc.REGISTRIES,
snrc.REGISTRARS,
snrc.CONTROLLERS,
snrc.eth_call,
snrc.chain_now,
) = self._saved
def _reserved_as(self, code):
def eth_call(to, data):
if data.startswith(snrc.selector("reservedNames(bytes32)")):
return "0x" + snrc.encode_uint(code)
if data.startswith(snrc.selector("GRACE_PERIOD()")):
return "0x" + snrc.encode_uint(90 * 86400)
if data.startswith(snrc.selector("prices()")):
return "0x" + snrc.encode_uint(0)
return "0x" + snrc.encode_uint(0)
return eth_call
def test_every_enum_value_has_a_code_and_a_sentence(self):
for code, (name, sentence) in snrc.RESERVED_REASONS.items():
snrc.eth_call = self._reserved_as(code)
reg = snrc.name_status("acme.testing")
self.assertEqual(reg["status"], "reserved", name)
self.assertEqual(reg["reasonCode"], name)
self.assertEqual(reg["reason"], sentence)
def test_a_trademark_reservation_says_so(self):
snrc.eth_call = self._reserved_as(2)
_, body = snrc.resolve("acme.testing")
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."""
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."""
snrc.eth_call = self._reserved_as(99)
reg = snrc.name_status("acme.testing")
self.assertEqual(reg["status"], "reserved")
self.assertEqual(reg["reasonCode"], "unspecified")
class ErrorCodeTests(unittest.TestCase):
REGISTRY = "0x58fc46996d975c57883564648bda5206d1a0102b"
@@ -704,6 +714,7 @@ class ErrorCodeTests(unittest.TestCase):
snrc.REGISTRARS = {"testing": self.REGISTRAR}
snrc.CONTROLLERS = {"testing": ""}
snrc.chain_now = lambda: int(time.time())
snrc._auction_params.clear()
def tearDown(self):
(