Repository: ethereum/web3.py (python)

Issue:
`ens_encode_name` -> `dns_encode_name`
### What feature should we add?

We are properly encoding a name for ENS but really it's just a few simple rules on top of DNS encoding. It is referred to as just [DNS Encoding](https://docs.ens.domains/resolution/names#dns-encoding) the name in the ENS docs. I think for consistency we should create a `dns_encode` method that returns `ens_encode` under the hood, then deprecate `ens_encode` and remove in `v8`. I think we should wrap the return value for the new method in `HexBytes` so that `to_0x_hex()` is accessible as an option on the return value. This is mainly to keep consistency across languages for web3 tooling, as well as consistency in naming with the ENS docs.

1. Rename `ens_encode` utility method as `dns_encode`
2. Wrap the return value in `HexBytes` so it can be more easily represented as a 0x-prefixed hex
3. Create `ens_encode` (to keep backward compatibility in `v7`) and `return bytes(dns_encode(name))`
4. Deprecate `ens_encode` to be removed in `v8` with a message to use `dns_encode`

Pull request:
``ens_encode_name`` -> ``dns_encode_name``
### What was wrong?

- For consistency with implementations across other languages, and with the ENS docs, rename ``ens_encode_name`` to ``dns_encode_name`` and deprecate the old name for removal in ``v8``.

- Closes #3700

### Todo:

