From 704b41355768b3e8723560c5f7ed3d7c12b58c3b Mon Sep 17 00:00:00 2001 From: Alex Potsides Date: Tue, 9 May 2023 09:04:54 +0100 Subject: [PATCH] fix: cache IPNS entries after resolving (#35) After resolving one or more IPNS records, use the selector to choose one and then cache the result. Fixes #20 --- packages/ipns/src/index.ts | 28 ++++++++++--- packages/ipns/src/routing/local-store.ts | 4 +- packages/ipns/test/resolve.spec.ts | 52 +++++++++++++++++++++++- 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/packages/ipns/src/index.ts b/packages/ipns/src/index.ts index 7e2345d1..932361b4 100644 --- a/packages/ipns/src/index.ts +++ b/packages/ipns/src/index.ts @@ -68,6 +68,7 @@ import { create, marshal, peerIdToRoutingKey, unmarshal } from 'ipns' import type { IPNSEntry } from 'ipns' import type { IPNSRouting, IPNSRoutingEvents } from './routing/index.js' import { ipnsValidator } from 'ipns/validator' +import { ipnsSelector } from 'ipns/selector' import { CID } from 'multiformats/cid' import { resolveDnslink } from './utils/resolve-dns-link.js' import { logger } from '@libp2p/logger' @@ -78,6 +79,7 @@ import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' import type { Datastore } from 'interface-datastore' import { localStore, LocalStore } from './routing/local-store.js' +import { CodeError } from '@libp2p/interfaces/errors' const log = logger('helia:ipns') @@ -295,16 +297,30 @@ class DefaultIPNS implements IPNS { ] } - const unmarshaledRecord = await Promise.any( - routers.map(async (router) => { - const unmarshaledRecord = await router.get(routingKey, options) - await ipnsValidator(routingKey, unmarshaledRecord) + const records: Uint8Array[] = [] - return unmarshaledRecord + await Promise.all( + routers.map(async (router) => { + try { + const record = await router.get(routingKey, options) + await ipnsValidator(routingKey, record) + + records.push(record) + } catch (err) { + log.error('error finding IPNS record', err) + } }) ) - return unmarshal(unmarshaledRecord) + if (records.length === 0) { + throw new CodeError('Could not find record for routing key', 'ERR_NOT_FOUND') + } + + const record = records[ipnsSelector(routingKey, records)] + + await this.localStore.put(routingKey, record, options) + + return unmarshal(record) } } diff --git a/packages/ipns/src/routing/local-store.ts b/packages/ipns/src/routing/local-store.ts index 4ea0a375..f9c1dbc8 100644 --- a/packages/ipns/src/routing/local-store.ts +++ b/packages/ipns/src/routing/local-store.ts @@ -25,12 +25,12 @@ export interface LocalStore extends IPNSRouting { */ export function localStore (datastore: Datastore): LocalStore { return { - async put (routingKey: Uint8Array, marshaledRecord: Uint8Array, options: PutOptions = {}) { + async put (routingKey: Uint8Array, marshalledRecord: Uint8Array, options: PutOptions = {}) { try { const key = dhtRoutingKey(routingKey) // Marshal to libp2p record as the DHT does - const record = new Libp2pRecord(routingKey, marshaledRecord, new Date()) + const record = new Libp2pRecord(routingKey, marshalledRecord, new Date()) options.onProgress?.(new CustomProgressEvent('ipns:routing:datastore:put')) await datastore.put(key, record.serialize(), options) diff --git a/packages/ipns/test/resolve.spec.ts b/packages/ipns/test/resolve.spec.ts index 70bec018..051e68bc 100644 --- a/packages/ipns/test/resolve.spec.ts +++ b/packages/ipns/test/resolve.spec.ts @@ -8,15 +8,21 @@ import { CID } from 'multiformats/cid' import { createEd25519PeerId } from '@libp2p/peer-id-factory' import Sinon from 'sinon' import { StubbedInstance, stubInterface } from 'sinon-ts' +import { create, marshal, peerIdToRoutingKey } from 'ipns' +import { Datastore, Key } from 'interface-datastore' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { Libp2pRecord } from '@libp2p/record' const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn') describe('resolve', () => { let name: IPNS let routing: StubbedInstance + let datastore: Datastore beforeEach(async () => { - const datastore = new MemoryDatastore() + datastore = new MemoryDatastore() routing = stubInterface() routing.get.throws(new Error('Not found')) @@ -98,4 +104,48 @@ describe('resolve', () => { expect(onProgress).to.have.property('called', true) }) + + it('should cache a record', async function () { + const peerId = await createEd25519PeerId() + const routingKey = peerIdToRoutingKey(peerId) + const dhtKey = new Key('/dht/record/' + uint8ArrayToString(routingKey, 'base32'), false) + + expect(datastore.has(dhtKey)).to.be.false('already had record') + + const bytes = uint8ArrayFromString(`/ipfs/${cid.toString()}`) + const record = await create(peerId, bytes, 0n, 60000) + const marshalledRecord = marshal(record) + + routing.get.withArgs(routingKey).resolves(marshalledRecord) + + const result = await name.resolve(peerId) + expect(result.toString()).to.equal(cid.toString(), 'incorrect record resolved') + + expect(datastore.has(dhtKey)).to.be.true('did not cache record locally') + }) + + it('should cache the most recent record', async function () { + const peerId = await createEd25519PeerId() + const routingKey = peerIdToRoutingKey(peerId) + const dhtKey = new Key('/dht/record/' + uint8ArrayToString(routingKey, 'base32'), false) + + const marshalledRecordA = marshal(await create(peerId, uint8ArrayFromString(`/ipfs/${cid.toString()}`), 0n, 60000)) + const marshalledRecordB = marshal(await create(peerId, uint8ArrayFromString(`/ipfs/${cid.toString()}`), 10n, 60000)) + + // records should not match + expect(marshalledRecordA).to.not.equalBytes(marshalledRecordB) + + // cache has older record + await datastore.put(dhtKey, marshalledRecordA) + routing.get.withArgs(routingKey).resolves(marshalledRecordB) + + const result = await name.resolve(peerId) + expect(result.toString()).to.equal(cid.toString(), 'incorrect record resolved') + + const cached = await datastore.get(dhtKey) + const record = Libp2pRecord.deserialize(cached) + + // should have cached the updated record + expect(record.value).to.equalBytes(marshalledRecordB) + }) })