diff --git a/package.json b/package.json index 5ad91c0e96..33d788a78c 100644 --- a/package.json +++ b/package.json @@ -59,12 +59,13 @@ "it-pipe": "^1.1.0", "it-protocol-buffers": "^0.2.0", "libp2p-crypto": "^0.17.6", - "libp2p-interfaces": "^0.3.0", + "libp2p-interfaces": "libp2p/js-libp2p-interfaces#feat/record-interface", "libp2p-utils": "^0.1.2", "mafmt": "^7.0.0", "merge-options": "^2.0.0", "moving-average": "^1.0.0", "multiaddr": "^7.4.3", + "multicodec": "^1.0.2", "multistream-select": "^0.15.0", "mutable-proxy": "^1.0.0", "node-forge": "^0.9.1", diff --git a/src/index.js b/src/index.js index ad7a78281c..115a732f47 100644 --- a/src/index.js +++ b/src/index.js @@ -17,11 +17,13 @@ const { codes } = 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') @@ -59,6 +61,9 @@ 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 diff --git a/src/record-manager/README.md b/src/record-manager/README.md new file mode 100644 index 0000000000..19f1847c55 --- /dev/null +++ b/src/record-manager/README.md @@ -0,0 +1,62 @@ +# 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 distributiion 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 th 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, you’d 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. \ No newline at end of file diff --git a/src/record-manager/envelope/envelope.proto.js b/src/record-manager/envelope/envelope.proto.js new file mode 100644 index 0000000000..ca0074961a --- /dev/null +++ b/src/record-manager/envelope/envelope.proto.js @@ -0,0 +1,25 @@ +'use strict' + +const protons = require('protons') + +const message = ` +message Envelope { + // public_key is the public key of the keypair the enclosed payload was + // signed with. + bytes public_key = 1; + + // payload_type encodes the type of payload, so that it can be deserialized + // deterministically. + bytes payload_type = 2; + + // payload is the actual payload carried inside this envelope. + bytes payload = 3; + + // signature is the signature produced by the private key corresponding to + // the enclosed public key, over the payload, prefixing a domain string for + // additional security. + bytes signature = 5; +} +` + +module.exports = protons(message).Envelope diff --git a/src/record-manager/envelope/index.js b/src/record-manager/envelope/index.js new file mode 100644 index 0000000000..b148935399 --- /dev/null +++ b/src/record-manager/envelope/index.js @@ -0,0 +1,157 @@ +'use strict' + +const debug = require('debug') +const log = debug('libp2p:envelope') +log.error = debug('libp2p:envelope:error') +const errCode = require('err-code') + +const crypto = require('libp2p-crypto') +const multicodec = require('multicodec') +const PeerId = require('peer-id') + +const Protobuf = require('./envelope.proto') + +/** + * The Envelope is responsible for keeping arbitrary signed by a libp2p peer. + */ +class Envelope { + /** + * @constructor + * @param {object} params + * @param {PeerId} params.peerId + * @param {Buffer} params.payloadType + * @param {Buffer} params.payload marshaled record + * @param {Buffer} params.signature signature of the domain string :: type hint :: payload. + */ + constructor ({ peerId, payloadType, payload, signature }) { + this.peerId = peerId + this.payloadType = payloadType + this.payload = payload + this.signature = signature + + // Cache + this._marshal = undefined + } + + /** + * Marshal the envelope content. + * @return {Buffer} + */ + marshal () { + if (this._marshal) { + return this._marshal + } + // TODO: type for marshal (default: RSA) + const publicKey = crypto.keys.marshalPublicKey(this.peerId.pubKey) + + this._marshal = Protobuf.encode({ + public_key: publicKey, + payload_type: this.payloadType, + payload: this.payload, + signature: this.signature + }) + + return this._marshal + } + + /** + * Verifies if the other Envelope is identical to this one. + * @param {Envelope} other + * @return {boolean} + */ + isEqual (other) { + return this.peerId.pubKey.bytes.equals(other.peerId.pubKey.bytes) && + this.payloadType.equals(other.payloadType) && + this.payload.equals(other.payload) && + this.signature.equals(other.signature) + } + + /** + * Validate envelope data signature for the given domain. + * @param {string} domain + * @return {Promise} + */ + async validate (domain) { + const signData = createSignData(domain, this.payloadType, this.payload) + + try { + await this.peerId.pubKey.verify(signData, this.signature) + } catch (_) { + log.error('record signature verification failed') + // TODO + throw errCode(new Error('record signature verification failed'), 'ERRORS.ERR_SIGNATURE_VERIFICATION') + } + } +} + +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 + * @param {number} payloadType + * @param {Buffer} payload + * @return {Buffer} + */ +const createSignData = (domain, payloadType, payload) => { + // TODO: this should be compliant with the spec! + const domainBuffer = Buffer.from(domain) + const payloadTypeBuffer = Buffer.from(payloadType.toString()) + + return Buffer.concat([domainBuffer, payloadTypeBuffer, payload]) +} + +/** + * Unmarshal a serialized Envelope protobuf message. + * @param {Buffer} data + * @return {Envelope} + */ +const unmarshalEnvelope = async (data) => { + const envelopeData = Protobuf.decode(data) + const peerId = await PeerId.createFromPubKey(envelopeData.public_key) + + return new Envelope({ + peerId, + payloadType: envelopeData.payload_type, + payload: envelopeData.payload, + signature: envelopeData.signature + }) +} diff --git a/src/record-manager/index.js b/src/record-manager/index.js new file mode 100644 index 0000000000..b95dda786f --- /dev/null +++ b/src/record-manager/index.js @@ -0,0 +1,50 @@ +'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 diff --git a/src/record-manager/peer-record/consts.js b/src/record-manager/peer-record/consts.js new file mode 100644 index 0000000000..9f14e8d104 --- /dev/null +++ b/src/record-manager/peer-record/consts.js @@ -0,0 +1,18 @@ +'use strict' + +// const { Buffer } = require('buffer') +const multicodec = require('multicodec') + +// The domain string used for peer records contained in a Envelope. +module.exports.ENVELOPE_DOMAIN_PEER_RECORD = 'libp2p-peer-record' + +// The type hint used to identify peer records in a Envelope. +// Defined in https://github.com/multiformats/multicodec/blob/master/table.csv +// with name "libp2p-peer-record" +// TODO +// const b = Buffer.aloc(2) +// b.writeInt16BE(multicodec.LIBP2P_PEER_RECORD) +// module.exports.ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = b + +// const ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = Buffer.aloc(2) +module.exports.ENVELOPE_PAYLOAD_TYPE_PEER_RECORD = multicodec.LIBP2P_PEER_RECORD diff --git a/src/record-manager/peer-record/index.js b/src/record-manager/peer-record/index.js new file mode 100644 index 0000000000..3df4d12997 --- /dev/null +++ b/src/record-manager/peer-record/index.js @@ -0,0 +1,99 @@ +'use strict' + +const multiaddr = require('multiaddr') +const PeerId = require('peer-id') +const Record = require('libp2p-interfaces/src/record') + +const Protobuf = require('./peer-record.proto') +const { + ENVELOPE_DOMAIN_PEER_RECORD, + ENVELOPE_PAYLOAD_TYPE_PEER_RECORD +} = require('./consts') + +const arraysAreEqual = (a, b) => a.length === b.length && a.sort().every((item, index) => b[index].equals(item)) + +/** + * The PeerRecord is responsible for TODOTODOTRDO + */ +class PeerRecord extends Record { + /** + * @constructor + * @param {object} params + * @param {PeerId} params.peerId + * @param {Array} params.multiaddrs public addresses of the peer this record pertains to. + * @param {number} [params.seqNumber] monotonically-increasing sequence counter that's used to order PeerRecords in time. + */ + constructor ({ peerId, multiaddrs = [], seqNumber = Date.now() }) { + // TODO: verify domain/payload type + super(ENVELOPE_DOMAIN_PEER_RECORD, ENVELOPE_PAYLOAD_TYPE_PEER_RECORD) + + this.peerId = peerId + this.multiaddrs = multiaddrs + this.seqNumber = seqNumber + + // Cache + this._marshal = undefined + } + + /** + * Marshal a record to be used in an envelope. + * @return {Buffer} + */ + marshal () { + if (this._marshal) { + return this._marshal + } + + this._marshal = Protobuf.encode({ + peer_id: this.peerId.toBytes(), + seq: this.seqNumber, + addresses: this.multiaddrs.map((m) => ({ + multiaddr: m.buffer + })) + }) + + return this._marshal + } + + /** + * Verifies if the other PeerRecord is identical to this one. + * @param {Record} other + * @return {boolean} + */ + isEqual (other) { + // Validate PeerId + if (!this.peerId.equals(other.peerId)) { + return false + } + + // Validate seqNumber + if (this.seqNumber !== other.seqNumber) { + return false + } + + // Validate multiaddrs + if (this.multiaddrs.length !== other.multiaddrs.length || !arraysAreEqual(this.multiaddrs, other.multiaddrs)) { + return false + } + + return true + } +} + +exports = module.exports = PeerRecord + +/** + * Unmarshal Peer Record Protobuf. + * @param {Buffer} buf marshaled peer record. + * @return {PeerRecord} + */ +exports.createFromProtobuf = (buf) => { + // Decode + const peerRecord = Protobuf.decode(buf) + + const peerId = PeerId.createFromBytes(peerRecord.peer_id) + const multiaddrs = (peerRecord.addresses || []).map((a) => multiaddr(a.multiaddr)) + const seqNumber = peerRecord.seq + + return new PeerRecord({ peerId, multiaddrs, seqNumber }) +} diff --git a/src/record-manager/peer-record/peer-record.proto.js b/src/record-manager/peer-record/peer-record.proto.js new file mode 100644 index 0000000000..9da916ca87 --- /dev/null +++ b/src/record-manager/peer-record/peer-record.proto.js @@ -0,0 +1,29 @@ +'use strict' + +const protons = require('protons') + +// PeerRecord messages contain information that is useful to share with other peers. +// Currently, a PeerRecord contains the public listen addresses for a peer, but this +// is expected to expand to include other information in the future. +// PeerRecords are designed to be serialized to bytes and placed inside of +// SignedEnvelopes before sharing with other peers. +const message = ` +message PeerRecord { + // AddressInfo is a wrapper around a binary multiaddr. It is defined as a + // separate message to allow us to add per-address metadata in the future. + message AddressInfo { + bytes multiaddr = 1; + } + + // peer_id contains a libp2p peer id in its binary representation. + bytes peer_id = 1; + + // seq contains a monotonically-increasing sequence counter to order PeerRecords in time. + uint64 seq = 2; + + // addresses is a list of public listen addresses for the peer. + repeated AddressInfo addresses = 3; +} +` + +module.exports = protons(message).PeerRecord diff --git a/test/record-manager/envelope.spec.js b/test/record-manager/envelope.spec.js new file mode 100644 index 0000000000..bffa28a8b8 --- /dev/null +++ b/test/record-manager/envelope.spec.js @@ -0,0 +1,88 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +chai.use(require('chai-bytes')) +const { expect } = chai + +const multicodec = require('multicodec') + +const Envelope = require('../../src/record-manager/envelope') +const Record = require('libp2p-interfaces/src/record') + +const peerUtils = require('../utils/creators/peer') + +const domain = '/test-domain' + +class TestRecord extends Record { + constructor (data) { + super(domain, multicodec.LIBP2P_PEER_RECORD) + this.data = data + } + + marshal () { + return Buffer.from(this.data) + } + + isEqual (other) { + return Buffer.compare(this.data, other.data) + } +} + +describe('Envelope', () => { + const payloadType = Buffer.from(`${multicodec.print[multicodec.LIBP2P_PEER_RECORD]}${domain}`) + let peerId + let testRecord + + before(async () => { + [peerId] = await peerUtils.createPeerId() + testRecord = new TestRecord('test-data') + }) + + it('creates an envelope with a random key', () => { + const payload = testRecord.marshal() + const signature = Buffer.from(Math.random().toString(36).substring(7)) + + const envelope = new Envelope({ + peerId, + payloadType, + payload, + signature + }) + + expect(envelope).to.exist() + expect(envelope.peerId.equals(peerId)).to.eql(true) + expect(envelope.payloadType).to.equalBytes(payloadType) + expect(envelope.payload).to.equalBytes(payload) + expect(envelope.signature).to.equalBytes(signature) + }) + + it('can seal a record', async () => { + const envelope = await Envelope.seal(testRecord, peerId) + expect(envelope).to.exist() + expect(envelope.peerId.equals(peerId)).to.eql(true) + expect(envelope.payloadType).to.equalBytes(payloadType) + expect(envelope.payload).to.exist() + expect(envelope.signature).to.exist() + }) + + it('can open and verify a sealed record', async () => { + const envelope = await Envelope.seal(testRecord, peerId) + const rawEnvelope = envelope.marshal() + + const unmarshalledEnvelope = await Envelope.openAndCertify(rawEnvelope, testRecord.domain) + expect(unmarshalledEnvelope).to.exist() + + const isEqual = envelope.isEqual(unmarshalledEnvelope) + expect(isEqual).to.eql(true) + }) + + it.skip('throw on open and verify when a different domain is used', async () => { + const envelope = await Envelope.seal(testRecord, peerId) + const rawEnvelope = envelope.marshal() + + await expect(Envelope.openAndCertify(rawEnvelope, '/fake-domain')) + .to.eventually.rejected() + }) +}) diff --git a/test/record-manager/index.spec.js b/test/record-manager/index.spec.js new file mode 100644 index 0000000000..cb1faaee3e --- /dev/null +++ b/test/record-manager/index.spec.js @@ -0,0 +1,63 @@ +'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? +}) diff --git a/test/record-manager/peer-record.spec.js b/test/record-manager/peer-record.spec.js new file mode 100644 index 0000000000..fcf62df7f8 --- /dev/null +++ b/test/record-manager/peer-record.spec.js @@ -0,0 +1,117 @@ +'use strict' +/* eslint-env mocha */ + +const chai = require('chai') +chai.use(require('dirty-chai')) +const { expect } = chai + +const multiaddr = require('multiaddr') + +const tests = require('libp2p-interfaces/src/record/tests') +const PeerRecord = require('../../src/record-manager/peer-record') + +const peerUtils = require('../utils/creators/peer') + +describe('interface-record compliance', () => { + tests({ + async setup () { + const [peerId] = await peerUtils.createPeerId() + return new PeerRecord({ peerId }) + }, + async teardown () { + // cleanup resources created by setup() + } + }) +}) + +describe('PeerRecord', () => { + let peerId + + before(async () => { + [peerId] = await peerUtils.createPeerId() + }) + + it('creates a peer record with peerId', () => { + const peerRecord = new PeerRecord({ peerId }) + + expect(peerRecord).to.exist() + expect(peerRecord.peerId).to.exist() + expect(peerRecord.multiaddrs).to.exist() + expect(peerRecord.multiaddrs).to.have.lengthOf(0) + expect(peerRecord.seqNumber).to.exist() + }) + + it('creates a peer record with provided data', () => { + const multiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/2000') + ] + const seqNumber = Date.now() + const peerRecord = new PeerRecord({ peerId, multiaddrs, seqNumber }) + + expect(peerRecord).to.exist() + expect(peerRecord.peerId).to.exist() + expect(peerRecord.multiaddrs).to.exist() + expect(peerRecord.multiaddrs).to.eql(multiaddrs) + expect(peerRecord.seqNumber).to.exist() + expect(peerRecord.seqNumber).to.eql(seqNumber) + }) + + it('marshals and unmarshals a peer record', () => { + const multiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/2000') + ] + const seqNumber = Date.now() + const peerRecord = new PeerRecord({ peerId, multiaddrs, seqNumber }) + + // Marshal + const rawData = peerRecord.marshal() + expect(rawData).to.exist() + + // Unmarshal + const unmarshalPeerRecord = PeerRecord.createFromProtobuf(rawData) + expect(unmarshalPeerRecord).to.exist() + + const isEqual = peerRecord.isEqual(unmarshalPeerRecord) + expect(isEqual).to.eql(true) + }) + + it('isEqual returns false if the peer record has a different peerId', async () => { + const peerRecord0 = new PeerRecord({ peerId }) + + const [peerId1] = await peerUtils.createPeerId({ fixture: false }) + const peerRecord1 = new PeerRecord({ peerId: peerId1 }) + + const isEqual = peerRecord0.isEqual(peerRecord1) + expect(isEqual).to.eql(false) + }) + + it('isEqual returns false if the peer record has a different seqNumber', () => { + const ts0 = Date.now() + const peerRecord0 = new PeerRecord({ peerId, seqNumber: ts0 }) + + const ts1 = ts0 + 20 + const peerRecord1 = new PeerRecord({ peerId, seqNumber: ts1 }) + + const isEqual = peerRecord0.isEqual(peerRecord1) + expect(isEqual).to.eql(false) + }) + + it('isEqual returns false if the peer record has a different multiaddrs', () => { + const multiaddrs = [ + multiaddr('/ip4/127.0.0.1/tcp/2000') + ] + const peerRecord0 = new PeerRecord({ peerId, multiaddrs }) + + const multiaddrs1 = [ + multiaddr('/ip4/127.0.0.1/tcp/2001') + ] + const peerRecord1 = new PeerRecord({ peerId, multiaddrs: multiaddrs1 }) + + const isEqual = peerRecord0.isEqual(peerRecord1) + expect(isEqual).to.eql(false) + }) +}) + +describe('PeerRecord inside Envelope', () => { + // TODO +})