compare block instead of wall clock

This commit is contained in:
Alain Brenzikofer
2026-09-04 08:42:54 +02:00
parent 9b2ff82cc7
commit 41678c74bb
3 changed files with 58 additions and 12 deletions
+6 -4
View File
@@ -152,10 +152,12 @@ about it. `expires` and `graceEnds` are Unix timestamps in seconds; both are
The split between `grace` and `expired` mirrors the registrar's own
`available(id)` rule (`expires + GRACE_PERIOD < now`), with `GRACE_PERIOD` read
from the contract rather than assumed. Note that `available(id)` alone cannot
distinguish these: it is also true for a name nobody ever registered, since
`0 + GRACE_PERIOD < now`. A zero expiry is what separates *never taken* from
*taken and since released*.
from the contract rather than assumed and `now` taken from the latest block's
timestamp rather than the host clock — the registrar compares against that same
clock, so a skewed machine cannot misstate a registration. Note that
`available(id)` alone cannot distinguish these: it is also true for a name
nobody ever registered, since `0 + GRACE_PERIOD < now`. A zero expiry is what
separates *never taken* from *taken and since released*.
Subnames report the status of the 2LD they sit under, which is the useful
answer — a subname is only as valid as the name above it.
+11 -2
View File
@@ -66,7 +66,6 @@ import hashlib
import json
import os
import sys
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import unquote, urlparse
from urllib.request import Request, urlopen
@@ -202,6 +201,16 @@ def node_of(name: str) -> bytes:
# ---------- Registration status ----------
def chain_now() -> int:
"""The latest block's timestamp - the same clock the registrar reads.
Asked explicitly rather than taken from the host clock, so a skewed clock
on this machine cannot misstate a registration.
"""
block = rpc("eth_getBlockByNumber", ["latest", False])
return decode_uint(block["timestamp"])
def grace_period(registrar: str) -> int:
"""The registrar's own GRACE_PERIOD, in seconds.
@@ -279,7 +288,7 @@ def name_status(name: str):
status, grace = "unregistered", 0
else:
grace = grace_period(registrar)
status = expiry_status(expires, grace, int(time.time()))
status = expiry_status(expires, grace, chain_now())
# `reserved` only displaces the two states that read as "you could take
# this". A registered name is registered, and one in grace belongs to its
+41 -6
View File
@@ -101,12 +101,13 @@ class EncodedLabelhashTests(unittest.TestCase):
GRACE = 90 * 86400
def setUp(self):
self._saved = (snrc.REGISTRARS, snrc.CONTROLLERS, snrc.eth_call)
self._saved = (snrc.REGISTRARS, snrc.CONTROLLERS, snrc.eth_call, snrc.chain_now)
snrc.REGISTRARS = {"testing": self.REGISTRAR}
snrc.CONTROLLERS = {"testing": ""}
snrc.chain_now = lambda: int(time.time())
def tearDown(self):
snrc.REGISTRARS, snrc.CONTROLLERS, snrc.eth_call = self._saved
snrc.REGISTRARS, snrc.CONTROLLERS, snrc.eth_call, snrc.chain_now = self._saved
def test_the_encoded_form_is_recognised(self):
self.assertTrue(
@@ -217,14 +218,44 @@ class NameStatusTests(unittest.TestCase):
return eth_call
def setUp(self):
self._saved = (snrc.REGISTRARS, snrc.CONTROLLERS, snrc.eth_call)
self._saved = (
snrc.REGISTRARS,
snrc.CONTROLLERS,
snrc.eth_call,
snrc.chain_now,
snrc.rpc,
)
snrc.REGISTRARS = {"testing": self.REGISTRAR}
# These cases are about expiry alone. ReservedTests covers what a
# configured controller adds.
snrc.CONTROLLERS = {"testing": ""}
snrc.chain_now = lambda: int(time.time())
def tearDown(self):
snrc.REGISTRARS, snrc.CONTROLLERS, snrc.eth_call = self._saved
(
snrc.REGISTRARS,
snrc.CONTROLLERS,
snrc.eth_call,
snrc.chain_now,
snrc.rpc,
) = self._saved
def test_now_is_the_latest_blocks_timestamp(self):
# setUp replaces chain_now with the fixture clock; this is about the
# real one, saved as the 4th element of the setUp snapshot
real_chain_now = self._saved[3]
snrc.rpc = lambda method, params: {"timestamp": "0x65f1a2c0", "number": "0x123"}
self.assertEqual(real_chain_now(), 0x65F1A2C0)
def test_status_reads_the_chain_clock_not_the_host_clock(self):
"""The registrar compares expiry to block.timestamp, so the resolver
must too - a host clock years ahead must not turn a live name into a
claimable one."""
future = int(time.time()) + 3600
snrc.eth_call = self._expiry(future)
self.assertEqual(snrc.name_status("alice.testing")["status"], "registered")
snrc.chain_now = lambda: future + 3650 * 86400
self.assertEqual(snrc.name_status("alice.testing")["status"], "expired")
def test_zero_expiry_means_never_registered(self):
snrc.eth_call = self._expiry(0)
@@ -313,12 +344,13 @@ class ReservedTests(unittest.TestCase):
CONTROLLER = "0x281ca41311c2aa808c917c4674639d7567b75714"
def setUp(self):
self._saved = (snrc.REGISTRARS, snrc.CONTROLLERS, snrc.eth_call)
self._saved = (snrc.REGISTRARS, snrc.CONTROLLERS, snrc.eth_call, snrc.chain_now)
snrc.REGISTRARS = {"testing": self.REGISTRAR}
snrc.CONTROLLERS = {"testing": self.CONTROLLER}
snrc.chain_now = lambda: int(time.time())
def tearDown(self):
snrc.REGISTRARS, snrc.CONTROLLERS, snrc.eth_call = self._saved
snrc.REGISTRARS, snrc.CONTROLLERS, snrc.eth_call, snrc.chain_now = self._saved
def _chain(self, expires, reserved):
def eth_call(to, data):
@@ -380,10 +412,12 @@ class ReservedReasonTests(unittest.TestCase):
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}
snrc.chain_now = lambda: int(time.time())
def tearDown(self):
(
@@ -391,6 +425,7 @@ class ReservedReasonTests(unittest.TestCase):
snrc.REGISTRARS,
snrc.CONTROLLERS,
snrc.eth_call,
snrc.chain_now,
) = self._saved
def _chain(self, expires, reserved):