Skip to content

Commit

Permalink
Add tests related to ENSIP-10 implementation:
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
fselmo committed Apr 18, 2022
1 parent beaca23 commit 79c490b
Show file tree
Hide file tree
Showing 7 changed files with 315 additions and 23 deletions.
41 changes: 20 additions & 21 deletions ens/contract_data.py

Large diffs are not rendered by default.

39 changes: 38 additions & 1 deletion tests/ens/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
reverse_resolver_abi,
reverse_resolver_bytecode,
reverse_resolver_bytecode_runtime,
simple_extended_resolver_abi,
simple_extended_resolver_bytecode,
simple_extended_resolver_bytecode_runtime,
simple_resolver_abi,
simple_resolver_bytecode,
simple_resolver_bytecode_runtime,
Expand Down Expand Up @@ -107,6 +110,15 @@ def ENSRegistryFactory(w3):
)


def ExtendedResolver(w3):
return w3.eth.contract(
bytecode=simple_extended_resolver_bytecode,
bytecode_runtime=simple_extended_resolver_bytecode_runtime,
abi=simple_extended_resolver_abi,
ContractFactoryClass=Contract,
)


# session scope for performance
@pytest.fixture(scope="session")
def ens_setup():
Expand Down Expand Up @@ -226,13 +238,38 @@ def ens_setup():
second_account
).transact({'from': ens_key})

simple_resolver_namehash = bytes32(0x65db4c1c4f4ab9e6917fa7896ce546b1fe03e9341e98187e3917afb60aa9835a) # noqa: E501
# ns.namehash('simple-resolver.eth')
simple_resolver_namehash = bytes32(
0x65db4c1c4f4ab9e6917fa7896ce546b1fe03e9341e98187e3917afb60aa9835a
)

ens_contract.functions.setResolver(
simple_resolver_namehash,
simple_resolver.address
).transact({'from': second_account})

# --- setup extended resolver example --- #

# create extended resolver
extended_resolver = deploy(w3, ExtendedResolver, ens_key, args=[ens_contract.address])

# set owner of simple-resolver.eth to an account controlled by tests
ens_contract.functions.setSubnodeOwner(
eth_namehash,
w3.keccak(text='extended-resolver'),
second_account
).transact({'from': ens_key})

# ns.namehash('extended-resolver.eth')
extended_resolver_namehash = bytes32(
0xf0a378cc2afe91730d0105e67d6bb037cc5b8b6bfec5b5962d9b637ff6497e55
)

ens_contract.functions.setResolver(
extended_resolver_namehash,
extended_resolver.address
).transact({'from': second_account})

# --- finish setup --- #

# make the registrar the owner of the 'eth' name
Expand Down
127 changes: 127 additions & 0 deletions tests/ens/test_contracts/SimpleExtendedResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* The SimpleExtendedResolver is really only meant to test the validation of the parent ens domain
* `extended-resolver.eth` and, separately, the subdomains of this parent domain. We then "resolve"
* to arbitrary addresses 0x000000000000000000000000000000000000dEaD for subdomain validations and
* 0x000000000000000000000000000000000000bEEF for the parent domain validation so that we can be
* sure each case was validated via the appropriate logic via the `resolve()` function of the contract.
*/


pragma solidity >=0.4.24;

interface ENS {

// Logged when the owner of a node assigns a new owner to a subnode.
event NewOwner(bytes32 node, bytes32 label, address owner);

// Logged when the owner of a node transfers ownership to a new account.
event Transfer(bytes32 node, address owner);

// Logged when the resolver for a node changes.
event NewResolver(bytes32 node, address resolver);

// Logged when the TTL of a node changes
event NewTTL(bytes32 node, uint64 ttl);


function setSubnodeOwner(bytes32 node, bytes32 label, address owner) external;
function setResolver(bytes32 node, address resolver) external;
function setOwner(bytes32 node, address owner) external;
function setTTL(bytes32 node, uint64 ttl) external;
function owner(bytes32 node) external view returns (address);
function resolver(bytes32 node) external view returns (address);
function ttl(bytes32 node) external view returns (uint64);
}

pragma solidity >= 0.7.0;

abstract contract ResolverBase {
bytes4 private constant INTERFACE_META_ID = 0x01ffc9a7;

function supportsInterface(bytes4 interfaceID) virtual public pure returns(bool) {
return interfaceID == INTERFACE_META_ID;
}

function isAuthorised(bytes32 node) internal virtual view returns(bool);

modifier authorised(bytes32 node) {
require(isAuthorised(node));
_;
}

function bytesToAddress(bytes memory b) internal pure returns(address payable a) {
require(b.length == 20);
assembly {
a := div(mload(add(b, 32)), exp(256, 12))
}
}

function addressToBytes(address a) internal pure returns(bytes memory b) {
b = new bytes(20);
assembly {
mstore(add(b, 32), mul(a, exp(256, 12)))
}
}
}

