chore: refactor and better docs

This commit is contained in:
Vasco Santos 2020-06-24 15:10:08 +02:00 committed by Jacob Heun
parent 02a5095b9c
commit 71daac24b1
12 changed files with 174 additions and 226 deletions

View File

@ -18,13 +18,11 @@ const { codes, messages } = require('./errors')
const AddressManager = require('./address-manager')
const ConnectionManager = require('./connection-manager')
const RecordManager = require('./record-manager')
const TransportManager = require('./transport-manager')
const Circuit = require('./circuit')
const Dialer = require('./dialer')
const Keychain = require('./keychain')
const Metrics = require('./metrics')
const TransportManager = require('./transport-manager')
const Upgrader = require('./upgrader')
const PeerStore = require('./peer-store')
const PersistentPeerStore = require('./peer-store/persistent')
@ -62,9 +60,6 @@ class Libp2p extends EventEmitter {
this.addresses = this._options.addresses
this.addressManager = new AddressManager(this._options.addresses)
// Records
this.RecordManager = new RecordManager(this)
this._modules = this._options.modules
this._config = this._options.config
this._transport = [] // Transport instances/references

View File

@ -1,62 +0,0 @@
# Record Manager
All libp2p nodes keep a `PeerStore`, that among other information stores a set of known addresses for each peer. Addresses for a peer can come from a variety of sources.
Libp2p peer records were created to enable the distribution of verifiable address records, which we can prove originated from the addressed peer itself.
With such guarantees, libp2p can prioritize addresses based on their authenticity, with the most strict strategy being to only dial certified addresses.
The libp2p record manager is responsible for keeping a local peer record updated, as well as to inform third parties of possible updates. (TODO: REMOVE and modules: Moreover, it provides an API for the creation and validation of libp2p **envelopes**.)
## Envelop
Libp2p nodes need to store data in a public location (e.g. a DHT), or rely on potentially untrustworthy intermediaries to relay information over its lifetime. Accordingly, libp2p nodes need to be able to verify that the data came from a specific peer and that it hasn't been tampered with.
Libp2p provides an all-purpose data container called **envelope**, which includes a signature of the data, so that its authenticity can be verified. This envelope stores a marshaled record implementing the [interface-record](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/record).
Envelope signatures can be used for a variety of purposes, and a signature made for a specific purpose IS NOT be considered valid for a different purpose. We separate signatures into `domains` by prefixing the data to be signed with a string unique to each domain. This string is not contained within the envelope data. Instead, each libp2p subsystem that makes use of signed envelopes will provide their own domain string when creating the envelope, and again when validating the envelope. If the domain string used to validate it is different from the one used to sign, the signature validation will fail.
## Records
The Records are designed to be serialized to bytes and placed inside of the envelopes before being shared with other peers.
### Peer Record
A peer record contains the peers' publicly reachable listen addresses, and may be extended in the future to contain additional metadata relevant to routing.
Each peer record contains a `seq` field, so that we can order peer records by time and identify if a received record is more recent than the stored one.
They should be used either through a direct exchange (as in the libp2p identify protocol), or through a peer routing provider, such as a DHT.
## Libp2p flows
Once a libp2p node has started and is listening on a set of multiaddrs, the **Record Manager** will kick in, create a peer record for the peer and wrap it inside a signed envelope. Everytime a libp2p subsystem needs to share its peer record, it will get the cached computed peer record and send its envelope.
**_NOT_YET_IMPLEMENTED_** While creating peer records is fairly trivial, addresses should not be static and can be modified at arbitrary times. When a libp2p node changes its listen addresses, the **Record Manager** will compute a new peer record, wrap it inside a signed envelope and inform the interested subsystems.
Considering that a node can discover other peers' addresses from a variety of sources, Libp2p Peerstore should be able to differentiate the addresses that were obtained through a signed peer record. Once all these pieces are in place, we will also need a way to prioritize addresses based on their authenticity, that is, the dialer can prioritize self-certified addresses over addresses from an unknown origin.
When a libp2p node receives a new signed peer record, the `seq` number of the record must be compared with potentially stored records, so that we do not override correct data.
### Notes:
- Possible design for AddressBook
```
addr_book_record
\_ peer_id: bytes
\_ signed_addrs: []AddrEntry
\_ unsigned_addrs: []AddrEntry
\_ certified_record
\_ seq: bytes
\_ raw: bytes
```
## Future Work
- Peers may not know their own addresses. It's often impossible to automatically infer one's own public address, and peers may need to rely on third party peers to inform them of their observed public addresses.
- A peer may inadvertently or maliciously sign an address that they do not control. In other words, a signature isn't a guarantee that a given address is valid.
- Some addresses may be ambiguous. For example, addresses on a private subnet are valid within that subnet but are useless on the public internet.
- Modular dialer? (taken from go PR notes)
- With the modular dialer, users should easily be able to configure precedence. With dialer v1, anything we do to prioritise dials is gonna be spaghetti and adhoc. With the modular dialer, youd be able to specify the order of dials when instantiating the pipeline.
- Multiple parallel dials. We already have the issue where new addresses aren't added to existing dials.

View File

@ -1,50 +0,0 @@
'use strict'
const debug = require('debug')
const log = debug('libp2p:record-manager')
log.error = debug('libp2p:record-manager:error')
const Envelope = require('./envelope')
const PeerRecord = require('./peer-record')
/**
* Responsible for managing the node signed peer record.
* The record is generated on start and should be regenerated when
* the public addresses of the peer change.
*/
class RecordManager {
/**
* @constructor
* @param {Libp2p} libp2p
*/
constructor (libp2p) {
this.libp2p = libp2p
this._signedPeerRecord = undefined // TODO: map for multiple domains?
}
/**
* Start record manager. Compute current peer record and monitor address changes.
* @return {void}
*/
async start () {
const peerRecord = new PeerRecord({
peerId: this.libp2p.peerId,
multiaddrs: this.libp2p.multiaddrs
})
this._signedPeerRecord = await Envelope.seal(peerRecord, this.libp2p.peerId)
// TODO: listen for address changes on AddressManager
}
/**
* Get signed peer record envelope.
* @return {Envelope}
*/
getPeerRecordEnvelope () {
// TODO: create here if not existing?
return this._signedPeerRecord
}
}
module.exports = RecordManager

128
src/record/README.md Normal file
View File

@ -0,0 +1,128 @@
# Libp2p Records
Libp2p nodes need to store data in a public location (e.g. a DHT), or rely on potentially untrustworthy intermediaries to relay information over its lifetime. Accordingly, libp2p nodes need to be able to verify that the data came from a specific peer and that it hasn't been tampered with.
## Envelope
Libp2p provides an all-purpose data container called **envelope**. It was created to enable the distribution of verifiable records, which we can prove originated from the addressed peer itself. The envelope includes a signature of the data, so that its authenticity is verified.
This envelope stores a marshaled record implementing the [interface-record](https://github.com/libp2p/js-libp2p-interfaces/tree/master/src/record). These Records are designed to be serialized to bytes and placed inside of the envelopes before being shared with other peers.
You can read further about the envelope in [libp2p/specs#217](https://github.com/libp2p/specs/pull/217).
### Usage
- create an envelope with an instance of an `interface-record` implementation and prepare it for being exchanged:
```js
const Envelope = require('libp2p/src/record/envelop')
// ... create a record named rec with domain X
const e = await Envelope.seal(rec, peerId)
const wireData = e.marshal()
```
- consume a received envelope, as well as to get back the record:
```js
const Envelope = require('libp2p/src/record/envelop')
// const Record = ...
// ... receive envelope data
const domain = 'X'
let e
try {
e = await Envelope.openAndCertify(data, domain)
} catch (err) {}
const rec = Record.createFromProtobuf(e.payload)
```
## Peer Record
All libp2p nodes keep a `PeerStore`, that among other information stores a set of known addresses for each peer, which can come from a variety of sources.
Libp2p peer records were created to enable the distribution of verifiable address records, which we can prove originated from the addressed peer itself. With such guarantees, libp2p can prioritize addresses based on their authenticity, with the most strict strategy being to only dial certified addresses.
A peer record contains the peers' publicly reachable listen addresses, and may be extended in the future to contain additional metadata relevant to routing. It also contains a `seq` field, so that we can order peer records by time and identify if a received record is more recent than the stored one.
You can read further about the Peer Record in [libp2p/specs#217](https://github.com/libp2p/specs/pull/217).
### Usage
- create a new Peer Record
```js
const PeerRecord = require('libp2p/src/record/peer-record')
const pr = new PeerRecord({
peerId: node.peerId,
multiaddrs: node.multiaddrs
})
```
- create a Peer Record from a protobuf
```js
const PeerRecord = require('libp2p/src/record/peer-record')
const pr = PeerRecord.createFromProtobuf(data)
```
### Libp2p Flows
#### Self Record
Once a libp2p node has started and is listening on a set of multiaddrs, its own peer record can be created.
The identify service is responsible for creating the self record when the identify protocol kicks in for the first time. This record should be stored for future needs of the identify protocol when connecting with other peers.
#### Self record Updates
**_NOT_YET_IMPLEMENTED_**
While creating peer records is fairly trivial, addresses should not be static and can be modified at arbitrary times. This can happen via an Address Manager API, or even through AutoRelay/AutoNAT.
When a libp2p node changes its listen addresses, the identify service should be informed. Once that happens, the identify service should create a new self record and store it. With the new record, the identify push/delta protocol will be used to communicate this change to the connected peers.
#### Subsystem receiving a record
Considering that a node can discover other peers' addresses from a variety of sources, Libp2p Peerstore should be able to differentiate the addresses that were obtained through a signed peer record.
Once a record is received and its signature properly validated, its envelope should be stored in the AddressBook on its byte representations. However, the `seq` number of the record must be compared with potentially stored records, so that we do not override correct data.
The AddressBook Addresses must be updated with the content of the envelope with a certified property that allows other subsystems to identify that the known certified addresses of a peer.
#### Subsystem providing a record
Libp2p subsystems that exchange other peers information should provide the envelope that they received by those peers. As a result, other peers can verify if the envelope was really created by the addressed peer.
When a subsystem wants to provide a record, it should get it from the AddressBook if it exists. Other subsystems should also be able to provide the self record that will also be stored in the AddressBook.
### Future Work
- Persistence only considering certified addresses?
- Peers may not know their own addresses. It's often impossible to automatically infer one's own public address, and peers may need to rely on third party peers to inform them of their observed public addresses.
- A peer may inadvertently or maliciously sign an address that they do not control. In other words, a signature isn't a guarantee that a given address is valid.
- Some addresses may be ambiguous. For example, addresses on a private subnet are valid within that subnet but are useless on the public internet.
- Once all these pieces are in place, we will also need a way to prioritize addresses based on their authenticity, that is, the dialer can prioritize self-certified addresses over addresses from an unknown origin.
- Modular dialer? (taken from go PR notes)
- With the modular dialer, users should easily be able to configure precedence. With dialer v1, anything we do to prioritise dials is gonna be spaghetti and adhoc. With the modular dialer, youd be able to specify the order of dials when instantiating the pipeline.
- Multiple parallel dials. We already have the issue where new addresses aren't added to existing dials.
### Notes:
- Possible design for AddressBook
```
addr_book_record
\_ peer_id: bytes
\_ signed_addrs: []AddrEntry
\_ unsigned_addrs: []AddrEntry
\_ certified_record
\_ seq: bytes
\_ raw: bytes
```

View File

@ -84,46 +84,6 @@ class Envelope {
}
}
exports = module.exports = Envelope
/**
* Seal marshals the given Record, places the marshaled bytes inside an Envelope
* and signs with the given private key.
* @async
* @param {Record} record
* @param {PeerId} peerId
* @return {Envelope}
*/
exports.seal = async (record, peerId) => {
const domain = record.domain
const payloadType = Buffer.from(`${multicodec.print[record.codec]}${domain}`)
const payload = record.marshal()
const signData = createSignData(domain, payloadType, payload)
const signature = await peerId.privKey.sign(signData)
return new Envelope({
peerId,
payloadType,
payload,
signature
})
}
// ConsumeEnvelope unmarshals a serialized Envelope and validates its
// signature using the provided 'domain' string. If validation fails, an error
// is returned, along with the unmarshalled envelope so it can be inspected.
//
// On success, ConsumeEnvelope returns the Envelope itself, as well as the inner payload,
// unmarshalled into a concrete Record type. The actual type of the returned Record depends
// on what has been registered for the Envelope's PayloadType (see RegisterType for details).
exports.openAndCertify = async (data, domain) => {
const envelope = await unmarshalEnvelope(data)
await envelope.validate(domain)
return envelope
}
/**
* Helper function that prepares a buffer to sign or verify a signature.
* @param {string} domain
@ -155,3 +115,43 @@ const unmarshalEnvelope = async (data) => {
signature: envelopeData.signature
})
}
/**
* Seal marshals the given Record, places the marshaled bytes inside an Envelope
* and signs with the given private key.
* @async
* @param {Record} record
* @param {PeerId} peerId
* @return {Envelope}
*/
Envelope.seal = async (record, peerId) => {
const domain = record.domain
const payloadType = Buffer.from(`${multicodec.print[record.codec]}${domain}`)
const payload = record.marshal()
const signData = createSignData(domain, payloadType, payload)
const signature = await peerId.privKey.sign(signData)
return new Envelope({
peerId,
payloadType,
payload,
signature
})
}
/**
* Open and certify a given marshalled envelope.
* Data is unmarshalled and the siganture validated with the given domain.
* @param {Buffer} data
* @param {string} domain
* @return {Envelope}
*/
Envelope.openAndCertify = async (data, domain) => {
const envelope = await unmarshalEnvelope(data)
await envelope.validate(domain)
return envelope
}
module.exports = Envelope

View File

@ -80,14 +80,12 @@ class PeerRecord extends Record {
}
}
exports = module.exports = PeerRecord
/**
* Unmarshal Peer Record Protobuf.
* @param {Buffer} buf marshaled peer record.
* @return {PeerRecord}
*/
exports.createFromProtobuf = (buf) => {
PeerRecord.createFromProtobuf = (buf) => {
// Decode
const peerRecord = Protobuf.decode(buf)
@ -97,3 +95,5 @@ exports.createFromProtobuf = (buf) => {
return new PeerRecord({ peerId, multiaddrs, seqNumber })
}
module.exports = PeerRecord

View File

@ -1,63 +0,0 @@
'use strict'
/* eslint-env mocha */
const chai = require('chai')
chai.use(require('dirty-chai'))
const { expect } = chai
const { Buffer } = require('buffer')
const multiaddr = require('multiaddr')
const Envelope = require('../../src/record-manager/envelope')
const RecordManager = require('../../src/record-manager')
const peerUtils = require('../utils/creators/peer')
describe('Record manager', () => {
let peerId
let recordManager
before(async () => {
[peerId] = await peerUtils.createPeerId()
})
beforeEach(() => {
recordManager = new RecordManager({
peerId,
multiaddrs: [
multiaddr('/ip4/127.0.0.1/tcp/2000'),
multiaddr('/ip4/127.0.0.1/tcp/2001')
]
})
})
it('needs to start to create a signed peer record', async () => {
let envelope = recordManager.getPeerRecordEnvelope()
expect(envelope).to.not.exist()
await recordManager.start()
envelope = recordManager.getPeerRecordEnvelope()
expect(envelope).to.exist()
})
it('can marshal the created signed peer record envelope', async () => {
await recordManager.start()
const envelope = recordManager.getPeerRecordEnvelope()
expect(envelope).to.exist()
expect(peerId.equals(envelope.peerId)).to.eql(true)
expect(envelope.payload).to.exist()
expect(envelope.signature).to.exist()
const marshledEnvelope = envelope.marshal()
expect(marshledEnvelope).to.exist()
expect(Buffer.isBuffer(marshledEnvelope)).to.eql(true)
const decodedEnvelope = await Envelope.openAndCertify(marshledEnvelope, 'domain') // TODO: domain
expect(decodedEnvelope).to.exist()
const isEqual = envelope.isEqual(decodedEnvelope)
expect(isEqual).to.eql(true)
})
// TODO: test signature validation?
})

View File

@ -8,7 +8,7 @@ const { expect } = chai
const multicodec = require('multicodec')
const Envelope = require('../../src/record-manager/envelope')
const Envelope = require('../../src/record/envelope')
const Record = require('libp2p-interfaces/src/record')
const peerUtils = require('../utils/creators/peer')

View File

@ -8,7 +8,7 @@ const { expect } = chai
const multiaddr = require('multiaddr')
const tests = require('libp2p-interfaces/src/record/tests')
const PeerRecord = require('../../src/record-manager/peer-record')
const PeerRecord = require('../../src/record/peer-record')
const peerUtils = require('../utils/creators/peer')