Skip to content
This repository has been archived by the owner on Oct 3, 2023. It is now read-only.

Commit

Permalink
fix!: only discover bootstrap peers once and tag them on discovery (#142
Browse files Browse the repository at this point in the history
)

Bootstrap peers should be used in an intial DHT self query to find peers
that are KAD-close to us.

We do not need to rediscover the same peers over and over again, instead
we should just discover them once, use them to query for peers near our
PeerId then we can disconnect from them like any other peer

1. Instead of "discovering" the same peers every few seconds, only
discover them once
2. Tag the peers in the peer store with an expiring value to prevent any
potential connection being culled before we've used the bootstrap nodes
to query for peers close to us

BREAKING CHANGE: the `interval` option has been renamed `timeout` and
peers are now only discovered once
  • Loading branch information
achingbrain authored Sep 23, 2022
1 parent 15f9302 commit cd41d94
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 76 deletions.
62 changes: 37 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,37 +24,49 @@ $ npm i @libp2p/bootstrap

## Usage

The configured bootstrap peers will be discovered after the configured timeout. This will ensure
there are some peers in the peer store for the node to use to discover other peers.

They will be tagged with a tag with the name `'bootstrap'` tag, the value `50` and it will expire
after two minutes which means the nodes connections may be closed if the maximum number of
connections is reached.

Clients that need constant connections to bootstrap nodes (e.g. browsers) can set the TTL to `Infinity`.