- [x] Clean up commit history
- [x] Add or update documentation related to these changes
- [x] Add entry to the [release notes](https://github.com/ethereum/web3.py/blob/main/newsfragments/README.md)

#### Cute Animal Picture

<img width="645" alt="Screenshot 2025-05-19 at 14 34 07" src="https://github.com/user-attachments/assets/2ab558b3-4f26-43fc-b22b-492de8f192c8" />


Patch:
diff --git a/ens/async_ens.py b/ens/async_ens.py
index 6e837733..f4da906a 100644
--- a/ens/async_ens.py
+++ b/ens/async_ens.py
@@ -57,7 +57,7 @@ from ens.utils import (
     address_in,
     address_to_reverse_domain,
     default,
-    ens_encode_name,
+    dns_encode_name,
     init_async_web3,
     is_empty_name,
     is_none_or_zero_address,
@@ -500,7 +500,7 @@ class AsyncENS(BaseENS):
 
             calldata = resolver.encode_abi(*contract_func_with_args)
             contract_call_result = await resolver.caller.resolve(
-                ens_encode_name(normal_name),
+                dns_encode_name(normal_name),
                 calldata,
             )
             result = self._decode_ensip10_resolve_data(
diff --git a/ens/ens.py b/ens/ens.py
index 2cb2150a..a5be5f71 100644
--- a/ens/ens.py
+++ b/ens/ens.py
@@ -56,7 +56,7 @@ from .utils import (
     address_in,
     address_to_reverse_domain,
     default,
-    ens_encode_name,
+    dns_encode_name,
     init_web3,
     is_empty_name,
     is_none_or_zero_address,
@@ -482,7 +482,7 @@ class ENS(BaseENS):
 
             calldata = resolver.encode_abi(*contract_func_with_args)
             contract_call_result = resolver.caller.resolve(
-                ens_encode_name(normal_name),
+                dns_encode_name(normal_name),
                 calldata,
             )
             result = self._decode_ensip10_resolve_data(
diff --git a/ens/utils.py b/ens/utils.py
index 9db364a6..824a5bd0 100644
--- a/ens/utils.py
+++ b/ens/utils.py
@@ -13,6 +13,7 @@ from typing import (
     Union,
     cast,
 )
+import warnings
 
 from eth_typing import (
     Address,
@@ -132,7 +133,7 @@ def normalize_name(name: str) -> str:
     return normalize_name_ensip15(name).as_text
 
 
-def ens_encode_name(name: str) -> bytes:
+def dns_encode_name(name: str) -> HexBytes:
     r"""
     Encode a name according to DNS standards specified in section 3.1
     of RFC1035 with the following validations:
@@ -145,7 +146,7 @@ def ens_encode_name(name: str) -> bytes:
     :param str name: the dot-separated ENS name
     """
     if is_empty_name(name):
-        return b"\x00"
+        return HexBytes(b"\x00")
 
     normalized_name = normalize_name(name)
 
@@ -163,7 +164,17 @@ def ens_encode_name(name: str) -> bytes:
     dns_prepped_labels = [to_bytes(len(label)) + label for label in labels_as_bytes]
 
     # return the joined prepped labels in order and append the zero byte at the end:
-    return b"".join(dns_prepped_labels) + b"\x00"
+    return HexBytes(b"".join(dns_prepped_labels) + b"\x00")
+
+
+def ens_encode_name(name: str) -> bytes:
+    warnings.warn(
+        "``ens_encode_name`` is deprecated and will be removed in the next "
+        "major version. Use ``dns_encode_name`` instead.",
+        DeprecationWarning,
+        stacklevel=2,
+    )
+    return bytes(dns_encode_name(name))
 
 
 def is_valid_name(name: str) -> bool:
diff --git a/newsfragments/3700.deprecation.rst b/newsfragments/3700.deprecation.rst
new file mode 100644
index 00000000..fb80e417
--- /dev/null
+++ b/newsfragments/3700.deprecation.rst
@@ -0,0 +1,1 @@
+Deprecate ``ens_encode_name`` in favor of ``dns_encode_name``.
diff --git a/newsfragments/3700.feature.rst b/newsfragments/3700.feature.rst
new file mode 100644
index 00000000..572bc50c
--- /dev/null
+++ b/newsfragments/3700.feature.rst
@@ -0,0 +1,1 @@
+Introduce ``ens.utils.dns_encode_name`` as a rename of the current ``ens_encode_name``, for consistency across other language implementations and with the ENS docs. Returns ``HexBytes`` instead of ``bytes``.


Test patch:
diff --git a/tests/ens/test_offchain_resolution.py b/tests/ens/test_offchain_resolution.py
index e3ae3366..9e4f3b16 100644
--- a/tests/ens/test_offchain_resolution.py
+++ b/tests/ens/test_offchain_resolution.py
@@ -6,7 +6,7 @@ from aiohttp import (
 import requests
 
 from ens.utils import (
-    ens_encode_name,
+    dns_encode_name,
 )
 from web3.exceptions import (
     OffchainLookup,
@@ -172,14 +172,14 @@ def test_offchain_resolver_function_call_raises_with_ccip_read_disabled(
     # should fail here with `ccip_read_enabled` flag set to False
     with pytest.raises(OffchainLookup):
         offchain_resolver.functions.resolve(
-            ens_encode_name("offchainexample.eth"),
+            dns_encode_name("offchainexample.eth"),
             ENCODED_ADDR_CALLDATA,
         ).call(ccip_read_enabled=False)
 
     # pass flag on specific call via ContractCaller is also an option
     with pytest.raises(OffchainLookup):
         offchain_resolver.caller(ccip_read_enabled=False).resolve(
-            ens_encode_name("offchainexample.eth"),
+            dns_encode_name("offchainexample.eth"),
             ENCODED_ADDR_CALLDATA,
         )
 
diff --git a/tests/ens/test_utils.py b/tests/ens/test_utils.py
index 432e8c06..008cd34f 100644
--- a/tests/ens/test_utils.py
+++ b/tests/ens/test_utils.py
@@ -1,4 +1,7 @@
 import pytest
+from unittest import (
+    mock,
+)
 from unittest.mock import (
     patch,
 )
@@ -20,6 +23,7 @@ from ens.exceptions import (
     ENSValidationError,
 )
 from ens.utils import (
+    dns_encode_name,
     ens_encode_name,
     init_async_web3,
     init_web3,
@@ -70,8 +74,8 @@ def test_init_web3_adds_expected_middleware():
         (f"abc-123.{'b' * 255}", b"\x07abc-123" + b"\xff" + b"b" * 255 + b"\x00"),
     ),
 )
-def test_ens_encode_name(name, expected):
-    assert ens_encode_name(name) == expected
+def test_dns_encode_name(name, expected):
+    assert dns_encode_name(name) == expected
 
 
 @pytest.mark.parametrize(
@@ -101,19 +105,19 @@ def test_ens_encode_name(name, expected):
         ),
     ),
 )
-def test_ens_encode_name_validating_total_encoded_name_size(name, expected):
+def test_dns_encode_name_validating_total_encoded_name_size(name, expected):
     # This test is important because dns validation technically limits the
     # total encoded domain name size to 255. ENSIP-10 expects the name to be
     # DNS encoded with one of the validation exceptions being that the
     # total encoded size can be any length.
-    ens_encoded = ens_encode_name(name)
-    assert len(ens_encoded) > 255
-    assert ens_encoded == expected
+    dns_encoded = dns_encode_name(name)
+    assert len(dns_encoded) > 255
+    assert dns_encoded == expected
 
 
 @pytest.mark.parametrize("empty_name", ("", ".", None, " ", "  "))
-def test_ens_encode_name_returns_single_zero_byte_for_empty_name(empty_name):
-    assert ens_encode_name(empty_name) == b"\00"
+def test_dns_encode_name_returns_single_zero_byte_for_empty_name(empty_name):
+    assert dns_encode_name(empty_name) == b"\00"
 
 
 @pytest.mark.parametrize(
@@ -127,21 +131,21 @@ def test_ens_encode_name_returns_single_zero_byte_for_empty_name(empty_name):
         (f"{'a' * 255}.{'1' * 255}.{'b' * 256}", 2),
     ),
 )
-def test_ens_encode_name_raises_ValidationError_on_label_lengths_over_63(
+def test_dns_encode_name_raises_validation_error_on_label_lengths_over_63(
     name, invalid_label_index
 ):
     with pytest.raises(
         ENSValidationError, match=f"Label at position {invalid_label_index} too long"
     ):
-        ens_encode_name(name)
+        dns_encode_name(name)
 
 
-def test_ens_encode_name_normalizes_name_before_encoding():
-    assert ens_encode_name("Öbb.at") == ens_encode_name("öbb.at")
-    assert ens_encode_name("nhÉéÉéÉé.eth") == ens_encode_name("nhéééééé.eth")
-    assert ens_encode_name("TESTER.eth") == ens_encode_name("tester.eth")
-    assert ens_encode_name("test\u200btest.com") == ens_encode_name("testtest.com")
-    assert ens_encode_name("O\u0308bb.at") == ens_encode_name("öbb.at")
+def test_dns_encode_name_normalizes_name_before_encoding():
+    assert dns_encode_name("Öbb.at") == dns_encode_name("öbb.at")
+    assert dns_encode_name("nhÉéÉéÉé.eth") == dns_encode_name("nhéééééé.eth")
+    assert dns_encode_name("TESTER.eth") == dns_encode_name("tester.eth")
+    assert dns_encode_name("test\u200btest.com") == dns_encode_name("testtest.com")
+    assert dns_encode_name("O\u0308bb.at") == dns_encode_name("öbb.at")
 
 
 @pytest.mark.parametrize(
@@ -167,7 +171,7 @@ def test_normal_name_to_hash(name, hashed):
         is_valid_name,
         BaseENS.namehash,
         BaseENS.nameprep,
-        ens_encode_name,
+        dns_encode_name,
         raw_name_to_hash,
     ),
 )
@@ -205,6 +209,16 @@ def test_label_to_hash_normalizes_name_using_ensip15():
     )
 
 
+@mock.patch("ens.utils.dns_encode_name")
+def test_ens_encode_name_issues_deprecation_warning_and_calls_dns_encode_name(
+    mock_dns_encode_name,
+):
+    with pytest.warns(DeprecationWarning, match=r"``ens_encode_name`` is deprecated"):
+        ens_encode_name("foo")
+
+    mock_dns_encode_name.assert_called_once_with("foo")
+
+
 # -- async -- #