mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-09-13 02:26:01 +00:00
merge ab/resolver-labelhash-query
# Conflicts: # scripts/resolver/README.md # scripts/resolver/service/snrc-resolve.py # scripts/resolver/service/test_snrc_resolve.py
This commit is contained in:
@@ -306,8 +306,8 @@ jobs:
|
||||
# Resolver test job
|
||||
# =============================
|
||||
|
||||
# The SNRC resolver is Python and stdlib-only apart from keccak, so it needs
|
||||
# none of the Haskell toolchain above and runs independently of it.
|
||||
# The SNRC resolver is Python, so this job needs none of the Haskell toolchain
|
||||
# above and runs independently of it.
|
||||
|
||||
resolver-test:
|
||||
name: "resolver (python)"
|
||||
@@ -323,8 +323,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install resolver dependencies
|
||||
# The only runtime dependency; declared in
|
||||
# scripts/resolver/service/pyproject.toml.
|
||||
# Must match scripts/resolver/service/pyproject.toml.
|
||||
run: python -m pip install "eth-hash[pycryptodome]>=0.7"
|
||||
|
||||
- name: Test
|
||||
|
||||
+23
-31
@@ -170,21 +170,19 @@ needs, because a subname is only as good as the name it sits under.
|
||||
|
||||
### Querying by labelhash
|
||||
|
||||
A client that asks whether a name is free is usually about to register it.
|
||||
Whoever runs the resolver sees that question and could register the name first.
|
||||
To avoid that, send the keccak hash of the label instead of the label itself,
|
||||
written in ENS's `[<64 hex>]` form. The answer is the same:
|
||||
A client asking whether a name is free is usually about to register it, and
|
||||
whoever runs the resolver could register it first. To avoid that, send the
|
||||
keccak hash of the label in ENS's `[<64 hex>]` form instead of the label:
|
||||
|
||||
```sh
|
||||
# instead of /resolve/acme.testing
|
||||
curl -s "http://127.0.0.1:8000/resolve/[$(printf acme | keccak-256sum | cut -d' ' -f1)].testing"
|
||||
```
|
||||
|
||||
This works because namehash is `keccak(parent || keccak(label))`. Passing
|
||||
`keccak(label)` gives the same node, so the resolver reads the same record. The
|
||||
registrar keys `nameExpires` and `reservedNames` on the labelhash as well, so
|
||||
the status fields do not need the label either. The resolver learns which name
|
||||
you meant only if it guesses the label and hashes it.
|
||||
namehash is `keccak(parent || keccak(label))`, so this reaches the same node and
|
||||
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.
|
||||
|
||||
Read the answer from `status`. A name is free only when the body says
|
||||
`unregistered`, which comes with a 404. Every other status means somebody holds
|
||||
@@ -192,33 +190,27 @@ the name or held it recently. Watch out for `noResolver`: it is also a 404, but
|
||||
the name is taken.
|
||||
|
||||
The hash must be keccak-256. `openssl dgst -sha3-256` and `sha3sum` compute
|
||||
SHA3-256, which is a different function. They return 64 valid-looking hex
|
||||
characters that point at the wrong node.
|
||||
SHA3-256, a different function that returns 64 valid-looking hex characters
|
||||
pointing at the wrong node.
|
||||
|
||||
The resolver lowercases the query before matching, so uppercase hex works too.
|
||||
HTTP clients that refuse raw brackets in a path can percent-encode them as
|
||||
`%5B` and `%5D`. Both forms reach the same name.
|
||||
Clients that refuse raw brackets in a path can percent-encode them as `%5B` and
|
||||
`%5D`.
|
||||
|
||||
Brackets keep the two forms from colliding. `[` and `]` are not valid in a
|
||||
normalised ENS name, and the dApp normalises before it registers, so no name
|
||||
registered through it can look like this. Nothing on chain checks the character
|
||||
set, but a `[<64 hex>]` label is 66 bytes and the registrar's `maxLabelLength`
|
||||
is 63, so it cannot be registered directly either. ENS uses this same encoding
|
||||
for a label whose preimage it does not know. A plain `0x…` label would not work
|
||||
here, because that is an ordinary name anyone can register.
|
||||
Brackets cannot collide with a real name: they are invalid in a normalised ENS
|
||||
name, and a `[<64 hex>]` label is 66 bytes against the registrar's
|
||||
`maxLabelLength` of 63. A plain `0x…` label is not treated as a hash, since that
|
||||
is an ordinary, registrable name.
|
||||
|
||||
Only 2LDs can be queried by hash. A 2LD is what a registration buys, so it is
|
||||
the only name worth hiding. Subnames are left out because nobody can race you
|
||||
for one: the owner of the 2LD creates them. In a subname the resolver hashes a
|
||||
`[<64 hex>]` label as written instead of decoding it, so such a query points at
|
||||
a node nobody can own. ENS tooling accepts the bracketed form at any depth;
|
||||
this resolver does not, on purpose.
|
||||
Only 2LDs can be queried this way, as only a 2LD can be raced for: subnames are
|
||||
created by the 2LD's owner. A bracket label in a subname is hashed as written,
|
||||
so it points at a node nobody can own. ENS tooling accepts the bracketed form at
|
||||
any depth; this resolver does not, on purpose.
|
||||
|
||||
This hides your interest in a name, and nothing more. The registration itself
|
||||
is public, and the controller's commit-reveal protects that step. The hash is
|
||||
also easy to guess for a short or well-known label, since an operator can hash
|
||||
candidate labels and compare. And once you register, the reveal publishes the
|
||||
labelhash, so an operator who logged your query can match it to the name.
|
||||
This hides interest in a name and nothing else: the registration itself is
|
||||
public, and commit-reveal covers that step. A short or well-known label is easy
|
||||
to guess by hashing candidates, and the reveal publishes the labelhash, so an
|
||||
operator who logged the query can match it to the name afterwards.
|
||||
|
||||
### Errors
|
||||
|
||||
|
||||
@@ -157,16 +157,8 @@ def namehash(name: str) -> bytes:
|
||||
return node
|
||||
|
||||
|
||||
# ENS writes a label whose preimage it does not know as `[<64 hex>]`, and this
|
||||
# resolver reuses that form for a label the caller withholds on purpose.
|
||||
# Brackets keep the two forms from colliding: `[` and `]` are not valid in a
|
||||
# normalised ENS name, and the dApp normalises before it registers. Nothing on
|
||||
# chain checks the character set, but a bracketed labelhash is 66 bytes and the
|
||||
# registrar's maxLabelLength is 63, so it cannot be registered directly either.
|
||||
# The ecosystem already reads this form back as a hash (ensjs
|
||||
# `isEncodedLabelhash`; the subgraph rejects any real label containing a
|
||||
# bracket). A plain `0x…` label would not work: that is an ordinary name anyone
|
||||
# can register.
|
||||
# ENS's encoding for a label whose preimage is unknown. Brackets are outside
|
||||
# the normalised character set, so it cannot collide with a registrable name.
|
||||
ENCODED_LABELHASH_LEN = 66 # "[" + 64 hex + "]"
|
||||
|
||||
|
||||
@@ -182,16 +174,11 @@ def is_encoded_labelhash(label: str) -> bool:
|
||||
def node_of(name: str) -> bytes:
|
||||
"""namehash, accepting an encoded labelhash in place of a 2LD's label.
|
||||
|
||||
A client that asks whether a name is free is usually about to register it,
|
||||
and whoever runs the resolver could register it first. namehash is
|
||||
keccak(parent || keccak(label)), so passing keccak(label) reaches the same
|
||||
node without sending the label.
|
||||
|
||||
Only 2LDs can be queried this way. A 2LD is what a registration buys, so it
|
||||
is the only name worth hiding. Subnames are left out because nobody can
|
||||
race a caller for one: the owner of the 2LD creates them. In a subname a
|
||||
`[<64 hex>]` label is hashed as written instead of decoded, so such a query
|
||||
points at a node nobody can own.
|
||||
keccak(parent || keccak(label)) reaches the same node without the label,
|
||||
so a caller can check a 2LD without disclosing which one they are about to
|
||||
register. Subnames are excluded - only the 2LD's owner creates them, so
|
||||
there is nothing to front-run - and a bracket label there is hashed as
|
||||
written.
|
||||
"""
|
||||
labels = name.split(".")
|
||||
if len(labels) == 2 and is_encoded_labelhash(labels[0]):
|
||||
|
||||
@@ -86,18 +86,11 @@ class SplitLinksTests(unittest.TestCase):
|
||||
|
||||
|
||||
class EncodedLabelhashTests(unittest.TestCase):
|
||||
"""Querying by labelhash instead of by label.
|
||||
"""`node_of` accepts a 2LD's label as an encoded labelhash `[<64 hex>]`,
|
||||
reaching the same node as the label itself."""
|
||||
|
||||
A client that asks whether a name is free is usually about to register it,
|
||||
and whoever runs the resolver could register it first. namehash is
|
||||
keccak(parent || keccak(label)), so supplying keccak(label) gives the same
|
||||
node and the same answer without sending the label.
|
||||
|
||||
The encoding is ENS's own `[<64 hex>]`. It cannot collide with a real name,
|
||||
because brackets are not valid in a normalised ENS name."""
|
||||
|
||||
# keccak-256("alice") = 9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501
|
||||
# - written out in full wherever a test needs a real labelhash.
|
||||
# keccak-256("alice"), written out in full wherever a test needs it.
|
||||
# 9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501
|
||||
|
||||
REGISTRAR = "0xef47eb4384b46c89e4482a677c2cbcbd2a6fd85a"
|
||||
GRACE = 90 * 86400
|
||||
@@ -125,9 +118,8 @@ class EncodedLabelhashTests(unittest.TestCase):
|
||||
|
||||
def test_non_hex_between_the_brackets_is_not(self):
|
||||
self.assertFalse(snrc.is_encoded_labelhash("[" + "z" * 64 + "]"))
|
||||
# uppercase hex is not it either: the handler lowercases the whole name
|
||||
# uppercase is rejected because the handler lowercases the whole name
|
||||
self.assertFalse(snrc.is_encoded_labelhash("[" + "A" * 64 + "]"))
|
||||
# explicitly disallowed prefix
|
||||
self.assertFalse(snrc.is_encoded_labelhash("[0x9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501]"))
|
||||
|
||||
def test_the_wrong_length_is_not(self):
|
||||
@@ -147,9 +139,6 @@ class EncodedLabelhashTests(unittest.TestCase):
|
||||
self.assertEqual(snrc.node_of("alice.testing"), snrc.namehash("alice.testing"))
|
||||
|
||||
def test_an_encoded_subname_is_not_the_name_it_would_decode_to(self):
|
||||
"""Only 2LDs are queried by hash. If a label in `[<64 hex>]` form were
|
||||
decoded in a subname, that subname would silently be the name the hash
|
||||
stands for - here `alice.alice.testing`."""
|
||||
self.assertNotEqual(
|
||||
snrc.node_of(
|
||||
"[9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501]"
|
||||
@@ -167,8 +156,6 @@ class EncodedLabelhashTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_a_0x_prefixed_label_is_taken_literally(self):
|
||||
"""`0x<64 hex>` is a registrable name, not a hash. Only the bracket
|
||||
form is read as a labelhash."""
|
||||
name = "0x9c0257114eb9399a2985f8e75dad7600c5d89fe3824ffa99ec1c3eb8bf3b0501.testing"
|
||||
self.assertEqual(snrc.node_of(name), snrc.namehash(name))
|
||||
self.assertNotEqual(snrc.node_of(name), snrc.node_of("alice.testing"))
|
||||
|
||||
Reference in New Issue
Block a user