```JavaScript
const Libp2p = require('libp2p')
const Bootstrap = require('libp2p-bootstrap')
const TCP = require('libp2p-tcp')
const { NOISE } = require('libp2p-noise')
const MPLEX = require('libp2p-mplex')
import { createLibp2p } from 'libp2p'
import { Bootstrap } from '@libp2p/bootstrap'
import { TCP } from 'libp2p/tcp'
import { Noise } from '@libp2p/noise'
import { Mplex } from '@libp2p/mplex'

let options = {
modules: {
transport: [ TCP ],
peerDiscovery: [ Bootstrap ],
streamMuxer: [ MPLEX ],
encryption: [ NOISE ]
},
config: {
peerDiscovery: {
[Bootstrap.tag]: {
list: [ // a list of bootstrap peer multiaddrs to connect to on node startup
"/ip4/104.131.131.82/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
"/dnsaddr/bootstrap.libp2p.io/ipfs/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
"/dnsaddr/bootstrap.libp2p.io/ipfs/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa"
],
interval: 5000 // default is 10 ms,
enabled: true
}
}
}
transports: [
new TCP()
],
streamMuxers: [
new Mplex()
],
connectionEncryption: [
new Noise()
],
peerDiscovery: [
new Bootstrap({
list: [ // a list of bootstrap peer multiaddrs to connect to on node startup
"/ip4/104.131.131.82/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
"/dnsaddr/bootstrap.libp2p.io/ipfs/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
"/dnsaddr/bootstrap.libp2p.io/ipfs/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa"
],
timeout: 1000, // in ms,
tagName: 'bootstrap',
tagValue: 50,
tagTTL: 120000 // in ms
})
]
}

async function start () {
let libp2p = await Libp2p.create(options)
let libp2p = await createLibp2p(options)

libp2p.on('peer:discovery', function (peerId) {
console.log('found peer: ', peerId.toB58String())
Expand Down
5 changes: 0 additions & 5 deletions examples/try.js

This file was deleted.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,17 +138,21 @@
"release": "aegir release"
},
"dependencies": {
"@libp2p/components": "^2.0.0",
"@libp2p/interface-peer-discovery": "^1.0.1",
"@libp2p/interface-peer-info": "^1.0.3",
"@libp2p/interfaces": "^3.0.3",
"@libp2p/logger": "^2.0.0",
"@libp2p/logger": "^2.0.1",
"@libp2p/peer-id": "^1.1.15",
"@multiformats/mafmt": "^11.0.3",
"@multiformats/multiaddr": "^11.0.0"
},
"devDependencies": {
"@libp2p/interface-peer-discovery-compliance-tests": "^1.0.2",
"@libp2p/interface-peer-id": "^1.0.4",
"aegir": "^37.5.3"
"@libp2p/peer-store": "^3.1.5",
"aegir": "^37.5.3",
"datastore-core": "^8.0.1",
"delay": "^5.0.0"
}
}
71 changes: 57 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,61 @@ import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interface-peer-
import type { PeerInfo } from '@libp2p/interface-peer-info'
import { peerIdFromString } from '@libp2p/peer-id'
import { symbol } from '@libp2p/interface-peer-discovery'
import { Components, Initializable } from '@libp2p/components'

const log = logger('libp2p:bootstrap')

const DEFAULT_BOOTSTRAP_TAG_NAME = 'bootstrap'
const DEFAULT_BOOTSTRAP_TAG_VALUE = 50
const DEFAULT_BOOTSTRAP_TAG_TTL = 120000
const DEFAULT_BOOTSTRAP_DISCOVERY_TIMEOUT = 1000

export interface BootstrapOptions {
/**
* The list of peer addresses in multi-address format
*/
list: string[]

/**
* The interval between emitting addresses in milliseconds
* How long to wait before discovering bootstrap nodes
*/
timeout?: number

/**
* Tag a bootstrap peer with this name before "discovering" it (default: 'bootstrap')
*/
tagName?: string

/**
* The bootstrap peer tag will have this value (default: 50)
*/
tagValue?: number

/**
* Cause the bootstrap peer tag to be removed after this number of ms (default: 2 minutes)
*/
interval?: number
tagTTL?: number
}

/**
* Emits 'peer' events on a regular interval for each peer in the provided list.
*/
export class Bootstrap extends EventEmitter<PeerDiscoveryEvents> implements PeerDiscovery {
export class Bootstrap extends EventEmitter<PeerDiscoveryEvents> implements PeerDiscovery, Initializable {
static tag = 'bootstrap'

private timer?: ReturnType<typeof setInterval>
private timer?: ReturnType<typeof setTimeout>
private readonly list: PeerInfo[]
private readonly interval: number
private readonly timeout: number
private components: Components = new Components()
private readonly _init: BootstrapOptions

constructor (options: BootstrapOptions = { list: [] }) {
if (options.list == null || options.list.length === 0) {
throw new Error('Bootstrap requires a list of peer addresses')
}
super()

this.interval = options.interval ?? 10000
this.timeout = options.timeout ?? DEFAULT_BOOTSTRAP_DISCOVERY_TIMEOUT
this.list = []

for (const candidate of options.list) {
Expand All @@ -62,6 +85,12 @@ export class Bootstrap extends EventEmitter<PeerDiscoveryEvents> implements Peer

this.list.push(peerData)
}

this._init = options
}

init (components: Components) {
this.components = components
}

get [symbol] (): true {
Expand All @@ -80,34 +109,48 @@ export class Bootstrap extends EventEmitter<PeerDiscoveryEvents> implements Peer
* Start emitting events
*/
start () {
if (this.timer != null) {
if (this.isStarted()) {
return
}

this.timer = setInterval(() => this._discoverBootstrapPeers(), this.interval)
log('Starting bootstrap node discovery')
this._discoverBootstrapPeers()
log('Starting bootstrap node discovery, discovering peers after %s ms', this.timeout)
this.timer = setTimeout(() => {
void this._discoverBootstrapPeers()
.catch(err => {
log.error(err)
})
}, this.timeout)
}

/**
* Emit each address in the list as a PeerInfo
*/
_discoverBootstrapPeers () {
async _discoverBootstrapPeers () {
if (this.timer == null) {
return
}

this.list.forEach((peerData) => {
for (const peerData of this.list) {
await this.components.getPeerStore().tagPeer(peerData.id, this._init.tagName ?? DEFAULT_BOOTSTRAP_TAG_NAME, {
value: this._init.tagValue ?? DEFAULT_BOOTSTRAP_TAG_VALUE,
ttl: this._init.tagTTL ?? DEFAULT_BOOTSTRAP_TAG_TTL
})

// check we are still running
if (this.timer == null) {
return
}

this.dispatchEvent(new CustomEvent<PeerInfo>('peer', { detail: peerData }))
})
}
}

/**
* Stop emitting events
*/
stop () {
if (this.timer != null) {
clearInterval(this.timer)
clearTimeout(this.timer)
}

this.timer = undefined
Expand Down
102 changes: 73 additions & 29 deletions test/bootstrap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,106 @@ import { IPFS } from '@multiformats/mafmt'
import { Bootstrap } from '../src/index.js'
import peerList from './fixtures/default-peers.js'
import partialValidPeerList from './fixtures/some-invalid-peers.js'
import type { PeerInfo } from '@libp2p/interface-peer-info'
import { isPeerId } from '@libp2p/interface-peer-id'
import { Components } from '@libp2p/components'
import { PersistentPeerStore } from '@libp2p/peer-store'
import { MemoryDatastore } from 'datastore-core'
import { multiaddr } from '@multiformats/multiaddr'
import { peerIdFromString } from '@libp2p/peer-id'
import delay from 'delay'

describe('bootstrap', () => {
let components: Components

beforeEach(() => {
const datastore = new MemoryDatastore()
const peerStore = new PersistentPeerStore()

components = new Components({
peerStore,
datastore
})

peerStore.init(components)
})

it('should throw if no peer list is provided', () => {
expect(() => new Bootstrap())
.to.throw('Bootstrap requires a list of peer addresses')
})

it('find the other peer', async function () {
it('should discover bootstrap peers', async function () {
this.timeout(5 * 1000)
const r = new Bootstrap({
list: peerList,
interval: 2000
timeout: 100
})
r.init(components)

const p = new Promise((resolve) => r.addEventListener('peer', resolve, {
once: true
}))
r.start()

await p
r.stop()
})

it('should tag bootstrap peers', async function () {
this.timeout(5 * 1000)

const tagName = 'tag-tag'
const tagValue = 10
const tagTTL = 50

const r = new Bootstrap({
list: peerList,
timeout: 100,
tagName,
tagValue,
tagTTL
})
r.init(components)

const p = new Promise((resolve) => r.addEventListener('peer', resolve, {
once: true
}))
r.start()

await p

const bootstrapper0ma = multiaddr(peerList[0])
const bootstrapper0PeerIdStr = bootstrapper0ma.getPeerId()

if (bootstrapper0PeerIdStr == null) {
throw new Error('bootstrapper had no PeerID')
}

const bootstrapper0PeerId = peerIdFromString(bootstrapper0PeerIdStr)

const tags = await components.getPeerStore().getTags(bootstrapper0PeerId)

expect(tags).to.have.lengthOf(1, 'bootstrap tag was not set')
expect(tags).to.have.nested.property('[0].name', tagName, 'bootstrap tag had incorrect name')
expect(tags).to.have.nested.property('[0].value', tagValue, 'bootstrap tag had incorrect value')

await delay(tagTTL * 2)

const tags2 = await components.getPeerStore().getTags(bootstrapper0PeerId)

expect(tags2).to.have.lengthOf(0, 'bootstrap tag did not expire')

r.stop()
})

it('not fail on malformed peers in peer list', async function () {
it('should not fail on malformed peers in peer list', async function () {
this.timeout(5 * 1000)

const r = new Bootstrap({
list: partialValidPeerList,
interval: 2000
timeout: 100
})
r.init(components)

const p = new Promise<void>((resolve) => {
r.addEventListener('peer', (evt) => {
Expand All @@ -55,28 +123,4 @@ describe('bootstrap', () => {
await p
r.stop()
})

it('stop emitting events when stop() called', async function () {
const interval = 100
const r = new Bootstrap({
list: peerList,
interval
})

let emitted: PeerInfo[] = []
r.addEventListener('peer', p => emitted.push(p.detail))

// Should fire emit event for each peer in list on start,
// so wait 50 milliseconds then check
const p = new Promise((resolve) => setTimeout(resolve, 50))
r.start()
await p
expect(emitted).to.have.length(peerList.length)

// After stop is called, no more peers should be emitted
emitted = []
r.stop()
await new Promise((resolve) => setTimeout(resolve, interval))
expect(emitted).to.have.length(0)
})
})
Loading

0 comments on commit cd41d94

Please sign in to comment.