diff --git a/bun.lockb b/bun.lockb index e638a6ff..90f88609 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/contracts/dnsregistrar/OffchainDNSResolver.sol b/contracts/dnsregistrar/OffchainDNSResolver.sol index dbbf21f6..5c7e5bbc 100644 --- a/contracts/dnsregistrar/OffchainDNSResolver.sol +++ b/contracts/dnsregistrar/OffchainDNSResolver.sol @@ -86,6 +86,7 @@ contract OffchainDNSResolver is IExtendedResolver, IERC165 { ) { // 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 || @@ -199,10 +200,24 @@ contract OffchainDNSResolver is IExtendedResolver, IERC165 { 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); + uint256 totalLength = 0; + + for (uint256 i = startIdx; i < lastIdx; ) { + uint256 fieldLength = data.readUint8(i); + totalLength += fieldLength; + i += fieldLength + 1; + } + + bytes memory result = new bytes(totalLength); + uint256 resultIdx = 0; + + for (uint256 i = startIdx; i < lastIdx; ) { + uint256 fieldLength = data.readUint8(i); + result.strcpy(resultIdx, data, i + 1, fieldLength); + resultIdx += fieldLength; + i += fieldLength + 1; + } + return result; } function parseAndResolve( diff --git a/contracts/utils/BytesUtils.sol b/contracts/utils/BytesUtils.sol index 0f8fd7c2..56f61836 100644 --- a/contracts/utils/BytesUtils.sol +++ b/contracts/utils/BytesUtils.sol @@ -332,6 +332,27 @@ library BytesUtils { } } + function strcpy( + bytes memory self, + uint256 selfOffset, + bytes memory src, + uint256 srcOffset, + uint256 length + ) internal pure { + require(selfOffset + length <= self.length); + require(srcOffset + length <= src.length); + + uint256 selfPtr; + uint256 srcPtr; + + assembly { + selfPtr := add(add(self, 32), selfOffset) + srcPtr := add(add(src, 32), srcOffset) + } + + memcpy(selfPtr, srcPtr, length); + } + /* * @dev Copies a substring into a new byte string. * @param self The byte string to copy from. diff --git a/deploy/dnssec-oracle/10_deploy_oracle.ts b/deploy/dnssec-oracle/10_deploy_oracle.ts index 2dd1a835..bdb265aa 100644 --- a/deploy/dnssec-oracle/10_deploy_oracle.ts +++ b/deploy/dnssec-oracle/10_deploy_oracle.ts @@ -12,7 +12,7 @@ const realAnchors = [ keyTag: 19036, algorithm: 8, digestType: 2, - digest: new Buffer( + digest: Buffer.from( '49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5', 'hex', ), @@ -27,7 +27,7 @@ const realAnchors = [ keyTag: 20326, algorithm: 8, digestType: 2, - digest: new Buffer( + digest: Buffer.from( 'E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D', 'hex', ), @@ -44,7 +44,7 @@ const dummyAnchor = { keyTag: 1278, // Empty body, flags == 0x0101, algorithm = 253, body = 0x0000 algorithm: 253, digestType: 253, - digest: new Buffer('', 'hex'), + digest: Buffer.from('', 'hex'), }, } diff --git a/loader.mjs b/loader.mjs new file mode 100644 index 00000000..2b08a40f --- /dev/null +++ b/loader.mjs @@ -0,0 +1,4 @@ +import { register } from 'node:module' +import { pathToFileURL } from 'node:url' + +register('ts-node/esm/transpile-only', pathToFileURL('./')) diff --git a/package.json b/package.json index 2040ce41..a6a09cb4 100644 --- a/package.json +++ b/package.json @@ -4,10 +4,10 @@ "description": "ENS contracts", "type": "module", "scripts": { - "compile": "NODE_OPTIONS=\"--experimental-loader ts-node/esm/transpile-only\" hardhat compile", - "test": "NODE_OPTIONS=\"--experimental-loader ts-node/esm/transpile-only\" hardhat test", - "test:parallel": "NODE_OPTIONS=\"--experimental-loader ts-node/esm/transpile-only\" hardhat test ./test/**/Test*.ts --parallel", - "test:local": "hardhat --network localhost test", + "compile": "NODE_OPTIONS=\"--import=./loader.mjs\" hardhat compile", + "test": "NODE_OPTIONS=\"--import=./loader.mjs\" hardhat test", + "test:parallel": "NODE_OPTIONS=\"--import=./loader.mjs\" hardhat test ./test/**/Test*.ts --parallel", + "test:local": "NODE_OPTIONS=\"--import=./loader.mjs\" hardhat --network localhost test", "test:deploy": "bun ./scripts/deploy-test.ts", "lint": "hardhat check", "build": "rm -rf ./build/deploy ./build/hardhat.config.js && hardhat compile && tsc", @@ -37,7 +37,7 @@ "abitype": "^1.0.2", "chai": "^5.1.1", "dotenv": "^16.4.5", - "hardhat": "^2.22.2", + "hardhat": "^2.22.9", "hardhat-abi-exporter": "^2.9.0", "hardhat-contract-sizer": "^2.6.1", "hardhat-deploy": "^0.12.4", diff --git a/scripts/deploy-test.ts b/scripts/deploy-test.ts index 80b92389..c527b044 100644 --- a/scripts/deploy-test.ts +++ b/scripts/deploy-test.ts @@ -21,7 +21,7 @@ execSync('bun run hardhat --network localhost deploy', { stdio: 'inherit', env: { ...process.env, - NODE_OPTIONS: '--experimental-loader ts-node/esm/transpile-only', + NODE_OPTIONS: '--import=./loader.mjs', BATCH_GATEWAY_URLS: '["https://example.com/"]', }, }) diff --git a/test/dnsregistrar/TestOffchainDNSResolver.ts b/test/dnsregistrar/TestOffchainDNSResolver.ts index 7cd4642a..b9f34358 100644 --- a/test/dnsregistrar/TestOffchainDNSResolver.ts +++ b/test/dnsregistrar/TestOffchainDNSResolver.ts @@ -13,6 +13,7 @@ import { zeroAddress, zeroHash, type Hex, + encodeFunctionResult, } from 'viem' import { expiration, @@ -76,7 +77,7 @@ async function fixture() { calldata, }: { name: string - texts: string[] + texts: (string | string[])[] calldata: Hex }) => { const proof = [ @@ -590,4 +591,166 @@ describe('OffchainDNSResolver', () => { ) .toBeRevertedWithCustomError('InvalidOperation') }) + + it('should correctly concatenate multiple texts in the TXT record and resolve', async function () { + const { doDnsResolveCallback, publicResolverAbi } = await loadFixture( + fixture, + ) + const resolver = await hre.viem.deployContract( + 'DummyExtendedDNSSECResolver', + [], + ) + const name = 'test.test' + const COIN_TYPE_ETH = 60 + const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' + const calldataAddr = encodeFunctionData({ + abi: publicResolverAbi, + functionName: 'addr', + args: [namehash(name)], + }) + + const resultTextSingle = encodeFunctionResult({ + abi: publicResolverAbi, + functionName: 'text', + result: `a[60]=${testAddress} t[smth]=smth.eth`, + }) + + // test with single string + await expect( + doDnsResolveCallback({ + name, + texts: [ + `ENS1 ${resolver.address} a[${COIN_TYPE_ETH}]=${testAddress} t[smth]=smth.eth`, + ], + calldata: calldataAddr, + }), + ).resolves.toEqual(resultTextSingle) + + const resultTextSplit = encodeFunctionResult({ + abi: publicResolverAbi, + functionName: 'text', + result: `a[${COIN_TYPE_ETH}]=${testAddress} t[smth]=smth.eth`, + }) + + // test with split strings + await expect( + doDnsResolveCallback({ + name, + texts: [ + [ + `ENS1 ${resolver.address}`, + ` a[${COIN_TYPE_ETH}]=${testAddress}`, + ` t[smth]=smth.eth`, + ], + ], + calldata: calldataAddr, + }), + ).resolves.toEqual(resultTextSplit) + + // test with very long string + const longData = 'x'.repeat(300) + + const resultTextLongData = encodeFunctionResult({ + abi: publicResolverAbi, + functionName: 'text', + result: `a[${COIN_TYPE_ETH}]=${testAddress} ${longData}`, + }) + await expect( + doDnsResolveCallback({ + name, + texts: [ + [ + `ENS1 ${resolver.address} a[${COIN_TYPE_ETH}]=${testAddress}`, + ' ', + longData.slice(0, 255), + longData.slice(255), + ], + ], + calldata: calldataAddr, + }), + ).resolves.toEqual(resultTextLongData) + }) + + it('should correctly do text resolution regardless of order', async function () { + const name = 'test.test' + const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' + + const { doDnsResolveCallback, publicResolverAbi } = await loadFixture( + fixture, + ) + + const resolver = await hre.viem.deployContract( + 'DummyExtendedDNSSECResolver', + [], + ) + + const callDataText = encodeFunctionData({ + abi: publicResolverAbi, + functionName: 'text', + args: [namehash(name), 'smth'], + }) + + const resultText = encodeFunctionResult({ + abi: publicResolverAbi, + functionName: 'text', + result: `t[smth]=smth.eth ${testAddress}`, + }) + + await expect( + doDnsResolveCallback({ + name, + texts: [`ENS1 ${resolver.address} t[smth]=smth.eth ${testAddress}`], + calldata: callDataText, + }), + ).resolves.toEqual(resultText) + }) + + it('should correctly do text resolution regardless of order', async function () { + const name = 'test.test' + const testAddress = '0xfefeFEFeFEFEFEFEFeFefefefefeFEfEfefefEfe' + + const { doDnsResolveCallback, publicResolverAbi } = await loadFixture( + fixture, + ) + + const resolver = await hre.viem.deployContract('ExtendedDNSResolver', []) + + const callDataText = encodeFunctionData({ + abi: publicResolverAbi, + functionName: 'text', + args: [namehash(name), 'smth'], + }) + + await expect( + doDnsResolveCallback({ + name, + texts: [`ENS1 ${resolver.address} t[smth]=smth.eth ${testAddress}`], + calldata: callDataText, + }), + ).resolves.toEqual(encodeAbiParameters([{ type: 'string' }], ['smth.eth'])) + }) + + it('should correctly do text resolution regardless of key-value pair amount', async function () { + const name = 'test.test' + + const { doDnsResolveCallback, publicResolverAbi } = await loadFixture( + fixture, + ) + + const resolver = await hre.viem.deployContract('ExtendedDNSResolver', []) + + const callDataText = encodeFunctionData({ + abi: publicResolverAbi, + functionName: 'text', + args: [namehash(name), 'bla'], + }) + + await expect( + doDnsResolveCallback({ + name, + texts: [`ENS1 ${resolver.address} t[smth]=smth.eth t[bla]=bla.eth`], + calldata: callDataText, + }), + ).resolves.toEqual(encodeAbiParameters([{ type: 'string' }], ['bla.eth'])) + }) }) diff --git a/test/dnssec-oracle/TestDNSSEC.ts b/test/dnssec-oracle/TestDNSSEC.ts index 95a7f138..5db12200 100644 --- a/test/dnssec-oracle/TestDNSSEC.ts +++ b/test/dnssec-oracle/TestDNSSEC.ts @@ -258,7 +258,7 @@ describe('DNSSEC', () => { inception, keyTag: 1278, signersName: '.', - signature: new Buffer([]), + signature: Buffer.from([]), }, }, rrs: [ @@ -297,7 +297,7 @@ describe('DNSSEC', () => { inception, keyTag: 1278, signersName: '.', - signature: new Buffer([]), + signature: Buffer.from([]), }, }, rrs: [ @@ -338,7 +338,7 @@ describe('DNSSEC', () => { inception, keyTag: 1278, signersName: '.', - signature: new Buffer([]), + signature: Buffer.from([]), }, }, rrs: [ @@ -379,7 +379,7 @@ describe('DNSSEC', () => { inception, keyTag: 1278, signersName: '.', - signature: new Buffer([]), + signature: Buffer.from([]), }, }, rrs: [ @@ -420,7 +420,7 @@ describe('DNSSEC', () => { inception, keyTag: 1278, signersName: 'com', - signature: new Buffer([]), + signature: Buffer.from([]), }, }, rrs: [ @@ -461,7 +461,7 @@ describe('DNSSEC', () => { inception, keyTag: 1278, signersName: '.', - signature: new Buffer([]), + signature: Buffer.from([]), }, }, rrs: [ @@ -494,7 +494,7 @@ describe('DNSSEC', () => { inception, keyTag: 1278, signersName: 'xample', - signature: new Buffer([]), + signature: Buffer.from([]), }, }, rrs: [ @@ -535,7 +535,7 @@ describe('DNSSEC', () => { inception, keyTag: 1278, signersName: '.', - signature: new Buffer([]), + signature: Buffer.from([]), }, }, rrs: [ @@ -568,7 +568,7 @@ describe('DNSSEC', () => { inception, keyTag: 1275, signersName: 'test', - signature: new Buffer([]), + signature: Buffer.from([]), }, }, rrs: [ @@ -667,7 +667,7 @@ describe('DNSSEC', () => { inception, keyTag: 1278, signersName: '.', - signature: new Buffer([]), + signature: Buffer.from([]), }, }, rrs: [ @@ -680,7 +680,7 @@ describe('DNSSEC', () => { keyTag: 1278, // Empty body, flags == 0x0101, algorithm = 253, body = 0x0000 algorithm: 253, digestType: 253, - digest: new Buffer('', 'hex'), + digest: Buffer.from('', 'hex'), }, }, ], @@ -701,7 +701,7 @@ describe('DNSSEC', () => { inception, keyTag: 1278, signersName: 'foo', - signature: new Buffer([]), + signature: Buffer.from([]), }, }, rrs: [ @@ -746,7 +746,7 @@ describe('DNSSEC', () => { inception, keyTag: 1278, signersName: '.', - signature: new Buffer([]), + signature: Buffer.from([]), }, }, rrs: [ @@ -759,7 +759,7 @@ describe('DNSSEC', () => { keyTag: 1278, // Empty body, flags == 0x0101, algorithm = 253, body = 0x0000 algorithm: 253, digestType: 253, - digest: new Buffer('', 'hex'), + digest: Buffer.from('', 'hex'), }, }, ], @@ -780,7 +780,7 @@ describe('DNSSEC', () => { inception, keyTag: 1278, signersName: 'test', - signature: new Buffer([]), + signature: Buffer.from([]), }, }, rrs: [ diff --git a/test/fixtures/anchors.ts b/test/fixtures/anchors.ts index e5ec7328..c3466217 100644 --- a/test/fixtures/anchors.ts +++ b/test/fixtures/anchors.ts @@ -10,7 +10,7 @@ export const realEntries = [ keyTag: 19036, algorithm: 8, digestType: 2, - digest: new Buffer( + digest: Buffer.from( '49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5', 'hex', ), @@ -25,7 +25,7 @@ export const realEntries = [ keyTag: 20326, algorithm: 8, digestType: 2, - digest: new Buffer( + digest: Buffer.from( 'E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D', 'hex', ), @@ -42,7 +42,7 @@ export const dummyEntry = { keyTag: 1278, // Empty body, flags == 0x0101, algorithm = 253, body = 0x0000 algorithm: 253, digestType: 253, - digest: new Buffer('', 'hex'), + digest: Buffer.from('', 'hex'), }, } as const diff --git a/test/fixtures/dns.ts b/test/fixtures/dns.ts index 91528f70..40d1d0c3 100644 --- a/test/fixtures/dns.ts +++ b/test/fixtures/dns.ts @@ -24,7 +24,7 @@ export const rrsetWithTexts = ({ texts, }: { name: string - texts: (string | { name: string; value: string })[] + texts: (string | string[])[] }) => ({ sig: { @@ -42,19 +42,19 @@ export const rrsetWithTexts = ({ inception, keyTag: 1278, signersName: '.', - signature: new Buffer([]), + signature: Buffer.from([]), }, }, rrs: texts.map( (text) => ({ - name: typeof text === 'string' ? name : text.name, + name, type: 'TXT', class: 'IN', ttl: 3600, - data: [ - Buffer.from(typeof text === 'string' ? text : text.value, 'ascii'), - ] as Buffer[], + data: Array.isArray(text) + ? text.map((t) => Buffer.from(t, 'ascii')) + : [Buffer.from(text, 'ascii')], } as const), ), } as const) @@ -81,7 +81,7 @@ export const testRrset = ({ inception, keyTag: 1278, signersName: '.', - signature: new Buffer([]), + signature: Buffer.from([]), }, }, rrs: [ @@ -118,7 +118,7 @@ export const rootKeys = ({ inception, keyTag: 1278, signersName: '.', - signature: new Buffer([]), + signature: Buffer.from([]), }, } as const