docs shortening and other review fixes

This commit is contained in:
Alain Brenzikofer
2026-09-05 22:52:42 +02:00
parent 783f0aa4be
commit 45d314cfe2
13 changed files with 195 additions and 280 deletions
+32 -51
View File
@@ -172,48 +172,33 @@ name it sits under.
### The post-grace auction
When grace ends the registrar will sell the name to anyone, but the price
oracle adds a premium that halves each day until it reaches zero. A name in
that window reports `auction` rather than `expired`, with `premium` (a decimal
string of attoUSD, because the value is a 256-bit integer that no JSON number
can hold) and `auctionEnds`.
When grace ends anyone may register the name, but the price oracle adds a
premium that halves each day until it reaches zero. A name in that window
reports `auction` instead of `expired`, with `premium` (attoUSD as a decimal
string, since no JSON number holds a 256-bit integer) and `auctionEnds`.
The premium depends only on when the registration lapsed, never on the label,
so it is answerable for a labelhash query too. The base price is not: it depends
on the label's length, which a hashed query does not carry. `premium` is
therefore the surcharge alone, and a client that knows its own name adds the
base price itself.
The premium depends only on when the registration lapsed, not on the label, so a
labelhash query gets it too. The base price does depend on the label's length,
which a hashed query does not carry, so `premium` is the surcharge alone and the
client adds the base price.
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. 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.
The oracle comes from the controller's `prices()`, so no extra configuration is
needed. Its window is read from the chain; zero days switches the auction off.
The curve is cached for `AUCTION_PARAMS_TTL` (5 minutes), so a `setPremium`
retune shows up within that; the decaying premium is read on every query.
**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 this service before the routers that query it.** Routers from v22 hash
the 2LD of every query, and two things only this version does are needed to
answer them: decoding a bracket label that sits under a subname
(`sub.[<hash>].tld`, which an older resolver hashes as literal text and so
answers about a node nobody asked about), and reporting `auction` at all — an
older resolver calls a name in its auction plain `expired`, which reads as "free
at the ordinary price" while the registrar charges the premium.
**Upgrade this service before the routers that query it.** An older resolver
reports a name in its auction as plain `expired`, which routers read as
"available at the ordinary price" while the registrar charges the premium. It
also fails to decode a bracket label under a subname (`sub.[<hash>].tld`), which
routers from v22 send. The same wrong quote happens when the auction cannot be
read at all, so set `SNRC_CONTROLLER_<TLD>` wherever `SNRC_REGISTRAR_<TLD>` is.
### Why a name is reserved
`reserved` carries both `reasonCode`, the controller's own reservation reason,
and `reason`, an English sentence for a human reading the REST API. Clients
should branch on `reasonCode` and word it themselves, so the wording follows the
user's language rather than the server's.
`reserved` carries `reasonCode`, the controller's reason, and `reason`, an
English sentence for a human reading this API. Clients should branch on
`reasonCode` and word it themselves, in the user's language.
| `reasonCode` | Meaning |
|---|---|
@@ -224,10 +209,9 @@ user's language rather than the server's.
| `internal` | reserved for SimpleX |
| `premium` | reserved as a premium name |
A controller deployed before reservation reasons existed stores a plain boolean,
whose `true` reads back as `unspecified`, so nothing needs migrating. A code
this resolver does not know also reads as `unspecified` — the name stays
reserved either way.
A controller from before reasons existed stores a boolean; its `true` reads as
`unspecified`, so nothing needs migrating. An unknown code also reads as
`unspecified` — the name stays reserved either way.
### Querying by labelhash
@@ -245,18 +229,15 @@ 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.
Only the second-level label is a registry key, and it is decoded wherever it
sits: `sub.[<hash>].testing` reaches the node `sub.name.testing` does. Subname
labels stay text; a bracket label left of the 2LD is an ordinary label. Routers
from v22 send every 2LD this way, so a registrable name normally 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
the name or the registry holds it back. Watch out for `noResolver`: it is also a
404, but the name is taken.
Read the answer from `status`. A name is free on `unregistered` (404), and on
`expired` or `auction` (410) — `auction` costs a premium on top. Every other
status means somebody holds the name. Watch `noResolver`: also a 404, but taken.
The hash must be keccak-256. `openssl dgst -sha3-256` and `sha3sum` compute
SHA3-256, a different function that returns 64 valid-looking hex characters
+15 -20
View File
@@ -105,9 +105,8 @@ CONTROLLERS = {
"simplex": os.environ.get("SNRC_CONTROLLER_SIMPLEX", ""), # not deployed yet
}
# `reservedNames` maps a name to SimplexController.Reason; 0 (None) means the
# name is not reserved. A controller predating the enum stores a bool, whose
# `true` decodes as 1 - the same "unspecified" this table already describes.
# `reservedNames` holds a SimplexController.Reason; 0 means not reserved. A
# controller from before the enum stores a bool, whose `true` decodes as 1.
RESERVED_REASONS = {
1: ("unspecified", "reserved for a brand or public interest"),
2: ("trademark", "reserved to protect a trademark"),
@@ -171,10 +170,9 @@ def is_encoded_labelhash(label: str) -> bool:
def node_of(name: str) -> bytes:
"""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."""
"""namehash, decoding the 2LD's label as a labelhash wherever it sits, so
`[hash].tld` and `sub.[hash].tld` reach the nodes their names do. A bracket
label anywhere else is hashed as written."""
labels = name.split(".")
if len(labels) < 2 or not is_encoded_labelhash(labels[-2]):
return namehash(name)
@@ -219,10 +217,8 @@ 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.
# The oracle and its curve change only on a retune, so they are read once per
# TTL rather than per query. The premium itself decays, so it is never cached.
AUCTION_PARAMS_TTL = 300
_auction_params: dict = {}
@@ -249,19 +245,18 @@ def auction_params(tld: str):
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."""
"""Past grace a name is registrable again, but at a premium decaying to zero
over the oracle's window. Returns when the premium reaches zero and what it
is now, in attoUSD, or (None, None) once prices are normal - which includes
an auction switched off with totalDays 0."""
oracle, start, total_days, floor = auction_params(tld)
if oracle == ZERO_ADDR:
return None, None
ends = grace_ends + total_days * 86400
if now >= ends:
return None, None
# decayedPremium is `pure`, so the premium quoted here is the oracle's own
# arithmetic rather than a reimplementation of its decay curve.
# decayedPremium is `pure`, so this is the oracle's own arithmetic rather
# than a second copy of its decay curve.
decayed = decode_uint(
eth_call(
oracle,
@@ -289,8 +284,8 @@ def name_status(name: str):
}
# nameExpires and reservedNames are keyed on uint256(keccak(label)).
# Only the 2LD's label is a registry key, wherever in the name it sits, so it
# is the only one decoded - the same rule node_of applies to the node.
# Only the 2LD's label is a registry key, wherever it sits - the same rule
# node_of applies to the node.
label = labels[-2]
if is_encoded_labelhash(label):
token = int(label[1:-1], 16)
+14 -21
View File
@@ -137,8 +137,8 @@ class EncodedLabelhashTests(unittest.TestCase):
self.assertEqual(snrc.node_of("alice.testing"), snrc.namehash("alice.testing"))
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."""
"""Only the 2LD is a key, so a bracket label left of it is hashed as
written."""
self.assertNotEqual(
snrc.node_of(
"[9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501]"
@@ -148,8 +148,7 @@ class EncodedLabelhashTests(unittest.TestCase):
)
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."""
"""`sub.[hash].tld` must reach the node `sub.name.tld` does."""
self.assertEqual(
snrc.node_of(
"sub."
@@ -213,7 +212,7 @@ class NameStatusTests(unittest.TestCase):
return eth_call
def _keys(self, status, expires, grace_ends):
"""Every branch answers with the same keys; only some carry a value."""
"""Every branch answers with the same keys; only some carry values."""
return {
"status": status,
"expires": expires,
@@ -314,8 +313,7 @@ class NameStatusTests(unittest.TestCase):
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."""
"""The token must come from the hash, not from hashing the brackets."""
seen = []
def eth_call(to, data):
@@ -480,16 +478,14 @@ class ReservedReasonTests(unittest.TestCase):
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."""
"""Before the enum `reservedNames` was a bool; its `true` decodes as 1."""
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."""
"""A new Reason still reads 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")
@@ -530,9 +526,8 @@ class ReservedReasonTests(unittest.TestCase):
class AuctionTests(unittest.TestCase):
"""Once grace ends the registrar will sell the name to anyone, but the price
oracle adds a premium that halves each day until it reaches zero. Reporting
such a name as plainly available would quote the normal price for it."""
"""Past grace anyone may register the name, but at a premium that halves
each day. Reporting it as plainly available would quote the normal price."""
REGISTRY = "0x58fc46996d975c57883564648bda5206d1a0102b"
REGISTRAR = "0xef47eb4384b46c89e4482a677c2cbcbd2a6fd85a"
@@ -569,9 +564,8 @@ class AuctionTests(unittest.TestCase):
) = self._saved
def _chain(self, expires, total_days=TOTAL_DAYS, oracle=None, reserved=0):
"""Answers as SimplexController and SimplexPriceOracle do, including the
oracle's own `decayedPremium` shift, so the arithmetic under test is the
resolver's and not a second copy of the decay curve."""
"""Answers as the controller and oracle do, including the oracle's own
`decayedPremium` shift, so the decay curve is not copied here."""
oracle = self.ORACLE if oracle is None else oracle
self.oracle_calls = []
@@ -603,8 +597,7 @@ class AuctionTests(unittest.TestCase):
def _lapsed(self, days_into_auction):
"""An expiry whose grace ended `days_into_auction` days ago. The extra
second clears the boundary, which the registrar counts as still in
grace."""
second clears the boundary, which counts as still in grace."""
return self.now - self.GRACE - 1 - days_into_auction * 86400
def test_a_name_just_past_grace_is_in_auction_not_merely_expired(self):
@@ -651,8 +644,8 @@ class AuctionTests(unittest.TestCase):
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."""
"""The curve changes only on a retune, 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)