From 2817e33058af0efcdb93ffb3c254e28bb005e893 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Fri, 25 Aug 2023 17:06:48 +0200 Subject: [PATCH] refactor using interfaces --- README.md | 4 +- package.json | 2 +- src/index.ts | 173 +++++++++++++++--------------- src/selector.ts | 17 ++- src/utils.ts | 155 +++++++++++++++++++++++++-- src/validator.ts | 94 +++------------- test/index.spec.ts | 237 ++++++++++++++++++++--------------------- test/selector.spec.ts | 21 ---- test/validator.spec.ts | 5 +- 9 files changed, 377 insertions(+), 331 deletions(-) diff --git a/README.md b/README.md index c8ed554..b546d87 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![codecov](https://img.shields.io/codecov/c/github/ipfs/js-ipns.svg?style=flat-square)](https://codecov.io/gh/ipfs/js-ipns) [![CI](https://img.shields.io/github/actions/workflow/status/ipfs/js-ipns/js-test-and-release.yml?branch=master\&style=flat-square)](https://github.com/ipfs/js-ipns/actions/workflows/js-test-and-release.yml?query=branch%3Amaster) -> IPNS Record definitions. +> IPNS Record definitions ## Table of contents @@ -62,7 +62,7 @@ const ipnsRecord = await ipns.create(privateKey, value, sequenceNumber, lifetime ```js import * as ipns from 'ipns' -await ipns.validate(publicKey, ipnsRecord) +await ipns.validate(publicKey, marshalledData) // if no error thrown, the record is valid ``` diff --git a/package.json b/package.json index c58e72d..8fcc63b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ipns", "version": "6.0.5", - "description": "ipns record definitions", + "description": "IPNS Record definitions", "author": "Vasco Santos ", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/ipfs/js-ipns#readme", diff --git a/src/index.ts b/src/index.ts index 0d28e03..0bb343c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,19 +1,16 @@ import { unmarshalPrivateKey } from '@libp2p/crypto/keys' import { logger } from '@libp2p/logger' -import * as cborg from 'cborg' import errCode from 'err-code' import { Key } from 'interface-datastore/key' import { base32upper } from 'multiformats/bases/base32' -import { CID } from 'multiformats/cid' import * as Digest from 'multiformats/hashes/digest' import { identity } from 'multiformats/hashes/identity' import NanoDate from 'timestamp-nano' 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 { IpnsEntry } from './pb/ipns.js' -import { createCborData, ipnsRecordDataForV1Sig, ipnsRecordDataForV2Sig, parseRFC3339 } from './utils.js' +import { createCborData, ipnsRecordDataForV1Sig, ipnsRecordDataForV2Sig, normalizeValue } from './utils.js' import type { PrivateKey } from '@libp2p/interface-keys' import type { PeerId } from '@libp2p/interface-peer-id' @@ -23,49 +20,35 @@ const ID_MULTIHASH_CODE = identity.code export const namespace = '/ipns/' export const namespaceLength = namespace.length -export class IPNSRecord { - readonly pb: IpnsEntry - readonly data: any - - constructor (pb: IpnsEntry) { - this.pb = pb - - if (pb.data == null) { - throw errCode(new Error('Record data is missing'), ERRORS.ERR_INVALID_RECORD_DATA) - } - - this.data = cborg.decode(pb.data) - } - - value (): string { - return normalizeValue(this.data.Value) - } - - validityType (): IpnsEntry.ValidityType { - if (this.data.ValidityType === 0) { - return IpnsEntry.ValidityType.EOL - } else { - throw errCode(new Error('Unknown validity type'), ERRORS.ERR_UNRECOGNIZED_VALIDITY) - } - } - - validity (): Date { - const validityType = this.validityType() - switch (validityType) { - case IpnsEntry.ValidityType.EOL: - return parseRFC3339(uint8ArrayToString(this.data.Validity)) - default: - throw errCode(new Error('Unknown validity type'), ERRORS.ERR_UNRECOGNIZED_VALIDITY) - } - } +export interface IPNSRecord { + value: string + signatureV1: Uint8Array // signature of the record + validityType: IpnsEntry.ValidityType // Type of validation being used + validity: NanoDate // expiration datetime for the record in RFC3339 format + sequence: bigint // number representing the version of the record + ttl?: bigint // ttl in nanoseconds + pubKey?: Uint8Array // the public portion of the key that signed this record (only present if it was not embedded in the IPNS key) + signatureV2: Uint8Array // the v2 signature of the record + data: Uint8Array // extensible data +} - sequence (): bigint { - return BigInt(this.data.Sequence ?? 0n) - } +export interface IPNSRecordV2 { + value: string + signatureV2: Uint8Array + validityType: IpnsEntry.ValidityType + validity: NanoDate + sequence: bigint + ttl?: bigint + pubKey?: Uint8Array + data: Uint8Array +} - ttl (): bigint { - return BigInt(this.data.TTL ?? 0n) - } +export interface IPNSRecordData { + Value: Uint8Array + Validity: Uint8Array + ValidityType: IpnsEntry.ValidityType + Sequence: bigint + TTL: bigint } export interface IDKeys { @@ -79,6 +62,14 @@ export interface CreateOptions { v1Compatible?: boolean } +export interface CreateV2OrV1Options { + v1Compatible: true +} + +export interface CreateV2Options { + v1Compatible: false +} + const defaultCreateOptions: CreateOptions = { v1Compatible: true } @@ -89,12 +80,14 @@ const defaultCreateOptions: CreateOptions = { * Note: This function does not embed the public key. If you want to do that, use `EmbedPublicKey`. * * @param {PeerId} peerId - peer id containing private key for signing the record. - * @param {string} value - value to be stored in the record. + * @param {string | Uint8Array} value - content path 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 const create = async (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise => { +export async function create (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, lifetime: number, options?: CreateV2OrV1Options): Promise +export async function create (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, lifetime: number, options: CreateV2Options): Promise +export async function create (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, lifetime: number, options: CreateOptions = defaultCreateOptions): Promise { // Validity in ISOString with nanoseconds precision and validity type EOL const expirationDate = new NanoDate(Date.now() + Number(lifetime)) const validityType = IpnsEntry.ValidityType.EOL @@ -109,12 +102,14 @@ export const create = async (peerId: PeerId, value: string | Uint8Array, seq: nu * WARNING: nano precision is not standard, make sure the value in seconds is 9 orders of magnitude lesser than the one provided. * * @param {PeerId} peerId - PeerId containing private key for signing the record. - * @param {string} value - value to be stored in the record. + * @param {string | Uint8Array} value - content path 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 const createWithExpiration = async (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise => { +export async function createWithExpiration (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, expiration: string, options?: CreateV2OrV1Options): Promise +export async function createWithExpiration (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, expiration: string, options: CreateV2Options): Promise +export async function createWithExpiration (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise { const expirationDate = NanoDate.fromString(expiration) const validityType = IpnsEntry.ValidityType.EOL @@ -124,10 +119,11 @@ export const createWithExpiration = async (peerId: PeerId, value: string | Uint8 return _create(peerId, value, seq, validityType, expirationDate, ttlNs, options) } -const _create = async (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, validityType: IpnsEntry.ValidityType, expirationDate: NanoDate, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise => { +const _create = async (peerId: PeerId, value: string | Uint8Array, seq: number | bigint, validityType: IpnsEntry.ValidityType, expirationDate: NanoDate, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise => { seq = BigInt(seq) const isoValidity = uint8ArrayFromString(expirationDate.toString()) - const encodedValue = uint8ArrayFromString(normalizeValue(value)) + const normalizedValue = normalizeValue(value) + const encodedValue = uint8ArrayFromString(normalizedValue) if (peerId.privateKey == null) { throw errCode(new Error('Missing private key'), ERRORS.ERR_MISSING_PRIVATE_KEY) @@ -137,21 +133,7 @@ const _create = async (peerId: PeerId, value: string | Uint8Array, seq: number | const data = createCborData(encodedValue, isoValidity, validityType, seq, ttl) const sigData = ipnsRecordDataForV2Sig(data) const signatureV2 = await privateKey.sign(sigData) - - const pb: IpnsEntry = { - signatureV2, - data - } - - if (options.v1Compatible === true) { - const signatureV1 = await signLegacyV1(privateKey, encodedValue, validityType, isoValidity) - pb.value = encodedValue - pb.validity = isoValidity - pb.validityType = validityType - pb.signatureV1 = signatureV1 - pb.sequence = seq - pb.ttl = ttl - } + let pubKey: Uint8Array | undefined // if we cannot derive the public key from the PeerId (e.g. RSA PeerIDs), // we have to embed it in the IPNS record @@ -159,12 +141,46 @@ const _create = async (peerId: PeerId, value: string | Uint8Array, seq: number | const digest = Digest.decode(peerId.toBytes()) if (digest.code !== ID_MULTIHASH_CODE || !uint8ArrayEquals(peerId.publicKey, digest.digest)) { - pb.pubKey = peerId.publicKey + pubKey = peerId.publicKey } } - log('ipns record for %b created', value) - return new IPNSRecord(pb) + if (options.v1Compatible === true) { + const signatureV1 = await signLegacyV1(privateKey, encodedValue, validityType, isoValidity) + + const record: IPNSRecord = { + value: normalizedValue, + signatureV1, + validity: expirationDate, + validityType, + sequence: seq, + ttl, + signatureV2, + data + } + + if (pubKey != null) { + record.pubKey = pubKey + } + + return record + } else { + const record: IPNSRecordV2 = { + value: normalizedValue, + validity: expirationDate, + validityType, + sequence: seq, + ttl, + signatureV2, + data + } + + if (pubKey != null) { + record.pubKey = pubKey + } + + return record + } } /** @@ -199,22 +215,3 @@ const signLegacyV1 = async (privateKey: PrivateKey, value: Uint8Array, validityT throw errCode(new Error('record signature creation failed'), ERRORS.ERR_SIGNATURE_CREATION) } } - -/** - * Normalizes the given record value. It ensures it is a string starting with '/'. - * If the given value is a cid, the returned path will be '/ipfs/{cid}'. - */ -const normalizeValue = (value: string | Uint8Array): string => { - const str = typeof value === 'string' ? value : uint8ArrayToString(value) - - if (str.startsWith('/')) { - return str - } - - try { - const cid = CID.parse(str) - return '/ipfs/' + cid.toV1().toString() - } catch (_) { - throw errCode(new Error('Value must be a valid content path starting with /'), ERRORS.ERR_INVALID_VALUE) - } -} diff --git a/src/selector.ts b/src/selector.ts index d801474..b7e1cd2 100644 --- a/src/selector.ts +++ b/src/selector.ts @@ -8,15 +8,12 @@ export const ipnsSelector: SelectFn = (key, data) => { })) entries.sort((a, b) => { - // having a newer signature version is better than an older signature version - if (a.record.pb.signatureV2 != null && b.record.pb.signatureV2 == null) { - return -1 - } else if (a.record.pb.signatureV2 == null && b.record.pb.signatureV2 != null) { - return 1 - } + // Before we'd sort based on the signature version. Unmarshal now fails if + // a record does not have SignatureV2, so that is no longer needed. V1-only + // records haven't been issues in a long time. - const aSeq = a.record.sequence() - const bSeq = b.record.sequence() + const aSeq = a.record.sequence + const bSeq = b.record.sequence // choose later sequence number if (aSeq > bSeq) { @@ -26,8 +23,8 @@ export const ipnsSelector: SelectFn = (key, data) => { } // choose longer lived record if sequence numbers the same - const recordAValidityDate = a.record.validity() - const recordBValidityDate = b.record.validity() + const recordAValidityDate = a.record.validity.toDate() + const recordBValidityDate = b.record.validity.toDate() if (recordAValidityDate.getTime() > recordBValidityDate.getTime()) { return -1 diff --git a/src/utils.ts b/src/utils.ts index 660178c..8999042 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,11 +3,15 @@ import { logger } from '@libp2p/logger' import { peerIdFromBytes, peerIdFromKeys } from '@libp2p/peer-id' import * as cborg from 'cborg' import errCode from 'err-code' +import { CID } from 'multiformats/cid' +import NanoDate from 'timestamp-nano' 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 { IpnsEntry } from './pb/ipns.js' -import { IPNSRecord } from './index.js' +import type { IPNSRecord, IPNSRecordV2, IPNSRecordData } from './index.js' import type { PublicKey } from '@libp2p/interface-keys' import type { PeerId } from '@libp2p/interface-peer-id' @@ -65,7 +69,7 @@ export function parseRFC3339 (time: string): Date { * 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): Promise => { +export const extractPublicKey = async (peerId: PeerId, record: IPNSRecord | IPNSRecordV2): Promise => { if (record == null || peerId == null) { const error = new Error('one or more of the provided parameters are not defined') @@ -75,15 +79,15 @@ export const extractPublicKey = async (peerId: PeerId, record: IPNSRecord): Prom let pubKey: PublicKey | undefined - if (record.pb.pubKey != null) { + if (record.pubKey != null) { try { - pubKey = unmarshalPublicKey(record.pb.pubKey) + pubKey = unmarshalPublicKey(record.pubKey) } catch (err) { log.error(err) throw err } - const otherId = await peerIdFromKeys(record.pb.pubKey) + 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) @@ -117,11 +121,29 @@ export const ipnsRecordDataForV2Sig = (data: Uint8Array): Uint8Array => { return uint8ArrayConcat([entryData, data]) } -export const marshal = (obj: IPNSRecord): Uint8Array => { - return IpnsEntry.encode(obj.pb) +export const marshal = (obj: IPNSRecord | IPNSRecordV2): Uint8Array => { + if ('signatureV1' in obj) { + return IpnsEntry.encode({ + value: uint8ArrayFromString(obj.value), + signatureV1: obj.signatureV1, + validityType: obj.validityType, + validity: uint8ArrayFromString(obj.validity.toString()), + sequence: obj.sequence, + ttl: obj.ttl, + pubKey: obj.pubKey, + signatureV2: obj.signatureV2, + data: obj.data + }) + } else { + return IpnsEntry.encode({ + pubKey: obj.pubKey, + signatureV2: obj.signatureV2, + data: obj.data + }) + } } -export const unmarshal = (buf: Uint8Array): IPNSRecord => { +export const unmarshal = (buf: Uint8Array): (IPNSRecord | IPNSRecordV2) => { const message = IpnsEntry.decode(buf) // protobufjs returns bigints as numbers @@ -134,7 +156,53 @@ export const unmarshal = (buf: Uint8Array): IPNSRecord => { message.ttl = BigInt(message.ttl) } - return new IPNSRecord(message) + // Check if we have the data field. If we don't, we fail. We've been producing + // V1+V2 records for quite a while and we don't support V1-only records anymore + // during validation. + if ((message.signatureV2 == null) || (message.data == null)) { + throw errCode(new Error('missing data or signatureV2'), ERRORS.ERR_SIGNATURE_VERIFICATION) + } + + const data = parseCborData(message.data) + const value = normalizeValue(data.Value ?? new Uint8Array(0)) + + let validity + try { + validity = NanoDate.fromDate(parseRFC3339(uint8ArrayToString(data.Validity))) + } catch (e) { + log.error('unrecognized validity format (not an rfc3339 format)') + throw errCode(new Error('unrecognized validity format (not an rfc3339 format)'), ERRORS.ERR_UNRECOGNIZED_FORMAT) + } + + if (message.value != null && message.signatureV1 != null) { + // V1+V2 + validateCborDataMatchesPbData(message) + return { + value, + validityType: IpnsEntry.ValidityType.EOL, + validity, + sequence: data.Sequence, + ttl: data.TTL, + pubKey: message.pubKey, + signatureV1: message.signatureV1, + signatureV2: message.signatureV2, + data: message.data + } + } else if (message.signatureV2 != null) { + // V2-only + return { + value, + validityType: IpnsEntry.ValidityType.EOL, + validity, + sequence: data.Sequence, + ttl: data.TTL, + pubKey: message.pubKey, + signatureV2: message.signatureV2, + data: message.data + } + } else { + throw new Error('invalid record: does not include signatureV1 or signatureV2') + } } export const peerIdToRoutingKey = (peerId: PeerId): Uint8Array => { @@ -167,3 +235,72 @@ export const createCborData = (value: Uint8Array, validity: Uint8Array, validity return cborg.encode(data) } + +export const parseCborData = (buf: Uint8Array): IPNSRecordData => { + const data = cborg.decode(buf) + + if (data.ValidityType === 0) { + data.ValidityType = IpnsEntry.ValidityType.EOL + } else { + throw errCode(new Error('Unknown validity type'), ERRORS.ERR_UNRECOGNIZED_VALIDITY) + } + + if (Number.isInteger(data.Sequence)) { + // sequence must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range + data.Sequence = BigInt(data.Sequence) + } + + if (Number.isInteger(data.TTL)) { + // ttl must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range + data.TTL = BigInt(data.TTL) + } + + return data +} + +/** + * Normalizes the given record value. It ensures it is a string starting with '/'. + * If the given value is a cid, the returned path will be '/ipfs/{cid}'. + */ +export const normalizeValue = (value: string | Uint8Array): string => { + const str = typeof value === 'string' ? value : uint8ArrayToString(value) + + if (str.startsWith('/')) { + return str + } + + try { + const cid = CID.parse(str) + return '/ipfs/' + cid.toV1().toString() + } catch (_) { + throw errCode(new Error('Value must be a valid content path starting with /'), ERRORS.ERR_INVALID_VALUE) + } +} + +const validateCborDataMatchesPbData = (entry: IpnsEntry): void => { + if (entry.data == null) { + throw errCode(new Error('Record data is missing'), ERRORS.ERR_INVALID_RECORD_DATA) + } + + 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) + } + + 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) + } + + if (data.ValidityType !== entry.validityType) { + throw errCode(new Error('Field "validityType" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) + } + + if (data.Sequence !== entry.sequence) { + throw errCode(new Error('Field "sequence" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) + } + + if (data.TTL !== entry.ttl) { + throw errCode(new Error('Field "ttl" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) + } +} diff --git a/src/validator.ts b/src/validator.ts index 1ab5162..6c1d981 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -1,12 +1,8 @@ import { logger } from '@libp2p/logger' -import * as cborg from 'cborg' import errCode from 'err-code' -import { equals as uint8ArrayEquals } from 'uint8arrays/equals' -import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import * as ERRORS from './errors.js' import { IpnsEntry } from './pb/ipns.js' -import { parseRFC3339, extractPublicKey, ipnsRecordDataForV2Sig, unmarshal, peerIdFromRoutingKey } from './utils.js' -import type { IPNSRecord } from './index.js' +import { extractPublicKey, ipnsRecordDataForV2Sig, unmarshal, peerIdFromRoutingKey } from './utils.js' import type { ValidateFn } from '@libp2p/interface-dht' import type { PublicKey } from '@libp2p/interface-keys' @@ -18,27 +14,20 @@ const log = logger('ipns:validator') const MAX_RECORD_SIZE = 1024 * 10 /** - * Validates the given IPNS record against the given public key + * Validates the given IPNS Record against the given public key. We need a "raw" + * record in order to be able to access to all of its fields. */ -export const validate = async (publicKey: PublicKey, record: IPNSRecord): Promise => { - const { value, validityType, validity } = record.pb - - // Ensure Signature V2 and Data are present and not empty. - if ((record.pb.signatureV2 == null) || (record.pb.data == null)) { - throw errCode(new Error('missing data or signatureV2'), ERRORS.ERR_SIGNATURE_VERIFICATION) - } - - // If Signature V1 is present, ensure that CBOR data matches Protobuf data (IPIP-428). - if (record.pb.signatureV1 != null || record.pb.value != null) { - validateCborDataMatchesPbData(record) - } +export const validate = async (publicKey: PublicKey, buf: Uint8Array): Promise => { + // 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) // Validate Signature V2 let isValid try { - const signature = record.pb.signatureV2 - const dataForSignature = ipnsRecordDataForV2Sig(record.pb.data) - isValid = await publicKey.verify(dataForSignature, signature) + const dataForSignature = ipnsRecordDataForV2Sig(record.data) + isValid = await publicKey.verify(dataForSignature, record.signatureV2) } catch (err) { isValid = false } @@ -48,70 +37,17 @@ export const validate = async (publicKey: PublicKey, record: IPNSRecord): Promis } // Validate according to the validity type - if (validity != null && validityType === IpnsEntry.ValidityType.EOL) { - let validityDate - - try { - validityDate = parseRFC3339(uint8ArrayToString(validity)) - } catch (e) { - log.error('unrecognized validity format (not an rfc3339 format)') - throw errCode(new Error('unrecognized validity format (not an rfc3339 format)'), ERRORS.ERR_UNRECOGNIZED_FORMAT) - } - - if (validityDate.getTime() < Date.now()) { + if (record.validityType === IpnsEntry.ValidityType.EOL) { + if (record.validity.toDate().getTime() < Date.now()) { log.error('record has expired') throw errCode(new Error('record has expired'), ERRORS.ERR_IPNS_EXPIRED_RECORD) } - } else if (validityType != null) { + } else if (record.validityType != null) { log.error('unrecognized validity type') throw errCode(new Error('unrecognized validity type'), ERRORS.ERR_UNRECOGNIZED_VALIDITY) } - log('ipns record for %b is valid', value) -} - -const validateCborDataMatchesPbData = (record: IPNSRecord): void => { - if (record.pb.data == null) { - throw errCode(new Error('Record data is missing'), ERRORS.ERR_INVALID_RECORD_DATA) - } - - const data = cborg.decode(record.pb.data) - - if (!uint8ArrayEquals(data.Value, record.pb.value ?? new Uint8Array(0))) { - throw errCode(new Error('Field "value" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) - } - - if (!uint8ArrayEquals(data.Validity, record.pb.validity ?? new Uint8Array(0))) { - throw errCode(new Error('Field "validity" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) - } - - if (data.ValidityType === 0) { - data.ValidityType = IpnsEntry.ValidityType.EOL - } else { - throw errCode(new Error('Unknown validity type'), ERRORS.ERR_UNRECOGNIZED_VALIDITY) - } - - if (data.ValidityType !== record.pb.validityType) { - throw errCode(new Error('Field "validityType" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) - } - - if (Number.isInteger(data.Sequence)) { - // sequence must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range - data.Sequence = BigInt(data.Sequence) - } - - if (data.Sequence !== record.pb.sequence) { - throw errCode(new Error('Field "sequence" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) - } - - if (Number.isInteger(data.TTL)) { - // ttl must be a BigInt, but DAG-CBOR doesn't preserve this for Numbers within the safe-integer range - data.TTL = BigInt(data.TTL) - } - - if (data.TTL !== record.pb.ttl) { - throw errCode(new Error('Field "ttl" did not match between protobuf and CBOR'), ERRORS.ERR_SIGNATURE_VERIFICATION) - } + log('ipns record for %b is valid', record.value) } export const ipnsValidator: ValidateFn = async (key, marshalledData) => { @@ -126,5 +62,5 @@ export const ipnsValidator: ValidateFn = async (key, marshalledData) => { const pubKey = await extractPublicKey(peerId, receivedRecord) // Record validation - await validate(pubKey, receivedRecord) + await validate(pubKey, marshalledData) } diff --git a/test/index.spec.ts b/test/index.spec.ts index 607c049..5a8bef9 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -6,11 +6,12 @@ import { peerIdFromKeys, peerIdFromString } from '@libp2p/peer-id' import { createEd25519PeerId } from '@libp2p/peer-id-factory' import { expect } from 'aegir/chai' import { base58btc } from 'multiformats/bases/base58' +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 { IpnsEntry } from '../src/pb/ipns.js' -import { unmarshal, marshal, extractPublicKey, peerIdToRoutingKey } from '../src/utils.js' +import { extractPublicKey, peerIdToRoutingKey, parseCborData, createCborData } from '../src/utils.js' import { ipnsValidator } from '../src/validator.js' import type { PeerId } from '@libp2p/interface-peer-id' @@ -31,21 +32,33 @@ describe('ipns', function () { const record = await ipns.create(peerId, contentPath, sequence, validity) - expect(record.value()).to.equal(contentPath) - expect(record.sequence()).to.equal(BigInt(0)) - expect(record.validityType()).to.equal(IpnsEntry.ValidityType.EOL) - expect(record.validity()).to.exist() - expect(record.ttl()).to.equal(BigInt(validity * 100000)) - - expect(record.pb).to.deep.include({ - value: uint8ArrayFromString(contentPath), - sequence: BigInt(sequence) - }) - expect(record.pb).to.have.property('validity') - expect(record.pb).to.have.property('signatureV1') - expect(record.pb).to.have.property('validityType') - expect(record.pb).to.have.property('signatureV2') - expect(record.pb).to.have.property('data') + expect(record.value).to.equal(contentPath) + expect(record.validityType).to.equal(IpnsEntry.ValidityType.EOL) + expect(record.validity).to.exist() + expect(record.sequence).to.equal(BigInt(0)) + expect(record.ttl).to.equal(BigInt(validity * 100000)) + expect(record.signatureV1).to.exist() + expect(record.signatureV2).to.exist() + expect(record.data).to.exist() + + // Protobuf must have all fields! + const pb = IpnsEntry.decode(ipns.marshal(record)) + expect(pb.value).to.equalBytes(uint8ArrayFromString(contentPath)) + expect(pb.validityType).to.equal(IpnsEntry.ValidityType.EOL) + expect(pb.validity).to.exist() + expect(pb.sequence).to.equal(BigInt(sequence)) + expect(pb.ttl).to.equal(BigInt(validity * 100000)) + expect(pb.signatureV1).to.exist() + expect(pb.signatureV2).to.exist() + expect(pb.data).to.exist() + + // Protobuf.Data must have all fields and match! + const data = parseCborData(pb.data ?? new Uint8Array(0)) + expect(data.Value).to.equalBytes(pb.value) + expect(data.ValidityType).to.equal(pb.validityType) + expect(data.Validity).to.equalBytes(pb.validity) + expect(data.Sequence).to.equal(pb.sequence) + expect(data.TTL).to.equal(pb.ttl) }) it('should create an ipns record (V2) correctly', async () => { @@ -54,43 +67,63 @@ describe('ipns', function () { const record = await ipns.create(peerId, contentPath, sequence, validity, { v1Compatible: false }) - expect(record.value()).to.equal(contentPath) - expect(record.sequence()).to.equal(BigInt(0)) - expect(record.validityType()).to.equal(IpnsEntry.ValidityType.EOL) - expect(record.validity()).to.exist() - expect(record.ttl()).to.equal(BigInt(validity * 100000)) - - expect(record.pb).to.not.have.property('value') - expect(record.pb).to.not.have.property('sequence') - expect(record.pb).to.not.have.property('validity') - expect(record.pb).to.not.have.property('signatureV1') - expect(record.pb).to.not.have.property('validityType') - expect(record.pb).to.have.property('signatureV2') - expect(record.pb).to.have.property('data') + expect(record.value).to.equal(contentPath) + expect(record.validityType).to.equal(IpnsEntry.ValidityType.EOL) + expect(record.validity).to.exist() + expect(record.sequence).to.equal(BigInt(0)) + expect(record.ttl).to.equal(BigInt(validity * 100000)) + expect(record.signatureV2).to.exist() + expect(record).to.not.have.property('signatureV1') + expect(record.data).to.exist() + + // PB must only have signature and data. + const pb = IpnsEntry.decode(ipns.marshal(record)) + expect(pb.value).to.not.exist() + expect(pb.validityType).to.not.exist() + expect(pb.validity).to.not.exist() + expect(pb.sequence).to.not.exist() + expect(pb.ttl).to.not.exist() + expect(pb.signatureV1).to.not.exist() + expect(pb.signatureV2).to.exist() + expect(pb.data).to.exist() + + // Protobuf.Data must have all fields and match! + const data = parseCborData(pb.data ?? new Uint8Array(0)) + expect(data.Value).to.equalBytes(uint8ArrayFromString(contentPath)) + expect(data.ValidityType).to.equal(IpnsEntry.ValidityType.EOL) + expect(data.Validity).to.exist() + expect(data.Sequence).to.equal(BigInt(sequence)) + expect(data.TTL).to.equal(BigInt(validity * 100000)) }) it('should be able to create a record (V1+V2) with a fixed expiration', async () => { const sequence = 0 - // 2033-05-18T03:33:20.000000000Z const expiration = '2033-05-18T03:33:20.000000000Z' const record = await ipns.createWithExpiration(peerId, contentPath, sequence, expiration) + const marshalledRecord = ipns.marshal(record) - await ipnsValidator(peerIdToRoutingKey(peerId), marshal(record)) - expect(record.pb).to.have.property('validity') - expect(record.validity().getTime()).to.equal(new Date('2033-05-18T03:33:20.000000000Z').getTime()) + await ipnsValidator(peerIdToRoutingKey(peerId), marshalledRecord) + + const pb = IpnsEntry.decode(marshalledRecord) + expect(pb).to.have.property('validity') + expect(pb.validity).to.equalBytes(uint8ArrayFromString(expiration)) }) it('should be able to create a record (V2) with a fixed expiration', async () => { const sequence = 0 - // 2033-05-18T03:33:20.000000000Z const expiration = '2033-05-18T03:33:20.000000000Z' const record = await ipns.createWithExpiration(peerId, contentPath, sequence, expiration, { v1Compatible: false }) + const marshalledRecord = ipns.marshal(record) + + await ipnsValidator(peerIdToRoutingKey(peerId), marshalledRecord) - await ipnsValidator(peerIdToRoutingKey(peerId), marshal(record)) - expect(record.pb).to.not.have.property('validity') - expect(record.validity().getTime()).to.equal(new Date('2033-05-18T03:33:20.000000000Z').getTime()) + const pb = IpnsEntry.decode(ipns.marshal(record)) + expect(pb).to.not.have.property('validity') + + const data = parseCborData(pb.data ?? new Uint8Array(0)) + expect(data.Validity).to.equalBytes(uint8ArrayFromString(expiration)) }) it('should create an ipns record (V1+V2) and validate it correctly', async () => { @@ -98,7 +131,7 @@ describe('ipns', function () { const validity = 1000000 const record = await ipns.create(peerId, contentPath, sequence, validity) - await ipnsValidator(peerIdToRoutingKey(peerId), marshal(record)) + await ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(record)) }) it('should create an ipns record (V2) and validate it correctly', async () => { @@ -106,87 +139,61 @@ describe('ipns', function () { const validity = 1000000 const record = await ipns.create(peerId, contentPath, sequence, validity, { v1Compatible: false }) - await ipnsValidator(peerIdToRoutingKey(peerId), marshal(record)) + await ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(record)) }) it('should normalize value when creating an ipns record (string v0 cid)', async () => { const inputValue = 'QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq' const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua' const record = await ipns.create(peerId, inputValue, 0, 1000000) - expect(record.value()).to.equal(expectedValue) - expect(record.pb).to.deep.include({ - value: uint8ArrayFromString(expectedValue) - }) + expect(record.value).to.equal(expectedValue) }) it('should normalize value when creating an ipns record (string v1 cid)', async () => { const inputValue = 'bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' const record = await ipns.create(peerId, inputValue, 0, 1000000) - expect(record.value()).to.equal(expectedValue) - expect(record.pb).to.deep.include({ - value: uint8ArrayFromString(expectedValue) - }) + expect(record.value).to.equal(expectedValue) }) it('should normalize value when creating an ipns record (bytes v0 cid)', async () => { const inputValue = uint8ArrayFromString('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq') const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua' const record = await ipns.create(peerId, inputValue, 0, 1000000) - expect(record.value()).to.equal(expectedValue) - expect(record.pb).to.deep.include({ - value: uint8ArrayFromString(expectedValue) - }) + expect(record.value).to.equal(expectedValue) }) it('should normalize value when creating an ipns record (bytes v1 cid)', async () => { const inputValue = uint8ArrayFromString('bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu') const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' const record = await ipns.create(peerId, inputValue, 0, 1000000) - expect(record.value()).to.equal(expectedValue) - expect(record.pb).to.deep.include({ - value: uint8ArrayFromString(expectedValue) - }) + expect(record.value).to.equal(expectedValue) }) - it('should normalize value when reading an ipns record (string v0 cid)', async () => { + it('should normalize value when reading an ipns record (bytes v0 cid)', async () => { const inputValue = 'QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq' const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua' const record = await ipns.create(peerId, inputValue, 0, 1000000) - // Force old value type. - record.data.Value = inputValue - expect(record.value()).to.equal(expectedValue) - }) - - it('should normalize value when reading an ipns record (string v1 cid)', async () => { - const inputValue = 'bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' - const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' - const record = await ipns.create(peerId, inputValue, 0, 1000000) - - // Force old value type. - record.data.Value = inputValue - expect(record.value()).to.equal(expectedValue) - }) - - it('should normalize value when reading an ipns record (bytes v0 cid)', async () => { - const inputValue = uint8ArrayFromString('QmWEekX7EZLUd9VXRNMRXW3LXe4F6x7mB8oPxY5XLptrBq') - const expectedValue = '/ipfs/bafybeidvkqhl6dwsdzx5km7tupo33ywt7czkl5topwogxx6lybko2d7pua' - const record = await ipns.create(peerId, inputValue, 0, 1000000) + const pb = IpnsEntry.decode(ipns.marshal(record)) + pb.data = createCborData(uint8ArrayFromString(inputValue), pb.validity ?? new Uint8Array(0), pb.validityType ?? '', pb.sequence ?? 0n, pb.ttl ?? 0n) + pb.value = uint8ArrayFromString(inputValue) - // Force old value type. - record.data.Value = inputValue - expect(record.value()).to.equal(expectedValue) + const modifiedRecord = ipns.unmarshal(IpnsEntry.encode(pb)) + expect(modifiedRecord.value).to.equal(expectedValue) }) it('should normalize value when reading an ipns record (bytes v1 cid)', async () => { - const inputValue = uint8ArrayFromString('bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu') + const inputValue = 'bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' const expectedValue = '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu' const record = await ipns.create(peerId, inputValue, 0, 1000000) - // Force old value type. - record.data.Value = inputValue - expect(record.value()).to.equal(expectedValue) + const pb = IpnsEntry.decode(ipns.marshal(record)) + pb.data = createCborData(uint8ArrayFromString(inputValue), pb.validity ?? new Uint8Array(0), pb.validityType ?? '', pb.sequence ?? 0n, pb.ttl ?? 0n) + pb.value = uint8ArrayFromString(inputValue) + + const modifiedRecord = ipns.unmarshal(IpnsEntry.encode(pb)) + expect(modifiedRecord.value).to.equal(expectedValue) }) it('should fail to validate a v1 (deprecated legacy) message', async () => { @@ -194,15 +201,16 @@ describe('ipns', function () { const validity = 1000000 const record = await ipns.create(peerId, contentPath, sequence, validity) + const pb = IpnsEntry.decode(ipns.marshal(record)) // remove the extra fields added for v2 sigs - delete record.pb.data - delete record.pb.signatureV2 + delete pb.data + delete pb.signatureV2 // confirm a v1 exists - expect(record.pb).to.have.property('signatureV1') + expect(pb).to.have.property('signatureV1') - await expect(ipnsValidator(peerIdToRoutingKey(peerId), marshal(record))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_INVALID_RECORD_DATA) + await expect(ipnsValidator(peerIdToRoutingKey(peerId), IpnsEntry.encode(pb))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) }) it('should fail to validate a v2 without v2 signature (ignore v1)', async () => { @@ -210,14 +218,15 @@ describe('ipns', function () { const validity = 1000000 const record = await ipns.create(peerId, contentPath, sequence, validity) + const pb = IpnsEntry.decode(ipns.marshal(record)) // remove v2 sig - delete record.pb.signatureV2 + delete pb.signatureV2 // confirm a v1 exists - expect(record.pb).to.have.property('signatureV1') + expect(pb).to.have.property('signatureV1') - await expect(ipnsValidator(peerIdToRoutingKey(peerId), marshal(record))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) + await expect(ipnsValidator(peerIdToRoutingKey(peerId), IpnsEntry.encode(pb))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) }) it('should fail to validate a bad record', async () => { @@ -227,9 +236,9 @@ describe('ipns', function () { const record = await ipns.create(peerId, contentPath, sequence, validity) // corrupt the record by changing the value to random bytes - record.pb.value = randomBytes(46) + record.value = uint8ArrayToString(randomBytes(46)) - await expect(ipnsValidator(peerIdToRoutingKey(peerId), marshal(record))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) + await expect(ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(record))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_SIGNATURE_VERIFICATION) }) it('should create an ipns record with a validity of 1 nanosecond correctly and it should not be valid 1ms later', async () => { @@ -240,7 +249,7 @@ describe('ipns', function () { await new Promise(resolve => setTimeout(resolve, 1)) - await expect(ipnsValidator(peerIdToRoutingKey(peerId), marshal(record))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_IPNS_EXPIRED_RECORD) + await expect(ipnsValidator(peerIdToRoutingKey(peerId), ipns.marshal(record))).to.eventually.be.rejected().with.property('code', ERRORS.ERR_IPNS_EXPIRED_RECORD) }) it('should create an ipns record, marshal and unmarshal it, as well as validate it correctly', async () => { @@ -249,29 +258,19 @@ describe('ipns', function () { const createdRecord = await ipns.create(peerId, contentPath, sequence, validity) - const marshalledData = marshal(createdRecord) - const unmarshalledData = unmarshal(marshalledData) - - expect(createdRecord.pb.value).to.equalBytes(unmarshalledData.pb.value) - expect(createdRecord.pb.validity).to.equalBytes(unmarshalledData.pb.validity) - expect(createdRecord.pb.validityType).to.equal(unmarshalledData.pb.validityType) - expect(createdRecord.pb.signatureV1).to.equalBytes(unmarshalledData.pb.signatureV1) - expect(createdRecord.pb.sequence).to.equal(unmarshalledData.pb.sequence) - expect(createdRecord.pb.ttl).to.equal(unmarshalledData.pb.ttl) - - if (unmarshalledData.pb.signatureV2 == null) { - throw new Error('No v2 sig found') - } + const marshalledData = ipns.marshal(createdRecord) + const unmarshalledData = ipns.unmarshal(marshalledData) - expect(createdRecord.pb.signatureV2).to.equalBytes(unmarshalledData.pb.signatureV2) + expect(createdRecord.value).to.equal(unmarshalledData.value) + expect(createdRecord.validity.toString()).to.equal(unmarshalledData.validity.toString()) + expect(createdRecord.validityType).to.equal(unmarshalledData.validityType) + expect(createdRecord.signatureV1).to.equalBytes('signatureV1' in unmarshalledData ? unmarshalledData.signatureV1 : new Uint8Array(0)) + expect(createdRecord.sequence).to.equal(unmarshalledData.sequence) + expect(createdRecord.ttl).to.equal(unmarshalledData.ttl) + expect(createdRecord.signatureV2).to.equalBytes(unmarshalledData.signatureV2) + expect(createdRecord.data).to.equalBytes(unmarshalledData.data) - if (unmarshalledData.pb.data == null) { - throw new Error('No v2 data found') - } - - expect(createdRecord.pb.data).to.equalBytes(unmarshalledData.pb.data) - - await ipnsValidator(peerIdToRoutingKey(peerId), marshal(unmarshalledData)) + await ipnsValidator(peerIdToRoutingKey(peerId), marshalledData) }) it('should get datastore key correctly', () => { @@ -300,10 +299,10 @@ describe('ipns', function () { const validity = 1000000 const record = await ipns.create(peerId, contentPath, sequence, validity) + expect(record.pubKey).to.equalBytes(peerId.publicKey) - expect(record.pb).to.deep.include({ - pubKey: peerId.publicKey - }) + const pb = IpnsEntry.decode(ipns.marshal(record)) + expect(pb.pubKey).to.equalBytes(peerId.publicKey) }) // It should have a public key embedded for newer ed25519 keys @@ -319,7 +318,7 @@ describe('ipns', function () { const ed25519 = await createEd25519PeerId() const record = await ipns.create(ed25519, contentPath, sequence, validity) - expect(record.pb).to.not.have.property('pubKey') // ed25519 keys should not be embedded + expect(record).to.not.have.property('pubKey') // ed25519 keys should not be embedded }) it('validator with no valid public key should error', async () => { @@ -327,9 +326,9 @@ describe('ipns', function () { const validity = 1000000 const record = await ipns.create(peerId, contentPath, sequence, validity) - delete record.pb.pubKey + delete record.pubKey - const marshalledData = marshal(record) + const marshalledData = ipns.marshal(record) const key = peerIdToRoutingKey(peerId) await expect(ipnsValidator(key, marshalledData)).to.eventually.be.rejected().with.property('code', ERRORS.ERR_UNDEFINED_PARAMETER) diff --git a/test/selector.spec.ts b/test/selector.spec.ts index c20320e..537a214 100644 --- a/test/selector.spec.ts +++ b/test/selector.spec.ts @@ -56,25 +56,4 @@ describe('selector', function () { valid = ipnsSelector(key, [marshalledData, marshalledNewData]) expect(valid).to.equal(1) // new data is the selected one }) - - it('should use validator.select to select an older record with a v2 sig when the newer record only uses v1', 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) - delete newRecord.pb.signatureV2 - - const marshalledData = marshal(record) - const marshalledNewData = marshal(newRecord) - - const key = peerIdToRoutingKey(peerId) - - let valid = ipnsSelector(key, [marshalledNewData, marshalledData]) - expect(valid).to.equal(1) // old data is the selected one - - valid = ipnsSelector(key, [marshalledData, marshalledNewData]) - expect(valid).to.equal(0) // old data is the selected one - }) }) diff --git a/test/validator.spec.ts b/test/validator.spec.ts index 4d3a976..afab026 100644 --- a/test/validator.spec.ts +++ b/test/validator.spec.ts @@ -7,6 +7,7 @@ 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' @@ -61,7 +62,7 @@ describe('validator', function () { const record = await ipns.create(peerId1, contentPath, sequence, validity) // corrupt the record by changing the value to random bytes - record.pb.value = randomBytes(record.pb.value?.length ?? 0) + record.value = uint8ArrayToString(randomBytes(record.value?.length ?? 0)) const marshalledData = marshal(record) const key = peerIdToRoutingKey(peerId1) @@ -86,7 +87,7 @@ describe('validator', function () { const validity = 1000000 const record = await ipns.create(peerId1, contentPath, sequence, validity) - record.pb.pubKey = peerId2.publicKey + record.pubKey = peerId2.publicKey const marshalledData = marshal(record) const key = peerIdToRoutingKey(peerId1)