From c678dc0f036bb42a625eecd550e9520047c5f1c2 Mon Sep 17 00:00:00 2001 From: tate Date: Thu, 11 Jan 2024 11:59:04 +1100 Subject: [PATCH] feat: dnssec oracle update --- .../ensjs/deploy/01_legacy_dnsregistrar.cjs | 120 ------------------ packages/ensjs/src/contracts/consts.ts | 4 +- packages/ensjs/src/contracts/dnsRegistrar.ts | 73 +++++++++-- packages/ensjs/src/contracts/dnssecImpl.ts | 32 ++--- packages/ensjs/src/contracts/index.ts | 3 +- .../functions/dns/getDnsImportData.test.ts | 103 ++------------- .../src/functions/dns/getDnsImportData.ts | 90 +++++-------- .../ensjs/src/functions/dns/importDnsName.ts | 15 +-- packages/ensjs/src/test/addTestContracts.ts | 4 +- 9 files changed, 129 insertions(+), 315 deletions(-) delete mode 100644 packages/ensjs/deploy/01_legacy_dnsregistrar.cjs diff --git a/packages/ensjs/deploy/01_legacy_dnsregistrar.cjs b/packages/ensjs/deploy/01_legacy_dnsregistrar.cjs deleted file mode 100644 index d594c4b0..00000000 --- a/packages/ensjs/deploy/01_legacy_dnsregistrar.cjs +++ /dev/null @@ -1,120 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -/* eslint-disable no-await-in-loop */ -/* eslint-disable import/no-extraneous-dependencies */ -const { readFile } = require('fs/promises') -const { resolve } = require('path') -const { labelhash } = require('viem') - -const ensContractsPath = './node_modules/@ensdomains/ens-contracts' -const mainnetArtifactsPath = resolve(ensContractsPath, './deployments/mainnet') - -const constructorArgs = [ - '0x00002b000100000e1000244a5c080249aac11d7b6f6446702e54a1607371607a1a41855200fd2ce1cdde32f24e8fb500002b000100000e1000244f660802e06d44b80b8f1d39a95c0b0d7c65d08458e880409bbc683457104237c7f8ec8d', -] - -/** - * @param {string} name - */ -const getMainnetArtifact = async (name) => { - const deployment = JSON.parse( - await readFile(resolve(mainnetArtifactsPath, `${name}.json`), 'utf8'), - ) - const artifact = { - _format: 'hh-sol-artifact-1', - contractName: `Legacy${name}`, - abi: deployment.abi, - bytecode: deployment.bytecode, - deployedBytecode: deployment.deployedBytecode, - linkReferences: {}, - deployedLinkReferences: {}, - } - return artifact -} - -/** - * @type {import('hardhat-deploy/types').DeployFunction} - */ -const func = async function (hre) { - const { getNamedAccounts, deployments } = hre - const { deploy } = deployments - const { deployer, owner } = await getNamedAccounts() - - const { address: legacyDnssecImplAddress } = await deploy( - 'LegacyDNSSECImpl', - { - from: deployer, - args: constructorArgs, - log: true, - contract: await getMainnetArtifact('DNSSECImpl'), - }, - ) - - const dnssec = await hre.ethers.getContract('LegacyDNSSECImpl') - - const algorithms = { - 5: 'RSASHA1Algorithm', - 7: 'RSASHA1Algorithm', - 8: 'RSASHA256Algorithm', - 13: 'P256SHA256Algorithm', - } - const digests = { - 1: 'SHA1Digest', - 2: 'SHA256Digest', - } - - const transactions = [] - for (const [id, alg] of Object.entries(algorithms)) { - const { address } = await deployments.get(alg) - if (address !== (await dnssec.algorithms(id))) { - transactions.push(await dnssec.setAlgorithm(id, address)) - } - } - - for (const [id, digest] of Object.entries(digests)) { - const { address } = await deployments.get(digest) - if (address !== (await dnssec.digests(id))) { - transactions.push(await dnssec.setDigest(id, address)) - } - } - - console.log( - `Waiting on ${transactions.length} transactions setting legacy DNSSEC parameters`, - ) - await Promise.all(transactions.map((tx) => tx.wait())) - - const root = await hre.ethers.getContract('Root') - const { address: registryAddress } = await hre.ethers.getContract( - 'ENSRegistry', - ) - const { address: publicSuffixListAddress } = await hre.ethers.getContract( - 'TLDPublicSuffixList', - ) - - const { address: legacyDnsRegistrarAddress } = await deploy( - 'LegacyDNSRegistrar', - { - from: deployer, - args: [legacyDnssecImplAddress, publicSuffixListAddress, registryAddress], - log: true, - contract: await getMainnetArtifact('DNSRegistrar'), - }, - ) - - const tx = await root - .connect(await hre.ethers.getSigner(owner)) - .setController(legacyDnsRegistrarAddress, true) - console.log( - `Setting LegacyDNSRegistrar as controller of Root... (${tx.hash})`, - ) - await tx.wait() - - const tx2 = await root - .connect(await hre.ethers.getSigner(owner)) - .setSubnodeOwner(labelhash('xyz'), legacyDnsRegistrarAddress) - console.log(`Setting LegacyDNSRegistrar as owner of xyz... (${tx2.hash})`) - await tx2.wait() -} - -func.dependencies = ['Root'] - -module.exports = func diff --git a/packages/ensjs/src/contracts/consts.ts b/packages/ensjs/src/contracts/consts.ts index 647e94af..c7dfdfb1 100644 --- a/packages/ensjs/src/contracts/consts.ts +++ b/packages/ensjs/src/contracts/consts.ts @@ -102,7 +102,7 @@ export const addresses = { address: '0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85', }, ensDnsRegistrar: { - address: '0x537625B0D7901FD20C57850d61580Bf1624Ef146', + address: '0x5a07C75Ae469Bf3ee2657B588e8E6ABAC6741b4f', }, ensEthRegistrarController: { address: '0xFED6a969AaA60E4961FCD3EBF1A2e8913ac65B72', @@ -120,7 +120,7 @@ export const addresses = { address: '0x4EF77b90762Eddb33C8Eba5B5a19558DaE53D7a1', }, ensDnssecImpl: { - address: '0x7b3ada1c8f012bae747cf99d6cbbf70d040b84cf', + address: '0xe62E4b6cE018Ad6e916fcC24545e20a33b9d8653', }, ensUniversalResolver: { address: '0xBaBC7678D7A63104f1658c11D6AE9A21cdA09725', diff --git a/packages/ensjs/src/contracts/dnsRegistrar.ts b/packages/ensjs/src/contracts/dnsRegistrar.ts index 1f1aeba2..64b28a36 100644 --- a/packages/ensjs/src/contracts/dnsRegistrar.ts +++ b/packages/ensjs/src/contracts/dnsRegistrar.ts @@ -1,4 +1,66 @@ +export const dnsRegistrarErrors = [ + { + inputs: [ + { + internalType: 'bytes', + name: 'name', + type: 'bytes', + }, + ], + name: 'InvalidPublicSuffix', + type: 'error', + }, + { + inputs: [], + name: 'NoOwnerRecordFound', + type: 'error', + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'offset', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'length', + type: 'uint256', + }, + ], + name: 'OffsetOutOfBoundsError', + type: 'error', + }, + { + inputs: [ + { + internalType: 'address', + name: 'caller', + type: 'address', + }, + { + internalType: 'address', + name: 'owner', + type: 'address', + }, + ], + name: 'PermissionDenied', + type: 'error', + }, + { + inputs: [], + name: 'PreconditionNotMet', + type: 'error', + }, + { + inputs: [], + name: 'StaleProof', + type: 'error', + }, +] as const + export const dnsRegistrarProveAndClaimSnippet = [ + ...dnsRegistrarErrors, { inputs: [ { @@ -19,10 +81,6 @@ export const dnsRegistrarProveAndClaimSnippet = [ name: 'input', type: 'tuple[]', }, - { - name: 'proof', - type: 'bytes', - }, ], name: 'proveAndClaim', outputs: [], @@ -32,6 +90,7 @@ export const dnsRegistrarProveAndClaimSnippet = [ ] as const export const dnsRegistrarProveAndClaimWithResolverSnippet = [ + ...dnsRegistrarErrors, { inputs: [ { @@ -41,10 +100,12 @@ export const dnsRegistrarProveAndClaimWithResolverSnippet = [ { components: [ { + internalType: 'bytes', name: 'rrset', type: 'bytes', }, { + internalType: 'bytes', name: 'sig', type: 'bytes', }, @@ -52,10 +113,6 @@ export const dnsRegistrarProveAndClaimWithResolverSnippet = [ name: 'input', type: 'tuple[]', }, - { - name: 'proof', - type: 'bytes', - }, { name: 'resolver', type: 'address', diff --git a/packages/ensjs/src/contracts/dnssecImpl.ts b/packages/ensjs/src/contracts/dnssecImpl.ts index 912284d7..92e49b9e 100644 --- a/packages/ensjs/src/contracts/dnssecImpl.ts +++ b/packages/ensjs/src/contracts/dnssecImpl.ts @@ -1,29 +1,31 @@ -export const dnssecImplRrDataSnippet = [ +export const dnssecImplVerifyRrSetSnippet = [ { inputs: [ { - name: 'dnstype', - type: 'uint16', - }, - { - name: 'name', - type: 'bytes', + components: [ + { + name: 'rrset', + type: 'bytes', + }, + { + name: 'sig', + type: 'bytes', + }, + ], + name: 'input', + type: 'tuple[]', }, ], - name: 'rrdata', + name: 'verifyRRSet', outputs: [ { - name: '', - type: 'uint32', + name: 'rrs', + type: 'bytes', }, { - name: '', + name: 'inception', type: 'uint32', }, - { - name: '', - type: 'bytes20', - }, ], stateMutability: 'view', type: 'function', diff --git a/packages/ensjs/src/contracts/index.ts b/packages/ensjs/src/contracts/index.ts index 3948ee30..a74370f3 100644 --- a/packages/ensjs/src/contracts/index.ts +++ b/packages/ensjs/src/contracts/index.ts @@ -24,12 +24,13 @@ export { type WalletWithEns, } from './consts.js' export { + dnsRegistrarErrors, dnsRegistrarProveAndClaimSnippet, dnsRegistrarProveAndClaimWithResolverSnippet, } from './dnsRegistrar.js' export { dnssecImplAnchorsSnippet, - dnssecImplRrDataSnippet, + dnssecImplVerifyRrSetSnippet, } from './dnssecImpl.js' export { erc165SupportsInterfaceSnippet } from './erc165.js' export { diff --git a/packages/ensjs/src/functions/dns/getDnsImportData.test.ts b/packages/ensjs/src/functions/dns/getDnsImportData.test.ts index 4bc5fd71..21e5e6d2 100644 --- a/packages/ensjs/src/functions/dns/getDnsImportData.test.ts +++ b/packages/ensjs/src/functions/dns/getDnsImportData.test.ts @@ -1,48 +1,25 @@ import { SignedSet } from '@ensdomains/dnsprovejs' -import type { Address, Hex } from 'viem' -import { - publicClient, - testClient, - waitForTransaction, - walletClient, -} from '../../test/addTestContracts.js' +import { toBytes } from 'viem' +import { publicClient } from '../../test/addTestContracts.js' import getDnsImportData, { type RrSetWithSig } from './getDnsImportData.js' -import importDnsName from './importDnsName.js' - -let snapshot: Hex -let accounts: Address[] - -beforeAll(async () => { - accounts = await walletClient.getAddresses() -}) - -beforeEach(async () => { - snapshot = await testClient.snapshot() -}) - -afterEach(async () => { - await testClient.revert({ id: snapshot }) -}) const decodeProofs = (proofs: RrSetWithSig[]) => proofs.map((proof) => - SignedSet.fromWire(proof.rrset as Buffer, proof.sig as Buffer), + SignedSet.fromWire( + toBytes(proof.rrset) as Buffer, + toBytes(proof.sig) as Buffer, + ), ) jest.setTimeout(10000) jest.retryTimes(2) -const wait = async (ms: number) => - // eslint-disable-next-line no-promise-executor-return - new Promise((resolve) => setTimeout(resolve, ms)) - -it('returns all rrsets when no proofs are known', async () => { +it('returns all rrsets', async () => { const result = await getDnsImportData(publicClient, { name: 'taytems.xyz', }) - expect(result.rrsets.length).toBeGreaterThan(0) - expect(result.proof).toBeInstanceOf(Uint8Array) - const decodedProofs = decodeProofs(result.rrsets) + expect(result.length).toBeGreaterThan(0) + const decodedProofs = decodeProofs(result) const rootProofs = decodedProofs.filter((x) => x.signature.name === '.') const tldProofs = decodedProofs.filter((x) => x.signature.name === 'xyz') const twoLDProofs = decodedProofs.filter( @@ -56,65 +33,3 @@ it('returns all rrsets when no proofs are known', async () => { expect(twoLDProofs.length).toBeGreaterThan(0) expect(threeLDProofs.length).toBeGreaterThan(0) }) -it('returns rrsets up to the first unknown proof', async () => { - const tx = await importDnsName(walletClient, { - name: 'taytems.xyz', - account: accounts[0], - dnsImportData: await getDnsImportData(publicClient, { - name: 'taytems.xyz', - }), - }) - expect(tx).toBeTruthy() - const receipt = await waitForTransaction(tx) - expect(receipt.status).toBe('success') - - await wait(5000) - - const result = await getDnsImportData(publicClient, { - name: 'lenster.xyz', - }) - const decodedProofs = decodeProofs(result.rrsets) - const rootProofs = decodedProofs.filter((x) => x.signature.name === '.') - const tldProofs = decodedProofs.filter((x) => x.signature.name === 'xyz') - const twoLDProofs = decodedProofs.filter( - (x) => x.signature.name === 'lenster.xyz', - ) - const threeLDProofs = decodedProofs.filter( - (x) => x.signature.name === '_ens.lenster.xyz', - ) - expect(rootProofs).toHaveLength(0) - expect(tldProofs).toHaveLength(0) - expect(twoLDProofs.length).toBeGreaterThan(0) - expect(threeLDProofs.length).toBeGreaterThan(0) -}) -it('returns empty rrsets for all known proofs when the last proof is known', async () => { - const tx = await importDnsName(walletClient, { - name: 'taytems.xyz', - account: accounts[0], - dnsImportData: await getDnsImportData(publicClient, { - name: 'taytems.xyz', - }), - }) - expect(tx).toBeTruthy() - const receipt = await waitForTransaction(tx) - expect(receipt.status).toBe('success') - - await wait(5000) - - const result = await getDnsImportData(publicClient, { - name: 'taytems.xyz', - }) - const decodedProofs = decodeProofs(result.rrsets) - const rootProofs = decodedProofs.filter((x) => x.signature.name === '.') - const tldProofs = decodedProofs.filter((x) => x.signature.name === 'xyz') - const twoLDProofs = decodedProofs.filter( - (x) => x.signature.name === 'taytems.xyz', - ) - const threeLDProofs = decodedProofs.filter( - (x) => x.signature.name === '_ens.taytems.xyz', - ) - expect(rootProofs).toHaveLength(0) - expect(tldProofs).toHaveLength(0) - expect(twoLDProofs).toHaveLength(0) - expect(threeLDProofs).toHaveLength(0) -}) diff --git a/packages/ensjs/src/functions/dns/getDnsImportData.ts b/packages/ensjs/src/functions/dns/getDnsImportData.ts index 345faa56..3e1bc3f2 100644 --- a/packages/ensjs/src/functions/dns/getDnsImportData.ts +++ b/packages/ensjs/src/functions/dns/getDnsImportData.ts @@ -1,16 +1,11 @@ -import type { ProvableAnswer, SignedSet } from '@ensdomains/dnsprovejs' +import { SignedSet, type ProvableAnswer } from '@ensdomains/dnsprovejs' import type * as packet from 'dns-packet' -import { toType } from 'dns-packet/types.js' -import { keccak256, toBytes, toHex } from 'viem' +import { toHex, type Hex } from 'viem' import { readContract } from 'viem/actions' import type { ClientWithEns } from '../../contracts/consts.js' -import { - dnssecImplAnchorsSnippet, - dnssecImplRrDataSnippet, -} from '../../contracts/dnssecImpl.js' +import { dnssecImplVerifyRrSetSnippet } from '../../contracts/dnssecImpl.js' import { getChainContractAddress } from '../../contracts/getChainContractAddress.js' -import { DnsNewerRecordTypeAvailableError } from '../../errors/dns.js' -import { packetToBytes } from '../../utils/hexEncodedName.js' +import { DnsNewerRecordTypeAvailableError } from '../../index.js' import type { Endpoint } from './types.js' export type GetDnsImportDataParameters = { @@ -21,14 +16,11 @@ export type GetDnsImportDataParameters = { } export type RrSetWithSig = { - rrset: Uint8Array - sig: Uint8Array + rrset: Hex + sig: Hex } -export type GetDnsImportDataReturnType = { - rrsets: RrSetWithSig[] - proof: Uint8Array -} +export type GetDnsImportDataReturnType = RrSetWithSig[] // Compares two serial numbers using RFC1982 serial number math. const serialNumberGt = (i1: number, i2: number): boolean => @@ -38,8 +30,8 @@ const encodeProofs = ( proofs: SignedSet[], ): RrSetWithSig[] => proofs.map((proof) => ({ - rrset: proof.toWire(true), - sig: proof.signature.data.signature, + rrset: toHex(proof.toWire(true)), + sig: toHex(proof.signature.data.signature), })) /** @@ -81,53 +73,31 @@ const getDnsImportData = async ( result.proofs as SignedSet[] ).concat([result.answer]) - const ensDnssecImplAddress = getChainContractAddress({ - client, - contract: 'ensDnssecImpl', + const rrsets = encodeProofs(allProofs) + + const [onchainRrData, inception] = await readContract(client, { + abi: dnssecImplVerifyRrSetSnippet, + address: getChainContractAddress({ + client, + contract: 'ensDnssecImpl', + }), + functionName: 'verifyRRSet', + args: [rrsets], }) - for (let i = allProofs.length - 1; i >= 0; i -= 1) { - const proof = allProofs[i] - const hexEncodedName = toHex(packetToBytes(proof.signature.name)) - const type = toType(proof.signature.data.typeCovered) - // eslint-disable-next-line no-await-in-loop - const [inception, expiration, hash] = await readContract(client, { - abi: dnssecImplRrDataSnippet, - address: ensDnssecImplAddress, - functionName: 'rrdata', - args: [type, hexEncodedName], + const lastProof = allProofs[allProofs.length - 1] + if (serialNumberGt(inception, lastProof.signature.data.inception)) + throw new DnsNewerRecordTypeAvailableError({ + typeCovered: lastProof.signature.data.typeCovered, + signatureName: lastProof.signature.name, + onchainInception: inception, + dnsInception: lastProof.signature.data.inception, }) - if (serialNumberGt(inception, proof.signature.data.inception)) - throw new DnsNewerRecordTypeAvailableError({ - typeCovered: proof.signature.data.typeCovered, - signatureName: proof.signature.name, - onchainInception: inception, - dnsInception: proof.signature.data.inception, - }) - const expired = serialNumberGt(Date.now() / 1000, expiration) - const proofHash = keccak256(proof.toWire(false)).slice(0, 42) - const isKnownProof = hash === proofHash && !expired - if (isKnownProof) { - if (i === allProofs.length - 1) { - return { rrsets: [], proof: proof.toWire(false) } - } - return { - rrsets: encodeProofs(allProofs.slice(i + 1, allProofs.length)), - proof: proof.toWire(false), - } - } - } - return { - rrsets: encodeProofs(allProofs), - proof: toBytes( - await readContract(client, { - abi: dnssecImplAnchorsSnippet, - address: ensDnssecImplAddress, - functionName: 'anchors', - }), - ), - } + if (toHex(lastProof.toWire(false)) !== onchainRrData) + throw new Error('Mismatched proof data') + + return rrsets } export default getDnsImportData diff --git a/packages/ensjs/src/functions/dns/importDnsName.ts b/packages/ensjs/src/functions/dns/importDnsName.ts index 87234b7f..dff1ab1c 100644 --- a/packages/ensjs/src/functions/dns/importDnsName.ts +++ b/packages/ensjs/src/functions/dns/importDnsName.ts @@ -1,5 +1,4 @@ import { - bytesToHex, encodeFunctionData, toHex, type Account, @@ -72,10 +71,6 @@ export const makeFunctionData = < resolverAddress, }: ImportDnsNameDataParameters, ): ImportDnsNameDataReturnType => { - const data = dnsImportData.rrsets.map((rrset) => ({ - rrset: bytesToHex(rrset.rrset), - sig: bytesToHex(rrset.sig), - })) const hexEncodedName = toHex(packetToBytes(name)) const dnsRegistrarAddress = getChainContractAddress({ client: wallet, @@ -95,7 +90,7 @@ export const makeFunctionData = < data: encodeFunctionData({ abi: dnsRegistrarProveAndClaimSnippet, functionName: 'proveAndClaim', - args: [hexEncodedName, data, bytesToHex(dnsImportData.proof)], + args: [hexEncodedName, dnsImportData], }), } } @@ -109,13 +104,7 @@ export const makeFunctionData = < data: encodeFunctionData({ abi: dnsRegistrarProveAndClaimWithResolverSnippet, functionName: 'proveAndClaimWithResolver', - args: [ - hexEncodedName, - data, - bytesToHex(dnsImportData.proof), - resolverAddress_, - address, - ], + args: [hexEncodedName, dnsImportData, resolverAddress_, address], }), } } diff --git a/packages/ensjs/src/test/addTestContracts.ts b/packages/ensjs/src/test/addTestContracts.ts index a78110b4..8993eddb 100644 --- a/packages/ensjs/src/test/addTestContracts.ts +++ b/packages/ensjs/src/test/addTestContracts.ts @@ -63,7 +63,7 @@ export const localhost = { address: deploymentAddresses.BaseRegistrarImplementation, }, ensDnsRegistrar: { - address: deploymentAddresses.LegacyDNSRegistrar, + address: deploymentAddresses.DNSRegistrar, }, ensEthRegistrarController: { address: deploymentAddresses.ETHRegistrarController, @@ -81,7 +81,7 @@ export const localhost = { address: deploymentAddresses.StaticBulkRenewal, }, ensDnssecImpl: { - address: deploymentAddresses.LegacyDNSSECImpl, + address: deploymentAddresses.DNSSECImpl, }, }, subgraphs: {