contract ExtendedResolver is ResolverBase {
ENS ens;

bytes4 constant private EXTENDED_RESOLVER_INTERFACE_ID = 0x9061b923;
string constant extendedResolverParentDomain = "\x11extended-resolver\x03eth\x00";
bytes32 constant extendedResolverNamehash = 0xf0a378cc2afe91730d0105e67d6bb037cc5b8b6bfec5b5962d9b637ff6497e55;

/**
* A mapping of authorisations. An address that is authorised for a name
* may make any changes to the name that the owner could, but may not update
* the set of authorisations.
* (node, owner, caller) => isAuthorised
*/
mapping(bytes32=>mapping(address=>mapping(address=>bool))) public authorisations;

event AuthorisationChanged(bytes32 node, address owner, address target, bool isAuthorised);

constructor(ENS _ens) public {
ens = _ens;
}

/**
* @dev Sets or clears an authorisation.
* Authorisations are specific to the caller. Any account can set an authorisation
* for any name, but the authorisation that is checked will be that of the
* current owner of a name. Thus, transferring a name effectively clears any
* existing authorisations, and new authorisations can be set in advance of
* an ownership transfer if desired.
*
* @param node The name to change the authorisation on.
* @param target The address that is to be authorised or deauthorised.
* @param isAuthorised True if the address should be authorised, or false if it should be deauthorised.
*/
function setAuthorisation(bytes32 node, address target, bool isAuthorised) external {
authorisations[node][msg.sender][target] = isAuthorised;
emit AuthorisationChanged(node, msg.sender, target, isAuthorised);
}

function isAuthorised(bytes32 node) override internal view returns(bool) {
address owner = ens.owner(node);
return owner == msg.sender || authorisations[node][owner][msg.sender];
}

function supportsInterface(bytes4 interfaceID) override public pure returns(bool) {
return interfaceID == EXTENDED_RESOLVER_INTERFACE_ID || super.supportsInterface(interfaceID);
}

// Simple resolve method solely used to test ENSIP-10 / Wildcard Resolution functionality
function resolve(bytes calldata dnsName, bytes calldata data) external view returns (bytes memory) {
// validate 'extended-resolver.eth' parent domain
if (keccak256(dnsName) == keccak256(bytes(extendedResolverParentDomain)) && data.length >= 36) {
require(bytes32(data[4:36]) == extendedResolverNamehash, "parent domain not validated appropriately");
return abi.encode(address(0x000000000000000000000000000000000000bEEF));
} else {
uint length = uint8(dnsName[0]);
// validate children of 'extended-resolver.eth' parent domain
require(keccak256(dnsName[1 + length:]) == keccak256(bytes(extendedResolverParentDomain)), "subdomain not validated appropriately");
return abi.encode(address(0x000000000000000000000000000000000000dEaD));
}
}
}
13 changes: 13 additions & 0 deletions tests/ens/test_contracts/SimpleResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
pragma solidity ^0.8.11;

contract SimpleResolver {
// deployed on ropsten at address = 0xD4D522c96111679bF86220deFE75e0aA1df890b4

function supportsInterface(bytes4 interfaceID) public returns (bool) {
return interfaceID == 0x3b3b57de;
}

function addr(bytes32 nodeID) public returns (address) {
return address(this);
}
}
2 changes: 1 addition & 1 deletion tests/ens/test_get_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,5 @@ def test_get_text_resolver_not_found(ens):


def test_get_text_for_resolver_with_unsupported_function(ens):
with pytest.raises(UnsupportedFunction):
with pytest.raises(UnsupportedFunction, match="does not support `text` function"):
ens.get_text('simple-resolver.eth', 'any_key')
28 changes: 28 additions & 0 deletions tests/ens/test_resolve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import pytest


def test_resolve(ens):
acct = ens.w3.eth.accounts[2]
ens.setup_address('tester.eth', acct)

assert ens.resolve('tester.eth') == acct

# clean up
ens.setup_address('tester.eth', None)


@pytest.mark.parametrize('subdomain', ('sub1', 'sub2', 'rändöm', '🌈rainbow', 'faß'))
def test_wildcard_resolution_for_extended_resolver_subdomains(ens, subdomain):
# validate children of `extended-resolver.eth` by asserting it returns the specified
# hard-coded address from `tests/test_contracts/SimpleExtendedResolver.sol` which requires
# certain conditions to be met for subdomains of `extended-resolver.eth`
resolved_child_address = ens.resolve(f'{subdomain}.extended-resolver.eth')
assert resolved_child_address == '0x000000000000000000000000000000000000dEaD'


