Skip to content

Commit

Permalink
feat: peerStore persistence
Browse files Browse the repository at this point in the history
  • Loading branch information
vasco-santos committed Apr 27, 2020
1 parent 8a86437 commit 5ddbf9f
Show file tree
Hide file tree
Showing 13 changed files with 497 additions and 64 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"debug": "^4.1.1",
"err-code": "^2.0.0",
"hashlru": "^2.3.0",
"interface-datastore": "^0.8.3",
"it-all": "^1.0.1",
"it-buffer": "^0.1.1",
"it-handshake": "^1.0.1",
Expand Down
3 changes: 3 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const DefaultConfig = {
metrics: {
enabled: false
},
peerStore: {
persistence: true
},
config: {
dht: {
enabled: false,
Expand Down
11 changes: 9 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const debug = require('debug')
const log = debug('libp2p')
log.error = debug('libp2p:error')

const { MemoryDatastore } = require('interface-datastore')
const PeerId = require('peer-id')

const peerRouting = require('./peer-routing')
Expand Down Expand Up @@ -42,9 +43,12 @@ class Libp2p extends EventEmitter {
// and add default values where appropriate
this._options = validateConfig(_options)

this.datastore = this._options.datastore
this.peerId = this._options.peerId
this.peerStore = new PeerStore()
this.datastore = this._options.datastore || new MemoryDatastore()
this.peerStore = new PeerStore({
datastore: this.datastore,
...this._options.peerStore
})

// Addresses {listen, announce, noAnnounce}
this.addresses = this._options.addresses
Expand Down Expand Up @@ -405,6 +409,9 @@ class Libp2p extends EventEmitter {
// Listen on the provided transports
await this.transportManager.listen()

// Start PeerStore
await this.peerStore.start()

if (this._config.pubsub.enabled) {
this.pubsub && this.pubsub.start()
}
Expand Down
44 changes: 44 additions & 0 deletions src/peer-store/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,51 @@ Access to its underlying books:
- `change:multiaadrs` - emitted when a known peer has a different set of multiaddrs.
- `change:protocols` - emitted when a known peer supports a different set of protocols.

## Data Persistence

The data stored in the PeerStore will be persisted by default. Keeping a record of the peers already discovered by the peer, as well as their known data aims to improve the efficiency of peers joining the network after being offline.

---
TODO: Discuss if we should make it persisted by default now. Taking into consideration that we will use a MemoryDatastore by default, unless the user configures a datastore to use, it will be worthless. It might make sense to make it disabled by default until we work on improving configuration and provide good defauls for each environment.
---

The libp2p node will need to receive a [datastore](https://github.com/ipfs/interface-datastore), in order to store this data in a persistent way. Otherwise, it will be stored on a [memory datastore](https://github.com/ipfs/interface-datastore/blob/master/src/memory.js).

A [datastore](https://github.com/ipfs/interface-datastore) stores its data in a key-value fashion. As a result, we need coherent keys so that we do not overwrite data.

Taking into account that a datastore allows queries using a key prefix, we can find all the information if we define a consistent namespace that allow us to find the content without having any information. The namespaces were defined as follows:

**AddressBook**

All the knownw peer addresses are stored with a key pattern as follows:

`/peers/addrs/<b32 peer id no padding>`

**ProtoBook**

All the knownw peer protocols are stored with a key pattern as follows:

`/peers/protos/<b32 peer id no padding>`

**KeyBook**

_NOT_YET_IMPLEMENTED_

All public and private keys are stored under the following pattern:

` /peers/keys/<b32 peer id no padding>/{pub, priv}`

**MetadataBook**

_NOT_YET_IMPLEMENTED_

Metadata is stored under the following key pattern:

`/peers/metadata/<b32 peer id no padding>/<key>`

## Future Considerations

- If multiaddr TTLs are added, the PeerStore may schedule jobs to delete all addresses that exceed the TTL to prevent AddressBook bloating
- Further API methods will probably need to be added in the context of multiaddr validity and confidence.
- When improving libp2p configuration for specific runtimes, we should take into account the PeerStore recommended datastore.
- When improving libp2p configuration, we should think about a possible way of allowing the configuration of Bootstrap to be influenced by the persisted peers, as a way to decrease the load on Bootstrap nodes.
39 changes: 22 additions & 17 deletions src/peer-store/address-book.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ const multiaddr = require('multiaddr')
const PeerId = require('peer-id')

const Book = require('./book')
const Protobuf = require('./pb/address-book.proto')

const {
ERR_INVALID_PARAMETERS
codes: { ERR_INVALID_PARAMETERS }
} = require('../errors')

/**
* The AddressBook is responsible for keeping the known multiaddrs
* of a peer.
* This data will be persisted in the PeerStore datastore as follows:
* /peers/addrs/<b32 peer id no padding>
*/
class AddressBook extends Book {
/**
Expand All @@ -35,7 +38,21 @@ class AddressBook extends Book {
* "peer" - emitted when a peer is discovered by the node.
* "change:multiaddrs" - emitted when the known multiaddrs of a peer change.
*/
super(peerStore, 'change:multiaddrs', 'multiaddrs')
super({
peerStore,
eventName: 'change:multiaddrs',
eventProperty: 'multiaddrs',
protoBuf: Protobuf,
dsPrefix: '/peers/addrs/',
eventTransformer: (data) => data.map((address) => address.multiaddr),
// TODO: should we already think about persist address data and store it accordingly?
dsSetTransformer: (data) => ({
addrs: data.map((address) => address.multiaddr.buffer)
}),
dsGetTransformer: (data) => data.addrs.map((a) => ({
multiaddr: multiaddr(a)
}))
})

/**
* Map known peers to their known Addresses.
Expand Down Expand Up @@ -78,20 +95,14 @@ class AddressBook extends Book {
}
}

this.data.set(id, addresses)
this._setPeerId(peerId)
this._setData(peerId, addresses)
log(`stored provided multiaddrs for ${id}`)

// Notify the existance of a new peer
if (!rec) {
this._ps.emit('peer', peerId)
}

this._ps.emit('change:multiaddrs', {
peerId,
multiaddrs: addresses.map((mi) => mi.multiaddr)
})

return this
}

Expand Down Expand Up @@ -127,16 +138,9 @@ class AddressBook extends Book {
return this
}

this._setPeerId(peerId)
this.data.set(id, addresses)

this._setData(peerId, addresses)
log(`added provided multiaddrs for ${id}`)

this._ps.emit('change:multiaddrs', {
peerId,
multiaddrs: addresses.map((mi) => mi.multiaddr)
})

// Notify the existance of a new peer
if (!rec) {
this._ps.emit('peer', peerId)
Expand All @@ -147,6 +151,7 @@ class AddressBook extends Book {

/**
* Transforms received multiaddrs into Address.
* @private
* @param {Array<Multiaddr>} multiaddrs
* @returns {Array<Address>}
*/
Expand Down
134 changes: 132 additions & 2 deletions src/peer-store/book.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,57 @@
'use strict'

const errcode = require('err-code')
const debug = require('debug')
const log = debug('libp2p:peer-store:book')
log.error = debug('libp2p:peer-store:book:error')

const { Key } = require('interface-datastore')
const PeerId = require('peer-id')

const {
ERR_INVALID_PARAMETERS
codes: { ERR_INVALID_PARAMETERS }
} = require('../errors')

const passthrough = data => data

/**
* The Book is the skeleton for the PeerStore books.
* It handles the PeerStore persistence and events.
*/
class Book {
constructor (peerStore, eventName, eventProperty) {
/**
* @constructor
* @param {Object} properties
* @param {PeerStore} properties.peerStore PeerStore instance.
* @param {string} properties.eventName Name of the event to emit by the PeerStore.
* @param {string} properties.eventProperty Name of the property to emit by the PeerStore.
* @param {Object} properties.protoBuf Suffix of the Datastore Key
* @param {String} properties.dsPrefix Prefix of the Datastore Key
* @param {String} [properties.dsSuffix] Suffix of the Datastore Key
* @param {function} [properties.eventTransformer] Transformer function of the provided data for being emitted.
* @param {function} [properties.dsSetTransformer] Transformer function of the provided data for being persisted.
* @param {function} [properties.dsGetTransformer] Transformer function of the persisted data to be loaded.
*/
constructor ({
peerStore,
eventName,
eventProperty,
protoBuf,
dsPrefix,
dsSuffix = '',
eventTransformer = passthrough,
dsSetTransformer = passthrough,
dsGetTransformer = passthrough
}) {
this._ps = peerStore
this.eventName = eventName
this.eventProperty = eventProperty
this.protoBuf = protoBuf
this.dsPrefix = dsPrefix
this.dsSuffix = dsSuffix
this.eventTransformer = eventTransformer
this.dsSetTransformer = dsSetTransformer
this.dsGetTransformer = dsGetTransformer

/**
* Map known peers to their data.
Expand All @@ -23,6 +60,91 @@ class Book {
this.data = new Map()
}

/**
* Load data from peerStore datastore into the books datastructures.
* This will not persist the replicated data nor emit modify events.
* @private
* @return {Promise<void>}
*/
async _loadData () {
if (!this._ps._datastore || !this._ps._enabledPersistance) {
return
}

const persistenceQuery = {
prefix: this.dsPrefix
}

for await (const { key, value } of this._ps._datastore.query(persistenceQuery)) {
try {
// PeerId to add to the book
const b32key = key.toString()
.replace(this.dsPrefix, '') // remove prefix from key
.replace(this.dsSuffix, '') // remove suffix from key
const peerId = PeerId.createFromCID(b32key)
// Data in the format to add to the book
const data = this.dsGetTransformer(this.protoBuf.decode(value))
// Add the book without persist the replicated data and emit modify
this._setData(peerId, data, {
persist: false,
emit: false
})
} catch (err) {
log.error(err)
}
}
}

/**
* Set data into the datastructure, persistence and emit it using the provided transformers.
* @private
* @param {PeerId} peerId peerId of the data to store
* @param {Array<*>} data data to store.
* @param {Object} [options] storing options.
* @param {boolean} [options.persist = true] persist the provided data.
* @param {boolean} [options.emit = true] emit the provided data.
* @return {Promise<void>}
*/
async _setData (peerId, data, { persist = true, emit = true } = {}) {
const b58key = peerId.toB58String()

// Store data in memory
this.data.set(b58key, data)
this._setPeerId(peerId)

// Emit event
emit && this._ps.emit(this.eventName, {
peerId,
[this.eventProperty]: this.eventTransformer(data)
})

// Add to Persistence datastore
persist && await this._persistData(peerId, data)
}

/**
* Persist data on the datastore
* @private
* @param {PeerId} peerId peerId of the data to persist
* @param {Array<*>} data data to persist
* @return {Promise<void>}
*/
async _persistData (peerId, data) {
if (!this._ps._datastore || !this._ps._enabledPersistance) {
return
}

const b32key = peerId.toString()
const k = `${this.dsPrefix}${b32key}${this.dsSuffix}`
try {
const value = this.protoBuf.encode(this.dsSetTransformer(data))

await this._ps._datastore.put(new Key(k), value)
} catch (err) {
log.error(err)
}
}

/**
* Set known data of a provided peer.
* @param {PeerId} peerId
Expand Down Expand Up @@ -75,9 +197,17 @@ class Book {
[this.eventProperty]: []
})

// Update Persistence datastore
this._persistData(peerId, [])

return true
}

/**
* Set PeerId into peerStore datastructure.
* @private
* @param {PeerId} peerId
*/
_setPeerId (peerId) {
if (!this._ps.peerIds.get(peerId)) {
this._ps.peerIds.set(peerId.toB58String(), peerId)
Expand Down
Loading

0 comments on commit 5ddbf9f

Please sign in to comment.