Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite ExtendedDNSResolver to support more record types #331

Merged
merged 13 commits into from
May 1, 2024
246 changes: 231 additions & 15 deletions contracts/resolvers/profiles/ExtendedDNSResolver.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,65 @@
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "../../resolvers/profiles/IExtendedDNSResolver.sol";
import "../../resolvers/profiles/IAddressResolver.sol";
import "../../resolvers/profiles/IAddrResolver.sol";
import "../../resolvers/profiles/ITextResolver.sol";
import "../../utils/HexUtils.sol";
import "../../dnssec-oracle/BytesUtils.sol";

/**
* @dev Resolves names on ENS by interpreting record data stored in a DNS TXT record.
* This resolver implements the IExtendedDNSResolver interface, meaning that when
* a DNS name specifies it as the resolver via a TXT record, this resolver's
* resolve() method is invoked, and is passed any additional information from that
* text record. This resolver implements a simple text parser allowing a variety
* of records to be specified in text, which will then be used to resolve the name
* in ENS.
*
* To use this, set a TXT record on your DNS name in the following format:
* ENS1 <address or name of ExtendedDNSResolver> <record data>
*
* For example:
* ENS1 2.dnsname.ens.eth a[60]=0x1234...
*
* The record data consists of a series of key=value pairs, separated by spaces. Keys
* may have an optional argument in square brackets, and values may be either unquoted
* - in which case they may not contain spaces - or single-quoted. Single quotes in
* a quoted value may be backslash-escaped.
*
* Record types:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about contenthash?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no browsers that would prioritize a contenthash in ENS over the A records of the domain right now, and no reasonable prospect that that'll change any time soon.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true, browsers won't care about any of these records, but some people still want to associate IPFS with DNS names. it would be easy to implement because it doesn't require a parameter, (c=<hash>), and then you have complete resolver support.

ps. I know these records need to be evm-readable, but you have 255 chars to work with and humans involved. how about addr[60]=0x00, text[avatar]='http...', and contenthash=<hash>. maybe support addr=0x00 and a=0x00 as well.

pps. dnsname.ens.eth is a mouthful, dns.resolver would be nice.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true, browsers won't care about any of these records, but some people still want to associate IPFS with DNS names. it would be easy to implement because it doesn't require a parameter, (c=), and then you have complete resolver support.

If there are any integrations that want to read this field from ENS names, I'll happily add support here.

ps. I know these records need to be evm-readable, but you have 255 chars to work with and humans involved. how about addr[60]=0x00, text[avatar]='http...', and contenthash=. maybe support addr=0x00 and a=0x00 as well.

256 characters is only 5 addresses. We don't have a lot of spare space to work with here.

* - a[<coinType>] - Specifies how an `addr()` request should be resolved for the specified
* `coinType`. Ethereum has `coinType` 60. The value must be 0x-prefixed hexadecimal, and will
* be returned unmodified; this means that non-EVM addresses will need to be translated
* into binary format and then encoded in hex.
* Examples:
* - a[60]=0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7
* - a[0]=0x00149010587f8364b964fcaa70687216b53bd2cbd798
* - a[e<chainId>] - Specifies how an `addr()` request should be resolved for the specified
* `chainId`. The value must be 0x-prefixed hexadecimal. When encoding an address for an
* EVM-based cryptocurrency that uses a chainId instead of a coinType, this syntax *must*
* be used in place of the coin type - eg, Optimism is `a[e10]`, not `a[2147483658]`.
* A list of supported cryptocurrencies for both syntaxes can be found here:
* https://github.com/ensdomains/address-encoder/blob/master/docs/supported-cryptocurrencies.md
* Example:
* - a[e10]=0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7
* - t[<key>] - Specifies how a `text()` request should be resolved for the specified `key`.
* Examples:
* - t[com.twitter]=nicksdjohnson
* - t[url]='https://ens.domains/'
* - t[note]='I\'m great'
*/
contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 {
using HexUtils for *;
using BytesUtils for *;
using Strings for *;

uint256 private constant COIN_TYPE_ETH = 60;

error NotImplemented();
error InvalidAddressFormat();
error InvalidAddressFormat(bytes addr);

function supportsInterface(
bytes4 interfaceId
Expand All @@ -27,21 +74,190 @@ contract ExtendedDNSResolver is IExtendedDNSResolver, IERC165 {
bytes calldata context
) external pure override returns (bytes memory) {
bytes4 selector = bytes4(data);
if (
selector == IAddrResolver.addr.selector ||
selector == IAddressResolver.addr.selector
) {
if (selector == IAddressResolver.addr.selector) {
(, uint256 coinType) = abi.decode(data[4:], (bytes32, uint256));
if (coinType != COIN_TYPE_ETH) return abi.encode("");
}
(address record, bool valid) = context.hexToAddress(
2,
context.length
);
if (!valid) revert InvalidAddressFormat();
return abi.encode(record);
if (selector == IAddrResolver.addr.selector) {
return _resolveAddr(context);
} else if (selector == IAddressResolver.addr.selector) {
return _resolveAddress(data, context);
} else if (selector == ITextResolver.text.selector) {
return _resolveText(data, context);
}
revert NotImplemented();
}

function _resolveAddress(
bytes calldata data,
bytes calldata context
) internal pure returns (bytes memory) {
(, uint256 coinType) = abi.decode(data[4:], (bytes32, uint256));
bytes memory value;
// Per https://docs.ens.domains/ensip/11#specification
if (coinType & 0x80000000 != 0) {
Arachnid marked this conversation as resolved.
Show resolved Hide resolved
value = _findValue(
context,
bytes.concat(
"a[e",
bytes((coinType & 0x7fffffff).toString()),
"]="
)
);
} else {
value = _findValue(
context,
bytes.concat("a[", bytes(coinType.toString()), "]=")
);
}
if (value.length == 0) {
return value;
}
(bytes memory record, bool valid) = value.hexToBytes(2, value.length);
if (!valid) revert InvalidAddressFormat(value);
return record;
}

function _resolveAddr(
bytes calldata context
) internal pure returns (bytes memory) {
bytes memory value = _findValue(context, "a[60]=");
if (value.length == 0) {
return value;
}
(bytes memory record, bool valid) = value.hexToBytes(2, value.length);
if (!valid) revert InvalidAddressFormat(value);
return record;
}

function _resolveText(
bytes calldata data,
bytes calldata context
) internal pure returns (bytes memory) {
(, string memory key) = abi.decode(data[4:], (bytes32, string));
bytes memory value = _findValue(
context,
bytes.concat("t[", bytes(key), "]=")
);
return value;
}

uint256 constant STATE_START = 0;
uint256 constant STATE_IGNORED_KEY = 1;
uint256 constant STATE_IGNORED_KEY_ARG = 2;
uint256 constant STATE_VALUE = 3;
uint256 constant STATE_QUOTED_VALUE = 4;
uint256 constant STATE_UNQUOTED_VALUE = 5;
uint256 constant STATE_IGNORED_VALUE = 6;
uint256 constant STATE_IGNORED_QUOTED_VALUE = 7;
uint256 constant STATE_IGNORED_UNQUOTED_VALUE = 8;

function _findValue(
Arachnid marked this conversation as resolved.
Show resolved Hide resolved
bytes memory data,
bytes memory key
) internal pure returns (bytes memory value) {
uint256 state = STATE_START;
uint256 len = data.length;
for (uint256 i = 0; i < len; ) {
if (state == STATE_START) {
if (data.equals(i, key, 0, key.length)) {
i += key.length;
state = STATE_VALUE;
} else {
state = STATE_IGNORED_KEY;
}
} else if (state == STATE_IGNORED_KEY) {
for (; i < len; i++) {
if (data[i] == "=") {
state = STATE_IGNORED_VALUE;
i += 1;
break;
} else if (data[i] == "[") {
state = STATE_IGNORED_KEY_ARG;
i += 1;
break;
}
}
} else if (state == STATE_IGNORED_KEY_ARG) {
for (; i < len; i++) {
if (data[i] == "]") {
state = STATE_IGNORED_VALUE;
i += 1;
if (data[i] == "=") {
i += 1;
}
break;
}
}
} else if (state == STATE_VALUE) {
if (data[i] == "'") {
state = STATE_QUOTED_VALUE;
i += 1;
} else {
state = STATE_UNQUOTED_VALUE;
}
} else if (state == STATE_QUOTED_VALUE) {
uint256 start = i;
uint256 valueLen = 0;
bool escaped = false;
for (; i < len; i++) {
if (escaped) {
data[start + valueLen] = data[i];
valueLen += 1;
escaped = false;
} else {
if (data[i] == "\\") {
escaped = true;
} else if (data[i] == "'") {
return data.substring(start, valueLen);
} else {
data[start + valueLen] = data[i];
valueLen += 1;
}
}
}
} else if (state == STATE_UNQUOTED_VALUE) {
uint256 start = i;
for (; i < len; i++) {
if (data[i] == " ") {
return data.substring(start, i - start);
}
}
return data.substring(start, len - start);
} else if (state == STATE_IGNORED_VALUE) {
if (data[i] == "'") {
state = STATE_IGNORED_QUOTED_VALUE;
i += 1;
} else {
state = STATE_IGNORED_UNQUOTED_VALUE;
}
} else if (state == STATE_IGNORED_QUOTED_VALUE) {
bool escaped = false;
for (; i < len; i++) {
if (escaped) {
escaped = false;
} else {
if (data[i] == "\\") {
escaped = true;
} else if (data[i] == "'") {
i += 1;
while (data[i] == " ") {
i += 1;
}
state = STATE_START;
break;
}
}
}
} else {
assert(state == STATE_IGNORED_UNQUOTED_VALUE);
for (; i < len; i++) {
if (data[i] == " ") {
while (data[i] == " ") {
i += 1;
}
state = STATE_START;
break;
}
}
}
}
return "";
}
}
24 changes: 21 additions & 3 deletions contracts/utils/HexUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,29 @@ library HexUtils {
bytes memory str,
uint256 idx,
uint256 lastIdx
) internal pure returns (bytes32 r, bool valid) {
) internal pure returns (bytes32, bool) {
require(lastIdx - idx <= 64);
(bytes memory r, bool valid) = hexToBytes(str, idx, lastIdx);
if (!valid) {
return (bytes32(0), false);
}
bytes32 ret;
assembly {
ret := shr(mul(4, sub(64, sub(lastIdx, idx))), mload(add(r, 32)))
}
return (ret, true);
}

function hexToBytes(
bytes memory str,
uint256 idx,
uint256 lastIdx
) internal pure returns (bytes memory r, bool valid) {
uint256 hexLength = lastIdx - idx;
if ((hexLength != 64 && hexLength != 40) || hexLength % 2 == 1) {
if (hexLength % 2 == 1) {
revert("Invalid string length");
}
r = new bytes(hexLength / 2);
valid = true;
assembly {
// check that the index to read to is not past the end of the string
Expand Down Expand Up @@ -58,7 +76,7 @@ library HexUtils {
break
}
let combined := or(shl(4, byte1), byte2)
r := or(shl(8, r), combined)
mstore8(add(add(r, 32), div(sub(i, idx), 2)), combined)
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions contracts/utils/TestHexUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ import {HexUtils} from "./HexUtils.sol";
contract TestHexUtils {
using HexUtils for *;

function hexToBytes(
bytes calldata name,
uint256 idx,
uint256 lastInx
) public pure returns (bytes memory, bool) {
return name.hexToBytes(idx, lastInx);
}

function hexStringToBytes32(
bytes calldata name,
uint256 idx,
Expand Down