Skip to content

Commit 79c490b

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. Unrelated to ENSIP-10 but served as a helpful abstraction to better implement it: - Test the new ``Contract.decodeABI()`` method from the
1 parent beaca23 commit 79c490b

File tree

7 files changed

+315
-23
lines changed

7 files changed

+315
-23
lines changed

ens/contract_data.py

Lines changed: 20 additions & 21 deletions
Large diffs are not rendered by default.

tests/ens/conftest.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
reverse_resolver_abi,
2121
reverse_resolver_bytecode,
2222
reverse_resolver_bytecode_runtime,
23+
simple_extended_resolver_abi,
24+
simple_extended_resolver_bytecode,
25+
simple_extended_resolver_bytecode_runtime,
2326
simple_resolver_abi,
2427
simple_resolver_bytecode,
2528
simple_resolver_bytecode_runtime,
@@ -107,6 +110,15 @@ def ENSRegistryFactory(w3):
107110
)
108111

109112

113+
def ExtendedResolver(w3):
114+
return w3.eth.contract(
115+
bytecode=simple_extended_resolver_bytecode,
116+
bytecode_runtime=simple_extended_resolver_bytecode_runtime,
117+
abi=simple_extended_resolver_abi,
118+
ContractFactoryClass=Contract,
119+
)
120+
121+
110122
# session scope for performance
111123
@pytest.fixture(scope="session")
112124
def ens_setup():
@@ -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 simple-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 children 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.11;
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
@@ -57,5 +57,5 @@ def test_get_text_resolver_not_found(ens):
5757

5858

5959
def test_get_text_for_resolver_with_unsupported_function(ens):
60-
with pytest.raises(UnsupportedFunction):
60+
with pytest.raises(UnsupportedFunction, match="does not support `text` function"):
6161
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_for_extended_resolver_subdomains(ens, subdomain):
16+
# validate children of `extended-resolver.eth` by asserting it returns the specified
17+
# hard-coded address from `tests/test_contracts/SimpleExtendedResolver.sol` which requires
18+
# certain conditions to be met for subdomains of `extended-resolver.eth`
19+
resolved_child_address = ens.resolve(f'{subdomain}.extended-resolver.eth')
20+
assert resolved_child_address == '0x000000000000000000000000000000000000dEaD'
21+
22+
23+
def test_wildcard_resolution_parent_ens_domain(ens):
24+
# validate `extended-resolver.eth` by asserting it returns the specified hard-coded address from
25+
# `tests/test_contracts/SimpleExtendedResolver.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)