From 2be5088795aa505bf6a9d7a7b2a2407cfe16eec6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Tanr=C4=B1kulu?= Date: Tue, 26 Mar 2024 22:48:56 +0100 Subject: [PATCH] sync testing with feature/better-offchain-dns --- .../mocks/DummyExtendedDNSSECResolver2.sol | 228 +++++++++++++----- contracts/utils/HexUtils.sol | 24 +- contracts/utils/TestHexUtils.sol | 8 + test/dnsregistrar/TestOffchainDNSResolver.js | 23 +- 4 files changed, 213 insertions(+), 70 deletions(-) diff --git a/contracts/dnsregistrar/mocks/DummyExtendedDNSSECResolver2.sol b/contracts/dnsregistrar/mocks/DummyExtendedDNSSECResolver2.sol index e6136ee5..0f0e348c 100644 --- a/contracts/dnsregistrar/mocks/DummyExtendedDNSSECResolver2.sol +++ b/contracts/dnsregistrar/mocks/DummyExtendedDNSSECResolver2.sol @@ -2,20 +2,23 @@ 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"; contract DummyExtendedDNSSECResolver2 is IExtendedDNSResolver, IERC165 { using HexUtils for *; + using BytesUtils for *; + using Strings for *; uint256 private constant COIN_TYPE_ETH = 60; - uint256 private constant ADDRESS_LENGTH = 40; error NotImplemented(); - error InvalidAddressFormat(); + error InvalidAddressFormat(bytes addr); function supportsInterface( bytes4 interfaceId @@ -29,73 +32,188 @@ contract DummyExtendedDNSSECResolver2 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 - ) { - // Parse address from context - bytes memory addrBytes = _parseAddressFromContext(context); - return abi.encode(address(uint160(uint256(bytes32(addrBytes))))); + if (selector == IAddrResolver.addr.selector) { + return _resolveAddr(context); + } else if (selector == IAddressResolver.addr.selector) { + return _resolveAddress(data, context); } else if (selector == ITextResolver.text.selector) { - // Parse text value from context - (, string memory key) = abi.decode(data[4:], (bytes32, string)); - string memory value = _parseTextFromContext(context, key); - return abi.encode(value); + return _resolveText(data, context); } revert NotImplemented(); } - function _parseAddressFromContext( - bytes memory context + function _resolveAddress( + bytes calldata data, + bytes calldata context ) internal pure returns (bytes memory) { - // Parse address from concatenated context - for (uint256 i = 0; i < context.length - ADDRESS_LENGTH + 2; i++) { - if (context[i] == "0" && context[i + 1] == "x") { - bytes memory candidate = new bytes(ADDRESS_LENGTH); - for (uint256 j = 0; j < ADDRESS_LENGTH; j++) { - candidate[j] = context[i + j + 2]; - } + (, uint256 coinType) = abi.decode(data[4:], (bytes32, uint256)); + bytes memory value; + // Per https://docs.ens.domains/ensip/11#specification + if (coinType & 0x80000000 != 0) { + 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; + } - (address candidateAddr, bool valid) = candidate.hexToAddress( - 0, - ADDRESS_LENGTH - ); - if (valid) { - return abi.encode(candidateAddr); - } - } + function _resolveAddr( + bytes calldata context + ) internal pure returns (bytes memory) { + bytes memory value = _findValue(context, "a[60]="); + if (value.length == 0) { + return value; } - revert InvalidAddressFormat(); + (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; } - function _parseTextFromContext( - bytes calldata context, - string memory key - ) internal pure returns (string memory) { - // Parse key-value pairs from concatenated context - string memory value = ""; - bool foundKey = false; - for (uint256 i = 0; i < context.length; i++) { - if (foundKey && context[i] == "=") { - i++; - while (i < context.length && context[i] != " ") { - string memory charStr = string( - abi.encodePacked(bytes1(context[i])) - ); - value = string(abi.encodePacked(value, charStr)); - i++; + 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( + 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; } - return value; - } - if (!foundKey && bytes(key)[0] == context[i]) { - bool isMatch = true; - for (uint256 j = 1; j < bytes(key).length; j++) { - if (context[i + j] != bytes(key)[j]) { - isMatch = false; + } 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; } } - foundKey = isMatch; } } return ""; diff --git a/contracts/utils/HexUtils.sol b/contracts/utils/HexUtils.sol index 7cb3a1e0..35ad52d0 100644 --- a/contracts/utils/HexUtils.sol +++ b/contracts/utils/HexUtils.sol @@ -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 @@ -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) } } } diff --git a/contracts/utils/TestHexUtils.sol b/contracts/utils/TestHexUtils.sol index 6dd9088e..b6624f09 100644 --- a/contracts/utils/TestHexUtils.sol +++ b/contracts/utils/TestHexUtils.sol @@ -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, diff --git a/test/dnsregistrar/TestOffchainDNSResolver.js b/test/dnsregistrar/TestOffchainDNSResolver.js index 4be9a9ca..996aa641 100644 --- a/test/dnsregistrar/TestOffchainDNSResolver.js +++ b/test/dnsregistrar/TestOffchainDNSResolver.js @@ -468,18 +468,17 @@ contract('OffchainDNSResolver', function (accounts) { const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' const resolver = await DummyExtendedDNSSECResolver2.new() const pr = await PublicResolver.at(resolver.address) - const callDataAddr = pr.contract.methods['addr(bytes32,uint256)']( + const callDataAddr = pr.contract.methods['addr(bytes32)']( namehash.hash(name), - COIN_TYPE_ETH, ).encodeABI() const resultAddr = await doDNSResolveCallback( name, - [`ENS1 ${resolver.address} ${testAddress} smth=smth.eth`], + [ + `ENS1 ${resolver.address} a[${COIN_TYPE_ETH}]=${testAddress} t[smth]=smth.eth`, + ], callDataAddr, ) - expect( - ethers.utils.defaultAbiCoder.decode(['address'], resultAddr)[0], - ).to.equal(testAddress) + expect(resultAddr).to.equal(testAddress.toLowerCase()) const callDataText = pr.contract.methods['text(bytes32,string)']( namehash.hash(name), @@ -487,12 +486,12 @@ contract('OffchainDNSResolver', function (accounts) { ).encodeABI() const resultText = await doDNSResolveCallback( name, - [`ENS1 ${resolver.address} ${testAddress} smth=smth.eth`], + [`ENS1 ${resolver.address} a[60]=${testAddress} t[smth]=smth.eth`], callDataText, ) expect( - ethers.utils.defaultAbiCoder.decode(['string'], resultText)[0], + ethers.utils.toUtf8String(ethers.utils.arrayify(resultText)), ).to.equal('smth.eth') }) @@ -508,12 +507,12 @@ contract('OffchainDNSResolver', function (accounts) { ).encodeABI() const resultText = await doDNSResolveCallback( name, - [`ENS1 ${resolver.address} smth=smth.eth ${testAddress}`], + [`ENS1 ${resolver.address} t[smth]=smth.eth ${testAddress}`], callDataText, ) expect( - ethers.utils.defaultAbiCoder.decode(['string'], resultText)[0], + ethers.utils.toUtf8String(ethers.utils.arrayify(resultText)), ).to.equal('smth.eth') }) @@ -528,12 +527,12 @@ contract('OffchainDNSResolver', function (accounts) { ).encodeABI() const resultText = await doDNSResolveCallback( name, - [`ENS1 ${resolver.address} smth=smth.eth bla=bla.eth`], + [`ENS1 ${resolver.address} t[smth]=smth.eth t[bla]=bla.eth`], callDataText, ) expect( - ethers.utils.defaultAbiCoder.decode(['string'], resultText)[0], + ethers.utils.toUtf8String(ethers.utils.arrayify(resultText)), ).to.equal('bla.eth') }) })