From be35dbc9cb4ae87907fd0d148497d7d8163b25bb Mon Sep 17 00:00:00 2001 From: Alain Brenzikofer Date: Wed, 9 Sep 2026 11:50:21 +0200 Subject: [PATCH] fix reverse compatibility with .testing mainnet --- scripts/resolver/service/snrc-resolve.py | 45 +++++++++++-- scripts/resolver/service/test_snrc_resolve.py | 63 +++++++++++++++++++ 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/scripts/resolver/service/snrc-resolve.py b/scripts/resolver/service/snrc-resolve.py index 00e9a6e38..7261d0967 100755 --- a/scripts/resolver/service/snrc-resolve.py +++ b/scripts/resolver/service/snrc-resolve.py @@ -257,22 +257,55 @@ def read_pricing_params(tld: str): return None +SECONDS_PER_YEAR = 31536000 +ATTO_PER_CENT = 10**16 + + def read_oracle_prices(controller: str, oracle: str): - """The oracle keeps the curve in US cents per year, which is the unit the - SMP protocol carries, so nothing is converted here.""" - base, tiers = decode_prices(eth_call(oracle, selector("prices()"))) + """SimplexPriceOracle keeps the curve in US cents per year, the unit the SMP + protocol carries. An ENS-shaped oracle prices in attoUSD per second and + charges a premium on lapsed names that it does not expose, so a quote from + it is only safe for a name that was never registered.""" + try: + base, tiers = decode_prices(eth_call(oracle, selector("prices()"))) + premium_unknown = False + except RuntimeError: + base, tiers = decode_letter_prices(oracle) + premium_unknown = True 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}, "basePrice": base, "minLabelLength": min_len, + "_premiumUnknown": premium_unknown, } +def decode_letter_prices(oracle: str): + """`price1Letter()`..`price6Letter()`, in attoUSD per second. Quotes round + up, so one is never below what the registry charges. Six and above is the + base price, as StablePriceOracle charges it.""" + tiers = { + n: ceil_div( + decode_uint(eth_call(oracle, selector(f"price{n}Letter()"))) * SECONDS_PER_YEAR, + ATTO_PER_CENT, + ) + for n in range(1, 7) + } + return tiers.pop(6), tiers + + +def ceil_div(a: int, b: int) -> int: + return -(-a // b) + + def decode_prices(hex_data: str): """`prices()` returns the base price and the lengths priced differently.""" raw = bytes.fromhex(hex_data[2:] if hex_data.startswith("0x") else hex_data) + # a short answer is not a curve: decoding it would quote every name as free + if len(raw) < 96: + raise RuntimeError("prices(): short response") base = int.from_bytes(raw[:32], "big") at = int.from_bytes(raw[32:64], "big") count = int.from_bytes(raw[at:at + 32], "big") @@ -327,8 +360,10 @@ def name_status(name: str): } if status in ("unregistered", "expired"): pricing = pricing_params(tld) - if pricing: - out.update(pricing) + # a lapsed name may carry a premium this resolver cannot read, and a + # quote without it would be below what the registry charges + if pricing and not (status == "expired" and pricing["_premiumUnknown"]): + out.update({k: v for k, v in pricing.items() if not k.startswith("_")}) return out diff --git a/scripts/resolver/service/test_snrc_resolve.py b/scripts/resolver/service/test_snrc_resolve.py index cd4d060f8..fdc25b9a5 100644 --- a/scripts/resolver/service/test_snrc_resolve.py +++ b/scripts/resolver/service/test_snrc_resolve.py @@ -676,6 +676,69 @@ class PricingTests(unittest.TestCase): self.assertEqual(body["basePrice"], self.BASE) +class EnsOracleTests(unittest.TestCase): + """.testing runs an ENS-shaped oracle: it prices in attoUSD per second and + charges a premium on lapsed names that it does not expose.""" + + REGISTRY = "0x58fc46996d975c57883564648bda5206d1a0102b" + REGISTRAR = "0xef47eb4384b46c89e4482a677c2cbcbd2a6fd85a" + CONTROLLER = "0x281ca41311c2aa808c917c4674639d7567b75714" + ORACLE = "0x1e0c9a2b9d1a4c8f7b3e5d6a9c2f4b8e1d7a3c50" + GRACE = 90 * 86400 + MIN_LENGTH = 6 + + 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} + self.now = int(time.time()) + snrc.chain_now = lambda: self.now + snrc._constants.clear() + + def tearDown(self): + (snrc.REGISTRIES, snrc.REGISTRARS, snrc.CONTROLLERS, snrc.eth_call, snrc.chain_now) = self._saved + + def _chain(self, expires, letter_cents=0): + def eth_call(to, data): + if data.startswith(snrc.selector("nameExpires(uint256)")): + return "0x" + snrc.encode_uint(expires) + if data.startswith(snrc.selector("GRACE_PERIOD()")): + return "0x" + snrc.encode_uint(self.GRACE) + if data.startswith(snrc.selector("reservedNames(bytes32)")): + return "0x" + snrc.encode_uint(0) + if data.startswith(snrc.selector("minCharLength()")): + return "0x" + snrc.encode_uint(self.MIN_LENGTH) + if data.startswith(snrc.selector("prices()")): + if to == self.CONTROLLER: + return "0x" + snrc.encode_uint(int(self.ORACLE, 16)) + raise RuntimeError("eth_call returned 0x") # no prices() on this oracle + for n in range(1, 7): + if data.startswith(snrc.selector(f"price{n}Letter()")): + rate = letter_cents * snrc.ATTO_PER_CENT // snrc.SECONDS_PER_YEAR + return "0x" + snrc.encode_uint(rate) + return self.fail("unexpected call " + data[:10]) + + return eth_call + + def test_a_never_registered_name_is_priced_from_the_letter_curve(self): + snrc.eth_call = self._chain(0) + reg = snrc.name_status("ghost.testing") + self.assertEqual(reg["status"], "unregistered") + self.assertEqual(reg["basePrice"], 0) + self.assertEqual(reg["minLabelLength"], self.MIN_LENGTH) + + def test_a_non_zero_letter_curve_converts_to_cents_per_year(self): + snrc.eth_call = self._chain(0, letter_cents=1200) + self.assertEqual(snrc.name_status("ghost.testing")["basePrice"], 1200) + + def test_a_lapsed_name_is_not_priced_because_the_premium_is_unreadable(self): + snrc.eth_call = self._chain(self.now - self.GRACE - 1) + reg = snrc.name_status("acme.testing") + self.assertEqual(reg["status"], "expired") + self.assertNotIn("basePrice", reg) + + class ErrorCodeTests(unittest.TestCase): REGISTRY = "0x58fc46996d975c57883564648bda5206d1a0102b" REGISTRAR = "0xef47eb4384b46c89e4482a677c2cbcbd2a6fd85a"