Skip to content

Commit 457f510

Browse files
committed
Add tests related to ENSIP-10 implementation:
- Test wildcard resolver functionality using a basic resolver with support for the `resolve()` method and validate the parent ens domain and its subdomains within the resolve method of the contract. - Test the new ``ens_encode_name()`` from the ``ens.utils`` module. - Test the new ``ENS.parent()`` method to extract a parent from an ENS name.
1 parent 1165f92 commit 457f510

File tree

8 files changed

+306
-22
lines changed

8 files changed

+306
-22
lines changed

ens/contract_data.py

Lines changed: 10 additions & 19 deletions
Large diffs are not rendered by default.

ens/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ def is_valid_ens_name(ens_name: str) -> bool:
256256
return True
257257

258258

259-
# borrowed from similar method at `web._utils.abi` due to circular dependency
259+
# borrowed from similar method at `web3._utils.abi` due to circular dependency
260260
def get_abi_output_types(abi: 'ABIFunction') -> List[str]:
261261
return (
262262
[] if abi['type'] == 'fallback'

tests/ens/conftest.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
from ens import ENS
1010
from ens.contract_data import (
11+
extended_resolver_abi,
12+
extended_resolver_bytecode,
13+
extended_resolver_bytecode_runtime,
1114
registrar_abi,
1215
registrar_bytecode,
1316
registrar_bytecode_runtime,
@@ -89,6 +92,15 @@ def SimpleResolver(w3):
8992
)
9093

9194

95+
def ExtendedResolver(w3):
96+
return w3.eth.contract(
97+
bytecode=extended_resolver_bytecode,
98+
bytecode_runtime=extended_resolver_bytecode_runtime,
99+
abi=extended_resolver_abi,
100+
ContractFactoryClass=Contract,
101+
)
102+
103+
92104
def ENSFactory(w3):
93105
return w3.eth.contract(
94106
bytecode="6060604052341561000f57600080fd5b60008080526020527fad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb58054600160a060020a033316600160a060020a0319909116179055610501806100626000396000f300606060405236156100805763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416630178b8bf811461008557806302571be3146100b757806306ab5923146100cd57806314ab9038146100f457806316a25cbd146101175780631896f70a1461014a5780635b0fc9c31461016c575b600080fd5b341561009057600080fd5b61009b60043561018e565b604051600160a060020a03909116815260200160405180910390f35b34156100c257600080fd5b61009b6004356101ac565b34156100d857600080fd5b6100f2600435602435600160a060020a03604435166101c7565b005b34156100ff57600080fd5b6100f260043567ffffffffffffffff60243516610289565b341561012257600080fd5b61012d600435610355565b60405167ffffffffffffffff909116815260200160405180910390f35b341561015557600080fd5b6100f2600435600160a060020a036024351661038c565b341561017757600080fd5b6100f2600435600160a060020a0360243516610432565b600090815260208190526040902060010154600160a060020a031690565b600090815260208190526040902054600160a060020a031690565b600083815260208190526040812054849033600160a060020a039081169116146101f057600080fd5b8484604051918252602082015260409081019051908190039020915083857fce0457fe73731f824cc272376169235128c118b49d344817417c6d108d155e8285604051600160a060020a03909116815260200160405180910390a3506000908152602081905260409020805473ffffffffffffffffffffffffffffffffffffffff1916600160a060020a03929092169190911790555050565b600082815260208190526040902054829033600160a060020a039081169116146102b257600080fd5b827f1d4f9bbfc9cab89d66e1a1562f2233ccbf1308cb4f63de2ead5787adddb8fa688360405167ffffffffffffffff909116815260200160405180910390a250600091825260208290526040909120600101805467ffffffffffffffff90921674010000000000000000000000000000000000000000027fffffffff0000000000000000ffffffffffffffffffffffffffffffffffffffff909216919091179055565b60009081526020819052604090206001015474010000000000000000000000000000000000000000900467ffffffffffffffff1690565b600082815260208190526040902054829033600160a060020a039081169116146103b557600080fd5b827f335721b01866dc23fbee8b6b2c7b1e14d6f05c28cd35a2c934239f94095602a083604051600160a060020a03909116815260200160405180910390a250600091825260208290526040909120600101805473ffffffffffffffffffffffffffffffffffffffff1916600160a060020a03909216919091179055565b600082815260208190526040902054829033600160a060020a0390811691161461045b57600080fd5b827fd4735d920b0f87494915f556dd9b54c8f309026070caea5c737245152564d26683604051600160a060020a03909116815260200160405180910390a250600091825260208290526040909120805473ffffffffffffffffffffffffffffffffffffffff1916600160a060020a039092169190911790555600a165627a7a7230582050975b6c54a16d216b563f4c4960d6ebc5881eb1ec73c2ef1f87920a251159530029", # noqa: E501
@@ -226,13 +238,38 @@ def ens_setup():
226238
second_account
227239
).transact({'from': ens_key})
228240

229-
simple_resolver_namehash = bytes32(0x65db4c1c4f4ab9e6917fa7896ce546b1fe03e9341e98187e3917afb60aa9835a) # noqa: E501
241+
# ns.namehash('simple-resolver.eth')
242+
simple_resolver_namehash = bytes32(
243+
0x65db4c1c4f4ab9e6917fa7896ce546b1fe03e9341e98187e3917afb60aa9835a
244+
)
230245

231246
ens_contract.functions.setResolver(
232247
simple_resolver_namehash,
233248
simple_resolver.address
234249
).transact({'from': second_account})
235250

251+
# --- setup extended resolver example --- #
252+
253+
# create extended resolver
254+
extended_resolver = deploy(w3, ExtendedResolver, ens_key, args=[ens_contract.address])
255+
256+
# set owner of extended-resolver.eth to an account controlled by tests
257+
ens_contract.functions.setSubnodeOwner(
258+
eth_namehash,
259+
w3.keccak(text='extended-resolver'),
260+
second_account
261+
).transact({'from': ens_key})
262+
263+
# ns.namehash('extended-resolver.eth')
264+
extended_resolver_namehash = bytes32(
265+
0xf0a378cc2afe91730d0105e67d6bb037cc5b8b6bfec5b5962d9b637ff6497e55
266+
)
267+
268+
ens_contract.functions.setResolver(
269+
extended_resolver_namehash,
270+
extended_resolver.address
271+
).transact({'from': second_account})
272+
236273
# --- finish setup --- #
237274

238275
# make the registrar the owner of the 'eth' name
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* The SimpleExtendedResolver is really only meant to test the validation of the parent ens domain
3+
* `extended-resolver.eth` and, separately, the subdomains of this parent domain. We then "resolve"
4+
* to arbitrary addresses 0x000000000000000000000000000000000000dEaD for subdomain validations and
5+
* 0x000000000000000000000000000000000000bEEF for the parent domain validation so that we can be
6+
* sure each case was validated via the appropriate logic via the `resolve()` function of the contract.
7+
*/
8+
9+
10+
pragma solidity >=0.4.24;
11+
12+
interface ENS {
13+
14+
// Logged when the owner of a node assigns a new owner to a subnode.
15+
event NewOwner(bytes32 node, bytes32 label, address owner);
16+
17+
// Logged when the owner of a node transfers ownership to a new account.
18+
event Transfer(bytes32 node, address owner);
19+
20+
// Logged when the resolver for a node changes.
21+
event NewResolver(bytes32 node, address resolver);
22+
23+
// Logged when the TTL of a node changes
24+
event NewTTL(bytes32 node, uint64 ttl);
25+
26+
27+
function setSubnodeOwner(bytes32 node, bytes32 label, address owner) external;
28+
function setResolver(bytes32 node, address resolver) external;
29+
function setOwner(bytes32 node, address owner) external;
30+
function setTTL(bytes32 node, uint64 ttl) external;
31+
function owner(bytes32 node) external view returns (address);
32+
function resolver(bytes32 node) external view returns (address);
33+
function ttl(bytes32 node) external view returns (uint64);
34+
}
35+
36+
pragma solidity >= 0.7.0;
37+
38+
abstract contract ResolverBase {
39+
bytes4 private constant INTERFACE_META_ID = 0x01ffc9a7;
40+
41+
function supportsInterface(bytes4 interfaceID) virtual public pure returns(bool) {
42+
return interfaceID == INTERFACE_META_ID;
43+
}
44+
45+
function isAuthorised(bytes32 node) internal virtual view returns(bool);
46+
47+
modifier authorised(bytes32 node) {
48+
require(isAuthorised(node));
49+
_;
50+
}
51+
52+
function bytesToAddress(bytes memory b) internal pure returns(address payable a) {
53+
require(b.length == 20);
54+
assembly {
55+
a := div(mload(add(b, 32)), exp(256, 12))
56+
}
57+
}
58+
59+
function addressToBytes(address a) internal pure returns(bytes memory b) {
60+
b = new bytes(20);
61+
assembly {
62+
mstore(add(b, 32), mul(a, exp(256, 12)))
63+
}
64+
}
65+
}
66+
67+
contract ExtendedResolver is ResolverBase {
68+
ENS ens;
69+
70+
bytes4 constant private EXTENDED_RESOLVER_INTERFACE_ID = 0x9061b923;
71+
string constant extendedResolverParentDomain = "\x11extended-resolver\x03eth\x00";
72+
bytes32 constant extendedResolverNamehash = 0xf0a378cc2afe91730d0105e67d6bb037cc5b8b6bfec5b5962d9b637ff6497e55;
73+
74+
/**
75+
* A mapping of authorisations. An address that is authorised for a name
76+
* may make any changes to the name that the owner could, but may not update
77+
* the set of authorisations.
78+
* (node, owner, caller) => isAuthorised
79+
*/
80+
mapping(bytes32=>mapping(address=>mapping(address=>bool))) public authorisations;
81+
82+
event AuthorisationChanged(bytes32 node, address owner, address target, bool isAuthorised);
83+
84+
constructor(ENS _ens) public {
85+
ens = _ens;
86+
}
87+
88+
/**
89+
* @dev Sets or clears an authorisation.
90+
* Authorisations are specific to the caller. Any account can set an authorisation
91+
* for any name, but the authorisation that is checked will be that of the
92+
* current owner of a name. Thus, transferring a name effectively clears any
93+
* existing authorisations, and new authorisations can be set in advance of
94+
* an ownership transfer if desired.
95+
*
96+
* @param node The name to change the authorisation on.
97+
* @param target The address that is to be authorised or deauthorised.
98+
* @param isAuthorised True if the address should be authorised, or false if it should be deauthorised.
99+
*/
100+
function setAuthorisation(bytes32 node, address target, bool isAuthorised) external {
101+
authorisations[node][msg.sender][target] = isAuthorised;
102+
emit AuthorisationChanged(node, msg.sender, target, isAuthorised);
103+
}
104+
105+
function isAuthorised(bytes32 node) override internal view returns(bool) {
106+
address owner = ens.owner(node);
107+
return owner == msg.sender || authorisations[node][owner][msg.sender];
108+
}
109+
110+
function supportsInterface(bytes4 interfaceID) override public pure returns(bool) {
111+
return interfaceID == EXTENDED_RESOLVER_INTERFACE_ID || super.supportsInterface(interfaceID);
112+
}
113+
114+
// Simple resolve method solely used to test ENSIP-10 / Wildcard Resolution functionality
115+
function resolve(bytes calldata dnsName, bytes calldata data) external view returns (bytes memory) {
116+
// validate 'extended-resolver.eth' parent domain
117+
if (keccak256(dnsName) == keccak256(bytes(extendedResolverParentDomain)) && data.length >= 36) {
118+
require(bytes32(data[4:36]) == extendedResolverNamehash, "parent domain not validated appropriately");
119+
return abi.encode(address(0x000000000000000000000000000000000000bEEF));
120+
} else {
121+
uint length = uint8(dnsName[0]);
122+
// validate subdomains of 'extended-resolver.eth' parent domain
123+
require(keccak256(dnsName[1 + length:]) == keccak256(bytes(extendedResolverParentDomain)), "subdomain not validated appropriately");
124+
return abi.encode(address(0x000000000000000000000000000000000000dEaD));
125+
}
126+
}
127+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
pragma solidity ^0.8.13;
2+
3+
contract SimpleResolver {
4+
// deployed on ropsten at address = 0xD4D522c96111679bF86220deFE75e0aA1df890b4
5+
6+
function supportsInterface(bytes4 interfaceID) public returns (bool) {
7+
return interfaceID == 0x3b3b57de;
8+
}
9+
10+
function addr(bytes32 nodeID) public returns (address) {
11+
return address(this);
12+
}
13+
}

tests/ens/test_get_text.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,5 @@ def test_get_text_resolver_not_found(ens):
7373

7474

7575
def test_get_text_for_resolver_with_unsupported_function(ens):
76-
with pytest.raises(UnsupportedFunction):
76+
with pytest.raises(UnsupportedFunction, match="does not support `text` function"):
7777
ens.get_text('simple-resolver.eth', 'any_key')

tests/ens/test_resolve.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import pytest
2+
3+
4+
def test_resolve(ens):
5+
acct = ens.w3.eth.accounts[2]
6+
ens.setup_address('tester.eth', acct)
7+
8+
assert ens.resolve('tester.eth') == acct
9+
10+
# clean up
11+
ens.setup_address('tester.eth', None)
12+
13+
14+
@pytest.mark.parametrize('subdomain', ('sub1', 'sub2', 'rändöm', '🌈rainbow', 'faß'))
15+
def test_wildcard_resolution_with_extended_resolver_for_subdomains(ens, subdomain):
16+
# validate subdomains of `extended-resolver.eth` by asserting it returns the specified
17+
# hard-coded address from `tests/test_contracts/ExtendedResolver.sol` which requires
18+
# certain conditions to be met that are specific to subdomains only
19+
resolved_child_address = ens.resolve(f'{subdomain}.extended-resolver.eth')
20+
assert resolved_child_address == '0x000000000000000000000000000000000000dEaD'
21+
22+
23+
def test_wildcard_resolution_with_extended_resolver_for_parent_ens_domain(ens):
24+
# validate `extended-resolver.eth` by asserting it returns the specified hard-coded address from
25+
# `tests/test_contracts/ExtendedResolver.sol` which requires a specific condition to be
26+
# met for the parent domain `extended-resolver.eth`
27+
resolved_parent_address = ens.resolve('extended-resolver.eth')
28+
assert resolved_parent_address == '0x000000000000000000000000000000000000bEEF'

tests/ens/test_utils.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1+
import pytest
2+
3+
from eth_utils import (
4+
ValidationError,
5+
to_bytes,
6+
)
17

28
from ens.utils import (
9+
ens_encode_name,
310
init_web3,
411
)
512

@@ -8,3 +15,84 @@ def test_init_adds_middlewares():
815
w3 = init_web3()
916
middlewares = map(str, w3.manager.middleware_onion)
1017
assert 'stalecheck_middleware' in next(middlewares)
18+
19+
20+
@pytest.mark.parametrize(
21+
'name,expected',
22+
(
23+
# test some allowed cases
24+
('tester.eth', b'\x06tester\x03eth\x00'),
25+
(
26+
'a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p',
27+
b'\x01a\x01b\x01c\x01d\x01e\x01f\x01g\x01h\x01i\x01j\x01k\x01l\x01m\x01n\x01o\x01p\x00'
28+
),
29+
('1.2.3.4.5.6.7.8.9.10', b'\x011\x012\x013\x014\x015\x016\x017\x018\x019\x0210\x00'),
30+
('abc.123.def-456.eth', b'\x03abc\x03123\x07def-456\x03eth\x00'),
31+
('abc.123.def-456.eth', b'\x03abc\x03123\x07def-456\x03eth\x00'),
32+
('nhéééééé.eth', b'\x0enh\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\x03eth\x00'),
33+
('🌈rainbow.eth', b'\x0b\xf0\x9f\x8c\x88rainbow\x03eth\x00'),
34+
('🐔🐔.tk', b'\x08\xf0\x9f\x90\x94\xf0\x9f\x90\x94\x02tk\x00'),
35+
36+
# test that label length may be less than 64
37+
(f"{'a' * 63}.b", b'?' + (b'a' * 63) + b'\x01b\x00'),
38+
(f"a.{'b'* 63}", b'\x01a' + b'?' + (b'b' * 63) + b'\x00'),
39+
(f"abc-123.{'b'* 63}", b'\x07abc-123' + b'?' + b'b' * 63 + b'\x00'),
40+
)
41+
)
42+
def test_ens_encode_name(name, expected):
43+
assert ens_encode_name(name) == expected
44+
45+
46+
@pytest.mark.parametrize(
47+
'name,expected',
48+
(
49+
(
50+
f"{'a' * 63}.{'b' * 63}.{'c' * 63}.{'d' * 63}.{'e' * 63}.{'f' * 63}.{'g' * 63}",
51+
b''.join([b'?' + to_bytes(text=label) * 63 for label in 'abcdefg']) + b'\x00'
52+
),
53+
(
54+
f"{'a-1' * 21}.{'b-2' * 21}.{'c-3' * 21}.{'d-4' * 21}.{'e-5' * 21}.{'f-6' * 21}",
55+
b''.join([
56+
b'?' + to_bytes(text=label) * 21 for label in [
57+
'a-1', 'b-2', 'c-3', 'd-4', 'e-5', 'f-6',
58+
]
59+
]) + b'\x00'
60+
),
61+
)
62+
)
63+
def test_ens_encode_name_validating_total_encoded_name_size(name, expected):
64+
# This test is important because dns encoding technically limits the total encoded domain name
65+
# size to 255. ENSIP-10 expects the name to be DNS encoded with one of the exceptions
66+
# being that the total encoded size can be any length.
67+
ens_encoded = ens_encode_name(name)
68+
assert len(ens_encoded) > 255
69+
assert ens_encoded == expected
70+
71+
72+
@pytest.mark.parametrize('empty_name', ('', '.'))
73+
def test_ens_encode_name_returns_single_zero_byte_for_empty_name(empty_name):
74+
assert ens_encode_name(empty_name) == b'\00'
75+
76+
77+
@pytest.mark.parametrize(
78+
'name,invalid_label_index',
79+
(
80+
('a' * 64, 0),
81+
(f"{'a' * 64}.b", 0),
82+
(f"a.{'b-1' * 21}x", 1),
83+
(f"{'a' * 64}.{'1' * 63}.{'b' * 63}", 0),
84+
(f"{'a' * 63}.{'1' * 64}.{'b' * 63}", 1),
85+
(f"{'a' * 63}.{'1' * 63}.{'b' * 64}", 2),
86+
)
87+
)
88+
def test_ens_encode_name_raises_ValidationError_on_label_lengths_over_63(name, invalid_label_index):
89+
with pytest.raises(ValidationError, match=f'Label at position {invalid_label_index} too long'):
90+
ens_encode_name(name)
91+
92+
93+
def test_ens_encode_name_normalizes_name_before_encoding():
94+
assert ens_encode_name('Öbb.at') == ens_encode_name('öbb.at')
95+
assert ens_encode_name('nhÉéÉéÉé.eth') == ens_encode_name('nhéééééé.eth')
96+
assert ens_encode_name('TESTER.eth') == ens_encode_name('tester.eth')
97+
assert ens_encode_name('test\u200btest.com') == ens_encode_name('testtest.com')
98+
assert ens_encode_name("O\u0308bb.at") == ens_encode_name("öbb.at")

0 commit comments

Comments
 (0)