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

Commit cd41d94

Browse files
authored
fix!: only discover bootstrap peers once and tag them on discovery (#142)
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
1 parent 15f9302 commit cd41d94

File tree

6 files changed

+186
-76
lines changed

6 files changed

+186
-76
lines changed

README.md

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,37 +24,49 @@ $ npm i @libp2p/bootstrap
2424

2525
## Usage
2626

27+
The configured bootstrap peers will be discovered after the configured timeout. This will ensure
28+
there are some peers in the peer store for the node to use to discover other peers.
29+
30+
They will be tagged with a tag with the name `'bootstrap'` tag, the value `50` and it will expire
31+
after two minutes which means the nodes connections may be closed if the maximum number of
32+
connections is reached.
33+
34+
Clients that need constant connections to bootstrap nodes (e.g. browsers) can set the TTL to `Infinity`.
35+
2736
```JavaScript
28-
const Libp2p = require('libp2p')
29-
const Bootstrap = require('libp2p-bootstrap')
30-
const TCP = require('libp2p-tcp')
31-
const { NOISE } = require('libp2p-noise')
32-
const MPLEX = require('libp2p-mplex')
37+
import { createLibp2p } from 'libp2p'
38+
import { Bootstrap } from '@libp2p/bootstrap'
39+
import { TCP } from 'libp2p/tcp'
40+
import { Noise } from '@libp2p/noise'
41+
import { Mplex } from '@libp2p/mplex'
3342

3443
let options = {
35-
modules: {
36-
transport: [ TCP ],
37-
peerDiscovery: [ Bootstrap ],
38-
streamMuxer: [ MPLEX ],
39-
encryption: [ NOISE ]
40-
},
41-
config: {
42-
peerDiscovery: {
43-
[Bootstrap.tag]: {
44-
list: [ // a list of bootstrap peer multiaddrs to connect to on node startup
45-
"/ip4/104.131.131.82/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
46-
"/dnsaddr/bootstrap.libp2p.io/ipfs/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
47-
"/dnsaddr/bootstrap.libp2p.io/ipfs/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa"
48-
],
49-
interval: 5000 // default is 10 ms,
50-
enabled: true
51-
}
52-
}
53-
}
44+
transports: [
45+
new TCP()
46+
],
47+
streamMuxers: [
48+
new Mplex()
49+
],
50+
connectionEncryption: [
51+
new Noise()
52+
],
53+
peerDiscovery: [
54+
new Bootstrap({
55+
list: [ // a list of bootstrap peer multiaddrs to connect to on node startup
56+
"/ip4/104.131.131.82/tcp/4001/ipfs/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
57+
"/dnsaddr/bootstrap.libp2p.io/ipfs/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
58+
"/dnsaddr/bootstrap.libp2p.io/ipfs/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa"
59+
],
60+
timeout: 1000, // in ms,
61+
tagName: 'bootstrap',
62+
tagValue: 50,
63+
tagTTL: 120000 // in ms
64+
})
65+
]
5466
}
5567

5668
async function start () {
57-
let libp2p = await Libp2p.create(options)
69+
let libp2p = await createLibp2p(options)
5870

5971
libp2p.on('peer:discovery', function (peerId) {
6072
console.log('found peer: ', peerId.toB58String())

examples/try.js

Lines changed: 0 additions & 5 deletions
This file was deleted.

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,17 +138,21 @@
138138
"release": "aegir release"
139139
},
140140
"dependencies": {
141+
"@libp2p/components": "^2.0.0",
141142
"@libp2p/interface-peer-discovery": "^1.0.1",
142143
"@libp2p/interface-peer-info": "^1.0.3",
143144
"@libp2p/interfaces": "^3.0.3",
144-
"@libp2p/logger": "^2.0.0",
145+
"@libp2p/logger": "^2.0.1",
145146
"@libp2p/peer-id": "^1.1.15",
146147
"@multiformats/mafmt": "^11.0.3",
147148
"@multiformats/multiaddr": "^11.0.0"
148149
},
149150
"devDependencies": {
150151
"@libp2p/interface-peer-discovery-compliance-tests": "^1.0.2",
151152
"@libp2p/interface-peer-id": "^1.0.4",
152-
"aegir": "^37.5.3"
153+
"@libp2p/peer-store": "^3.1.5",
154+
"aegir": "^37.5.3",
155+
"datastore-core": "^8.0.1",
156+
"delay": "^5.0.0"
153157
}
154158
}

src/index.ts

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,61 @@ import type { PeerDiscovery, PeerDiscoveryEvents } from '@libp2p/interface-peer-
66
import type { PeerInfo } from '@libp2p/interface-peer-info'
77
import { peerIdFromString } from '@libp2p/peer-id'
88
import { symbol } from '@libp2p/interface-peer-discovery'
9+
import { Components, Initializable } from '@libp2p/components'
910

1011
const log = logger('libp2p:bootstrap')
1112

13+
const DEFAULT_BOOTSTRAP_TAG_NAME = 'bootstrap'
14+
const DEFAULT_BOOTSTRAP_TAG_VALUE = 50
15+
const DEFAULT_BOOTSTRAP_TAG_TTL = 120000
16+
const DEFAULT_BOOTSTRAP_DISCOVERY_TIMEOUT = 1000
17+
1218
export interface BootstrapOptions {
1319
/**
1420
* The list of peer addresses in multi-address format
1521
*/
1622
list: string[]
1723

1824
/**
19-
* The interval between emitting addresses in milliseconds
25+
* How long to wait before discovering bootstrap nodes
26+
*/
27+
timeout?: number
28+
29+
/**
30+
* Tag a bootstrap peer with this name before "discovering" it (default: 'bootstrap')
31+
*/
32+
tagName?: string
33+
34+
/**
35+
* The bootstrap peer tag will have this value (default: 50)
36+
*/
37+
tagValue?: number
38+
39+
/**
40+
* Cause the bootstrap peer tag to be removed after this number of ms (default: 2 minutes)
2041
*/
21-
interval?: number
42+
tagTTL?: number
2243
}
2344

2445
/**
2546
* Emits 'peer' events on a regular interval for each peer in the provided list.
2647
*/
27-
export class Bootstrap extends EventEmitter<PeerDiscoveryEvents> implements PeerDiscovery {
48+
export class Bootstrap extends EventEmitter<PeerDiscoveryEvents> implements PeerDiscovery, Initializable {
2849
static tag = 'bootstrap'
2950

30-
private timer?: ReturnType<typeof setInterval>
51+
private timer?: ReturnType<typeof setTimeout>
3152
private readonly list: PeerInfo[]
32-
private readonly interval: number
53+
private readonly timeout: number
54+
private components: Components = new Components()
55+
private readonly _init: BootstrapOptions
3356

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

40-
this.interval = options.interval ?? 10000
63+
this.timeout = options.timeout ?? DEFAULT_BOOTSTRAP_DISCOVERY_TIMEOUT
4164
this.list = []
4265

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

6386
this.list.push(peerData)
6487
}
88+
89+
this._init = options
90+
}
91+
92+
init (components: Components) {
93+
this.components = components
6594
}
6695

6796
get [symbol] (): true {
@@ -80,34 +109,48 @@ export class Bootstrap extends EventEmitter<PeerDiscoveryEvents> implements Peer
80109
* Start emitting events
81110
*/
82111
start () {
83-
if (this.timer != null) {
112+
if (this.isStarted()) {
84113
return
85114
}
86115

87-
this.timer = setInterval(() => this._discoverBootstrapPeers(), this.interval)
88-
log('Starting bootstrap node discovery')
89-
this._discoverBootstrapPeers()
116+
log('Starting bootstrap node discovery, discovering peers after %s ms', this.timeout)
117+
this.timer = setTimeout(() => {
118+
void this._discoverBootstrapPeers()
119+
.catch(err => {
120+
log.error(err)
121+
})
122+
}, this.timeout)
90123
}
91124

92125
/**
93126
* Emit each address in the list as a PeerInfo
94127
*/
95-
_discoverBootstrapPeers () {
128+
async _discoverBootstrapPeers () {
96129
if (this.timer == null) {
97130
return
98131
}
99132

100-
this.list.forEach((peerData) => {
133+
for (const peerData of this.list) {
134+
await this.components.getPeerStore().tagPeer(peerData.id, this._init.tagName ?? DEFAULT_BOOTSTRAP_TAG_NAME, {
135+
value: this._init.tagValue ?? DEFAULT_BOOTSTRAP_TAG_VALUE,
136+
ttl: this._init.tagTTL ?? DEFAULT_BOOTSTRAP_TAG_TTL
137+
})
138+
139+
// check we are still running
140+
if (this.timer == null) {
141+
return
142+
}
143+
101144
this.dispatchEvent(new CustomEvent<PeerInfo>('peer', { detail: peerData }))
102-
})
145+
}
103146
}
104147

105148
/**
106149
* Stop emitting events
107150
*/
108151
stop () {
109152
if (this.timer != null) {
110-
clearInterval(this.timer)
153+
clearTimeout(this.timer)
111154
}
112155

113156
this.timer = undefined

test/bootstrap.spec.ts

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,106 @@ import { IPFS } from '@multiformats/mafmt'
55
import { Bootstrap } from '../src/index.js'
66
import peerList from './fixtures/default-peers.js'
77
import partialValidPeerList from './fixtures/some-invalid-peers.js'
8-
import type { PeerInfo } from '@libp2p/interface-peer-info'
98
import { isPeerId } from '@libp2p/interface-peer-id'
9+
import { Components } from '@libp2p/components'
10+
import { PersistentPeerStore } from '@libp2p/peer-store'
11+
import { MemoryDatastore } from 'datastore-core'
12+
import { multiaddr } from '@multiformats/multiaddr'
13+
import { peerIdFromString } from '@libp2p/peer-id'
14+
import delay from 'delay'
1015

1116
describe('bootstrap', () => {
17+
let components: Components
18+
19+
beforeEach(() => {
20+
const datastore = new MemoryDatastore()
21+
const peerStore = new PersistentPeerStore()
22+
23+
components = new Components({
24+
peerStore,
25+
datastore
26+
})
27+
28+
peerStore.init(components)
29+
})
30+
1231
it('should throw if no peer list is provided', () => {
1332
expect(() => new Bootstrap())
1433
.to.throw('Bootstrap requires a list of peer addresses')
1534
})
1635

17-
it('find the other peer', async function () {
36+
it('should discover bootstrap peers', async function () {
1837
this.timeout(5 * 1000)
1938
const r = new Bootstrap({
2039
list: peerList,
21-
interval: 2000
40+
timeout: 100
41+
})
42+
r.init(components)
43+
44+
const p = new Promise((resolve) => r.addEventListener('peer', resolve, {
45+
once: true
46+
}))
47+
r.start()
48+
49+
await p
50+
r.stop()
51+
})
52+
53+
it('should tag bootstrap peers', async function () {
54+
this.timeout(5 * 1000)
55+
56+
const tagName = 'tag-tag'
57+
const tagValue = 10
58+
const tagTTL = 50
59+
60+
const r = new Bootstrap({
61+
list: peerList,
62+
timeout: 100,
63+
tagName,
64+
tagValue,
65+
tagTTL
2266
})
67+
r.init(components)
2368

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

2974
await p
75+
76+
const bootstrapper0ma = multiaddr(peerList[0])
77+
const bootstrapper0PeerIdStr = bootstrapper0ma.getPeerId()
78+
79+
if (bootstrapper0PeerIdStr == null) {
80+
throw new Error('bootstrapper had no PeerID')
81+
}
82+
83+
const bootstrapper0PeerId = peerIdFromString(bootstrapper0PeerIdStr)
84+
85+
const tags = await components.getPeerStore().getTags(bootstrapper0PeerId)
86+
87+
expect(tags).to.have.lengthOf(1, 'bootstrap tag was not set')
88+
expect(tags).to.have.nested.property('[0].name', tagName, 'bootstrap tag had incorrect name')
89+
expect(tags).to.have.nested.property('[0].value', tagValue, 'bootstrap tag had incorrect value')
90+
91+
await delay(tagTTL * 2)
92+
93+
const tags2 = await components.getPeerStore().getTags(bootstrapper0PeerId)
94+
95+
expect(tags2).to.have.lengthOf(0, 'bootstrap tag did not expire')
96+
3097
r.stop()
3198
})
3299

33-
it('not fail on malformed peers in peer list', async function () {
100+
it('should not fail on malformed peers in peer list', async function () {
34101
this.timeout(5 * 1000)
35102

36103
const r = new Bootstrap({
37104
list: partialValidPeerList,
38-
interval: 2000
105+
timeout: 100
39106
})
107+
r.init(components)
40108

41109
const p = new Promise<void>((resolve) => {
42110
r.addEventListener('peer', (evt) => {
@@ -55,28 +123,4 @@ describe('bootstrap', () => {
55123
await p
56124
r.stop()
57125
})
58-
59-
it('stop emitting events when stop() called', async function () {
60-
const interval = 100
61-
const r = new Bootstrap({
62-
list: peerList,
63-
interval
64-
})
65-
66-
let emitted: PeerInfo[] = []
67-
r.addEventListener('peer', p => emitted.push(p.detail))
68-
69-
// Should fire emit event for each peer in list on start,
70-
// so wait 50 milliseconds then check
71-
const p = new Promise((resolve) => setTimeout(resolve, 50))
72-
r.start()
73-
await p
74-
expect(emitted).to.have.length(peerList.length)
75-
76-
// After stop is called, no more peers should be emitted
77-
emitted = []
78-
r.stop()
79-
await new Promise((resolve) => setTimeout(resolve, interval))
80-
expect(emitted).to.have.length(0)
81-
})
82126
})

0 commit comments

Comments
 (0)