mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-10-26 12:32:28 +00:00
review fixes
This commit is contained in:
1 parent
e3f00b46f6
commit
69cce33675
15 files changed
+425
-173
No files matched your search
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
(
|
||||
|
||||
Reference in new issue
Block a user