From 01f3f2df8f238d48b77dcc3f7b0d82cecdd09cb6 Mon Sep 17 00:00:00 2001 From: jtsmedley <38006759+jtsmedley@users.noreply.github.com> Date: Thu, 25 Jan 2024 16:38:05 -0600 Subject: [PATCH 1/8] Add Support for Custom TTL (#1) --- src/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3739c14..e423374 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import type { CID } from 'multiformats/cid' const log = logger('ipns') const ID_MULTIHASH_CODE = identity.code +const DEFAULT_TTL = BigInt(3.6e+12) // 1 Hour or 3600 Seconds export const namespace = '/ipns/' export const namespaceLength = namespace.length @@ -127,6 +128,7 @@ export interface IDKeys { } export interface CreateOptions { + lifetimeNs?: bigint v1Compatible?: boolean } @@ -139,7 +141,8 @@ export interface CreateV2Options { } const defaultCreateOptions: CreateOptions = { - v1Compatible: true + v1Compatible: true, + lifetimeNs: DEFAULT_TTL } /** @@ -166,8 +169,7 @@ export async function create (peerId: PeerId, value: CID | PeerId | string, seq: // Validity in ISOString with nanoseconds precision and validity type EOL const expirationDate = new NanoDate(Date.now() + Number(lifetime)) const validityType = IpnsEntry.ValidityType.EOL - const [ms, ns] = lifetime.toString().split('.') - const lifetimeNs = (BigInt(ms) * BigInt(100000)) + BigInt(ns ?? '0') + const lifetimeNs = typeof options.lifetimeNs === "bigint" ? options.lifetimeNs : DEFAULT_TTL return _create(peerId, value, seq, validityType, expirationDate, lifetimeNs, options) } @@ -194,11 +196,9 @@ export async function createWithExpiration (peerId: PeerId, value: CID | PeerId export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise { const expirationDate = NanoDate.fromString(expiration) const validityType = IpnsEntry.ValidityType.EOL + const lifetimeNs = typeof options.lifetimeNs === "bigint" ? options.lifetimeNs : DEFAULT_TTL - const ttlMs = expirationDate.toDate().getTime() - Date.now() - const ttlNs = (BigInt(ttlMs) * BigInt(100000)) + BigInt(expirationDate.getNano()) - - return _create(peerId, value, seq, validityType, expirationDate, ttlNs, options) + return _create(peerId, value, seq, validityType, expirationDate, lifetimeNs, options) } const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, expirationDate: NanoDate, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise => { From c2b48c9503effaaa6e4a944706481bd8302e04e6 Mon Sep 17 00:00:00 2001 From: jtsmedley <38006759+jtsmedley@users.noreply.github.com> Date: Thu, 25 Jan 2024 17:11:07 -0600 Subject: [PATCH 2/8] Merge Upstream Changes (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * deps: bump @libp2p/crypto from 3.0.4 to 4.0.0 (#304) Bumps [@libp2p/crypto](https://github.com/libp2p/js-libp2p) from 3.0.4 to 4.0.0. - [Release notes](https://github.com/libp2p/js-libp2p/releases) - [Changelog](https://github.com/libp2p/js-libp2p/blob/main/.release-please.json) - [Commits](https://github.com/libp2p/js-libp2p/compare/perf-v3.0.4...utils-v4.0.0) --- updated-dependencies: - dependency-name: "@libp2p/crypto" dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(release): 8.0.2 [skip ci] ## [8.0.2](https://github.com/ipfs/js-ipns/compare/v8.0.1...v8.0.2) (2024-01-15) ### Dependencies * bump @libp2p/crypto from 3.0.4 to 4.0.0 ([#304](https://github.com/ipfs/js-ipns/issues/304)) ([ed83244](https://github.com/ipfs/js-ipns/commit/ed832448a9c903dc2ea0dd6158cc73211eacded7)) * fix: mark package as side-effect free (#305) [Tree shaking](https://webpack.js.org/guides/tree-shaking/) results in smaller web bundles by deleting unused code. This module is side-effect free so mark it as such to signal to bundlers that unused exports can be excluded from bundles. * chore(release): 8.0.3 [skip ci] ## [8.0.3](https://github.com/ipfs/js-ipns/compare/v8.0.2...v8.0.3) (2024-01-16) ### Bug Fixes * mark package as side-effect free ([#305](https://github.com/ipfs/js-ipns/issues/305)) ([a389fe8](https://github.com/ipfs/js-ipns/commit/a389fe8f0e6dff4867ef22b6ddada43880476754)) * fix: log type as string (#306) The value field is a string so it needs to be logged as `%s` - logging as `%b` will throw. * chore(release): 8.0.4 [skip ci] ## [8.0.4](https://github.com/ipfs/js-ipns/compare/v8.0.3...v8.0.4) (2024-01-18) ### Bug Fixes * log type as string ([#306](https://github.com/ipfs/js-ipns/issues/306)) ([de68e4c](https://github.com/ipfs/js-ipns/commit/de68e4c0601702fb5d567a97e305b26f65c34fc2)) * fix!: treat validity as opaque (#307) Instead of parsing the validity field of an IPNS record as a timestamp during unmarshalling, treat it as an opaque string value. This ensures we can round-trip records supplied by other systems which may have a different interpretation of rfc3339 dates, for example. It also means we can handle different types of validity types, not just EOL. BREAKING CHANGE: the validity field is now a string * chore(release): 9.0.0 [skip ci] ## [9.0.0](https://github.com/ipfs/js-ipns/compare/v8.0.4...v9.0.0) (2024-01-18) ### ⚠ BREAKING CHANGES * the validity field is now a string ### Bug Fixes * treat validity as opaque ([#307](https://github.com/ipfs/js-ipns/issues/307)) ([461190e](https://github.com/ipfs/js-ipns/commit/461190e215173e0ac2aad1dca107de5cb65a52ef)) --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: semantic-release-bot Co-authored-by: Alex Potsides --- .aegir.js | 2 +- CHANGELOG.md | 32 +++++++++++++++++++++ package.json | 7 +++-- src/index.ts | 21 +++++++------- src/selector.ts | 20 +++++++------ src/utils.ts | 61 ++-------------------------------------- src/validator.ts | 5 ++-- test/conformance.spec.ts | 17 +++++++++++ test/fixtures/records.ts | 7 +++++ test/index.spec.ts | 20 +++++++++++-- 10 files changed, 108 insertions(+), 84 deletions(-) create mode 100644 test/fixtures/records.ts diff --git a/.aegir.js b/.aegir.js index a4f5d75..34c5d07 100644 --- a/.aegir.js +++ b/.aegir.js @@ -2,6 +2,6 @@ /** @type {import('aegir').PartialOptions} */ export default { build: { - bundlesizeMax: '143KB' + bundlesizeMax: '60KB' } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 57102ef..ea451fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +## [9.0.0](https://github.com/ipfs/js-ipns/compare/v8.0.4...v9.0.0) (2024-01-18) + + +### ⚠ BREAKING CHANGES + +* the validity field is now a string + +### Bug Fixes + +* treat validity as opaque ([#307](https://github.com/ipfs/js-ipns/issues/307)) ([461190e](https://github.com/ipfs/js-ipns/commit/461190e215173e0ac2aad1dca107de5cb65a52ef)) + +## [8.0.4](https://github.com/ipfs/js-ipns/compare/v8.0.3...v8.0.4) (2024-01-18) + + +### Bug Fixes + +* log type as string ([#306](https://github.com/ipfs/js-ipns/issues/306)) ([de68e4c](https://github.com/ipfs/js-ipns/commit/de68e4c0601702fb5d567a97e305b26f65c34fc2)) + +## [8.0.3](https://github.com/ipfs/js-ipns/compare/v8.0.2...v8.0.3) (2024-01-16) + + +### Bug Fixes + +* mark package as side-effect free ([#305](https://github.com/ipfs/js-ipns/issues/305)) ([a389fe8](https://github.com/ipfs/js-ipns/commit/a389fe8f0e6dff4867ef22b6ddada43880476754)) + +## [8.0.2](https://github.com/ipfs/js-ipns/compare/v8.0.1...v8.0.2) (2024-01-15) + + +### Dependencies + +* bump @libp2p/crypto from 3.0.4 to 4.0.0 ([#304](https://github.com/ipfs/js-ipns/issues/304)) ([ed83244](https://github.com/ipfs/js-ipns/commit/ed832448a9c903dc2ea0dd6158cc73211eacded7)) + ## [8.0.1](https://github.com/ipfs/js-ipns/compare/v8.0.0...v8.0.1) (2024-01-12) diff --git a/package.json b/package.json index c3c5899..b6be757 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ipns", - "version": "8.0.1", + "version": "9.0.0", "description": "IPNS record definitions", "author": "Vasco Santos ", "license": "Apache-2.0 OR MIT", @@ -166,7 +166,7 @@ "docs:no-publish": "NODE_OPTIONS=--max_old_space_size=8192 aegir docs --publish false" }, "dependencies": { - "@libp2p/crypto": "^3.0.3", + "@libp2p/crypto": "^4.0.0", "@libp2p/interface": "^1.1.0", "@libp2p/logger": "^4.0.3", "@libp2p/peer-id": "^4.0.3", @@ -183,5 +183,6 @@ "@libp2p/peer-id-factory": "^4.0.2", "aegir": "^42.1.1", "protons": "^7.3.3" - } + }, + "sideEffects": false } diff --git a/src/index.ts b/src/index.ts index e423374..653119d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,7 +40,7 @@ export interface IPNSRecordV1V2 { /** * expiration datetime for the record in RFC3339 format */ - validity: NanoDate + validity: string /** * number representing the version of the record @@ -85,9 +85,10 @@ export interface IPNSRecordV2 { validityType: IpnsEntry.ValidityType /** - * expiration datetime for the record in RFC3339 format + * If the validity type is EOL, this is the expiration datetime for the record + * in RFC3339 format */ - validity: NanoDate + validity: string /** * number representing the version of the record @@ -171,7 +172,7 @@ export async function create (peerId: PeerId, value: CID | PeerId | string, seq: const validityType = IpnsEntry.ValidityType.EOL const lifetimeNs = typeof options.lifetimeNs === "bigint" ? options.lifetimeNs : DEFAULT_TTL - return _create(peerId, value, seq, validityType, expirationDate, lifetimeNs, options) + return _create(peerId, value, seq, validityType, expirationDate.toString(), lifetimeNs, options) } /** @@ -198,12 +199,12 @@ export async function createWithExpiration (peerId: PeerId, value: CID | PeerId const validityType = IpnsEntry.ValidityType.EOL const lifetimeNs = typeof options.lifetimeNs === "bigint" ? options.lifetimeNs : DEFAULT_TTL - return _create(peerId, value, seq, validityType, expirationDate, lifetimeNs, options) + return _create(peerId, value, seq, validityType, expirationDate.toString(), lifetimeNs, options) } -const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, expirationDate: NanoDate, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise => { +const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, validityType: IpnsEntry.ValidityType, validity: string, ttl: bigint, options: CreateOptions = defaultCreateOptions): Promise => { seq = BigInt(seq) - const isoValidity = uint8ArrayFromString(expirationDate.toString()) + const isoValidity = uint8ArrayFromString(validity) const normalizedValue = normalizeValue(value) const encodedValue = uint8ArrayFromString(normalizedValue) @@ -212,7 +213,7 @@ const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number } const privateKey = await unmarshalPrivateKey(peerId.privateKey) - const data = createCborData(encodedValue, isoValidity, validityType, seq, ttl) + const data = createCborData(encodedValue, validityType, isoValidity, seq, ttl) const sigData = ipnsRecordDataForV2Sig(data) const signatureV2 = await privateKey.sign(sigData) let pubKey: Uint8Array | undefined @@ -233,7 +234,7 @@ const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number const record: IPNSRecord = { value: normalizedValue, signatureV1, - validity: expirationDate, + validity, validityType, sequence: seq, ttl, @@ -249,7 +250,7 @@ const _create = async (peerId: PeerId, value: CID | PeerId | string, seq: number } else { const record: IPNSRecordV2 = { value: normalizedValue, - validity: expirationDate, + validity, validityType, sequence: seq, ttl, diff --git a/src/selector.ts b/src/selector.ts index e0f2a07..5fab036 100644 --- a/src/selector.ts +++ b/src/selector.ts @@ -1,3 +1,5 @@ +import NanoDate from 'timestamp-nano' +import { IpnsEntry } from './pb/ipns.js' import { unmarshal } from './utils.js' export function ipnsSelector (key: Uint8Array, data: Uint8Array[]): number { @@ -21,16 +23,18 @@ export function ipnsSelector (key: Uint8Array, data: Uint8Array[]): number { return 1 } - // choose longer lived record if sequence numbers the same - const recordAValidityDate = a.record.validity.toDate() - const recordBValidityDate = b.record.validity.toDate() + if (a.record.validityType === IpnsEntry.ValidityType.EOL && b.record.validityType === IpnsEntry.ValidityType.EOL) { + // choose longer lived record if sequence numbers the same + const recordAValidityDate = NanoDate.fromString(a.record.validity).toDate() + const recordBValidityDate = NanoDate.fromString(b.record.validity).toDate() - if (recordAValidityDate.getTime() > recordBValidityDate.getTime()) { - return -1 - } + if (recordAValidityDate.getTime() > recordBValidityDate.getTime()) { + return -1 + } - if (recordAValidityDate.getTime() < recordBValidityDate.getTime()) { - return 1 + if (recordAValidityDate.getTime() < recordBValidityDate.getTime()) { + return 1 + } } return 0 diff --git a/src/utils.ts b/src/utils.ts index 9e99429..7df0c78 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,7 +6,6 @@ import * as cborg from 'cborg' import errCode from 'err-code' import { base36 } from 'multiformats/bases/base36' 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' @@ -20,53 +19,6 @@ const log = logger('ipns:utils') const IPNS_PREFIX = uint8ArrayFromString('/ipns/') const LIBP2P_CID_CODEC = 114 -/** - * Convert a JavaScript date into an `RFC3339Nano` formatted - * string - */ -export function toRFC3339 (time: Date): string { - const year = time.getUTCFullYear() - const month = String(time.getUTCMonth() + 1).padStart(2, '0') - const day = String(time.getUTCDate()).padStart(2, '0') - const hour = String(time.getUTCHours()).padStart(2, '0') - const minute = String(time.getUTCMinutes()).padStart(2, '0') - const seconds = String(time.getUTCSeconds()).padStart(2, '0') - const milliseconds = time.getUTCMilliseconds() - const nanoseconds = milliseconds * 1000 * 1000 - - return `${year}-${month}-${day}T${hour}:${minute}:${seconds}.${nanoseconds}Z` -} - -/** - * Parses a date string formatted as `RFC3339Nano` into a - * JavaScript Date object - */ -export function parseRFC3339 (time: string): Date { - const rfc3339Matcher = new RegExp( - // 2006-01-02T - '(\\d{4})-(\\d{2})-(\\d{2})T' + - // 15:04:05 - '(\\d{2}):(\\d{2}):(\\d{2})' + - // .999999999Z - '\\.(\\d+)Z' - ) - const m = String(time).trim().match(rfc3339Matcher) - - if (m == null) { - throw new Error('Invalid format') - } - - const year = parseInt(m[1], 10) - const month = parseInt(m[2], 10) - 1 - const date = parseInt(m[3], 10) - const hour = parseInt(m[4], 10) - const minute = parseInt(m[5], 10) - const second = parseInt(m[6], 10) - const millisecond = parseInt(m[7].padEnd(6, '0').slice(0, 3), 10) - - return new Date(Date.UTC(year, month, date, hour, minute, second, millisecond)) -} - /** * Extracts a public key from the passed PeerId, falling * back to the pubKey embedded in the ipns record @@ -129,7 +81,7 @@ export const marshal = (obj: IPNSRecord | IPNSRecordV2): Uint8Array => { value: uint8ArrayFromString(obj.value), signatureV1: obj.signatureV1, validityType: obj.validityType, - validity: uint8ArrayFromString(obj.validity.toString()), + validity: uint8ArrayFromString(obj.validity), sequence: obj.sequence, ttl: obj.ttl, pubKey: obj.pubKey, @@ -167,14 +119,7 @@ export function unmarshal (buf: Uint8Array): IPNSRecord { const data = parseCborData(message.data) const value = normalizeValue(data.Value) - - 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) - } + const validity = uint8ArrayToString(data.Validity) if (message.value != null && message.signatureV1 != null) { // V1+V2 @@ -219,7 +164,7 @@ export const peerIdFromRoutingKey = (key: Uint8Array): PeerId => { return peerIdFromBytes(key.slice(IPNS_PREFIX.length)) } -export const createCborData = (value: Uint8Array, validity: Uint8Array, validityType: string, sequence: bigint, ttl: bigint): Uint8Array => { +export const createCborData = (value: Uint8Array, validityType: IpnsEntry.ValidityType, validity: Uint8Array, sequence: bigint, ttl: bigint): Uint8Array => { let ValidityType if (validityType === IpnsEntry.ValidityType.EOL) { diff --git a/src/validator.ts b/src/validator.ts index a7102fe..e24745d 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -1,5 +1,6 @@ import { logger } from '@libp2p/logger' import errCode from 'err-code' +import NanoDate from 'timestamp-nano' import * as ERRORS from './errors.js' import { IpnsEntry } from './pb/ipns.js' import { extractPublicKey, ipnsRecordDataForV2Sig, unmarshal, peerIdFromRoutingKey } from './utils.js' @@ -37,7 +38,7 @@ export const validate = async (publicKey: PublicKey, buf: Uint8Array): Promise { diff --git a/test/conformance.spec.ts b/test/conformance.spec.ts index 83f7c59..ee1e046 100644 --- a/test/conformance.spec.ts +++ b/test/conformance.spec.ts @@ -65,4 +65,21 @@ describe('conformance', function () { expect(record.value).to.equal('/ipfs/bafkqadtwgiww63tmpeqhezldn5zgi') }) + + it('should round trip fixtures', () => { + const fixtures = [ + 'test/fixtures/k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w_v1-v2.ipns-record', + 'test/fixtures/k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c_v1-v2-broken-signature-v2.ipns-record', + 'test/fixtures/k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record', + 'test/fixtures/k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record' + ] + + for (const fixture of fixtures) { + const buf = loadFixture(fixture) + const record = ipns.unmarshal(buf) + const marshalled = ipns.marshal(record) + + expect(buf).to.equalBytes(marshalled) + } + }) }) diff --git a/test/fixtures/records.ts b/test/fixtures/records.ts new file mode 100644 index 0000000..b88243b --- /dev/null +++ b/test/fixtures/records.ts @@ -0,0 +1,7 @@ +import { peerIdFromString } from '@libp2p/peer-id' + +export const kuboRecord = { + bytes: Uint8Array.from([10, 52, 47, 105, 112, 102, 115, 47, 81, 109, 97, 52, 115, 87, 121, 111, 84, 105, 74, 75, 89, 120, 49, 119, 84, 106, 107, 120, 87, 89, 55, 49, 100, 89, 103, 49, 111, 87, 103, 69, 55, 83, 69, 57, 111, 105, 84, 71, 113, 71, 110, 121, 111, 82, 18, 64, 178, 225, 212, 157, 188, 23, 25, 166, 9, 89, 255, 63, 227, 160, 140, 70, 192, 237, 178, 167, 94, 6, 112, 184, 106, 130, 89, 252, 141, 158, 84, 53, 65, 125, 253, 93, 255, 17, 28, 93, 9, 176, 232, 89, 51, 118, 104, 236, 126, 137, 136, 72, 0, 127, 101, 88, 178, 83, 115, 6, 30, 28, 140, 5, 24, 0, 34, 27, 50, 48, 50, 52, 45, 48, 49, 45, 49, 57, 84, 49, 54, 58, 51, 51, 58, 50, 48, 46, 56, 53, 56, 50, 48, 57, 90, 40, 0, 48, 128, 192, 226, 133, 227, 104, 66, 64, 133, 91, 52, 64, 253, 186, 129, 154, 218, 85, 188, 18, 104, 96, 180, 216, 254, 176, 210, 145, 130, 209, 176, 150, 134, 33, 59, 197, 162, 193, 15, 252, 71, 190, 240, 25, 3, 169, 60, 24, 236, 68, 218, 171, 61, 235, 157, 73, 215, 0, 51, 52, 24, 195, 90, 158, 245, 199, 172, 204, 12, 249, 89, 7, 74, 136, 1, 165, 99, 84, 84, 76, 27, 0, 0, 3, 70, 48, 184, 160, 0, 101, 86, 97, 108, 117, 101, 88, 52, 47, 105, 112, 102, 115, 47, 81, 109, 97, 52, 115, 87, 121, 111, 84, 105, 74, 75, 89, 120, 49, 119, 84, 106, 107, 120, 87, 89, 55, 49, 100, 89, 103, 49, 111, 87, 103, 69, 55, 83, 69, 57, 111, 105, 84, 71, 113, 71, 110, 121, 111, 82, 104, 83, 101, 113, 117, 101, 110, 99, 101, 0, 104, 86, 97, 108, 105, 100, 105, 116, 121, 88, 27, 50, 48, 50, 52, 45, 48, 49, 45, 49, 57, 84, 49, 54, 58, 51, 51, 58, 50, 48, 46, 56, 53, 56, 50, 48, 57, 90, 108, 86, 97, 108, 105, 100, 105, 116, 121, 84, 121, 112, 101, 0]), + peerId: peerIdFromString('12D3KooWBT21CjaZgY3MvoFFwRJLBEqgk7zwa294Boh9wdX2RUX2'), + value: '/ipfs/Qma4sWyoTiJKYx1wTjkxWY71dYg1oWgE7SE9oiTGqGnyoR' +} diff --git a/test/index.spec.ts b/test/index.spec.ts index 4a787dc..307c0b2 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -16,6 +16,7 @@ import * as ipns from '../src/index.js' import { IpnsEntry } from '../src/pb/ipns.js' import { extractPublicKey, peerIdToRoutingKey, parseCborData, createCborData, ipnsRecordDataForV2Sig } from '../src/utils.js' import { ipnsValidator } from '../src/validator.js' +import { kuboRecord } from './fixtures/records.js' import type { PeerId } from '@libp2p/interface' describe('ipns', function () { @@ -186,7 +187,7 @@ describe('ipns', function () { 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.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)) @@ -199,7 +200,7 @@ describe('ipns', function () { 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.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)) @@ -397,4 +398,19 @@ describe('ipns', function () { expect(record).to.have.property('value', '/ipfs/bafkqae3imvwgy3zamzzg63janjzs22lqnzzqu') }) + + it('should round trip kubo records to bytes and back', async () => { + // the IPNS spec gives an example for the Validity field as + // 1970-01-01T00:00:00.000000001Z - e.g. nanosecond precision but Kubo only + // 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 reUnmarshalled = ipns.unmarshal(remarhshalled) + + expect(unmarshalled).to.deep.equal(reUnmarshalled) + expect(remarhshalled).to.equalBytes(kuboRecord.bytes) + }) }) From e16c6f8946d722d522853bd13389b8cfa77f399f Mon Sep 17 00:00:00 2001 From: jtsmedley <38006759+jtsmedley@users.noreply.github.com> Date: Wed, 20 Mar 2024 12:02:22 -0500 Subject: [PATCH 3/8] Refactor Naming of Lifetime to TTL (#4) --- src/index.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 653119d..9cd6423 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ import type { CID } from 'multiformats/cid' const log = logger('ipns') const ID_MULTIHASH_CODE = identity.code -const DEFAULT_TTL = BigInt(3.6e+12) // 1 Hour or 3600 Seconds +const DEFAULT_TTL_NS = BigInt(3.6e+12) // 1 Hour or 3600 Seconds export const namespace = '/ipns/' export const namespaceLength = namespace.length @@ -129,7 +129,7 @@ export interface IDKeys { } export interface CreateOptions { - lifetimeNs?: bigint + ttlNs?: bigint v1Compatible?: boolean } @@ -143,7 +143,7 @@ export interface CreateV2Options { const defaultCreateOptions: CreateOptions = { v1Compatible: true, - lifetimeNs: DEFAULT_TTL + ttlNs: DEFAULT_TTL_NS } /** @@ -170,9 +170,9 @@ export async function create (peerId: PeerId, value: CID | PeerId | string, seq: // Validity in ISOString with nanoseconds precision and validity type EOL const expirationDate = new NanoDate(Date.now() + Number(lifetime)) const validityType = IpnsEntry.ValidityType.EOL - const lifetimeNs = typeof options.lifetimeNs === "bigint" ? options.lifetimeNs : DEFAULT_TTL + const ttlNs = typeof options.ttlNs !== "undefined" ? BigInt(options.ttlNs) : DEFAULT_TTL_NS - return _create(peerId, value, seq, validityType, expirationDate.toString(), lifetimeNs, options) + return _create(peerId, value, seq, validityType, expirationDate.toString(), ttlNs, options) } /** @@ -197,9 +197,9 @@ export async function createWithExpiration (peerId: PeerId, value: CID | PeerId export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise { const expirationDate = NanoDate.fromString(expiration) const validityType = IpnsEntry.ValidityType.EOL - const lifetimeNs = typeof options.lifetimeNs === "bigint" ? options.lifetimeNs : DEFAULT_TTL + const ttlNs = typeof options.ttlNs !== "undefined" ? BigInt(options.ttlNs) : DEFAULT_TTL_NS - return _create(peerId, value, seq, validityType, expirationDate.toString(), lifetimeNs, options) + return _create(peerId, 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 => { From 09bcf6a9f7c347761005ae89c3887ce2d234e8c5 Mon Sep 17 00:00:00 2001 From: jtsmedley <38006759+jtsmedley@users.noreply.github.com> Date: Wed, 20 Mar 2024 12:33:41 -0500 Subject: [PATCH 4/8] Update Tests to Support New TTL --- src/index.ts | 4 ++-- test/index.spec.ts | 47 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9cd6423..8cd4414 100644 --- a/src/index.ts +++ b/src/index.ts @@ -170,7 +170,7 @@ export async function create (peerId: PeerId, value: CID | PeerId | string, seq: // 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 = typeof options.ttlNs !== "undefined" ? BigInt(options.ttlNs) : DEFAULT_TTL_NS + const ttlNs = typeof options.ttlNs !== 'undefined' ? BigInt(options.ttlNs) : DEFAULT_TTL_NS return _create(peerId, value, seq, validityType, expirationDate.toString(), ttlNs, options) } @@ -197,7 +197,7 @@ export async function createWithExpiration (peerId: PeerId, value: CID | PeerId export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise { const expirationDate = NanoDate.fromString(expiration) const validityType = IpnsEntry.ValidityType.EOL - const ttlNs = typeof options.ttlNs !== "undefined" ? BigInt(options.ttlNs) : DEFAULT_TTL_NS + const ttlNs = typeof options.ttlNs !== 'undefined' ? BigInt(options.ttlNs) : DEFAULT_TTL_NS return _create(peerId, value, seq, validityType, expirationDate.toString(), ttlNs, options) } diff --git a/test/index.spec.ts b/test/index.spec.ts index 307c0b2..a6dbc92 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -32,6 +32,7 @@ describe('ipns', function () { it('should create an ipns record (V1+V2) correctly', async () => { const sequence = 0 + const ttl = BigInt(3.6e+12) const validity = 1000000 const record = await ipns.create(peerId, contentPath, sequence, validity) @@ -40,7 +41,7 @@ describe('ipns', function () { 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.ttl).to.equal(ttl) expect(record.signatureV1).to.exist() expect(record.signatureV2).to.exist() expect(record.data).to.exist() @@ -51,7 +52,7 @@ describe('ipns', function () { 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.ttl).to.equal(ttl) expect(pb.signatureV1).to.exist() expect(pb.signatureV2).to.exist() expect(pb.data).to.exist() @@ -67,6 +68,7 @@ describe('ipns', function () { it('should create an ipns record (V2) correctly', async () => { const sequence = 0 + const ttl = BigInt(3.6e+12) const validity = 1000000 const record = await ipns.create(peerId, contentPath, sequence, validity, { v1Compatible: false }) @@ -75,7 +77,7 @@ describe('ipns', function () { 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.ttl).to.equal(ttl) expect(record.signatureV2).to.exist() expect(record).to.not.have.property('signatureV1') expect(record.data).to.exist() @@ -97,7 +99,7 @@ describe('ipns', function () { 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)) + expect(data.TTL).to.equal(ttl) }) it('should be able to create a record (V1+V2) with a fixed expiration', async () => { @@ -130,6 +132,43 @@ describe('ipns', function () { expect(data.Validity).to.equalBytes(uint8ArrayFromString(expiration)) }) + it('should be able to create a record (V1+V2) with a fixed ttl', async () => { + const sequence = 0 + const ttl = BigInt(0.6e+12) + const validity = 1000000 + + const record = await ipns.create(peerId, contentPath, sequence, validity, { + ttlNs: ttl, + }) + const marshalledRecord = ipns.marshal(record) + + await ipnsValidator(peerIdToRoutingKey(peerId), marshalledRecord) + + const pb = IpnsEntry.decode(marshalledRecord) + const data = parseCborData(pb.data ?? new Uint8Array(0)) + expect(data.TTL).to.equal(ttl) + }) + + it('should be able to create a record (V2) with a fixed ttl', async () => { + const sequence = 0 + const ttl = BigInt(1.6e+12) + const validity = 1000000 + + const record = await ipns.create(peerId, contentPath, sequence, validity,{ + ttlNs: ttl, + v1Compatible: false + }) + const marshalledRecord = ipns.marshal(record) + + await ipnsValidator(peerIdToRoutingKey(peerId), marshalledRecord) + + const pb = IpnsEntry.decode(ipns.marshal(record)) + expect(pb).to.not.have.property('ttl') + + const data = parseCborData(pb.data ?? new Uint8Array(0)) + expect(data.TTL).to.equal(ttl) + }) + it('should create an ipns record (V1+V2) and validate it correctly', async () => { const sequence = 0 const validity = 1000000 From ffafe6dc73cc29e78e212d235fb62002816dc4aa Mon Sep 17 00:00:00 2001 From: jtsmedley <38006759+jtsmedley@users.noreply.github.com> Date: Thu, 21 Mar 2024 20:03:55 -0500 Subject: [PATCH 5/8] Fix Tests Lint --- test/index.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/index.spec.ts b/test/index.spec.ts index a6dbc92..184bbe0 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -138,7 +138,7 @@ describe('ipns', function () { const validity = 1000000 const record = await ipns.create(peerId, contentPath, sequence, validity, { - ttlNs: ttl, + ttlNs: ttl }) const marshalledRecord = ipns.marshal(record) @@ -154,7 +154,7 @@ describe('ipns', function () { const ttl = BigInt(1.6e+12) const validity = 1000000 - const record = await ipns.create(peerId, contentPath, sequence, validity,{ + const record = await ipns.create(peerId, contentPath, sequence, validity, { ttlNs: ttl, v1Compatible: false }) From 0f3837fc571dc03a01b38b1604fc70b581b78b11 Mon Sep 17 00:00:00 2001 From: jtsmedley <38006759+jtsmedley@users.noreply.github.com> Date: Mon, 1 Apr 2024 12:01:28 -0500 Subject: [PATCH 6/8] Apply suggestions from code review Co-authored-by: Daniel Norman <1992255+2color@users.noreply.github.com> --- src/index.ts | 4 ++-- test/index.spec.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index 8cd4414..783ca94 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,7 +16,7 @@ import type { CID } from 'multiformats/cid' const log = logger('ipns') const ID_MULTIHASH_CODE = identity.code -const DEFAULT_TTL_NS = BigInt(3.6e+12) // 1 Hour or 3600 Seconds +const DEFAULT_TTL_NS = 60 * 60 * 1e+9 // 1 Hour or 3600 Seconds export const namespace = '/ipns/' export const namespaceLength = namespace.length @@ -129,7 +129,7 @@ export interface IDKeys { } export interface CreateOptions { - ttlNs?: bigint + ttlNs?: number | bigint v1Compatible?: boolean } diff --git a/test/index.spec.ts b/test/index.spec.ts index 184bbe0..12b4c53 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -32,7 +32,7 @@ describe('ipns', function () { it('should create an ipns record (V1+V2) correctly', async () => { const sequence = 0 - const ttl = BigInt(3.6e+12) + const ttl = 3.6e+12 const validity = 1000000 const record = await ipns.create(peerId, contentPath, sequence, validity) @@ -68,7 +68,7 @@ describe('ipns', function () { it('should create an ipns record (V2) correctly', async () => { const sequence = 0 - const ttl = BigInt(3.6e+12) + const ttl = 3.6e+12 const validity = 1000000 const record = await ipns.create(peerId, contentPath, sequence, validity, { v1Compatible: false }) @@ -134,7 +134,7 @@ describe('ipns', function () { it('should be able to create a record (V1+V2) with a fixed ttl', async () => { const sequence = 0 - const ttl = BigInt(0.6e+12) + const ttl = 0.6e+12 const validity = 1000000 const record = await ipns.create(peerId, contentPath, sequence, validity, { @@ -162,7 +162,7 @@ describe('ipns', function () { await ipnsValidator(peerIdToRoutingKey(peerId), marshalledRecord) - const pb = IpnsEntry.decode(ipns.marshal(record)) + const pb = IpnsEntry.decode(marshalledRecord) expect(pb).to.not.have.property('ttl') const data = parseCborData(pb.data ?? new Uint8Array(0)) From 6e57ddcebbe1864d92d2268452702f1349b2df71 Mon Sep 17 00:00:00 2001 From: jtsmedley <38006759+jtsmedley@users.noreply.github.com> Date: Tue, 2 Apr 2024 12:42:30 -0500 Subject: [PATCH 7/8] Apply suggestions from code review Co-authored-by: Alex Potsides --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 783ca94..aac9f0c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -170,7 +170,7 @@ export async function create (peerId: PeerId, value: CID | PeerId | string, seq: // 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 = typeof options.ttlNs !== 'undefined' ? BigInt(options.ttlNs) : DEFAULT_TTL_NS + const ttlNs = BigInt(options.ttlNs != null ? options.ttlNs : DEFAULT_TTL_NS) return _create(peerId, value, seq, validityType, expirationDate.toString(), ttlNs, options) } @@ -197,7 +197,7 @@ export async function createWithExpiration (peerId: PeerId, value: CID | PeerId export async function createWithExpiration (peerId: PeerId, value: CID | PeerId | string, seq: number | bigint, expiration: string, options: CreateOptions = defaultCreateOptions): Promise { const expirationDate = NanoDate.fromString(expiration) const validityType = IpnsEntry.ValidityType.EOL - const ttlNs = typeof options.ttlNs !== 'undefined' ? BigInt(options.ttlNs) : DEFAULT_TTL_NS + const ttlNs = BigInt(options.ttlNs != null ? options.ttlNs : DEFAULT_TTL_NS) return _create(peerId, value, seq, validityType, expirationDate.toString(), ttlNs, options) } From 4c159aa96e8d6932cbdfafc0b143a2adc1edb582 Mon Sep 17 00:00:00 2001 From: jtsmedley <38006759+jtsmedley@users.noreply.github.com> Date: Tue, 2 Apr 2024 12:59:02 -0500 Subject: [PATCH 8/8] Fix Tests wrapping TTL as a BigInt for Comparison --- test/index.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/index.spec.ts b/test/index.spec.ts index 12b4c53..1800945 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -32,7 +32,7 @@ describe('ipns', function () { it('should create an ipns record (V1+V2) correctly', async () => { const sequence = 0 - const ttl = 3.6e+12 + const ttl = BigInt(60 * 60 * 1e+9) const validity = 1000000 const record = await ipns.create(peerId, contentPath, sequence, validity) @@ -68,7 +68,7 @@ describe('ipns', function () { it('should create an ipns record (V2) correctly', async () => { const sequence = 0 - const ttl = 3.6e+12 + const ttl = BigInt(60 * 60 * 1e+9) const validity = 1000000 const record = await ipns.create(peerId, contentPath, sequence, validity, { v1Compatible: false }) @@ -134,7 +134,7 @@ describe('ipns', function () { it('should be able to create a record (V1+V2) with a fixed ttl', async () => { const sequence = 0 - const ttl = 0.6e+12 + const ttl = BigInt(0.6e+12) const validity = 1000000 const record = await ipns.create(peerId, contentPath, sequence, validity, {