From 975eacaf7adf43bb4383c7b0318d9af860bbe508 Mon Sep 17 00:00:00 2001
From: achingbrain <alex@achingbrain.net>
Date: Wed, 11 Sep 2024 16:27:52 +0100
Subject: [PATCH 1/3] fix!: update to libp2p@2.x.x deps

- Updates method names so they can be invidually imported but still be legible
- Operates on PrivateKey/PublicKeys instead of PeerIds to integrate with the libp2p@2.x.x keychain will less friction

BREAKING CHANGE: uses libp2p@2.x.x deps, operates on PrivateKey/PublicKeys instead of PeerIds
---
 README.md                |  15 ---
 package.json             |  24 +++--
 src/errors.ts            |  84 ++++++++++++++---
 src/index.ts             |  85 ++++++-----------
 src/pb/ipns.ts           |  39 +++++---
 src/selector.ts          |   4 +-
 src/utils.ts             |  96 ++++++++++---------
 src/validator.ts         |  41 +++++---
 test/conformance.spec.ts |  48 ++++++----
 test/index.spec.ts       | 197 ++++++++++++++++++++-------------------
 test/selector.spec.ts    |  31 +++---
 test/validator.spec.ts   |  74 +++++++--------
 12 files changed, 391 insertions(+), 347 deletions(-)

diff --git a/README.md b/README.md
index a5d9d17..f8d61c3 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,6 @@
   - [Validate record](#validate-record)
   - [Embed public key to record](#embed-public-key-to-record)
   - [Extract public key from record](#extract-public-key-from-record)
-  - [Datastore key](#datastore-key)
   - [Marshal data with proto buffer](#marshal-data-with-proto-buffer)
   - [Unmarshal data from proto buffer](#unmarshal-data-from-proto-buffer)
   - [Validator](#validator)
@@ -82,20 +81,6 @@ import * as ipns from 'ipns'
 const publicKey = await ipns.extractPublicKey(peerId, ipnsRecord)
 ```
 
-### Datastore key
-
-```js
-import * as ipns from 'ipns'
-
-ipns.getLocalKey(peerId)
-```
-
-Returns a key to be used for storing the IPNS record locally, that is:
-
-```
-/ipns/${base32(<HASH>)}
-```
-
 ### Marshal data with proto buffer
 
 ```js
diff --git a/package.json b/package.json
index 339dced..337a02a 100644
--- a/package.json
+++ b/package.json
@@ -166,23 +166,21 @@
     "docs:no-publish": "NODE_OPTIONS=--max_old_space_size=8192 aegir docs --publish false"
   },
   "dependencies": {
-    "@libp2p/crypto": "^4.0.0",
-    "@libp2p/interface": "^1.1.0",
-    "@libp2p/logger": "^4.0.3",
-    "@libp2p/peer-id": "^4.0.3",
-    "cborg": "^4.0.1",
-    "err-code": "^3.0.1",
-    "interface-datastore": "^8.1.0",
-    "multiformats": "^13.0.0",
-    "protons-runtime": "^5.2.1",
-    "timestamp-nano": "^1.0.0",
+    "@libp2p/crypto": "^5.0.0",
+    "@libp2p/interface": "^2.0.0",
+    "@libp2p/logger": "^5.0.0",
+    "cborg": "^4.2.3",
+    "interface-datastore": "^8.3.0",
+    "multiformats": "^13.2.2",
+    "protons-runtime": "^5.5.0",
+    "timestamp-nano": "^1.0.1",
     "uint8arraylist": "^2.4.8",
-    "uint8arrays": "^5.0.1"
+    "uint8arrays": "^5.1.0"
   },
   "devDependencies": {
-    "@libp2p/peer-id-factory": "^4.0.2",
+    "@libp2p/peer-id": "^5.0.0",
     "aegir": "^44.1.1",
-    "protons": "^7.3.3"
+    "protons": "^7.6.0"
   },
   "sideEffects": false
 }
diff --git a/src/errors.ts b/src/errors.ts
index 969b5eb..4075c76 100644
--- a/src/errors.ts
+++ b/src/errors.ts
@@ -1,13 +1,71 @@
-export const ERR_IPNS_EXPIRED_RECORD = 'ERR_IPNS_EXPIRED_RECORD'
-export const ERR_UNRECOGNIZED_VALIDITY = 'ERR_UNRECOGNIZED_VALIDITY'
-export const ERR_SIGNATURE_CREATION = 'ERR_SIGNATURE_CREATION'
-export const ERR_SIGNATURE_VERIFICATION = 'ERR_SIGNATURE_VERIFICATION'
-export const ERR_UNRECOGNIZED_FORMAT = 'ERR_UNRECOGNIZED_FORMAT'
-export const ERR_PEER_ID_FROM_PUBLIC_KEY = 'ERR_PEER_ID_FROM_PUBLIC_KEY'
-export const ERR_PUBLIC_KEY_FROM_ID = 'ERR_PUBLIC_KEY_FROM_ID'
-export const ERR_UNDEFINED_PARAMETER = 'ERR_UNDEFINED_PARAMETER'
-export const ERR_INVALID_RECORD_DATA = 'ERR_INVALID_RECORD_DATA'
-export const ERR_INVALID_VALUE = 'ERR_INVALID_VALUE'
-export const ERR_INVALID_EMBEDDED_KEY = 'ERR_INVALID_EMBEDDED_KEY'
-export const ERR_MISSING_PRIVATE_KEY = 'ERR_MISSING_PRIVATE_KEY'
-export const ERR_RECORD_TOO_LARGE = 'ERR_RECORD_TOO_LARGE'
+export class SignatureCreationError extends Error {
+  static name = 'SignatureCreationError'
+
+  constructor (message = 'Record signature creation failed') {
+    super(message)
+    this.name = 'SignatureCreationError'
+  }
+}
+
+export class SignatureVerificationError extends Error {
+  static name = 'SignatureVerificationError'
+
+  constructor (message = 'Record signature verification failed') {
+    super(message)
+    this.name = 'SignatureVerificationError'
+  }
+}
+
+export class RecordExpiredError extends Error {
+  static name = 'RecordExpiredError'
+
+  constructor (message = 'Record has expired') {
+    super(message)
+    this.name = 'RecordExpiredError'
+  }
+}
+
+export class UnsupportedValidityError extends Error {
+  static name = 'UnsupportedValidityError'
+
+  constructor (message = 'The validity type is unsupported') {
+    super(message)
+    this.name = 'UnsupportedValidityError'
+  }
+}
+
+export class RecordTooLargeError extends Error {
+  static name = 'RecordTooLargeError'
+
+  constructor (message = 'The record is too large') {
+    super(message)
+    this.name = 'RecordTooLargeError'
+  }
+}
+
+export class InvalidValueError extends Error {
+  static name = 'InvalidValueError'
+
+  constructor (message = 'Value must be a valid content path starting with /') {
+    super(message)
+    this.name = 'InvalidValueError'
+  }
+}
+
+export class InvalidRecordDataError extends Error {
+  static name = 'InvalidRecordDataError'
+
+  constructor (message = 'Invalid record data') {
+    super(message)
+    this.name = 'InvalidRecordDataError'
+  }
+}
+
+export class InvalidEmbeddedPublicKeyError extends Error {
+  static name = 'InvalidEmbeddedPublicKeyError'
+
+  constructor (message = 'Invalid embedded public key') {
+    super(message)
+    this.name = 'InvalidEmbeddedPublicKeyError'
+  }
+}
diff --git a/src/index.ts b/src/index.ts
index f6373cc..7dca7c9 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,21 +1,15 @@
-import { unmarshalPrivateKey } from '@libp2p/crypto/keys'
+import { publicKeyToProtobuf } from '@libp2p/crypto/keys'
 import { logger } from '@libp2p/logger'
-import errCode from 'err-code'
-import { Key } from 'interface-datastore/key'
-import { base32upper } from 'multiformats/bases/base32'
-import * as Digest from 'multiformats/hashes/digest'
-import { identity } from 'multiformats/hashes/identity'
+import { type Key } from 'interface-datastore/key'
 import NanoDate from 'timestamp-nano'
-import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
 import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
-import * as ERRORS from './errors.js'
+import { SignatureCreationError } from './errors.js'
 import { IpnsEntry } from './pb/ipns.js'
 import { createCborData, ipnsRecordDataForV1Sig, ipnsRecordDataForV2Sig, normalizeValue } from './utils.js'
-import type { PrivateKey, PeerId } from '@libp2p/interface'
+import type { PrivateKey, PublicKey } from '@libp2p/interface'
 import type { CID } from 'multiformats/cid'
 
 const log = logger('ipns')
-const ID_MULTIHASH_CODE = identity.code
 const DEFAULT_TTL_NS = 60 * 60 * 1e+9 // 1 Hour or 3600 Seconds
 
 export const namespace = '/ipns/'
@@ -157,22 +151,22 @@ const defaultCreateOptions: CreateOptions = {
  * * PeerIDs will create recursive records, eg. the record value will be `/ipns/${cidV1Libp2pKey}`
  * * String paths will be stored in the record as-is, but they must start with `"/"`
  *
- * @param {PeerId} peerId - peer id containing private key for signing the record.
- * @param {CID | PeerId | string} value - content to be stored in the record.
+ * @param {PrivateKey} privateKey - the private key for signing the record.
+ * @param {CID | PublicKey | string} value - content to be stored in the record.
  * @param {number | bigint} seq - number representing the current version of the record.
  * @param {number} lifetime - lifetime of the record (in milliseconds).
  * @param {CreateOptions} options - additional create options.
  */
-export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
-export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise<IPNSRecordV2>
-export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options: CreateOptions): Promise<IPNSRecordV1V2>
-export async function create (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
+export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
+export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise<IPNSRecordV2>
+export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options: CreateOptions): Promise<IPNSRecordV1V2>
+export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
   // Validity in ISOString with nanoseconds precision and validity type EOL
   const expirationDate = new NanoDate(Date.now() + Number(lifetime))
   const validityType = IpnsEntry.ValidityType.EOL
   const ttlNs = BigInt(options.ttlNs ?? DEFAULT_TTL_NS)
 
-  return _create(peerId, value, seq, validityType, expirationDate.toString(), ttlNs, options)
+  return _create(privateKey, value, seq, validityType, expirationDate.toString(), ttlNs, options)
 }
 
 /**
@@ -185,34 +179,28 @@ export async function create (peerId: PeerId, value: CID | PeerId | string, seq:
  * * PeerIDs will create recursive records, eg. the record value will be `/ipns/${cidV1Libp2pKey}`
  * * String paths will be stored in the record as-is, but they must start with `"/"`
  *
- * @param {PeerId} peerId - PeerId containing private key for signing the record.
- * @param {CID | PeerId | string} value - content to be stored in the record.
+ * @param {PrivateKey} privateKey - the private key for signing the record.
+ * @param {CID | PublicKey | string} value - content to be stored in the record.
  * @param {number | bigint} seq - number representing the current version of the record.
  * @param {string} expiration - expiration datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
  * @param {CreateOptions} options - additional creation options.
  */
-export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
-export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateV2Options): Promise<IPNSRecordV2>
-export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateOptions): Promise<IPNSRecordV1V2>
-export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
+export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
+export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options: CreateV2Options): Promise<IPNSRecordV2>
+export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options: CreateOptions): Promise<IPNSRecordV1V2>
+export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
   const expirationDate = NanoDate.fromString(expiration)
   const validityType = IpnsEntry.ValidityType.EOL
   const ttlNs = BigInt(options.ttlNs ?? DEFAULT_TTL_NS)
 
-  return _create(peerId, value, seq, validityType, expirationDate.toString(), ttlNs, options)
+  return _create(privateKey, value, seq, validityType, expirationDate.toString(), ttlNs, options)
 }
 
