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

fix!: only discover bootstrap peers once and tag them on discovery #142

Merged
merged 3 commits into from
Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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