Skip to content

Commit

Permalink
feat!: use helia router for IPNS put/get (#387)
Browse files Browse the repository at this point in the history
Uses the configured `.routing` property on the Helia interface to
put/get IPNS records by default.

Removes the `libp2p` routing as `helia` configures this as a
`.routing` implementation and `@helia/http` doesn't have it.

Updates the description of the `pubsub` routing to make it's
limitations clearer.

BREAKING CHANGE: `helia.routing` is the default routing used, the `libp2p` routing has been removed as it is redundant
  • Loading branch information
achingbrain authored Jan 17, 2024
1 parent 3477b27 commit ce74026
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 90 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-env mocha */

import { ipns } from '@helia/ipns'
import { libp2p } from '@helia/ipns/routing'
import { peerIdFromString } from '@libp2p/peer-id'
import { createEd25519PeerId, createRSAPeerId, createSecp256k1PeerId } from '@libp2p/peer-id-factory'
import { expect } from 'aegir/chai'
Expand All @@ -24,7 +23,7 @@ import type { HeliaLibp2p } from 'helia'
import type { Controller } from 'ipfsd-ctl'

keyTypes.forEach(type => {
describe(`@helia/ipns - libp2p routing with ${type} keys`, () => {
describe(`@helia/ipns - default routing with ${type} keys`, () => {
let helia: HeliaLibp2p
let kubo: Controller
let name: IPNS
Expand Down Expand Up @@ -114,11 +113,7 @@ keyTypes.forEach(type => {
message: 'Kubo could not find Helia on the DHT'
})

name = ipns(helia, {
routers: [
libp2p(helia)
]
})
name = ipns(helia)
}

afterEach(async () => {
Expand Down
44 changes: 41 additions & 3 deletions packages/ipns/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,58 @@

IPNS operations using a Helia node

## Example - Using libp2p and pubsub routers
## Example - Getting started

With IPNSRouting routers:

```typescript
import { createHelia } from 'helia'
import { ipns } from '@helia/ipns'
import { libp2p, pubsub } from '@helia/ipns/routing'
import { unixfs } from '@helia/unixfs'

const helia = await createHelia()
const name = ipns(helia)

// create a public key to publish as an IPNS name
const keyInfo = await helia.libp2p.services.keychain.createKey('my-key')
const peerId = await helia.libp2p.services.keychain.exportPeerId(keyInfo.name)

// store some data to publish
const fs = unixfs(helia)
const cid = await fs.add(Uint8Array.from([0, 1, 2, 3, 4]))

// publish the name
await name.publish(peerId, cid)

// resolve the name
const cid = name.resolve(peerId)
```

## Example - Using custom PubSub router

Additional IPNS routers can be configured - these enable alternative means to
publish and resolve IPNS names.

One example is the PubSub router - this requires an instance of Helia with
libp2p PubSub configured.

It works by subscribing to a pubsub topic for each IPNS name that we try to
resolve. Updated IPNS records are shared on these topics so an update must
occur before the name is resolvable.

This router is only suitable for networks where IPNS updates are frequent
and multiple peers are listening on the topic(s), otherwise update messages
may fail to be published with "Insufficient peers" errors.

```typescript
import { createHelia } from 'helia'
import { ipns } from '@helia/ipns'
import { pubsub } from '@helia/ipns/routing'
import { unixfs } from '@helia/unixfs'

const helia = await createHelia()
const name = ipns(helia, {
routers: [
libp2p(helia),
pubsub(helia)
]
})
Expand Down
5 changes: 2 additions & 3 deletions packages/ipns/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@
"lint": "aegir lint",
"dep-check": "aegir dep-check",
"build": "aegir build",
"docs": "aegir docs",
"test": "aegir test",
"test:chrome": "aegir test -t browser --cov",
"test:chrome-webworker": "aegir test -t webworker",
Expand All @@ -163,6 +164,7 @@
"release": "aegir release"
},
"dependencies": {
"@helia/interface": "^3.0.1",
"@libp2p/interface": "^1.1.1",
"@libp2p/kad-dht": "^12.0.2",
"@libp2p/logger": "^4.0.4",
Expand All @@ -188,8 +190,5 @@
},
"browser": {
"./dist/src/dns-resolvers/resolver.js": "./dist/src/dns-resolvers/resolver.browser.js"
},
"typedoc": {
"entryPoint": "./src/index.ts"
}
}
54 changes: 49 additions & 5 deletions packages/ipns/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,58 @@
*
* IPNS operations using a Helia node
*
* @example Using libp2p and pubsub routers
* @example Getting started
*
* With {@link IPNSRouting} routers:
*
* ```typescript
* import { createHelia } from 'helia'
* import { ipns } from '@helia/ipns'
* import { libp2p, pubsub } from '@helia/ipns/routing'
* import { unixfs } from '@helia/unixfs'
*
* const helia = await createHelia()
* const name = ipns(helia)
*
* // create a public key to publish as an IPNS name
* const keyInfo = await helia.libp2p.services.keychain.createKey('my-key')
* const peerId = await helia.libp2p.services.keychain.exportPeerId(keyInfo.name)
*
* // store some data to publish
* const fs = unixfs(helia)
* const cid = await fs.add(Uint8Array.from([0, 1, 2, 3, 4]))
*
* // publish the name
* await name.publish(peerId, cid)
*
* // resolve the name
* const cid = name.resolve(peerId)
* ```
*
* @example Using custom PubSub router
*
* Additional IPNS routers can be configured - these enable alternative means to
* publish and resolve IPNS names.
*
* One example is the PubSub router - this requires an instance of Helia with
* libp2p PubSub configured.
*
* It works by subscribing to a pubsub topic for each IPNS name that we try to
* resolve. Updated IPNS records are shared on these topics so an update must
* occur before the name is resolvable.
*
* This router is only suitable for networks where IPNS updates are frequent
* and multiple peers are listening on the topic(s), otherwise update messages
* may fail to be published with "Insufficient peers" errors.
*
* ```typescript
* import { createHelia } from 'helia'
* import { ipns } from '@helia/ipns'
* import { pubsub } from '@helia/ipns/routing'
* import { unixfs } from '@helia/unixfs'
*
* const helia = await createHelia()
* const name = ipns(helia, {
* routers: [
* libp2p(helia),
* pubsub(helia)
* ]
* })
Expand Down Expand Up @@ -121,9 +159,11 @@ import { ipnsValidator } from 'ipns/validator'
import { CID } from 'multiformats/cid'
import { CustomProgressEvent } from 'progress-events'
import { defaultResolver } from './dns-resolvers/default.js'
import { helia } from './routing/helia.js'
import { localStore, type LocalStore } from './routing/local-store.js'
import type { IPNSRouting, IPNSRoutingEvents } from './routing/index.js'
import type { DNSResponse } from './utils/dns.js'
import type { Routing } from '@helia/interface'
import type { AbortOptions, PeerId } from '@libp2p/interface'
import type { Datastore } from 'interface-datastore'
import type { IPNSRecord } from 'ipns'
Expand Down Expand Up @@ -249,6 +289,7 @@ export type { IPNSRouting } from './routing/index.js'

export interface IPNSComponents {
datastore: Datastore
routing: Routing
}

class DefaultIPNS implements IPNS {
Expand All @@ -258,7 +299,10 @@ class DefaultIPNS implements IPNS {
private readonly defaultResolvers: DNSResolver[]

constructor (components: IPNSComponents, routers: IPNSRouting[] = [], resolvers: DNSResolver[] = []) {
this.routers = routers
this.routers = [
helia(components.routing),
...routers
]
this.localStore = localStore(components.datastore)
this.defaultResolvers = resolvers.length > 0 ? resolvers : [defaultResolver()]
}
Expand Down Expand Up @@ -407,7 +451,7 @@ export interface IPNSOptions {
resolvers?: DNSResolver[]
}

export function ipns (components: IPNSComponents, { routers = [], resolvers = [] }: IPNSOptions): IPNS {
export function ipns (components: IPNSComponents, { routers = [], resolvers = [] }: IPNSOptions = {}): IPNS {
return new DefaultIPNS(components, routers, resolvers)
}

Expand Down
45 changes: 45 additions & 0 deletions packages/ipns/src/routing/helia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { CustomProgressEvent, type ProgressEvent } from 'progress-events'
import type { GetOptions, PutOptions } from './index.js'
import type { IPNSRouting } from '../index.js'
import type { Routing } from '@helia/interface'

export interface HeliaRoutingComponents {
routing: Routing
}

export type HeliaRoutingProgressEvents =
ProgressEvent<'ipns:routing:helia:error', Error>

export class HeliaRouting implements IPNSRouting {
private readonly routing: Routing

constructor (routing: Routing) {
this.routing = routing
}

async put (routingKey: Uint8Array, marshaledRecord: Uint8Array, options: PutOptions = {}): Promise<void> {
try {
await this.routing.put(routingKey, marshaledRecord, options)
} catch (err: any) {
options.onProgress?.(new CustomProgressEvent<Error>('ipns:routing:helia:error', err))
}
}

async get (routingKey: Uint8Array, options: GetOptions = {}): Promise<Uint8Array> {
try {
return await this.routing.get(routingKey, options)
} catch (err: any) {
options.onProgress?.(new CustomProgressEvent<Error>('ipns:routing:helia:error', err))
}

throw new Error('Not found')
}
}

/**
* The helia routing uses any available Routers configured on the passed Helia
* node. This could be libp2p, HTTP API Delegated Routing, etc.
*/
export function helia (routing: Routing): IPNSRouting {
return new HeliaRouting(routing)
}
6 changes: 3 additions & 3 deletions packages/ipns/src/routing/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Libp2pContentRoutingProgressEvents } from './libp2p.js'
import type { HeliaRoutingProgressEvents } from './helia.js'
import type { DatastoreProgressEvents } from './local-store.js'
import type { PubSubProgressEvents } from './pubsub.js'
import type { AbortOptions } from '@libp2p/interface'
Expand All @@ -19,8 +19,8 @@ export interface IPNSRouting {

export type IPNSRoutingEvents =
DatastoreProgressEvents |
Libp2pContentRoutingProgressEvents |
HeliaRoutingProgressEvents |
PubSubProgressEvents

export { libp2p } from './libp2p.js'
export { helia } from './helia.js'
export { pubsub } from './pubsub.js'
47 changes: 0 additions & 47 deletions packages/ipns/src/routing/libp2p.ts

This file was deleted.

17 changes: 12 additions & 5 deletions packages/ipns/test/publish.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,22 @@ import Sinon from 'sinon'
import { type StubbedInstance, stubInterface } from 'sinon-ts'
import { ipns } from '../src/index.js'
import type { IPNS, IPNSRouting } from '../src/index.js'
import type { Routing } from '@helia/interface'

const cid = CID.parse('QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn')

describe('publish', () => {
let name: IPNS
let routing: StubbedInstance<IPNSRouting>
let customRouting: StubbedInstance<IPNSRouting>
let heliaRouting: StubbedInstance<Routing>

beforeEach(async () => {
const datastore = new MemoryDatastore()
routing = stubInterface<IPNSRouting>()
routing.get.throws(new Error('Not found'))
customRouting = stubInterface<IPNSRouting>()
customRouting.get.throws(new Error('Not found'))
heliaRouting = stubInterface<Routing>()

name = ipns({ datastore }, { routers: [routing] })
name = ipns({ datastore, routing: heliaRouting }, { routers: [customRouting] })
})

it('should publish an IPNS record with the default params', async function () {
Expand All @@ -40,6 +43,9 @@ describe('publish', () => {

expect(ipnsEntry).to.have.property('sequence', 1n)
expect(ipnsEntry).to.have.property('ttl', BigInt(lifetime) * 100000n)

expect(heliaRouting.put.called).to.be.true()
expect(customRouting.put.called).to.be.true()
})

it('should publish a record offline', async () => {
Expand All @@ -48,7 +54,8 @@ describe('publish', () => {
offline: true
})

expect(routing.put.called).to.be.false()
expect(heliaRouting.put.called).to.be.false()
expect(customRouting.put.called).to.be.false()
})

it('should emit progress events', async function () {
Expand Down
Loading

0 comments on commit ce74026

Please sign in to comment.