mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-10-24 02:02:45 +00:00
eth_call guard and cache constants
This commit is contained in:
1 parent
99adfe9af0
commit
e6dc6c8846
3 files changed
+61
-21
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
(
|
||||
|
||||
Reference in new issue
Block a user