-const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, validity: string, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
+const _create = async (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, validity: string, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
   seq = BigInt(seq)
   const isoValidity = uint8ArrayFromString(validity)
   const normalizedValue = normalizeValue(value)
   const encodedValue = uint8ArrayFromString(normalizedValue)
-
-  if (peerId.privateKey == null) {
-    throw errCode(new Error('Missing private key'), ERRORS.ERR_MISSING_PRIVATE_KEY)
-  }
-
-  const privateKey = await unmarshalPrivateKey(peerId.privateKey)
   const data = createCborData(encodedValue, validityType, isoValidity, seq, ttl)
   const sigData = ipnsRecordDataForV2Sig(data)
   const signatureV2 = await privateKey.sign(sigData)
@@ -220,12 +208,8 @@ const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number
 
   // if we cannot derive the public key from the PeerId (e.g. RSA PeerIDs),
   // we have to embed it in the IPNS record
-  if (peerId.publicKey != null) {
-    const digest = Digest.decode(peerId.toBytes())
-
-    if (digest.code !== ID_MULTIHASH_CODE || !uint8ArrayEquals(peerId.publicKey, digest.digest)) {
-      pubKey = peerId.publicKey
-    }
+  if (privateKey.type === 'RSA') {
+    pubKey = publicKeyToProtobuf(privateKey.publicKey)
   }
 
   if (options.v1Compatible === true) {
@@ -266,24 +250,13 @@ const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number
   }
 }
 
-/**
- * rawStdEncoding with RFC4648
- */
-const rawStdEncoding = (key: Uint8Array): string => base32upper.encode(key).slice(1)
-
-/**
- * Get key for storing the record locally.
- * Format: /ipns/${base32(<HASH>)}
- *
- * @param {Uint8Array} key - peer identifier object.
- */
-export const getLocalKey = (key: Uint8Array): Key => new Key(`/ipns/${rawStdEncoding(key)}`)
-
-export { unmarshal } from './utils.js'
-export { marshal } from './utils.js'
-export { peerIdToRoutingKey } from './utils.js'
-export { peerIdFromRoutingKey } from './utils.js'
-export { extractPublicKey } from './utils.js'
+export { unmarshalIPNSRecord } from './utils.js'
+export { marshalIPNSRecord } from './utils.js'
+export { multihashToIPNSRoutingKey } from './utils.js'
+export { multihashFromIPNSRoutingKey } from './utils.js'
+export { publicKeyToIPNSRoutingKey } from './utils.js'
+export { publicKeyFromIPNSRoutingKey } from './utils.js'
+export { extractPublicKeyFromIPNSRecord } from './utils.js'
 
 /**
  * Sign ipns record data using the legacy V1 signature scheme
@@ -295,6 +268,6 @@ const signLegacyV1 = async (privateKey: PrivateKey, value: Uint8Array, validityT
     return await privateKey.sign(dataForSignature)
   } catch (error: any) {
     log.error('record signature creation failed', error)
-    throw errCode(new Error('record signature creation failed'), ERRORS.ERR_SIGNATURE_CREATION)
+    throw new SignatureCreationError('Record signature creation failed')
   }
 }
diff --git a/src/pb/ipns.ts b/src/pb/ipns.ts
index fb18014..db74fb7 100644
--- a/src/pb/ipns.ts
+++ b/src/pb/ipns.ts
@@ -4,8 +4,7 @@
 /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */
 /* eslint-disable @typescript-eslint/no-empty-interface */
 
-import { enumeration, encodeMessage, decodeMessage, message } from 'protons-runtime'
-import type { Codec } from 'protons-runtime'
+import { type Codec, decodeMessage, type DecodeOptions, encodeMessage, enumeration, message } from 'protons-runtime'
 import type { Uint8ArrayList } from 'uint8arraylist'
 
 export interface IpnsEntry {
@@ -92,7 +91,7 @@ export namespace IpnsEntry {
         if (opts.lengthDelimited !== false) {
           w.ldelim()
         }
-      }, (reader, length) => {
+      }, (reader, length, opts = {}) => {
         const obj: any = {}
 
         const end = length == null ? reader.len : reader.pos + length
@@ -101,36 +100,46 @@ export namespace IpnsEntry {
           const tag = reader.uint32()
 
           switch (tag >>> 3) {
-            case 1:
+            case 1: {
               obj.value = reader.bytes()
               break
-            case 2:
+            }
+            case 2: {
               obj.signatureV1 = reader.bytes()
               break
-            case 3:
+            }
+            case 3: {
               obj.validityType = IpnsEntry.ValidityType.codec().decode(reader)
               break
-            case 4:
+            }
+            case 4: {
               obj.validity = reader.bytes()
               break
-            case 5:
+            }
+            case 5: {
               obj.sequence = reader.uint64()
               break
-            case 6:
+            }
+            case 6: {
               obj.ttl = reader.uint64()
               break
-            case 7:
+            }
+            case 7: {
               obj.pubKey = reader.bytes()
               break
-            case 8:
+            }
+            case 8: {
               obj.signatureV2 = reader.bytes()
               break
-            case 9:
+            }
+            case 9: {
               obj.data = reader.bytes()
               break
-            default:
+            }
+            default: {
               reader.skipType(tag & 7)
               break
+            }
           }
         }
 
@@ -145,7 +154,7 @@ export namespace IpnsEntry {
     return encodeMessage(obj, IpnsEntry.codec())
   }
 
-  export const decode = (buf: Uint8Array | Uint8ArrayList): IpnsEntry => {
-    return decodeMessage(buf, IpnsEntry.codec())
+  export const decode = (buf: Uint8Array | Uint8ArrayList, opts?: DecodeOptions<IpnsEntry>): IpnsEntry => {
+    return decodeMessage(buf, IpnsEntry.codec(), opts)
   }
 }
diff --git a/src/selector.ts b/src/selector.ts
index 5fab036..c669a5b 100644
--- a/src/selector.ts
+++ b/src/selector.ts
@@ -1,10 +1,10 @@
 import NanoDate from 'timestamp-nano'
 import { IpnsEntry } from './pb/ipns.js'
-import { unmarshal } from './utils.js'
+import { unmarshalIPNSRecord } from './utils.js'
 
 export function ipnsSelector (key: Uint8Array, data: Uint8Array[]): number {
   const entries = data.map((buf, index) => ({
-    record: unmarshal(buf),
+    record: unmarshalIPNSRecord(buf),
     index
   }))
 
diff --git a/src/utils.ts b/src/utils.ts
index 7df0c78..f210830 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,60 +1,42 @@
-import { unmarshalPublicKey } from '@libp2p/crypto/keys'
-import { isPeerId } from '@libp2p/interface'
+import { publicKeyFromMultihash, publicKeyFromProtobuf } from '@libp2p/crypto/keys'
+import { InvalidMultihashError } from '@libp2p/interface'
 import { logger } from '@libp2p/logger'
-import { peerIdFromBytes, peerIdFromKeys } from '@libp2p/peer-id'
 import * as cborg from 'cborg'
-import errCode from 'err-code'
 import { base36 } from 'multiformats/bases/base36'
-import { CID } from 'multiformats/cid'
+import { CID, type MultihashDigest } from 'multiformats/cid'
+import * as Digest from 'multiformats/hashes/digest'
 import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
 import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
 import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
 import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
-import * as ERRORS from './errors.js'
+import { InvalidRecordDataError, InvalidValueError, SignatureVerificationError, UnsupportedValidityError } from './errors.js'
 import { IpnsEntry } from './pb/ipns.js'
 import type { IPNSRecord, IPNSRecordV2, IPNSRecordData } from './index.js'
-import type { PublicKey, PeerId } from '@libp2p/interface'
+import type { PublicKey, Ed25519PublicKey, Secp256k1PublicKey } from '@libp2p/interface'
 
 const log = logger('ipns:utils')
 const IPNS_PREFIX = uint8ArrayFromString('/ipns/')
 const LIBP2P_CID_CODEC = 114
 
 /**
- * Extracts a public key from the passed PeerId, falling
- * back to the pubKey embedded in the ipns record
+ * Extracts a public key from the passed PeerId, falling back to the pubKey
+ * embedded in the ipns record
  */
-export const extractPublicKey = async (peerId: PeerId, record: IPNSRecord | IPNSRecordV2): Promise<PublicKey> => {
-  if (record == null || peerId == null) {
-    const error = new Error('one or more of the provided parameters are not defined')
-
-    log.error(error)
-    throw errCode(error, ERRORS.ERR_UNDEFINED_PARAMETER)
-  }
-
+export const extractPublicKeyFromIPNSRecord = (record: IPNSRecord | IPNSRecordV2): PublicKey | undefined => {
   let pubKey: PublicKey | undefined
 
   if (record.pubKey != null) {
     try {
-      pubKey = unmarshalPublicKey(record.pubKey)
+      pubKey = publicKeyFromProtobuf(record.pubKey)
     } catch (err) {
       log.error(err)
       throw err
     }
-
-    const otherId = await peerIdFromKeys(record.pubKey)
-
-    if (!otherId.equals(peerId)) {
-      throw errCode(new Error('Embedded public key did not match PeerID'), ERRORS.ERR_INVALID_EMBEDDED_KEY)
-    }
-  } else if (peerId.publicKey != null) {
-    pubKey = unmarshalPublicKey(peerId.publicKey)
   }
 
   if (pubKey != null) {
     return pubKey
   }
-
-  throw errCode(new Error('no public key is available'), ERRORS.ERR_UNDEFINED_PARAMETER)
 }
 
 /**
@@ -75,7 +57,7 @@ export const ipnsRecordDataForV2Sig = (data: Uint8Array): Uint8Array => {
   return uint8ArrayConcat([entryData, data])
 }
 
-export const marshal = (obj: IPNSRecord | IPNSRecordV2): Uint8Array => {
+export const marshalIPNSRecord = (obj: IPNSRecord | IPNSRecordV2): Uint8Array => {
   if ('signatureV1' in obj) {
     return IpnsEntry.encode({
       value: uint8ArrayFromString(obj.value),
@@ -97,7 +79,7 @@ export const marshal = (obj: IPNSRecord | IPNSRecordV2): Uint8Array => {
   }
 }
 
-export function unmarshal (buf: Uint8Array): IPNSRecord {
+export function unmarshalIPNSRecord (buf: Uint8Array): IPNSRecord {
   const message = IpnsEntry.decode(buf)
 
   // protobufjs returns bigints as numbers
@@ -114,7 +96,7 @@ export function unmarshal (buf: Uint8Array): IPNSRecord {
   // V1+V2 records for quite a while and we don't support V1-only records during
   // validation any more
   if (message.signatureV2 == null || message.data == null) {
-    throw errCode(new Error('missing data or signatureV2'), ERRORS.ERR_SIGNATURE_VERIFICATION)
+    throw new SignatureVerificationError('Missing data or signatureV2')
   }
 
   const data = parseCborData(message.data)
@@ -153,15 +135,33 @@ export function unmarshal (buf: Uint8Array): IPNSRecord {
   }
 }
 
-export const peerIdToRoutingKey = (peerId: PeerId): Uint8Array => {
+export const publicKeyToIPNSRoutingKey = (publicKey: PublicKey): Uint8Array => {
+  return multihashToIPNSRoutingKey(publicKey.toMultihash())
+}
+
+export const multihashToIPNSRoutingKey = (digest: MultihashDigest): Uint8Array => {
   return uint8ArrayConcat([
     IPNS_PREFIX,
-    peerId.toBytes()
+    digest.bytes
   ])
 }
 
-export const peerIdFromRoutingKey = (key: Uint8Array): PeerId => {
-  return peerIdFromBytes(key.slice(IPNS_PREFIX.length))
+export const publicKeyFromIPNSRoutingKey = (key: Uint8Array): Ed25519PublicKey | Secp256k1PublicKey | undefined => {
+  try {
+    // @ts-expect-error digest code may not be 0
+    return publicKeyFromMultihash(multihashFromIPNSRoutingKey(key))
+  } catch {}
+}
+
+export const multihashFromIPNSRoutingKey = (key: Uint8Array): MultihashDigest<0x00> | MultihashDigest<0x12> => {
+  const digest = Digest.decode(key.slice(IPNS_PREFIX.length))
+
+  if (digest.code !== 0x00 && digest.code !== 0x12) {
+    throw new InvalidMultihashError('Multihash in IPNS key was not identity or sha2-256')
+  }
+
+  // @ts-expect-error digest may not have correct code even though we just checked
+  return digest
 }
 
 export const createCborData = (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array, sequence: bigint, ttl: bigint): Uint8Array => {
@@ -170,7 +170,7 @@ export const createCborData = (value: Uint8Array, validityType: IpnsEntry.Validi
   if (validityType === IpnsEntry.ValidityType.EOL) {
     ValidityType = 0
   } else {
-    throw errCode(new Error('Unknown validity type'), ERRORS.ERR_UNRECOGNIZED_VALIDITY)
+    throw new UnsupportedValidityError('The validity type is unsupported')
   }
 
   const data = {
@@ -190,7 +190,7 @@ export const parseCborData = (buf: Uint8Array): IPNSRecordData => {
   if (data.ValidityType === 0) {
     data.ValidityType = IpnsEntry.ValidityType.EOL
   } else {
-    throw errCode(new Error('Unknown validity type'), ERRORS.ERR_UNRECOGNIZED_VALIDITY)
+    throw new UnsupportedValidityError('The validity type is unsupported')
   }
 
   if (Number.isInteger(data.Sequence)) {
@@ -211,10 +211,10 @@ export const parseCborData = (buf: Uint8Array): IPNSRecordData => {
  * string starting with '/'. PeerIDs become `/ipns/${cidV1Libp2pKey}`,
  * CIDs become `/ipfs/${cidAsV1}`.
  */
-export const normalizeValue = (value?: CID | PeerId | string | Uint8Array): string => {
+export const normalizeValue = (value?: CID | PublicKey | string | Uint8Array): string => {
   if (value != null) {
     // if we have a PeerId, turn it into an ipns path
-    if (isPeerId(value)) {
+    if (hasToCID(value)) {
       return `/ipns/${value.toCID().toString(base36)}`
     }
 
@@ -256,33 +256,37 @@ export const normalizeValue = (value?: CID | PeerId | string | Uint8Array): stri
     }
   }
 
-  throw errCode(new Error('Value must be a valid content path starting with /'), ERRORS.ERR_INVALID_VALUE)
+  throw new InvalidValueError('Value must be a valid content path starting with /')
 }
 
 const validateCborDataMatchesPbData = (entry: IpnsEntry): void => {
   if (entry.data == null) {
-    throw errCode(new Error('Record data is missing'), ERRORS.ERR_INVALID_RECORD_DATA)
+    throw new InvalidRecordDataError('Record data is missing')
   }
 
   const data = parseCborData(entry.data)
 
   if (!uint8ArrayEquals(data.Value, entry.value ?? new Uint8Array(0))) {
-    throw errCode(new Error('Field "value" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION)
+    throw new SignatureVerificationError('Field "value" did not match between protobuf and CBOR')
   }
 
   if (!uint8ArrayEquals(data.Validity, entry.validity ?? new Uint8Array(0))) {
-    throw errCode(new Error('Field "validity" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION)
+    throw new SignatureVerificationError('Field "validity" did not match between protobuf and CBOR')
   }
 
   if (data.ValidityType !== entry.validityType) {
-    throw errCode(new Error('Field "validityType" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION)
+    throw new SignatureVerificationError('Field "validityType" did not match between protobuf and CBOR')
   }
 
   if (data.Sequence !== entry.sequence) {
-    throw errCode(new Error('Field "sequence" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION)
+    throw new SignatureVerificationError('Field "sequence" did not match between protobuf and CBOR')
   }
 
   if (data.TTL !== entry.ttl) {
-    throw errCode(new Error('Field "ttl" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION)
+    throw new SignatureVerificationError('Field "ttl" did not match between protobuf and CBOR')
   }
 }
+
+function hasToCID (obj?: any): obj is { toCID(): CID } {
+  return typeof obj?.toCID === 'function'
+}
diff --git a/src/validator.ts b/src/validator.ts
index e24745d..c4a41ac 100644
--- a/src/validator.ts
+++ b/src/validator.ts
@@ -1,9 +1,9 @@
 import { logger } from '@libp2p/logger'
-import errCode from 'err-code'
 import NanoDate from 'timestamp-nano'
-import * as ERRORS from './errors.js'
+import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
+import { InvalidEmbeddedPublicKeyError, RecordExpiredError, RecordTooLargeError, SignatureVerificationError, UnsupportedValidityError } from './errors.js'
 import { IpnsEntry } from './pb/ipns.js'
-import { extractPublicKey, ipnsRecordDataForV2Sig, unmarshal, peerIdFromRoutingKey } from './utils.js'
+import { extractPublicKeyFromIPNSRecord, ipnsRecordDataForV2Sig, publicKeyFromIPNSRoutingKey, publicKeyToIPNSRoutingKey, unmarshalIPNSRecord } from './utils.js'
 import type { PublicKey } from '@libp2p/interface'
 
 const log = logger('ipns:validator')
@@ -21,30 +21,32 @@ export const validate = async (publicKey: PublicKey, buf: Uint8Array): Promise<v
   // unmarshal ensures that (1) SignatureV2 and Data are present, (2) that ValidityType
   // and Validity are of valid types and have a value, (3) that CBOR data matches protobuf
   // if it's a V1+V2 record.
-  const record = unmarshal(buf)
+  const record = unmarshalIPNSRecord(buf)
 
   // Validate Signature V2
   let isValid
+
   try {
     const dataForSignature = ipnsRecordDataForV2Sig(record.data)
     isValid = await publicKey.verify(dataForSignature, record.signatureV2)
   } catch (err) {
     isValid = false
   }
+
   if (!isValid) {
     log.error('record signature verification failed')
-    throw errCode(new Error('record signature verification failed'), ERRORS.ERR_SIGNATURE_VERIFICATION)
+    throw new SignatureVerificationError('Record signature verification failed')
   }
 
   // Validate according to the validity type
   if (record.validityType === IpnsEntry.ValidityType.EOL) {
     if (NanoDate.fromString(record.validity).toDate().getTime() < Date.now()) {
       log.error('record has expired')
-      throw errCode(new Error('record has expired'), ERRORS.ERR_IPNS_EXPIRED_RECORD)
+      throw new RecordExpiredError('record has expired')
     }
   } else if (record.validityType != null) {
-    log.error('unrecognized validity type')
-    throw errCode(new Error('unrecognized validity type'), ERRORS.ERR_UNRECOGNIZED_VALIDITY)
+    log.error('the validity type is unsupported')
+    throw new UnsupportedValidityError('The validity type is unsupported')
   }
 
   log('ipns record for %s is valid', record.value)
@@ -52,15 +54,26 @@ export const validate = async (publicKey: PublicKey, buf: Uint8Array): Promise<v
 
 export async function ipnsValidator (key: Uint8Array, marshalledData: Uint8Array): Promise<void> {
   if (marshalledData.byteLength > MAX_RECORD_SIZE) {
-    throw errCode(new Error('record too large'), ERRORS.ERR_RECORD_TOO_LARGE)
+    throw new RecordTooLargeError('The record is too large')
   }
 
-  const peerId = peerIdFromRoutingKey(key)
-  const receivedRecord = unmarshal(marshalledData)
+  // try to extract public key from routing key
+  const routingPubKey = publicKeyFromIPNSRoutingKey(key)
 
-  // extract public key
-  const pubKey = await extractPublicKey(peerId, receivedRecord)
+  // extract public key from record
+  const receivedRecord = unmarshalIPNSRecord(marshalledData)
+  const recordPubKey = extractPublicKeyFromIPNSRecord(receivedRecord) ?? routingPubKey
+
+  if (recordPubKey == null) {
+    throw new InvalidEmbeddedPublicKeyError('Could not extract public key from IPNS record or routing key')
+  }
+
+  const routingKey = publicKeyToIPNSRoutingKey(recordPubKey)
+
+  if (!uint8ArrayEquals(key, routingKey)) {
+    throw new InvalidEmbeddedPublicKeyError('Embedded public key did not match routing key')
+  }
 
   // Record validation
-  await validate(pubKey, marshalledData)
+  await validate(recordPubKey, marshalledData)
 }
diff --git a/test/conformance.spec.ts b/test/conformance.spec.ts
index ee1e046..78df60b 100644
--- a/test/conformance.spec.ts
+++ b/test/conformance.spec.ts
@@ -1,30 +1,34 @@
 /* eslint-env mocha */
 
-import { unmarshalPublicKey } from '@libp2p/crypto/keys'
 import { peerIdFromCID } from '@libp2p/peer-id'
 import { expect } from 'aegir/chai'
 import loadFixture from 'aegir/fixtures'
 import { base36 } from 'multiformats/bases/base36'
 import { CID } from 'multiformats/cid'
-import * as ERRORS from '../src/errors.js'
-import * as ipns from '../src/index.js'
+import { SignatureVerificationError } from '../src/errors.js'
+import { marshalIPNSRecord, unmarshalIPNSRecord } from '../src/index.js'
 import { validate } from '../src/validator.js'
 
 describe('conformance', function () {
   it('should reject a v1 only record', async () => {
     const buf = loadFixture('test/fixtures/k51qzi5uqu5dm4tm0wt8srkg9h9suud4wuiwjimndrkydqm81cqtlb5ak6p7ku_v1.ipns-record')
 
-    expect(() => ipns.unmarshal(buf)).to.throw(/missing data or signatureV2/)
-      .with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION)
+    expect(() => unmarshalIPNSRecord(buf)).to.throw(/Missing data or signatureV2/)
+      .with.property('name', SignatureVerificationError.name)
   })
 
   it('should validate a record with v1 and v2 signatures', async () => {
     const buf = loadFixture('test/fixtures/k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w_v1-v2.ipns-record')
-    const record = ipns.unmarshal(buf)
+    const record = unmarshalIPNSRecord(buf)
 
     const cid = CID.parse('k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w', base36)
     const peerId = peerIdFromCID(cid)
-    const publicKey = unmarshalPublicKey(peerId.publicKey ?? new Uint8Array())
+
+    if (peerId.publicKey == null) {
+      throw new Error('Peer ID embedded in CID had no public key')
+    }
+
+    const publicKey = peerId.publicKey
     await validate(publicKey, buf)
 
     expect(record.value).to.equal('/ipfs/bafkqaddwgevxmmraojswg33smq')
@@ -33,34 +37,44 @@ describe('conformance', function () {
   it('should reject a record with inconsistent value fields', async () => {
     const buf = loadFixture('test/fixtures/k51qzi5uqu5dlmit2tuwdvnx4sbnyqgmvbxftl0eo3f33wwtb9gr7yozae9kpw_v1-v2-broken-v1-value.ipns-record')
 
-    expect(() => ipns.unmarshal(buf)).to.throw(/Field "value" did not match/)
-      .with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION)
+    expect(() => unmarshalIPNSRecord(buf)).to.throw(/Field "value" did not match/)
+      .with.property('name', SignatureVerificationError.name)
   })
 
   it('should reject a record with v1 and v2 signatures but invalid v2', async () => {
     const buf = loadFixture('test/fixtures/k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c_v1-v2-broken-signature-v2.ipns-record')
     const cid = CID.parse('k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c', base36)
     const peerId = peerIdFromCID(cid)
-    const publicKey = unmarshalPublicKey(peerId.publicKey ?? new Uint8Array())
 
-    await expect(validate(publicKey, buf)).to.eventually.be.rejectedWith(/record signature verification failed/)
-      .with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION)
+    if (peerId.publicKey == null) {
+      throw new Error('Peer ID embedded in CID had no public key')
+    }
+
+    const publicKey = peerId.publicKey
+
+    await expect(validate(publicKey, buf)).to.eventually.be.rejectedWith(/Record signature verification failed/)
+      .with.property('name', SignatureVerificationError.name)
   })
 
   it('should reject a record with v1 and v2 signatures but invalid v1', async () => {
     const buf = loadFixture('test/fixtures/k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record')
-    const record = ipns.unmarshal(buf)
+    const record = unmarshalIPNSRecord(buf)
 
     expect(record.value).to.equal('/ipfs/bafkqahtwgevxmmrao5uxi2bamjzg623fnyqhg2lhnzqxi5lsmuqhmmi')
   })
 
   it('should validate a record with only v2 signature', async () => {
     const buf = loadFixture('test/fixtures/k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record')
-    const record = ipns.unmarshal(buf)
+    const record = unmarshalIPNSRecord(buf)
 
     const cid = CID.parse('k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f', base36)
     const peerId = peerIdFromCID(cid)
-    const publicKey = unmarshalPublicKey(peerId.publicKey ?? new Uint8Array())
+
+    if (peerId.publicKey == null) {
+      throw new Error('Peer ID embedded in CID had no public key')
+    }
+
+    const publicKey = peerId.publicKey
     await validate(publicKey, buf)
 
     expect(record.value).to.equal('/ipfs/bafkqadtwgiww63tmpeqhezldn5zgi')
@@ -76,8 +90,8 @@ describe('conformance', function () {
 
     for (const fixture of fixtures) {
       const buf = loadFixture(fixture)
-      const record = ipns.unmarshal(buf)
-      const marshalled = ipns.marshal(record)
+      const record = unmarshalIPNSRecord(buf)
+      const marshalled = marshalIPNSRecord(record)
 
       expect(buf).to.equalBytes(marshalled)
     }
diff --git a/test/index.spec.ts b/test/index.spec.ts
index 1800945..cdc9552 100644
--- a/test/index.spec.ts
+++ b/test/index.spec.ts
@@ -1,33 +1,32 @@
 /* eslint-env mocha */
 
 import { randomBytes } from '@libp2p/crypto'
-import { generateKeyPair, unmarshalPrivateKey } from '@libp2p/crypto/keys'
-import { peerIdFromKeys, peerIdFromString } from '@libp2p/peer-id'
-import { createEd25519PeerId } from '@libp2p/peer-id-factory'
+import { generateKeyPair, publicKeyToProtobuf } from '@libp2p/crypto/keys'
+import { peerIdFromPrivateKey } from '@libp2p/peer-id'
 import { expect } from 'aegir/chai'
 import * as cbor from 'cborg'
 import { base36 } from 'multiformats/bases/base36'
 import { base58btc } from 'multiformats/bases/base58'
 import { CID } from 'multiformats/cid'
+import * as Digest from 'multiformats/hashes/digest'
 import { toString as uint8ArrayToString } from 'uint8arrays'
 import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
-import * as ERRORS from '../src/errors.js'
-import * as ipns from '../src/index.js'
+import { InvalidEmbeddedPublicKeyError, InvalidValueError, RecordExpiredError, SignatureVerificationError } from '../src/errors.js'
+import { createIPNSRecord, createIPNSRecordWithExpiration } from '../src/index.js'
 import { IpnsEntry } from '../src/pb/ipns.js'
-import { extractPublicKey, peerIdToRoutingKey, parseCborData, createCborData, ipnsRecordDataForV2Sig } from '../src/utils.js'
+import { extractPublicKeyFromIPNSRecord, parseCborData, createCborData, ipnsRecordDataForV2Sig, marshalIPNSRecord, unmarshalIPNSRecord, publicKeyToIPNSRoutingKey, multihashToIPNSRoutingKey, multihashFromIPNSRoutingKey } from '../src/utils.js'
 import { ipnsValidator } from '../src/validator.js'
 import { kuboRecord } from './fixtures/records.js'
-import type { PeerId } from '@libp2p/interface'
+import type { PrivateKey } from '@libp2p/interface'
 
 describe('ipns', function () {
   this.timeout(20 * 1000)
 
   const contentPath = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
-  let peerId: PeerId
+  let privateKey: PrivateKey
 
   before(async () => {
-    const rsa = await generateKeyPair('RSA', 2048)
-    peerId = await peerIdFromKeys(rsa.public.bytes, rsa.bytes)
+    privateKey = await generateKeyPair('RSA', 2048)
   })
 
   it('should create an ipns record (V1+V2) correctly', async () => {
@@ -35,7 +34,7 @@ describe('ipns', function () {
     const ttl = BigInt(60 * 60 * 1e+9)
     const validity = 1000000
 
-    const record = await ipns.create(peerId, contentPath, sequence, validity)
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, validity)
 
     expect(record.value).to.equal(contentPath)
     expect(record.validityType).to.equal(IpnsEntry.ValidityType.EOL)
@@ -47,7 +46,7 @@ describe('ipns', function () {
     expect(record.data).to.exist()
 
     // Protobuf must have all fields!
-    const pb = IpnsEntry.decode(ipns.marshal(record))
+    const pb = IpnsEntry.decode(marshalIPNSRecord(record))
     expect(pb.value).to.equalBytes(uint8ArrayFromString(contentPath))
     expect(pb.validityType).to.equal(IpnsEntry.ValidityType.EOL)
     expect(pb.validity).to.exist()
@@ -71,7 +70,7 @@ describe('ipns', function () {
     const ttl = BigInt(60 * 60 * 1e+9)
     const validity = 1000000
 
-    const record = await ipns.create(peerId, contentPath, sequence, validity, { v1Compatible: false })
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, validity, { v1Compatible: false })
 
     expect(record.value).to.equal(contentPath)
     expect(record.validityType).to.equal(IpnsEntry.ValidityType.EOL)
@@ -83,7 +82,7 @@ describe('ipns', function () {
     expect(record.data).to.exist()
 
     // PB must only have signature and data.
-    const pb = IpnsEntry.decode(ipns.marshal(record))
+    const pb = IpnsEntry.decode(marshalIPNSRecord(record))
     expect(pb.value).to.not.exist()
     expect(pb.validityType).to.not.exist()
     expect(pb.validity).to.not.exist()
@@ -106,10 +105,10 @@ describe('ipns', function () {
     const sequence = 0
     const expiration = '2033-05-18T03:33:20.000000000Z'
 
-    const record = await ipns.createWithExpiration(peerId, contentPath, sequence, expiration)
-    const marshalledRecord = ipns.marshal(record)
+    const record = await createIPNSRecordWithExpiration(privateKey, contentPath, sequence, expiration)
+    const marshalledRecord = marshalIPNSRecord(record)
 
-    await ipnsValidator(peerIdToRoutingKey(peerId), marshalledRecord)
+    await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord)
 
     const pb = IpnsEntry.decode(marshalledRecord)
     expect(pb).to.have.property('validity')
@@ -120,12 +119,12 @@ describe('ipns', function () {
     const sequence = 0
     const expiration = '2033-05-18T03:33:20.000000000Z'
 
-    const record = await ipns.createWithExpiration(peerId, contentPath, sequence, expiration, { v1Compatible: false })
-    const marshalledRecord = ipns.marshal(record)
+    const record = await createIPNSRecordWithExpiration(privateKey, contentPath, sequence, expiration, { v1Compatible: false })
+    const marshalledRecord = marshalIPNSRecord(record)
 
-    await ipnsValidator(peerIdToRoutingKey(peerId), marshalledRecord)
+    await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord)
 
-    const pb = IpnsEntry.decode(ipns.marshal(record))
+    const pb = IpnsEntry.decode(marshalIPNSRecord(record))
     expect(pb).to.not.have.property('validity')
 
     const data = parseCborData(pb.data ?? new Uint8Array(0))
@@ -137,12 +136,12 @@ describe('ipns', function () {
     const ttl = BigInt(0.6e+12)
     const validity = 1000000
 
-    const record = await ipns.create(peerId, contentPath, sequence, validity, {
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, validity, {
       ttlNs: ttl
     })
-    const marshalledRecord = ipns.marshal(record)
+    const marshalledRecord = marshalIPNSRecord(record)
 
-    await ipnsValidator(peerIdToRoutingKey(peerId), marshalledRecord)
+    await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord)
 
     const pb = IpnsEntry.decode(marshalledRecord)
     const data = parseCborData(pb.data ?? new Uint8Array(0))
@@ -154,13 +153,13 @@ describe('ipns', function () {
     const ttl = BigInt(1.6e+12)
     const validity = 1000000
 
-    const record = await ipns.create(peerId, contentPath, sequence, validity, {
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, validity, {
       ttlNs: ttl,
       v1Compatible: false
     })
-    const marshalledRecord = ipns.marshal(record)
+    const marshalledRecord = marshalIPNSRecord(record)
 
-    await ipnsValidator(peerIdToRoutingKey(peerId), marshalledRecord)
+    await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord)
 
     const pb = IpnsEntry.decode(marshalledRecord)
     expect(pb).to.not.have.property('ttl')
@@ -173,99 +172,107 @@ describe('ipns', function () {
     const sequence = 0
     const validity = 1000000
 
-    const record = await ipns.create(peerId, contentPath, sequence, validity)
-    await ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(record))
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, validity)
+    await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record))
   })
 
   it('should create an ipns record (V2) and validate it correctly', async () => {
     const sequence = 0
     const validity = 1000000
 
-    const record = await ipns.create(peerId, contentPath, sequence, validity, { v1Compatible: false })
-    await ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(record))
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, validity, { v1Compatible: false })
+    await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record))
   })
 
   it('should normalize value when creating an ipns record (arbitrary string path)', async () => {
     const inputValue = '/foo/bar/baz'
     const expectedValue = '/foo/bar/baz'
-    const record = await ipns.create(peerId, inputValue, 0, 1000000)
+    const record = await createIPNSRecord(privateKey, inputValue, 0, 1000000)
     expect(record.value).to.equal(expectedValue)
   })
 
-  it('should normalize value when creating a recursive ipns record (peer id)', async () => {
-    const inputValue = await createEd25519PeerId()
-    const expectedValue = `/ipns/${inputValue.toCID().toString(base36)}`
-    const record = await ipns.create(peerId, inputValue, 0, 1000000)
+  it('should normalize value when creating a recursive ipns record (Ed25519 public key)', async () => {
+    const otherKey = await generateKeyPair('Ed25519')
+    const expectedValue = `/ipns/${otherKey.publicKey.toCID().toString(base36)}`
+    const record = await createIPNSRecord(privateKey, otherKey.publicKey, 0, 1000000)
+    expect(record.value).to.equal(expectedValue)
+  })
+
+  it('should normalize value when creating a recursive ipns record (RSA public key)', async () => {
+    const otherKey = await generateKeyPair('RSA', 512)
+    const expectedValue = `/ipns/${otherKey.publicKey.toCID().toString(base36)}`
+    const record = await createIPNSRecord(privateKey, otherKey.publicKey, 0, 1000000)
     expect(record.value).to.equal(expectedValue)
   })
 
   it('should normalize value when creating a recursive ipns record (peer id as CID)', async () => {
-    const inputValue = await createEd25519PeerId()
-    const expectedValue = `/ipns/${inputValue.toCID().toString(base36)}`
-    const record = await ipns.create(peerId, inputValue.toCID(), 0, 1000000)
+    const otherKey = await generateKeyPair('Ed25519')
+    const peerId = peerIdFromPrivateKey(otherKey)
+    const expectedValue = `/ipns/${peerId.toCID().toString(base36)}`
+    const record = await createIPNSRecord(privateKey, peerId.toCID(), 0, 1000000)
     expect(record.value).to.equal(expectedValue)
   })
 
   it('should normalize value when creating an ipns record (v0 cid)', async () => {
     const inputValue = CID.parse('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq')
     const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua'
-    const record = await ipns.create(peerId, inputValue, 0, 1000000)
+    const record = await createIPNSRecord(privateKey, inputValue, 0, 1000000)
     expect(record.value).to.equal(expectedValue)
   })
 
   it('should normalize value when creating an ipns record (v1 cid)', async () => {
     const inputValue = CID.parse('bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu')
     const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
-    const record = await ipns.create(peerId, inputValue, 0, 1000000)
+    const record = await createIPNSRecord(privateKey, inputValue, 0, 1000000)
     expect(record.value).to.equal(expectedValue)
   })
 
   it('should normalize value when reading an ipns record (string v0 cid path)', async () => {
     const inputValue = '/ipfs/QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq'
     const expectedValue = '/ipfs/QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq'
-    const record = await ipns.create(peerId, inputValue, 0, 1000000)
+    const record = await createIPNSRecord(privateKey, inputValue, 0, 1000000)
 
-    const pb = IpnsEntry.decode(ipns.marshal(record))
+    const pb = IpnsEntry.decode(marshalIPNSRecord(record))
     pb.data = createCborData(uint8ArrayFromString(inputValue), pb.validityType ?? IpnsEntry.ValidityType.EOL, pb.validity ?? new Uint8Array(0), pb.sequence ?? 0n, pb.ttl ?? 0n)
     pb.value = uint8ArrayFromString(inputValue)
 
-    const modifiedRecord = ipns.unmarshal(IpnsEntry.encode(pb))
+    const modifiedRecord = unmarshalIPNSRecord(IpnsEntry.encode(pb))
     expect(modifiedRecord.value).to.equal(expectedValue)
   })
 
   it('should normalize value when reading an ipns record (string v1 cid path)', async () => {
     const inputValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
     const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
-    const record = await ipns.create(peerId, inputValue, 0, 1000000)
+    const record = await createIPNSRecord(privateKey, inputValue, 0, 1000000)
 
-    const pb = IpnsEntry.decode(ipns.marshal(record))
+    const pb = IpnsEntry.decode(marshalIPNSRecord(record))
     pb.data = createCborData(uint8ArrayFromString(inputValue), pb.validityType ?? IpnsEntry.ValidityType.EOL, pb.validity ?? new Uint8Array(0), pb.sequence ?? 0n, pb.ttl ?? 0n)
     pb.value = uint8ArrayFromString(inputValue)
 
-    const modifiedRecord = ipns.unmarshal(IpnsEntry.encode(pb))
+    const modifiedRecord = unmarshalIPNSRecord(IpnsEntry.encode(pb))
     expect(modifiedRecord.value).to.equal(expectedValue)
   })
 
   it('should fail to normalize non-path value', async () => {
     const inputValue = 'hello'
 
-    await expect(ipns.create(peerId, inputValue, 0, 1000000)).to.eventually.be.rejected
-      .with.property('code', ERRORS.ERR_INVALID_VALUE)
+    await expect(createIPNSRecord(privateKey, inputValue, 0, 1000000)).to.eventually.be.rejected
+      .with.property('name', InvalidValueError.name)
   })
 
   it('should fail to normalize path value that is too short', async () => {
     const inputValue = '/'
 
-    await expect(ipns.create(peerId, inputValue, 0, 1000000)).to.eventually.be.rejected
-      .with.property('code', ERRORS.ERR_INVALID_VALUE)
+    await expect(createIPNSRecord(privateKey, inputValue, 0, 1000000)).to.eventually.be.rejected
+      .with.property('name', InvalidValueError.name)
   })
 
   it('should fail to validate a v1 (deprecated legacy) message', async () => {
     const sequence = 0
     const validity = 1000000
 
-    const record = await ipns.create(peerId, contentPath, sequence, validity)
-    const pb = IpnsEntry.decode(ipns.marshal(record))
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, validity)
+    const pb = IpnsEntry.decode(marshalIPNSRecord(record))
 
     // remove the extra fields added for v2 sigs
     delete pb.data
@@ -274,15 +281,16 @@ describe('ipns', function () {
     // confirm a v1 exists
     expect(pb).to.have.property('signatureV1')
 
-    await expect(ipnsValidator(peerIdToRoutingKey(peerId), IpnsEntry.encode(pb))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION)
+    await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), IpnsEntry.encode(pb))).to.eventually.be.rejected()
+      .with.property('name', SignatureVerificationError.name)
   })
 
   it('should fail to validate a v2 without v2 signature (ignore v1)', async () => {
     const sequence = 0
     const validity = 1000000
 
-    const record = await ipns.create(peerId, contentPath, sequence, validity)
-    const pb = IpnsEntry.decode(ipns.marshal(record))
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, validity)
+    const pb = IpnsEntry.decode(marshalIPNSRecord(record))
 
     // remove v2 sig
     delete pb.signatureV2
@@ -290,40 +298,43 @@ describe('ipns', function () {
     // confirm a v1 exists
     expect(pb).to.have.property('signatureV1')
 
-    await expect(ipnsValidator(peerIdToRoutingKey(peerId), IpnsEntry.encode(pb))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION)
+    await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), IpnsEntry.encode(pb))).to.eventually.be.rejected()
+      .with.property('name', SignatureVerificationError.name)
   })
 
   it('should fail to validate a bad record', async () => {
     const sequence = 0
     const validity = 1000000
 
-    const record = await ipns.create(peerId, contentPath, sequence, validity)
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, validity)
 
     // corrupt the record by changing the value to random bytes
     record.value = uint8ArrayToString(randomBytes(46))
 
-    await expect(ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(record))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION)
+    await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record))).to.eventually.be.rejected()
+      .with.property('name', SignatureVerificationError.name)
   })
 
   it('should create an ipns record with a validity of 1 nanosecond correctly and it should not be valid 1ms later', async () => {
     const sequence = 0
     const validity = 0.00001
 
-    const record = await ipns.create(peerId, contentPath, sequence, validity)
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, validity)
 
     await new Promise(resolve => setTimeout(resolve, 1))
 
-    await expect(ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(record))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_IPNS_EXPIRED_RECORD)
+    await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record))).to.eventually.be.rejected()
+      .with.property('name', RecordExpiredError.name)
   })
 
   it('should create an ipns record, marshal and unmarshal it, as well as validate it correctly', async () => {
     const sequence = 0
     const validity = 1000000
 
-    const createdRecord = await ipns.create(peerId, contentPath, sequence, validity)
+    const createdRecord = await createIPNSRecord(privateKey, contentPath, sequence, validity)
 
-    const marshalledData = ipns.marshal(createdRecord)
-    const unmarshalledData = ipns.unmarshal(marshalledData)
+    const marshalledData = marshalIPNSRecord(createdRecord)
+    const unmarshalledData = unmarshalIPNSRecord(marshalledData)
 
     expect(createdRecord.value).to.equal(unmarshalledData.value)
     expect(createdRecord.validity.toString()).to.equal(unmarshalledData.validity.toString())
@@ -334,14 +345,7 @@ describe('ipns', function () {
     expect(createdRecord.signatureV2).to.equalBytes(unmarshalledData.signatureV2)
     expect(createdRecord.data).to.equalBytes(unmarshalledData.data)
 
-    await ipnsValidator(peerIdToRoutingKey(peerId), marshalledData)
-  })
-
-  it('should get datastore key correctly', () => {
-    const datastoreKey = ipns.getLocalKey(base58btc.decode(`z${peerId.toString()}`))
-
-    expect(datastoreKey).to.exist()
-    expect(datastoreKey.toString()).to.startWith('/ipns/CIQ')
+    await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledData)
   })
 
   it('should be able to turn routing key back into id', () => {
@@ -351,10 +355,11 @@ describe('ipns', function () {
     ]
 
     keys.forEach(key => {
-      const routingKey = ipns.peerIdToRoutingKey(peerIdFromString(key))
-      const id = ipns.peerIdFromRoutingKey(routingKey)
+      const digest = Digest.decode(base58btc.decode(`z${key}`))
+      const routingKey = multihashToIPNSRoutingKey(digest)
+      const id = multihashFromIPNSRoutingKey(routingKey)
 
-      expect(id.toString()).to.equal(key)
+      expect(base58btc.encode(id.bytes)).to.equal(`z${key}`)
     })
   })
 
@@ -362,11 +367,11 @@ describe('ipns', function () {
     const sequence = 0
     const validity = 1000000
 
-    const record = await ipns.create(peerId, contentPath, sequence, validity)
-    expect(record.pubKey).to.equalBytes(peerId.publicKey)
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, validity)
+    expect(record.pubKey).to.equalBytes(publicKeyToProtobuf(privateKey.publicKey))
 
-    const pb = IpnsEntry.decode(ipns.marshal(record))
-    expect(pb.pubKey).to.equalBytes(peerId.publicKey)
+    const pb = IpnsEntry.decode(marshalIPNSRecord(record))
+    expect(pb.pubKey).to.equalBytes(publicKeyToProtobuf(privateKey.publicKey))
   })
 
   // It should have a public key embedded for newer ed25519 keys
@@ -379,8 +384,8 @@ describe('ipns', function () {
     const sequence = 0
     const validity = 1000000
 
-    const ed25519 = await createEd25519PeerId()
-    const record = await ipns.create(ed25519, contentPath, sequence, validity)
+    const privateKey = await generateKeyPair('Ed25519')
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, validity)
 
     expect(record).to.not.have.property('pubKey') // ed25519 keys should not be embedded
   })
@@ -389,25 +394,24 @@ describe('ipns', function () {
     const sequence = 0
     const validity = 1000000
 
-    const record = await ipns.create(peerId, contentPath, sequence, validity)
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, validity)
     delete record.pubKey
 
-    const marshalledData = ipns.marshal(record)
-    const key = peerIdToRoutingKey(peerId)
+    const marshalledData = marshalIPNSRecord(record)
+    const key = publicKeyToIPNSRoutingKey(privateKey.publicKey)
 
-    await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected().with.property('code', ERRORS.ERR_UNDEFINED_PARAMETER)
+    await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected()
+      .with.property('name', InvalidEmbeddedPublicKeyError.name)
   })
 
   it('should be able to export a previously embedded public key from an ipns record', async () => {
     const sequence = 0
     const validity = 1000000
 
-    const record = await ipns.create(peerId, contentPath, sequence, validity)
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, validity)
 
-    const publicKey = await extractPublicKey(peerId, record)
-    expect(publicKey).to.deep.include({
-      bytes: peerId.publicKey
-    })
+    const publicKey = extractPublicKeyFromIPNSRecord(record)
+    expect(publicKey?.equals(privateKey.publicKey)).to.be.true()
   })
 
   it('should unmarshal a record with raw CID bytes', async () => {
@@ -415,7 +419,7 @@ describe('ipns', function () {
     // but IPNS records should have string path values
 
     // create a dummy record with an arbitrary string path
-    const input = await ipns.create(peerId, '/foo', 0n, 10000, {
+    const input = await createIPNSRecord(privateKey, '/foo', 0n, 10000, {
       v1Compatible: false
     })
 
@@ -428,12 +432,11 @@ describe('ipns', function () {
     input.data = cbor.encode(data)
 
     // re-sign record
-    const privateKey = await unmarshalPrivateKey(peerId.privateKey ?? new Uint8Array(0))
     const sigData = ipnsRecordDataForV2Sig(input.data)
     input.signatureV2 = await privateKey.sign(sigData)
 
-    const buf = ipns.marshal(input)
-    const record = ipns.unmarshal(buf)
+    const buf = marshalIPNSRecord(input)
+    const record = unmarshalIPNSRecord(buf)
 
     expect(record).to.have.property('value', '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu')
   })
@@ -444,10 +447,10 @@ describe('ipns', function () {
     // uses microsecond precision. The value is a timestamp as defined by
     // rfc3339 which doesn't have a strong opinion on fractions of seconds so
     // both are valid but we must be able to round trip them intact.
-    const unmarshalled = ipns.unmarshal(kuboRecord.bytes)
-    const remarhshalled = ipns.marshal(unmarshalled)
+    const unmarshalled = unmarshalIPNSRecord(kuboRecord.bytes)
+    const remarhshalled = marshalIPNSRecord(unmarshalled)
 
-    const reUnmarshalled = ipns.unmarshal(remarhshalled)
+    const reUnmarshalled = unmarshalIPNSRecord(remarhshalled)
 
     expect(unmarshalled).to.deep.equal(reUnmarshalled)
     expect(remarhshalled).to.equalBytes(kuboRecord.bytes)
diff --git a/test/selector.spec.ts b/test/selector.spec.ts
index 20295c8..e63909d 100644
--- a/test/selector.spec.ts
+++ b/test/selector.spec.ts
@@ -1,35 +1,32 @@
 /* eslint-env mocha */
 
 import { generateKeyPair } from '@libp2p/crypto/keys'
-import { peerIdFromKeys } from '@libp2p/peer-id'
 import { expect } from 'aegir/chai'
-import * as ipns from '../src/index.js'
+import { createIPNSRecord, marshalIPNSRecord, publicKeyToIPNSRoutingKey } from '../src/index.js'
 import { ipnsSelector } from '../src/selector.js'
-import { marshal, peerIdToRoutingKey } from '../src/utils.js'
-import type { PeerId } from '@libp2p/interface'
+import type { PrivateKey } from '@libp2p/interface'
 
 describe('selector', function () {
   this.timeout(20 * 1000)
 
   const contentPath = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
-  let peerId: PeerId
+  let privateKey: PrivateKey
 
   before(async () => {
-    const rsa = await generateKeyPair('RSA', 2048)
-    peerId = await peerIdFromKeys(rsa.public.bytes, rsa.bytes)
+    privateKey = await generateKeyPair('RSA', 2048)
   })
 
   it('should use validator.select to select the record with the highest sequence number', async () => {
     const sequence = 0
     const lifetime = 1000000
 
-    const record = await ipns.create(peerId, contentPath, sequence, lifetime)
-    const newRecord = await ipns.create(peerId, contentPath, (sequence + 1), lifetime)
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, lifetime)
+    const newRecord = await createIPNSRecord(privateKey, contentPath, (sequence + 1), lifetime)
 
-    const marshalledData = marshal(record)
-    const marshalledNewData = marshal(newRecord)
+    const marshalledData = marshalIPNSRecord(record)
+    const marshalledNewData = marshalIPNSRecord(newRecord)
 
-    const key = peerIdToRoutingKey(peerId)
+    const key = publicKeyToIPNSRoutingKey(privateKey.publicKey)
 
     let valid = ipnsSelector(key, [marshalledNewData, marshalledData])
     expect(valid).to.equal(0) // new data is the selected one
@@ -42,13 +39,13 @@ describe('selector', function () {
     const sequence = 0
     const lifetime = 1000000
 
-    const record = await ipns.create(peerId, contentPath, sequence, lifetime)
-    const newRecord = await ipns.create(peerId, contentPath, sequence, (lifetime + 1))
+    const record = await createIPNSRecord(privateKey, contentPath, sequence, lifetime)
+    const newRecord = await createIPNSRecord(privateKey, contentPath, sequence, (lifetime + 1))
 
-    const marshalledData = marshal(record)
-    const marshalledNewData = marshal(newRecord)
+    const marshalledData = marshalIPNSRecord(record)
+    const marshalledNewData = marshalIPNSRecord(newRecord)
 
-    const key = peerIdToRoutingKey(peerId)
+    const key = publicKeyToIPNSRoutingKey(privateKey.publicKey)
 
     let valid = ipnsSelector(key, [marshalledNewData, marshalledData])
     expect(valid).to.equal(0) // new data is the selected one
diff --git a/test/validator.spec.ts b/test/validator.spec.ts
index 11a0c2e..fe73028 100644
--- a/test/validator.spec.ts
+++ b/test/validator.spec.ts
@@ -1,43 +1,32 @@
 /* eslint-env mocha */
 
 import { randomBytes } from '@libp2p/crypto'
-import { generateKeyPair } from '@libp2p/crypto/keys'
-import { peerIdFromKeys } from '@libp2p/peer-id'
+import { generateKeyPair, publicKeyToProtobuf } from '@libp2p/crypto/keys'
 import { expect } from 'aegir/chai'
-import { base58btc } from 'multiformats/bases/base58'
-import { concat as uint8ArrayConcat } from 'uint8arrays/concat'
-import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
 import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
-import * as ERRORS from '../src/errors.js'
-import * as ipns from '../src/index.js'
-import { marshal, peerIdToRoutingKey } from '../src/utils.js'
+import { InvalidEmbeddedPublicKeyError, RecordTooLargeError, SignatureVerificationError } from '../src/errors.js'
+import { createIPNSRecord, marshalIPNSRecord, publicKeyToIPNSRoutingKey } from '../src/index.js'
 import { ipnsValidator } from '../src/validator.js'
-import type { PeerId } from '@libp2p/interface'
+import type { PrivateKey } from '@libp2p/interface'
 
 describe('validator', function () {
   this.timeout(20 * 1000)
 
   const contentPath = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu'
-  let peerId1: PeerId
-  let peerId2: PeerId
+  let privateKey1: PrivateKey
+  let privateKey2: PrivateKey
 
   before(async () => {
-    const rsa = await generateKeyPair('RSA', 2048)
-    peerId1 = await peerIdFromKeys(rsa.public.bytes, rsa.bytes)
-
-    const rsa2 = await generateKeyPair('RSA', 2048)
-    peerId2 = await peerIdFromKeys(rsa2.public.bytes, rsa2.bytes)
+    privateKey1 = await generateKeyPair('RSA', 2048)
+    privateKey2 = await generateKeyPair('RSA', 2048)
   })
 
   it('should validate a (V2) record', async () => {
     const sequence = 0
     const validity = 1000000
-
-    const record = await ipns.create(peerId1, contentPath, sequence, validity, { v1Compatible: false })
-    const marshalledData = marshal(record)
-
-    const keyBytes = base58btc.decode(`z${peerId1.toString()}`)
-    const key = uint8ArrayConcat([uint8ArrayFromString('/ipns/'), keyBytes])
+    const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: false })
+    const marshalledData = marshalIPNSRecord(record)
+    const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey)
 
     await ipnsValidator(key, marshalledData)
   })
@@ -45,12 +34,9 @@ describe('validator', function () {
   it('should validate a (V1+V2) record', async () => {
     const sequence = 0
     const validity = 1000000
-
-    const record = await ipns.create(peerId1, contentPath, sequence, validity, { v1Compatible: true })
-    const marshalledData = marshal(record)
-
-    const keyBytes = base58btc.decode(`z${peerId1.toString()}`)
-    const key = uint8ArrayConcat([uint8ArrayFromString('/ipns/'), keyBytes])
+    const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: true })
+    const marshalledData = marshalIPNSRecord(record)
+    const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey)
 
     await ipnsValidator(key, marshalledData)
   })
@@ -59,46 +45,50 @@ describe('validator', function () {
     const sequence = 0
     const validity = 1000000
 
-    const record = await ipns.create(peerId1, contentPath, sequence, validity)
+    const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity)
 
     // corrupt the record by changing the value to random bytes
     record.value = uint8ArrayToString(randomBytes(record.value?.length ?? 0))
-    const marshalledData = marshal(record)
+    const marshalledData = marshalIPNSRecord(record)
 
-    const key = peerIdToRoutingKey(peerId1)
+    const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey)
 
-    await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION)
+    await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected()
+      .with.property('name', SignatureVerificationError.name)
   })
 
   it('should use validator.validate to verify that a record is not valid when it is passed with the wrong IPNS key', async () => {
     const sequence = 0
     const validity = 1000000
 
-    const record = await ipns.create(peerId1, contentPath, sequence, validity)
-    const marshalledData = marshal(record)
+    const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity)
+    const marshalledData = marshalIPNSRecord(record)
 
-    const key = peerIdToRoutingKey(peerId2)
+    const key = publicKeyToIPNSRoutingKey(privateKey2.publicKey)
 
-    await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected().with.property('code', ERRORS.ERR_INVALID_EMBEDDED_KEY)
+    await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected()
+      .with.property('name', InvalidEmbeddedPublicKeyError.name)
   })
 
   it('should use validator.validate to verify that a record is not valid when the wrong key is embedded', async () => {
     const sequence = 0
     const validity = 1000000
 
-    const record = await ipns.create(peerId1, contentPath, sequence, validity)
-    record.pubKey = peerId2.publicKey
-    const marshalledData = marshal(record)
+    const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity)
+    record.pubKey = publicKeyToProtobuf(privateKey2.publicKey)
+    const marshalledData = marshalIPNSRecord(record)
 
-    const key = peerIdToRoutingKey(peerId1)
+    const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey)
 
-    await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected().with.property('code', ERRORS.ERR_INVALID_EMBEDDED_KEY)
+    await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected()
+      .with.property('name', InvalidEmbeddedPublicKeyError.name)
   })
 
   it('should limit the size of incoming records', async () => {
     const marshalledData = new Uint8Array(1024 * 1024)
     const key = new Uint8Array()
 
-    await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected().with.property('code', ERRORS.ERR_RECORD_TOO_LARGE)
+    await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected()
+      .with.property('name', RecordTooLargeError.name)
   })
 })

From 927cc47d2f975eba7da9b3b2e1e62e56193a8a9a Mon Sep 17 00:00:00 2001
From: achingbrain <alex@achingbrain.net>
Date: Wed, 11 Sep 2024 16:59:20 +0100
Subject: [PATCH 2/3] chore: be stricter about cid types

---
 src/utils.ts       | 4 ++--
 test/index.spec.ts | 1 +
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/utils.ts b/src/utils.ts
index f210830..e7a8424 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -139,7 +139,7 @@ export const publicKeyToIPNSRoutingKey = (publicKey: PublicKey): Uint8Array => {
   return multihashToIPNSRoutingKey(publicKey.toMultihash())
 }
 
-export const multihashToIPNSRoutingKey = (digest: MultihashDigest): Uint8Array => {
+export const multihashToIPNSRoutingKey = (digest: MultihashDigest<0x00 | 0x12>): Uint8Array => {
   return uint8ArrayConcat([
     IPNS_PREFIX,
     digest.bytes
@@ -153,7 +153,7 @@ export const publicKeyFromIPNSRoutingKey = (key: Uint8Array): Ed25519PublicKey |
   } catch {}
 }
 
-export const multihashFromIPNSRoutingKey = (key: Uint8Array): MultihashDigest<0x00> | MultihashDigest<0x12> => {
+export const multihashFromIPNSRoutingKey = (key: Uint8Array): MultihashDigest<0x00 | 0x12> => {
   const digest = Digest.decode(key.slice(IPNS_PREFIX.length))
 
   if (digest.code !== 0x00 && digest.code !== 0x12) {
diff --git a/test/index.spec.ts b/test/index.spec.ts
index cdc9552..abb5b22 100644
--- a/test/index.spec.ts
+++ b/test/index.spec.ts
@@ -356,6 +356,7 @@ describe('ipns', function () {
 
     keys.forEach(key => {
       const digest = Digest.decode(base58btc.decode(`z${key}`))
+      // @ts-expect-error digest may have the wrong hash type
       const routingKey = multihashToIPNSRoutingKey(digest)
       const id = multihashFromIPNSRoutingKey(routingKey)
 

From 67bdddf0726111a5a272c1cf938ad3f428c8deb0 Mon Sep 17 00:00:00 2001
From: achingbrain <alex@achingbrain.net>
Date: Thu, 12 Sep 2024 09:01:41 +0100
Subject: [PATCH 3/3] chore: simplify

---
 src/index.ts           |  21 ++++---
 src/utils.ts           | 128 +++++++++++++++++++++++------------------
 src/validator.ts       |  13 ++++-
 test/index.spec.ts     |  26 ++++-----
 test/selector.spec.ts  |   6 +-
 test/utils.spec.ts     |  24 +++++---
 test/validator.spec.ts |  12 ++--
 7 files changed, 132 insertions(+), 98 deletions(-)

diff --git a/src/index.ts b/src/index.ts
index 7dca7c9..041f9d9 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,6 +8,7 @@ import { IpnsEntry } from './pb/ipns.js'
 import { createCborData, ipnsRecordDataForV1Sig, ipnsRecordDataForV2Sig, normalizeValue } from './utils.js'
 import type { PrivateKey, PublicKey } from '@libp2p/interface'
 import type { CID } from 'multiformats/cid'
+import type { MultihashDigest } from 'multiformats/hashes/interface'
 
 const log = logger('ipns')
 const DEFAULT_TTL_NS = 60 * 60 * 1e+9 // 1 Hour or 3600 Seconds
@@ -157,10 +158,10 @@ const defaultCreateOptions: CreateOptions = {
  * @param {number} lifetime - lifetime of the record (in milliseconds).
  * @param {CreateOptions} options - additional create options.
  */
-export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
-export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise<IPNSRecordV2>
-export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options: CreateOptions): Promise<IPNSRecordV1V2>
-export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
+export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
+export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise<IPNSRecordV2>
+export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options: CreateOptions): Promise<IPNSRecordV1V2>
+export async function createIPNSRecord (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
   // Validity in ISOString with nanoseconds precision and validity type EOL
   const expirationDate = new NanoDate(Date.now() + Number(lifetime))
   const validityType = IpnsEntry.ValidityType.EOL
@@ -185,10 +186,10 @@ export async function createIPNSRecord (privateKey: PrivateKey, value: CID | Pub
  * @param {string} expiration - expiration datetime for record in the [RFC3339]{@link https://www.ietf.org/rfc/rfc3339.txt} with nanoseconds precision.
  * @param {CreateOptions} options - additional creation options.
  */
-export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
-export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options: CreateV2Options): Promise<IPNSRecordV2>
-export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options: CreateOptions): Promise<IPNSRecordV1V2>
-export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
+export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise<IPNSRecordV1V2>
+export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options: CreateV2Options): Promise<IPNSRecordV2>
+export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options: CreateOptions): Promise<IPNSRecordV1V2>
+export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> {
   const expirationDate = NanoDate.fromString(expiration)
   const validityType = IpnsEntry.ValidityType.EOL
   const ttlNs = BigInt(options.ttlNs ?? DEFAULT_TTL_NS)
@@ -196,7 +197,7 @@ export async function createIPNSRecordWithExpiration (privateKey: PrivateKey, va
   return _create(privateKey, value, seq, validityType, expirationDate.toString(), ttlNs, options)
 }
 
-const _create = async (privateKey: PrivateKey, value: CID | PublicKey | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, validity: string, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
+const _create = async (privateKey: PrivateKey, value: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, validity: string, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise<IPNSRecord> => {
   seq = BigInt(seq)
   const isoValidity = uint8ArrayFromString(validity)
   const normalizedValue = normalizeValue(value)
@@ -254,8 +255,6 @@ export { unmarshalIPNSRecord } from './utils.js'
 export { marshalIPNSRecord } from './utils.js'
 export { multihashToIPNSRoutingKey } from './utils.js'
 export { multihashFromIPNSRoutingKey } from './utils.js'
-export { publicKeyToIPNSRoutingKey } from './utils.js'
-export { publicKeyFromIPNSRoutingKey } from './utils.js'
 export { extractPublicKeyFromIPNSRecord } from './utils.js'
 
 /**
diff --git a/src/utils.ts b/src/utils.ts
index e7a8424..ddd77ea 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,4 +1,4 @@
-import { publicKeyFromMultihash, publicKeyFromProtobuf } from '@libp2p/crypto/keys'
+import { publicKeyFromProtobuf } from '@libp2p/crypto/keys'
 import { InvalidMultihashError } from '@libp2p/interface'
 import { logger } from '@libp2p/logger'
 import * as cborg from 'cborg'
@@ -12,17 +12,19 @@ import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
 import { InvalidRecordDataError, InvalidValueError, SignatureVerificationError, UnsupportedValidityError } from './errors.js'
 import { IpnsEntry } from './pb/ipns.js'
 import type { IPNSRecord, IPNSRecordV2, IPNSRecordData } from './index.js'
-import type { PublicKey, Ed25519PublicKey, Secp256k1PublicKey } from '@libp2p/interface'
+import type { PublicKey } from '@libp2p/interface'
 
 const log = logger('ipns:utils')
 const IPNS_PREFIX = uint8ArrayFromString('/ipns/')
-const LIBP2P_CID_CODEC = 114
+const LIBP2P_CID_CODEC = 0x72
+const IDENTITY_CODEC = 0x0
+const SHA2_256_CODEC = 0x12
 
 /**
  * Extracts a public key from the passed PeerId, falling back to the pubKey
  * embedded in the ipns record
  */
-export const extractPublicKeyFromIPNSRecord = (record: IPNSRecord | IPNSRecordV2): PublicKey | undefined => {
+export function extractPublicKeyFromIPNSRecord (record: IPNSRecord | IPNSRecordV2): PublicKey | undefined {
   let pubKey: PublicKey | undefined
 
   if (record.pubKey != null) {
@@ -42,7 +44,7 @@ export const extractPublicKeyFromIPNSRecord = (record: IPNSRecord | IPNSRecordV2
 /**
  * Utility for creating the record data for being signed
  */
-export const ipnsRecordDataForV1Sig = (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array): Uint8Array => {
+export function ipnsRecordDataForV1Sig (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array): Uint8Array {
   const validityTypeBuffer = uint8ArrayFromString(validityType)
 
   return uint8ArrayConcat([value, validity, validityTypeBuffer])
@@ -51,13 +53,13 @@ export const ipnsRecordDataForV1Sig = (value: Uint8Array, validityType: IpnsEntr
 /**
  * Utility for creating the record data for being signed
  */
-export const ipnsRecordDataForV2Sig = (data: Uint8Array): Uint8Array => {
+export function ipnsRecordDataForV2Sig (data: Uint8Array): Uint8Array {
   const entryData = uint8ArrayFromString('ipns-signature:')
 
   return uint8ArrayConcat([entryData, data])
 }
 
-export const marshalIPNSRecord = (obj: IPNSRecord | IPNSRecordV2): Uint8Array => {
+export function marshalIPNSRecord (obj: IPNSRecord | IPNSRecordV2): Uint8Array {
   if ('signatureV1' in obj) {
     return IpnsEntry.encode({
       value: uint8ArrayFromString(obj.value),
@@ -100,7 +102,7 @@ export function unmarshalIPNSRecord (buf: Uint8Array): IPNSRecord {
   }
 
   const data = parseCborData(message.data)
-  const value = normalizeValue(data.Value)
+  const value = normalizeByteValue(data.Value)
   const validity = uint8ArrayToString(data.Validity)
 
   if (message.value != null && message.signatureV1 != null) {
@@ -135,36 +137,24 @@ export function unmarshalIPNSRecord (buf: Uint8Array): IPNSRecord {
   }
 }
 
-export const publicKeyToIPNSRoutingKey = (publicKey: PublicKey): Uint8Array => {
-  return multihashToIPNSRoutingKey(publicKey.toMultihash())
-}
-
-export const multihashToIPNSRoutingKey = (digest: MultihashDigest<0x00 | 0x12>): Uint8Array => {
+export function multihashToIPNSRoutingKey (digest: MultihashDigest<0x00 | 0x12>): Uint8Array {
   return uint8ArrayConcat([
     IPNS_PREFIX,
     digest.bytes
   ])
 }
 
-export const publicKeyFromIPNSRoutingKey = (key: Uint8Array): Ed25519PublicKey | Secp256k1PublicKey | undefined => {
-  try {
-    // @ts-expect-error digest code may not be 0
-    return publicKeyFromMultihash(multihashFromIPNSRoutingKey(key))
-  } catch {}
-}
-
-export const multihashFromIPNSRoutingKey = (key: Uint8Array): MultihashDigest<0x00 | 0x12> => {
+export function multihashFromIPNSRoutingKey (key: Uint8Array): MultihashDigest<0x00 | 0x12> {
   const digest = Digest.decode(key.slice(IPNS_PREFIX.length))
 
-  if (digest.code !== 0x00 && digest.code !== 0x12) {
+  if (!isCodec(digest, IDENTITY_CODEC) && !isCodec(digest, SHA2_256_CODEC)) {
     throw new InvalidMultihashError('Multihash in IPNS key was not identity or sha2-256')
   }
 
-  // @ts-expect-error digest may not have correct code even though we just checked
   return digest
 }
 
-export const createCborData = (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array, sequence: bigint, ttl: bigint): Uint8Array => {
+export function createCborData (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array, sequence: bigint, ttl: bigint): Uint8Array {
   let ValidityType
 
   if (validityType === IpnsEntry.ValidityType.EOL) {
@@ -184,7 +174,7 @@ export const createCborData = (value: Uint8Array, validityType: IpnsEntry.Validi
   return cborg.encode(data)
 }
 
-export const parseCborData = (buf: Uint8Array): IPNSRecordData => {
+export function parseCborData (buf: Uint8Array): IPNSRecordData {
   const data = cborg.decode(buf)
 
   if (data.ValidityType === 0) {
@@ -206,35 +196,40 @@ export const parseCborData = (buf: Uint8Array): IPNSRecordData => {
   return data
 }
 
+export function normalizeByteValue (value: Uint8Array): string {
+  const string = uint8ArrayToString(value).trim()
+
+  // if we have a path, check it is a valid path
+  if (string.startsWith('/')) {
+    return string
+  }
+
+  // try parsing what we have as CID bytes or a CID string
+  try {
+    return `/ipfs/${CID.decode(value).toV1().toString()}`
+  } catch {
+    // fall through
+  }
+
+  try {
+    return `/ipfs/${CID.parse(string).toV1().toString()}`
+  } catch {
+    // fall through
+  }
+
+  throw new InvalidValueError('Value must be a valid content path starting with /')
+}
+
 /**
  * Normalizes the given record value. It ensures it is a PeerID, a CID or a
  * string starting with '/'. PeerIDs become `/ipns/${cidV1Libp2pKey}`,
  * CIDs become `/ipfs/${cidAsV1}`.
  */
-export const normalizeValue = (value?: CID | PublicKey | string | Uint8Array): string => {
+export function normalizeValue (value?: CID | PublicKey | MultihashDigest<0x00 | 0x12> | string): string {
   if (value != null) {
-    // if we have a PeerId, turn it into an ipns path
-    if (hasToCID(value)) {
-      return `/ipns/${value.toCID().toString(base36)}`
-    }
-
-    // if the value is bytes, stringify it and see if we have a path
-    if (value instanceof Uint8Array) {
-      const string = uint8ArrayToString(value)
-
-      if (string.startsWith('/')) {
-        value = string
-      }
-    }
-
-    // if we have a path, check it is a valid path
-    const string = value.toString().trim()
-    if (string.startsWith('/') && string.length > 1) {
-      return string
-    }
+    const cid = asCID(value)
 
     // if we have a CID, turn it into an ipfs path
-    const cid = CID.asCID(value)
     if (cid != null) {
       // PeerID encoded as a CID
       if (cid.code === LIBP2P_CID_CODEC) {
@@ -244,22 +239,22 @@ export const normalizeValue = (value?: CID | PublicKey | string | Uint8Array): s
       return `/ipfs/${cid.toV1().toString()}`
     }
 
-    // try parsing what we have as CID bytes or a CID string
-    try {
-      if (value instanceof Uint8Array) {
-        return `/ipfs/${CID.decode(value).toV1().toString()}`
-      }
+    if (hasBytes(value)) {
+      return `/ipns/${base36.encode(value.bytes)}`
+    }
+
+    // if we have a path, check it is a valid path
+    const string = value.toString().trim()
 
-      return `/ipfs/${CID.parse(string).toV1().toString()}`
-    } catch {
-      // fall through
+    if (string.startsWith('/') && string.length > 1) {
+      return string
     }
   }
 
   throw new InvalidValueError('Value must be a valid content path starting with /')
 }
 
-const validateCborDataMatchesPbData = (entry: IpnsEntry): void => {
+function validateCborDataMatchesPbData (entry: IpnsEntry): void {
   if (entry.data == null) {
     throw new InvalidRecordDataError('Record data is missing')
   }
@@ -287,6 +282,29 @@ const validateCborDataMatchesPbData = (entry: IpnsEntry): void => {
   }
 }
 
+function hasBytes (obj?: any): obj is { bytes: Uint8Array } {
+  return obj.bytes instanceof Uint8Array
+}
+
 function hasToCID (obj?: any): obj is { toCID(): CID } {
   return typeof obj?.toCID === 'function'
 }
+
+function asCID (obj?: any): CID | null {
+  if (hasToCID(obj)) {
+    return obj.toCID()
+  }
+
+  // try parsing as a CID string
+  try {
+    return CID.parse(obj)
+  } catch {
+    // fall through
+  }
+
+  return CID.asCID(obj)
+}
+
+export function isCodec <T extends number> (digest: MultihashDigest, codec: T): digest is MultihashDigest<T> {
+  return digest.code === codec
+}
diff --git a/src/validator.ts b/src/validator.ts
index c4a41ac..2671c39 100644
--- a/src/validator.ts
+++ b/src/validator.ts
@@ -1,9 +1,10 @@
+import { publicKeyFromMultihash } from '@libp2p/crypto/keys'
 import { logger } from '@libp2p/logger'
 import NanoDate from 'timestamp-nano'
 import { equals as uint8ArrayEquals } from 'uint8arrays/equals'
 import { InvalidEmbeddedPublicKeyError, RecordExpiredError, RecordTooLargeError, SignatureVerificationError, UnsupportedValidityError } from './errors.js'
 import { IpnsEntry } from './pb/ipns.js'
-import { extractPublicKeyFromIPNSRecord, ipnsRecordDataForV2Sig, publicKeyFromIPNSRoutingKey, publicKeyToIPNSRoutingKey, unmarshalIPNSRecord } from './utils.js'
+import { extractPublicKeyFromIPNSRecord, ipnsRecordDataForV2Sig, isCodec, multihashFromIPNSRoutingKey, multihashToIPNSRoutingKey, unmarshalIPNSRecord } from './utils.js'
 import type { PublicKey } from '@libp2p/interface'
 
 const log = logger('ipns:validator')
@@ -58,7 +59,13 @@ export async function ipnsValidator (key: Uint8Array, marshalledData: Uint8Array
   }
 
   // try to extract public key from routing key
-  const routingPubKey = publicKeyFromIPNSRoutingKey(key)
+  const routingMultihash = multihashFromIPNSRoutingKey(key)
+  let routingPubKey: PublicKey | undefined
+
+  // identity hash
+  if (isCodec(routingMultihash, 0x0)) {
+    routingPubKey = publicKeyFromMultihash(routingMultihash)
+  }
 
   // extract public key from record
   const receivedRecord = unmarshalIPNSRecord(marshalledData)
@@ -68,7 +75,7 @@ export async function ipnsValidator (key: Uint8Array, marshalledData: Uint8Array
     throw new InvalidEmbeddedPublicKeyError('Could not extract public key from IPNS record or routing key')
   }
 
-  const routingKey = publicKeyToIPNSRoutingKey(recordPubKey)
+  const routingKey = multihashToIPNSRoutingKey(recordPubKey.toMultihash())
 
   if (!uint8ArrayEquals(key, routingKey)) {
     throw new InvalidEmbeddedPublicKeyError('Embedded public key did not match routing key')
diff --git a/test/index.spec.ts b/test/index.spec.ts
index abb5b22..6b9497c 100644
--- a/test/index.spec.ts
+++ b/test/index.spec.ts
@@ -14,7 +14,7 @@ import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
 import { InvalidEmbeddedPublicKeyError, InvalidValueError, RecordExpiredError, SignatureVerificationError } from '../src/errors.js'
 import { createIPNSRecord, createIPNSRecordWithExpiration } from '../src/index.js'
 import { IpnsEntry } from '../src/pb/ipns.js'
-import { extractPublicKeyFromIPNSRecord, parseCborData, createCborData, ipnsRecordDataForV2Sig, marshalIPNSRecord, unmarshalIPNSRecord, publicKeyToIPNSRoutingKey, multihashToIPNSRoutingKey, multihashFromIPNSRoutingKey } from '../src/utils.js'
+import { extractPublicKeyFromIPNSRecord, parseCborData, createCborData, ipnsRecordDataForV2Sig, marshalIPNSRecord, unmarshalIPNSRecord, multihashToIPNSRoutingKey, multihashFromIPNSRoutingKey } from '../src/utils.js'
 import { ipnsValidator } from '../src/validator.js'
 import { kuboRecord } from './fixtures/records.js'
 import type { PrivateKey } from '@libp2p/interface'
@@ -108,7 +108,7 @@ describe('ipns', function () {
     const record = await createIPNSRecordWithExpiration(privateKey, contentPath, sequence, expiration)
     const marshalledRecord = marshalIPNSRecord(record)
 
-    await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord)
+    await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalledRecord)
 
     const pb = IpnsEntry.decode(marshalledRecord)
     expect(pb).to.have.property('validity')
@@ -122,7 +122,7 @@ describe('ipns', function () {
     const record = await createIPNSRecordWithExpiration(privateKey, contentPath, sequence, expiration, { v1Compatible: false })
     const marshalledRecord = marshalIPNSRecord(record)
 
-    await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord)
+    await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalledRecord)
 
     const pb = IpnsEntry.decode(marshalIPNSRecord(record))
     expect(pb).to.not.have.property('validity')
@@ -141,7 +141,7 @@ describe('ipns', function () {
     })
     const marshalledRecord = marshalIPNSRecord(record)
 
-    await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord)
+    await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalledRecord)
 
     const pb = IpnsEntry.decode(marshalledRecord)
     const data = parseCborData(pb.data ?? new Uint8Array(0))
@@ -159,7 +159,7 @@ describe('ipns', function () {
     })
     const marshalledRecord = marshalIPNSRecord(record)
 
-    await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledRecord)
+    await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalledRecord)
 
     const pb = IpnsEntry.decode(marshalledRecord)
     expect(pb).to.not.have.property('ttl')
@@ -173,7 +173,7 @@ describe('ipns', function () {
     const validity = 1000000
 
     const record = await createIPNSRecord(privateKey, contentPath, sequence, validity)
-    await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record))
+    await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalIPNSRecord(record))
   })
 
   it('should create an ipns record (V2) and validate it correctly', async () => {
@@ -181,7 +181,7 @@ describe('ipns', function () {
     const validity = 1000000
 
     const record = await createIPNSRecord(privateKey, contentPath, sequence, validity, { v1Compatible: false })
-    await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record))
+    await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalIPNSRecord(record))
   })
 
   it('should normalize value when creating an ipns record (arbitrary string path)', async () => {
@@ -281,7 +281,7 @@ describe('ipns', function () {
     // confirm a v1 exists
     expect(pb).to.have.property('signatureV1')
 
-    await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), IpnsEntry.encode(pb))).to.eventually.be.rejected()
+    await expect(ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), IpnsEntry.encode(pb))).to.eventually.be.rejected()
       .with.property('name', SignatureVerificationError.name)
   })
 
@@ -298,7 +298,7 @@ describe('ipns', function () {
     // confirm a v1 exists
     expect(pb).to.have.property('signatureV1')
 
-    await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), IpnsEntry.encode(pb))).to.eventually.be.rejected()
+    await expect(ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), IpnsEntry.encode(pb))).to.eventually.be.rejected()
       .with.property('name', SignatureVerificationError.name)
   })
 
@@ -311,7 +311,7 @@ describe('ipns', function () {
     // corrupt the record by changing the value to random bytes
     record.value = uint8ArrayToString(randomBytes(46))
 
-    await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record))).to.eventually.be.rejected()
+    await expect(ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalIPNSRecord(record))).to.eventually.be.rejected()
       .with.property('name', SignatureVerificationError.name)
   })
 
@@ -323,7 +323,7 @@ describe('ipns', function () {
 
     await new Promise(resolve => setTimeout(resolve, 1))
 
-    await expect(ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalIPNSRecord(record))).to.eventually.be.rejected()
+    await expect(ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalIPNSRecord(record))).to.eventually.be.rejected()
       .with.property('name', RecordExpiredError.name)
   })
 
@@ -345,7 +345,7 @@ describe('ipns', function () {
     expect(createdRecord.signatureV2).to.equalBytes(unmarshalledData.signatureV2)
     expect(createdRecord.data).to.equalBytes(unmarshalledData.data)
 
-    await ipnsValidator(publicKeyToIPNSRoutingKey(privateKey.publicKey), marshalledData)
+    await ipnsValidator(multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash()), marshalledData)
   })
 
   it('should be able to turn routing key back into id', () => {
@@ -399,7 +399,7 @@ describe('ipns', function () {
     delete record.pubKey
 
     const marshalledData = marshalIPNSRecord(record)
-    const key = publicKeyToIPNSRoutingKey(privateKey.publicKey)
+    const key = multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash())
 
     await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected()
       .with.property('name', InvalidEmbeddedPublicKeyError.name)
diff --git a/test/selector.spec.ts b/test/selector.spec.ts
index e63909d..e263d87 100644
--- a/test/selector.spec.ts
+++ b/test/selector.spec.ts
@@ -2,7 +2,7 @@
 
 import { generateKeyPair } from '@libp2p/crypto/keys'
 import { expect } from 'aegir/chai'
-import { createIPNSRecord, marshalIPNSRecord, publicKeyToIPNSRoutingKey } from '../src/index.js'
+import { createIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey } from '../src/index.js'
 import { ipnsSelector } from '../src/selector.js'
 import type { PrivateKey } from '@libp2p/interface'
 
@@ -26,7 +26,7 @@ describe('selector', function () {
     const marshalledData = marshalIPNSRecord(record)
     const marshalledNewData = marshalIPNSRecord(newRecord)
 
-    const key = publicKeyToIPNSRoutingKey(privateKey.publicKey)
+    const key = multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash())
 
     let valid = ipnsSelector(key, [marshalledNewData, marshalledData])
     expect(valid).to.equal(0) // new data is the selected one
@@ -45,7 +45,7 @@ describe('selector', function () {
     const marshalledData = marshalIPNSRecord(record)
     const marshalledNewData = marshalIPNSRecord(newRecord)
 
-    const key = publicKeyToIPNSRoutingKey(privateKey.publicKey)
+    const key = multihashToIPNSRoutingKey(privateKey.publicKey.toMultihash())
 
     let valid = ipnsSelector(key, [marshalledNewData, marshalledData])
     expect(valid).to.equal(0) // new data is the selected one
diff --git a/test/utils.spec.ts b/test/utils.spec.ts
index 86467b1..69efc9a 100644
--- a/test/utils.spec.ts
+++ b/test/utils.spec.ts
@@ -2,7 +2,7 @@ import { peerIdFromString } from '@libp2p/peer-id'
 import { expect } from 'aegir/chai'
 import { CID } from 'multiformats/cid'
 import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
-import { normalizeValue, peerIdFromRoutingKey, peerIdToRoutingKey } from '../src/utils.js'
+import { normalizeValue, multihashFromIPNSRoutingKey, multihashToIPNSRoutingKey, normalizeByteValue } from '../src/utils.js'
 import type { PeerId } from '@libp2p/interface'
 
 describe('utils', () => {
@@ -40,8 +40,18 @@ describe('utils', () => {
       'string path': {
         input: '/hello',
         output: '/hello'
-      },
+      }
+    }
+
+    Object.entries(cases).forEach(([name, { input, output }]) => {
+      it(`should normalize a ${name}`, async () => {
+        expect(normalizeValue(await input)).to.equal(output)
+      })
+    })
+  })
 
+  describe('normalizeByteValue', () => {
+    const cases: Record<string, { input: Uint8Array, output: string }> = {
       // Uint8Array input
       'v0 CID bytes': {
         input: CID.parse('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq').bytes,
@@ -74,8 +84,8 @@ describe('utils', () => {
     }
 
     Object.entries(cases).forEach(([name, { input, output }]) => {
-      it(`should normalize a ${name}`, async () => {
-        expect(normalizeValue(await input)).to.equal(output)
+      it(`should normalize a ${name}`, () => {
+        expect(normalizeByteValue(input)).to.equal(output)
       })
     })
   })
@@ -89,10 +99,10 @@ describe('utils', () => {
 
     Object.entries(cases).forEach(([name, input]) => {
       it(`should round trip a ${name} key`, async () => {
-        const key = peerIdToRoutingKey(input)
-        const output = peerIdFromRoutingKey(key)
+        const key = multihashToIPNSRoutingKey(input.toMultihash())
+        const output = multihashFromIPNSRoutingKey(key)
 
-        expect(input.equals(output)).to.be.true()
+        expect(input.toMultihash().bytes).to.equalBytes(output.bytes)
       })
     })
   })
diff --git a/test/validator.spec.ts b/test/validator.spec.ts
index fe73028..673d443 100644
--- a/test/validator.spec.ts
+++ b/test/validator.spec.ts
@@ -5,7 +5,7 @@ import { generateKeyPair, publicKeyToProtobuf } from '@libp2p/crypto/keys'
 import { expect } from 'aegir/chai'
 import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
 import { InvalidEmbeddedPublicKeyError, RecordTooLargeError, SignatureVerificationError } from '../src/errors.js'
-import { createIPNSRecord, marshalIPNSRecord, publicKeyToIPNSRoutingKey } from '../src/index.js'
+import { createIPNSRecord, marshalIPNSRecord, multihashToIPNSRoutingKey } from '../src/index.js'
 import { ipnsValidator } from '../src/validator.js'
 import type { PrivateKey } from '@libp2p/interface'
 
@@ -26,7 +26,7 @@ describe('validator', function () {
     const validity = 1000000
     const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: false })
     const marshalledData = marshalIPNSRecord(record)
-    const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey)
+    const key = multihashToIPNSRoutingKey(privateKey1.publicKey.toMultihash())
 
     await ipnsValidator(key, marshalledData)
   })
@@ -36,7 +36,7 @@ describe('validator', function () {
     const validity = 1000000
     const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity, { v1Compatible: true })
     const marshalledData = marshalIPNSRecord(record)
-    const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey)
+    const key = multihashToIPNSRoutingKey(privateKey1.publicKey.toMultihash())
 
     await ipnsValidator(key, marshalledData)
   })
@@ -51,7 +51,7 @@ describe('validator', function () {
     record.value = uint8ArrayToString(randomBytes(record.value?.length ?? 0))
     const marshalledData = marshalIPNSRecord(record)
 
-    const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey)
+    const key = multihashToIPNSRoutingKey(privateKey1.publicKey.toMultihash())
 
     await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected()
       .with.property('name', SignatureVerificationError.name)
@@ -64,7 +64,7 @@ describe('validator', function () {
     const record = await createIPNSRecord(privateKey1, contentPath, sequence, validity)
     const marshalledData = marshalIPNSRecord(record)
 
-    const key = publicKeyToIPNSRoutingKey(privateKey2.publicKey)
+    const key = multihashToIPNSRoutingKey(privateKey2.publicKey.toMultihash())
 
     await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected()
       .with.property('name', InvalidEmbeddedPublicKeyError.name)
@@ -78,7 +78,7 @@ describe('validator', function () {
     record.pubKey = publicKeyToProtobuf(privateKey2.publicKey)
     const marshalledData = marshalIPNSRecord(record)
 
-    const key = publicKeyToIPNSRoutingKey(privateKey1.publicKey)
+    const key = multihashToIPNSRoutingKey(privateKey1.publicKey.toMultihash())
 
     await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected()
       .with.property('name', InvalidEmbeddedPublicKeyError.name)