From 085e8b139f89e921922498e2cae766e0d6690c24 Mon Sep 17 00:00:00 2001 From: st-mn <56998558+st-mn@users.noreply.github.com> Date: Sat, 11 Oct 2025 18:50:12 +0000 Subject: [PATCH 1/2] Fix: Prevent .eth.eth Suffix in bytesToPacket Decoding Problem When decoding DNS-encoded ENS names using bytesToPacket, certain packet bytes (e.g., 0x08383433362E6574680365746800) would incorrectly produce names with a double .eth suffix, such as 8436.eth.eth instead of the expected 8436.eth. This occurs when the encoded packet contains two labels: one that already ends with .eth and a trailing eth label, leading to an erroneous concatenation. Solution This PR updates the bytesToPacket function in hexEncodedName.ts to handle this edge case: After decoding the labels, if the last label is 'eth' and the previous label already ends with '.eth', the function removes the trailing 'eth' label before joining the labels. This ensures that names like 8436.eth.eth are correctly normalized to 8436.eth, while still allowing valid cases such as [labelhash].eth to decode as expected. Implementation Details The function now checks the decoded label array for the described pattern and mutates the array accordingly before joining. The logic is robust against other edge cases and does not affect names that are supposed to end with .eth. Testing Added/updated unit tests in hexEncodedName.test.ts to cover the problematic case and ensure no regression for other valid ENS names. All tests pass, confirming the fix. References Fixes: https://github.com/ensdomains/ensjs/issues/222 Context: https://github.com/namehash/ensnode/issues/36#issuecomment-2603237533 new file: packages/ensjs/src/utils/hexEncodedName.test.ts modified: packages/ensjs/src/utils/hexEncodedName.ts --- .../ensjs/src/utils/hexEncodedName.test.ts | 14 ++++++++++++ packages/ensjs/src/utils/hexEncodedName.ts | 22 +++++++++++++------ 2 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 packages/ensjs/src/utils/hexEncodedName.test.ts diff --git a/packages/ensjs/src/utils/hexEncodedName.test.ts b/packages/ensjs/src/utils/hexEncodedName.test.ts new file mode 100644 index 000000000..ea42e0029 --- /dev/null +++ b/packages/ensjs/src/utils/hexEncodedName.test.ts @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest' +import { bytesToPacket } from './hexEncodedName.js' +import { hexToBytes } from 'viem' + +describe('bytesToPacket', () => { + it('should decode 08383433362E6574680365746800 to 8436.eth', () => { + const bytes = hexToBytes('0x08383433362E6574680365746800') + expect(bytesToPacket(bytes)).toBe('8436.eth') + }) + it('should not append .eth if already present', () => { + const bytes = hexToBytes('0x04746573740365746800') // test.eth + expect(bytesToPacket(bytes)).toBe('test.eth') + }) +}) diff --git a/packages/ensjs/src/utils/hexEncodedName.ts b/packages/ensjs/src/utils/hexEncodedName.ts index 609d41dcb..37cd02d49 100644 --- a/packages/ensjs/src/utils/hexEncodedName.ts +++ b/packages/ensjs/src/utils/hexEncodedName.ts @@ -32,18 +32,26 @@ export function packetToBytes(packet: string): ByteArray { export function bytesToPacket(bytes: ByteArray): string { let offset = 0 - let result = '' + const labels: string[] = []; while (offset < bytes.length) { - const len = bytes[offset] + const len = bytes[offset]; if (len === 0) { - offset += 1 - break + offset += 1; + break; } + labels.push(bytesToString(bytes.subarray(offset + 1, offset + len + 1))); + offset += len + 1; + } - result += `${bytesToString(bytes.subarray(offset + 1, offset + len + 1))}.` - offset += len + 1 + // If the last label is 'eth' and the previous label ends with '.eth', remove the last 'eth' + if ( + labels.length > 1 && + labels[labels.length - 1] === 'eth' && + labels[labels.length - 2].endsWith('.eth') + ) { + labels.pop(); } - return result.replace(/\.$/, '') + return labels.join('.') } From ab4f0e4afd0089f3ffe4f454092f8a73f3fd26c5 Mon Sep 17 00:00:00 2001 From: st-mn <56998558+st-mn@users.noreply.github.com> Date: Sat, 11 Oct 2025 19:13:37 +0000 Subject: [PATCH 2/2] modified: packages/ensjs/src/utils/hexEncodedName.test.ts modified: packages/ensjs/src/utils/hexEncodedName.ts --- packages/ensjs/src/utils/hexEncodedName.test.ts | 4 ++-- packages/ensjs/src/utils/hexEncodedName.ts | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/ensjs/src/utils/hexEncodedName.test.ts b/packages/ensjs/src/utils/hexEncodedName.test.ts index ea42e0029..f12d9ec52 100644 --- a/packages/ensjs/src/utils/hexEncodedName.test.ts +++ b/packages/ensjs/src/utils/hexEncodedName.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect } from 'vitest' -import { bytesToPacket } from './hexEncodedName.js' import { hexToBytes } from 'viem' +import { describe, expect, it } from 'vitest' +import { bytesToPacket } from './hexEncodedName.js' describe('bytesToPacket', () => { it('should decode 08383433362E6574680365746800 to 8436.eth', () => { diff --git a/packages/ensjs/src/utils/hexEncodedName.ts b/packages/ensjs/src/utils/hexEncodedName.ts index 37cd02d49..942b86afd 100644 --- a/packages/ensjs/src/utils/hexEncodedName.ts +++ b/packages/ensjs/src/utils/hexEncodedName.ts @@ -32,16 +32,16 @@ export function packetToBytes(packet: string): ByteArray { export function bytesToPacket(bytes: ByteArray): string { let offset = 0 - const labels: string[] = []; + const labels: string[] = [] while (offset < bytes.length) { - const len = bytes[offset]; + const len = bytes[offset] if (len === 0) { - offset += 1; - break; + offset += 1 + break } - labels.push(bytesToString(bytes.subarray(offset + 1, offset + len + 1))); - offset += len + 1; + labels.push(bytesToString(bytes.subarray(offset + 1, offset + len + 1))) + offset += len + 1 } // If the last label is 'eth' and the previous label ends with '.eth', remove the last 'eth' @@ -50,7 +50,7 @@ export function bytesToPacket(bytes: ByteArray): string { labels[labels.length - 1] === 'eth' && labels[labels.length - 2].endsWith('.eth') ) { - labels.pop(); + labels.pop() } return labels.join('.')