def test_wildcard_resolution_parent_ens_domain(ens):
# validate `extended-resolver.eth` by asserting it returns the specified hard-coded address from
# `tests/test_contracts/SimpleExtendedResolver.sol` which requires a specific condition to be
# met for the parent domain `extended-resolver.eth`
resolved_parent_address = ens.resolve('extended-resolver.eth')
assert resolved_parent_address == '0x000000000000000000000000000000000000bEEF'
88 changes: 88 additions & 0 deletions tests/ens/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import pytest

from eth_utils import (
ValidationError,
to_bytes,
)

from ens.utils import (
ens_encode_name,
init_web3,
)

Expand All @@ -8,3 +15,84 @@ def test_init_adds_middlewares():
w3 = init_web3()
middlewares = map(str, w3.manager.middleware_onion)
assert 'stalecheck_middleware' in next(middlewares)


@pytest.mark.parametrize(
'name,expected',
(
# test some allowed cases
('tester.eth', b'\x06tester\x03eth\x00'),
(
'a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p',
b'\x01a\x01b\x01c\x01d\x01e\x01f\x01g\x01h\x01i\x01j\x01k\x01l\x01m\x01n\x01o\x01p\x00'
),
('1.2.3.4.5.6.7.8.9.10', b'\x011\x012\x013\x014\x015\x016\x017\x018\x019\x0210\x00'),
('abc.123.def-456.eth', b'\x03abc\x03123\x07def-456\x03eth\x00'),
('abc.123.def-456.eth', b'\x03abc\x03123\x07def-456\x03eth\x00'),
('nhéééééé.eth', b'\x0enh\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\xc3\xa9\x03eth\x00'),
('🌈rainbow.eth', b'\x0b\xf0\x9f\x8c\x88rainbow\x03eth\x00'),
('🐔🐔.tk', b'\x08\xf0\x9f\x90\x94\xf0\x9f\x90\x94\x02tk\x00'),
# test that label length may be less than 64
(f"{'a' * 63}.b", b'?' + (b'a' * 63) + b'\x01b\x00'),
(f"a.{'b'* 63}", b'\x01a' + b'?' + (b'b' * 63) + b'\x00'),
(f"abc-123.{'b'* 63}", b'\x07abc-123' + b'?' + b'b' * 63 + b'\x00'),
)
)
def test_ens_encode_name(name, expected):
assert ens_encode_name(name) == expected


@pytest.mark.parametrize(
'name,expected',
(
(
f"{'a' * 63}.{'b' * 63}.{'c' * 63}.{'d' * 63}.{'e' * 63}.{'f' * 63}.{'g' * 63}",
b''.join([b'?' + to_bytes(text=label) * 63 for label in 'abcdefg']) + b'\x00'
),
(
f"{'a-1' * 21}.{'b-2' * 21}.{'c-3' * 21}.{'d-4' * 21}.{'e-5' * 21}.{'f-6' * 21}",
b''.join([
b'?' + to_bytes(text=label) * 21 for label in [
'a-1', 'b-2', 'c-3', 'd-4', 'e-5', 'f-6',
]
]) + b'\x00'
),
)
)
def test_ens_encode_name_validating_total_encoded_name_size(name, expected):
# This test is important because dns encoding technically limits the total encoded domain name
# size to 255. ENSIP-10 expects the name to be DNS encoded with one of the 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


@pytest.mark.parametrize('empty_name', ('', '.'))
def test_ens_encode_name_returns_single_zero_byte_for_empty_name(empty_name):
assert ens_encode_name(empty_name) == b'\00'


@pytest.mark.parametrize(
'name,invalid_label_index',
(
('a' * 64, 0),
(f"{'a' * 64}.b", 0),
(f"a.{'b-1' * 21}x", 1),
(f"{'a' * 64}.{'1' * 63}.{'b' * 63}", 0),
(f"{'a' * 63}.{'1' * 64}.{'b' * 63}", 1),
(f"{'a' * 63}.{'1' * 63}.{'b' * 64}", 2),
)
)
def test_ens_encode_name_raises_ValidationError_on_label_lengths_over_63(name, invalid_label_index):
with pytest.raises(ValidationError, match=f'Label at position {invalid_label_index} too long'):
ens_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")

0 comments on commit 79c490b

Please sign in to comment.