eth_call guard and cache constants

This commit is contained in:
Alain Brenzikofer committed 2026-09-07 08:09:43 +02:00
1 parent 99adfe9af0
commit e6dc6c8846
3 files changed
+61 -21

No files matched your search

+7 -3
View File
@@ -183,8 +183,9 @@ length, which a hash does not carry. The client adds that.
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.
Deployment constants - the grace period, the oracle and its curve - are cached
for `CONSTANTS_TTL` (5 minutes), so a retune shows up within that. Per-name
values and the decaying premium are read on every query.
**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
@@ -280,7 +281,10 @@ hold the same value, so one field is enough to read.
`upstreamError` says only which exception type the RPC call raised. The text
goes to the resolver's log instead, because `SNRC_RPC` can carry a provider key
and urlopen puts the URL it failed on into the message.
and urlopen puts the URL it failed on into the message. It is also the answer
when a registrar, controller or oracle address has no contract behind it: the
empty reply is refused rather than read as zero, which would make every name
look free.
### Status codes
+26 -12
View File
@@ -193,9 +193,26 @@ def chain_now() -> int:
return decode_uint(block["timestamp"])
# Deployment constants - the grace period, the oracle and its curve - change only
# when the owner retunes a contract, so they are read once per TTL rather than on
# every query. Per-name values and the decaying premium are never cached.
CONSTANTS_TTL = 300
_constants: dict = {}
def cached(key, read):
"""`read()` at most once per CONSTANTS_TTL for `key`."""
hit = _constants.get(key)
if hit and time.time() - hit[0] < CONSTANTS_TTL:
return hit[1]
value = read()
_constants[key] = (time.time(), value)
return value
def grace_period(registrar: str) -> int:
"""A deployment can configure a different window, so it is read on chain."""
return decode_uint(eth_call(registrar, selector("GRACE_PERIOD()")))
return cached(("grace", registrar), lambda: decode_uint(eth_call(registrar, selector("GRACE_PERIOD()"))))
def expiry_status(expires: int, grace: int, now: int) -> str:
@@ -219,18 +236,13 @@ def reservation_reason(tld: str, token: int) -> int:
return decode_uint(raw)
# 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 = {}
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]
return cached(("auction", tld), lambda: read_auction_params(tld))
def read_auction_params(tld: str):
params = (ZERO_ADDR, 0, 0, 0)
controller = CONTROLLERS.get(tld)
if controller:
@@ -242,7 +254,6 @@ def auction_params(tld: str):
decode_uint(eth_call(oracle, selector("totalDays()"))),
decode_uint(eth_call(oracle, selector("endValue()"))),
)
_auction_params[tld] = (time.time(), params)
return params
@@ -329,7 +340,10 @@ def selector(signature: str) -> str:
def eth_call(to: str, data: str) -> str:
return rpc("eth_call", [{"to": to, "data": data}, "latest"])
result = rpc("eth_call", [{"to": to, "data": data}, "latest"])
if result == "0x":
raise RuntimeError(f"empty return from {to}: no contract at that address?")
return result
def decode_address(hex_data: str) -> str:
+28 -6
View File
@@ -97,7 +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()
snrc._constants.clear()
def tearDown(self):
snrc.REGISTRARS, snrc.CONTROLLERS, snrc.eth_call, snrc.chain_now = self._saved
@@ -235,7 +235,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()
snrc._constants.clear()
def tearDown(self):
(
@@ -259,6 +259,28 @@ class NameStatusTests(unittest.TestCase):
snrc.chain_now = lambda: future + 3650 * 86400
self.assertEqual(snrc.name_status("alice.testing")["status"], "expired")
def test_a_registrar_that_is_not_a_contract_is_an_error_not_a_free_name(self):
"""An address with no code answers eth_call with empty data. Read as
zero, that would make every name look free."""
snrc.eth_call = self._saved[2] # the real one, so its guard runs
snrc.rpc = lambda method, params: "0x"
with self.assertRaises(RuntimeError):
snrc.name_status("alice.testing")
def test_the_grace_period_is_read_once_not_per_query(self):
seen = []
def eth_call(to, data):
seen.append(data[:10])
if data.startswith(snrc.selector("GRACE_PERIOD()")):
return "0x" + snrc.encode_uint(self.GRACE)
return "0x" + snrc.encode_uint(int(time.time()) - 3600)
snrc.eth_call = eth_call
snrc.name_status("alice.testing")
snrc.name_status("alice.testing")
self.assertEqual(seen.count(snrc.selector("GRACE_PERIOD()")), 1)
def test_zero_expiry_means_never_registered(self):
snrc.eth_call = self._expiry(0)
self.assertEqual(
@@ -361,7 +383,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()
snrc._constants.clear()
def tearDown(self):
snrc.REGISTRARS, snrc.CONTROLLERS, snrc.eth_call, snrc.chain_now = self._saved
@@ -429,7 +451,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()
snrc._constants.clear()
def tearDown(self):
(
@@ -553,7 +575,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()
snrc._constants.clear()
def tearDown(self):
(
@@ -704,7 +726,7 @@ class ErrorCodeTests(unittest.TestCase):
snrc.REGISTRARS = {"testing": self.REGISTRAR}
snrc.CONTROLLERS = {"testing": ""}
snrc.chain_now = lambda: int(time.time())
snrc._auction_params.clear()
snrc._constants.clear()
def tearDown(self):
(