Skip to content

Commit

Permalink
Merge pull request #203 from ensdomains/ccip-dnsregistrar
Browse files Browse the repository at this point in the history
Implement offchain DNS Registrar support
  • Loading branch information
Arachnid authored Feb 23, 2023
2 parents e385779 + 0a81b61 commit 03fa257
Show file tree
Hide file tree
Showing 17 changed files with 600 additions and 142 deletions.
38 changes: 8 additions & 30 deletions contracts/dnsregistrar/DNSClaimChecker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ library DNSClaimChecker {
) {
bool found;
address addr;
(addr, found) = parseRR(data, iter.rdataOffset);
(addr, found) = parseRR(data, iter.rdataOffset, iter.nextOffset);
if (found) {
return (addr, true);
}
Expand All @@ -40,11 +40,12 @@ library DNSClaimChecker {
return (address(0x0), false);
}

function parseRR(
bytes memory rdata,
uint256 idx
) internal pure returns (address, bool) {
while (idx < rdata.length) {
function parseRR(bytes memory rdata, uint256 idx, uint256 endIdx)
internal
pure
returns (address, bool)
{
while (idx < endIdx) {
uint256 len = rdata.readUint8(idx);
idx += 1;

Expand All @@ -66,29 +67,6 @@ library DNSClaimChecker {
) internal pure returns (address, bool) {
// TODO: More robust parsing that handles whitespace and multiple key/value pairs
if (str.readUint32(idx) != 0x613d3078) return (address(0x0), false); // 0x613d3078 == 'a=0x'
if (len < 44) return (address(0x0), false);
return hexToAddress(str, idx + 4);
}

function hexToAddress(
bytes memory str,
uint256 idx
) internal pure returns (address, bool) {
if (str.length - idx < 40) return (address(0x0), false);
uint256 ret = 0;
for (uint256 i = idx; i < idx + 40; i++) {
ret <<= 4;
uint256 x = str.readUint8(i);
if (x >= 48 && x < 58) {
ret |= x - 48;
} else if (x >= 65 && x < 71) {
ret |= x - 55;
} else if (x >= 97 && x < 103) {
ret |= x - 87;
} else {
return (address(0x0), false);
}
}
return (address(uint160(ret)), true);
return str.hexToAddress(idx + 4, idx + len);
}
}
90 changes: 48 additions & 42 deletions contracts/dnsregistrar/DNSRegistrar.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import "./IDNSRegistrar.sol";
* @dev An ENS registrar that allows the owner of a DNS name to claim the
* corresponding name in ENS.
*/
// TODO: Record inception time of any claimed name, so old proofs can't be used to revert changes to a name.
contract DNSRegistrar is IDNSRegistrar, IERC165 {
using BytesUtils for bytes;
using Buffer for Buffer.buffer;
Expand All @@ -27,11 +26,16 @@ contract DNSRegistrar is IDNSRegistrar, IERC165 {
ENS public immutable ens;
DNSSEC public immutable oracle;
PublicSuffixList public suffixes;
address public immutable previousRegistrar;
address public immutable resolver;
// A mapping of the most recent signatures seen for each claimed domain.
mapping(bytes32 => uint32) public inceptions;

error NoOwnerRecordFound();
error PermissionDenied(address caller, address owner);
error PreconditionNotMet();
error StaleProof();
error InvalidPublicSuffix(bytes name);

struct OwnerRecord {
bytes name;
Expand All @@ -46,12 +50,18 @@ contract DNSRegistrar is IDNSRegistrar, IERC165 {
bytes dnsname,
uint32 inception
);
event NewOracle(address oracle);
event NewPublicSuffixList(address suffixes);

constructor(DNSSEC _dnssec, PublicSuffixList _suffixes, ENS _ens) {
constructor(
address _previousRegistrar,
address _resolver,
DNSSEC _dnssec,
PublicSuffixList _suffixes,
ENS _ens
) {
previousRegistrar = _previousRegistrar;
resolver = _resolver;
oracle = _dnssec;
emit NewOracle(address(oracle));
suffixes = _suffixes;
emit NewPublicSuffixList(address(suffixes));
ens = _ens;
Expand Down Expand Up @@ -98,30 +108,17 @@ contract DNSRegistrar is IDNSRegistrar, IERC165 {
name,
input
);
require(
msg.sender == owner,
"Only owner can call proveAndClaimWithResolver"
);
if(msg.sender != owner) {
revert PermissionDenied(msg.sender, owner);
}
ens.setSubnodeRecord(rootNode, labelHash, owner, resolver, 0);
if (addr != address(0)) {
require(
resolver != address(0),
"Cannot set addr if resolver is not set"
);
// Set ourselves as the owner so we can set a record on the resolver
ens.setSubnodeRecord(
rootNode,
labelHash,
address(this),
resolver,
0
);
if(resolver == address(0)) {
revert PreconditionNotMet();
}
bytes32 node = keccak256(abi.encodePacked(rootNode, labelHash));
// Set the resolver record
AddrResolver(resolver).setAddr(node, addr);
// Transfer the record to the owner
ens.setOwner(node, owner);
} else {
ens.setSubnodeRecord(rootNode, labelHash, owner, resolver, 0);
}
}

Expand All @@ -143,54 +140,63 @@ contract DNSRegistrar is IDNSRegistrar, IERC165 {
uint256 labelLen = name.readUint8(0);
labelHash = name.keccak(1, labelLen);

// Parent name must be in the public suffix list.
bytes memory parentName = name.substring(
labelLen + 1,
name.length - labelLen - 1
);
require(
suffixes.isPublicSuffix(parentName),
"Parent name must be a public suffix"
);

// Make sure the parent name is enabled
parentNode = enableNode(parentName, 0);
parentNode = enableNode(parentName);

bytes32 node = keccak256(abi.encodePacked(parentNode, labelHash));
if (!RRUtils.serialNumberGte(inception, inceptions[node])) {
revert StaleProof();
}
inceptions[node] = inception;

(addr, ) = DNSClaimChecker.getOwnerAddress(name, data);
bool found;
(addr, found) = DNSClaimChecker.getOwnerAddress(name, data);
if(!found) {
revert NoOwnerRecordFound();
}

emit Claim(node, addr, name, inception);
}

function enableNode(
bytes memory domain,
uint256 offset
) internal returns (bytes32 node) {
function enableNode(bytes memory domain)
public
returns (bytes32 node)
{
// Name must be in the public suffix list.
if(!suffixes.isPublicSuffix(domain)) {
revert InvalidPublicSuffix(domain);
}
return _enableNode(domain, 0);
}

function _enableNode(bytes memory domain, uint256 offset)
internal
returns(bytes32 node)
{
uint256 len = domain.readUint8(offset);
if (len == 0) {
return bytes32(0);
}

bytes32 parentNode = enableNode(domain, offset + len + 1);
bytes32 parentNode = _enableNode(domain, offset + len + 1);
bytes32 label = domain.keccak(offset + 1, len);
node = keccak256(abi.encodePacked(parentNode, label));
address owner = ens.owner(node);
require(
owner == address(0) || owner == address(this),
"Cannot enable a name owned by someone else"
);
if (owner != address(this)) {
if (owner == address(0) || owner == previousRegistrar) {
if (parentNode == bytes32(0)) {
Root root = Root(ens.owner(bytes32(0)));
root.setSubnodeOwner(label, address(this));
ens.setResolver(node, resolver);
} else {
ens.setSubnodeOwner(parentNode, label, address(this));
ens.setSubnodeRecord(parentNode, label, address(this), resolver, 0);
}
} else if(owner != address(this)) {
revert PreconditionNotMet();
}
return node;
}
Expand Down
165 changes: 165 additions & 0 deletions contracts/dnsregistrar/OffchainDNSResolver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "../../contracts/resolvers/profiles/IAddrResolver.sol";
import "../../contracts/resolvers/profiles/IExtendedResolver.sol";
import "../../contracts/resolvers/profiles/IExtendedDNSResolver.sol";
import "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import "../dnssec-oracle/BytesUtils.sol";
import "../dnssec-oracle/DNSSEC.sol";
import "../dnssec-oracle/RRUtils.sol";
import "../registry/ENSRegistry.sol";

error OffchainLookup(
address sender,
string[] urls,
bytes callData,
bytes4 callbackFunction,
bytes extraData
);

interface IDNSGateway {
function resolve(bytes memory name, uint16 qtype) external returns(DNSSEC.RRSetWithSignature[] memory);
}

uint16 constant CLASS_INET = 1;
uint16 constant TYPE_TXT = 16;

contract OffchainDNSResolver is IExtendedResolver {
using RRUtils for *;
using BytesUtils for bytes;

ENS public immutable ens;
DNSSEC public immutable oracle;
string public gatewayURL;

error CouldNotResolve(bytes name);

constructor(ENS _ens, DNSSEC _oracle, string memory _gatewayURL) {
ens = _ens;
oracle = _oracle;
gatewayURL = _gatewayURL;
}

function resolve(
bytes calldata name,
bytes calldata data
) external view returns (bytes memory) {
string[] memory urls = new string[](1);
urls[0] = gatewayURL;

revert OffchainLookup(
address(this),
urls,
abi.encodeCall(IDNSGateway.resolve, (name, TYPE_TXT)),
OffchainDNSResolver.resolveCallback.selector,
abi.encode(name, data)
);
}

function resolveCallback(bytes calldata response, bytes calldata extraData)
external
view
returns (bytes memory)
{
(bytes memory name, bytes memory query) = abi.decode(extraData, (bytes, bytes));
DNSSEC.RRSetWithSignature[] memory rrsets = abi.decode(response, (DNSSEC.RRSetWithSignature[]));

(bytes memory data, ) = oracle.verifyRRSet(rrsets);
for (
RRUtils.RRIterator memory iter = data.iterateRRs(0);
!iter.done();
iter.next()
) {
// Ignore records with wrong name, type, or class
bytes memory rrname = RRUtils.readName(iter.data, iter.offset);
if(!rrname.equals(name) || iter.class != CLASS_INET || iter.dnstype != TYPE_TXT) {
continue;
}

// Look for a valid ENS-DNS TXT record
(address dnsresolver, bytes memory context) = parseRR(iter.data, iter.rdataOffset, iter.nextOffset);

// If we found a valid record, try to resolve it
if(dnsresolver != address(0)) {
if(IERC165(dnsresolver).supportsInterface(IExtendedDNSResolver.resolve.selector)) {
return IExtendedDNSResolver(dnsresolver).resolve(name, query, context);
} else if(IERC165(dnsresolver).supportsInterface(IExtendedResolver.resolve.selector)) {
return IExtendedResolver(dnsresolver).resolve(name, query);
} else {
(bool ok, bytes memory ret) = address(dnsresolver).staticcall(query);
if(ok) {
return ret;
} else {
revert CouldNotResolve(name);
}
}
}
}

// No valid records; revert.
revert CouldNotResolve(name);
}

function parseRR(bytes memory data, uint256 idx, uint256 lastIdx) internal view returns (address, bytes memory) {
bytes memory txt = readTXT(data, idx, lastIdx);

// Must start with the magic word
if(txt.length < 5 || !txt.equals(0, "ENS1 ", 0, 5)) {
return (address(0), "");
}

// Parse the name or address
uint256 lastTxtIdx = txt.find(5, txt.length - 5, " ");
if(lastTxtIdx > txt.length) {
address dnsResolver = parseAndResolve(txt, 5, txt.length);
return (dnsResolver, "");
} else {
address dnsResolver = parseAndResolve(txt, 5, lastTxtIdx);
return (dnsResolver, txt.substring(lastTxtIdx + 1, txt.length - lastTxtIdx - 1));
}
}

function readTXT(bytes memory data, uint256 startIdx, uint256 lastIdx) internal pure returns(bytes memory) {
// TODO: Concatenate multiple text fields
uint256 fieldLength = data.readUint8(startIdx);
assert(startIdx + fieldLength < lastIdx);
return data.substring(startIdx + 1, fieldLength);
}

function parseAndResolve(bytes memory nameOrAddress, uint256 idx, uint256 lastIdx) internal view returns(address) {
if(nameOrAddress[idx] == '0' && nameOrAddress[idx + 1] == 'x') {
(address ret, bool valid) = nameOrAddress.hexToAddress(idx + 2, lastIdx);
if(valid) {
return ret;
}
}
return resolveName(nameOrAddress, idx, lastIdx);
}

function resolveName(bytes memory name, uint256 idx, uint256 lastIdx) internal view returns(address) {
bytes32 node = textNamehash(name, idx, lastIdx);
address resolver = ens.resolver(node);
if(resolver == address(0)) {
return address(0);
}
return IAddrResolver(resolver).addr(node);
}

/**
* @dev Namehash function that operates on dot-separated names (not dns-encoded names)
* @param name Name to hash
* @param idx Index to start at
* @param lastIdx Index to end at
*/
function textNamehash(bytes memory name, uint256 idx, uint256 lastIdx) internal view returns(bytes32) {
uint256 separator = name.find(idx, name.length - idx, bytes1('.'));
bytes32 parentNode = bytes32(0);
if(separator < lastIdx) {
parentNode = textNamehash(name, separator + 1, lastIdx);
} else {
separator = lastIdx;
}
return keccak256(abi.encodePacked(parentNode, name.keccak(idx, separator - idx)));
}
}
Loading

0 comments on commit 03fa257

Please sign in to comment.