mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-09-16 04:06:46 +00:00
docs shortening and other review fixes
This commit is contained in:
+32
-51
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user