diff --git a/src/peer-store/address-book.js b/src/peer-store/address-book.js index 5ace70b84d..4cd5bd0c31 100644 --- a/src/peer-store/address-book.js +++ b/src/peer-store/address-book.js @@ -9,10 +9,12 @@ const multiaddr = require('multiaddr') const PeerId = require('peer-id') const Book = require('./book') +const PeerRecord = require('../record/peer-record') const { codes: { ERR_INVALID_PARAMETERS } } = require('../errors') +const Envelope = require('../record/envelope') /** * The AddressBook is responsible for keeping the known multiaddrs @@ -23,8 +25,23 @@ class AddressBook extends Book { * Address object * @typedef {Object} Address * @property {Multiaddr} multiaddr peer multiaddr. + * @property {boolean} isCertified obtained from a signed peer record. */ + /** + * CertifiedRecord object + * @typedef {Object} CertifiedRecord + * @property {Buffer} raw raw envelope. + * @property {number} seqNumber seq counter. + */ + + /** + * Entry object for the addressBook + * @typedef {Object} Entry + * @property {Array
} addresses peer Addresses. + * @property {CertifiedRecord} record certified peer record. + */ + /** * @constructor * @param {PeerStore} peerStore @@ -39,16 +56,95 @@ class AddressBook extends Book { peerStore, eventName: 'change:multiaddrs', eventProperty: 'multiaddrs', - eventTransformer: (data) => data.map((address) => address.multiaddr) + eventTransformer: (data) => { + if (!data.addresses) { + return [] + } + return data.addresses.map((address) => address.multiaddr) + } }) /** - * Map known peers to their known Addresses. - * @type {Map>} + * Map known peers to their known Address Entries. + * @type {Map>} */ this.data = new Map() } + /** + * ConsumePeerRecord adds addresses from a signed peer.PeerRecord contained in a record envelope. + * This will return a boolean that indicates if the record was successfully processed and integrated + * into the AddressBook. + * @param {Envelope} envelope + * @return {boolean} + */ + consumePeerRecord (envelope) { + let peerRecord + try { + peerRecord = PeerRecord.createFromProtobuf(envelope.payload) + } catch (err) { + log.error('invalid peer record received') + return false + } + + // Verify peerId + if (peerRecord.peerId.toB58String() !== envelope.peerId.toB58String()) { + log('signing key does not match PeerId in the PeerRecord') + return false + } + + const peerId = peerRecord.peerId + const id = peerId.toB58String() + const entry = this.data.get(id) || {} + const storedRecord = entry.record + + // ensure seq is greater than, or equal to, the last received + if (storedRecord && + storedRecord.seqNumber >= peerRecord.seqNumber) { + return false + } + + // ensure the record has multiaddrs + if (!peerRecord.multiaddrs || !peerRecord.multiaddrs.length) { + return false + } + + const addresses = this._toAddresses(peerRecord.multiaddrs, true) + + // TODO: new record with different addresses from stored record + // - Remove the older ones? + // - Change to uncertified? + + // TODO: events + // Should a multiaddr only modified to certified trigger an event? + // - Needed for persistent peer store + this._setData(peerId, { + addresses, + record: { + raw: envelope.marshal(), + seqNumber: peerRecord.seqNumber + } + }) + log(`stored provided peer record for ${id}`) + + return true + } + + /** + * Get an Envelope containing a PeerRecord for the given peer. + * @param {PeerId} peerId + * @return {Promise} + */ + getPeerRecord (peerId) { + const entry = this.data.get(peerId.toB58String()) + + if (!entry || !entry.record || !entry.record.raw) { + return + } + + return Envelope.createFromProtobuf(entry.record.raw) + } + /** * Set known multiaddrs of a provided peer. * @override @@ -64,7 +160,8 @@ class AddressBook extends Book { const addresses = this._toAddresses(multiaddrs) const id = peerId.toB58String() - const rec = this.data.get(id) + const entry = this.data.get(id) || {} + const rec = entry.addresses // Not replace multiaddrs if (!addresses.length) { @@ -83,7 +180,10 @@ class AddressBook extends Book { } } - this._setData(peerId, addresses) + this._setData(peerId, { + addresses, + record: entry.record + }) log(`stored provided multiaddrs for ${id}`) // Notify the existance of a new peer @@ -109,7 +209,9 @@ class AddressBook extends Book { const addresses = this._toAddresses(multiaddrs) const id = peerId.toB58String() - const rec = this.data.get(id) + + const entry = this.data.get(id) || {} + const rec = entry.addresses // Add recorded uniquely to the new array (Union) rec && rec.forEach((mi) => { @@ -125,7 +227,10 @@ class AddressBook extends Book { return this } - this._setData(peerId, addresses) + this._setData(peerId, { + addresses, + record: entry.record + }) log(`added provided multiaddrs for ${id}`) @@ -137,13 +242,31 @@ class AddressBook extends Book { return this } + /** + * Get the known data of a provided peer. + * @override + * @param {PeerId} peerId + * @returns {Array} + */ + get (peerId) { + // TODO: should we return Entry instead?? + if (!PeerId.isPeerId(peerId)) { + throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS) + } + + const entry = this.data.get(peerId.toB58String()) + + return entry && entry.addresses ? [...entry.addresses] : undefined + } + /** * Transforms received multiaddrs into Address. * @private * @param {Array} multiaddrs + * @param {boolean} [isCertified] * @returns {Array
} */ - _toAddresses (multiaddrs) { + _toAddresses (multiaddrs, isCertified = false) { if (!multiaddrs) { log.error('multiaddrs must be provided to store data') throw errcode(new Error('multiaddrs must be provided'), ERR_INVALID_PARAMETERS) @@ -158,7 +281,8 @@ class AddressBook extends Book { } addresses.push({ - multiaddr: addr + multiaddr: addr, + isCertified }) }) @@ -177,13 +301,13 @@ class AddressBook extends Book { throw errcode(new Error('peerId must be an instance of peer-id'), ERR_INVALID_PARAMETERS) } - const record = this.data.get(peerId.toB58String()) + const entry = this.data.get(peerId.toB58String()) - if (!record) { + if (!entry || !entry.addresses) { return undefined } - return record.map((address) => { + return entry.addresses.map((address) => { const multiaddr = address.multiaddr const idString = multiaddr.getPeerId() diff --git a/src/peer-store/persistent/index.js b/src/peer-store/persistent/index.js index 0cf54c0d9e..c6df7b5dc3 100644 --- a/src/peer-store/persistent/index.js +++ b/src/peer-store/persistent/index.js @@ -296,9 +296,11 @@ class PersistentPeerStore extends PeerStore { this.addressBook._setData( peerId, - decoded.addrs.map((address) => ({ - multiaddr: multiaddr(address.multiaddr) - })), + { + addresses: decoded.addrs.map((address) => ({ + multiaddr: multiaddr(address.multiaddr) + })) + }, { emit: false }) break case 'keys': diff --git a/src/peer-store/persistent/pb/address-book.proto.js b/src/peer-store/persistent/pb/address-book.proto.js index e507d05d28..18a51f3efe 100644 --- a/src/peer-store/persistent/pb/address-book.proto.js +++ b/src/peer-store/persistent/pb/address-book.proto.js @@ -4,11 +4,26 @@ const protons = require('protons') const message = ` message Addresses { + // Address represents a single multiaddr. message Address { required bytes multiaddr = 1; } + // CertifiedRecord contains a serialized signed PeerRecord used to + // populate the signedAddrs list. + message CertifiedRecord { + // The Seq counter from the signed PeerRecord envelope + uint64 seq = 1; + + // The serialized bytes of the SignedEnvelope containing the PeerRecord. + bytes raw = 2; + } + + // The known multiaddrs. repeated Address addrs = 1; + + // The most recently received signed PeerRecord. + CertifiedRecord certified_record = 2; } ` diff --git a/src/record/envelope/index.js b/src/record/envelope/index.js index 8320f0f005..a55d1faef2 100644 --- a/src/record/envelope/index.js +++ b/src/record/envelope/index.js @@ -112,11 +112,6 @@ const formatSignaturePayload = (domain, payloadType, 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) @@ -129,6 +124,13 @@ const unmarshalEnvelope = async (data) => { }) } +/** + * Unmarshal a serialized Envelope protobuf message. + * @param {Buffer} data + * @return {Promise} + */ +Envelope.createFromProtobuf = unmarshalEnvelope + /** * Seal marshals the given Record, places the marshaled bytes inside an Envelope * and signs it with the given peerId's private key. diff --git a/test/peer-store/address-book.spec.js b/test/peer-store/address-book.spec.js index f1c4139dac..eb77bc746e 100644 --- a/test/peer-store/address-book.spec.js +++ b/test/peer-store/address-book.spec.js @@ -1,15 +1,20 @@ 'use strict' /* eslint-env mocha */ +/* eslint max-nested-callbacks: ["error", 6] */ const chai = require('chai') chai.use(require('dirty-chai')) const { expect } = chai -const pDefer = require('p-defer') +const { Buffer } = require('buffer') const multiaddr = require('multiaddr') const arrayEquals = require('libp2p-utils/src/array-equals') +const PeerId = require('peer-id') +const pDefer = require('p-defer') const PeerStore = require('../../src/peer-store') +const Envelope = require('../../src/record/envelope') +const PeerRecord = require('../../src/record/peer-record') const peerUtils = require('../utils/creators/peer') const { @@ -396,4 +401,237 @@ describe('addressBook', () => { return defer.promise }) }) + + describe('certified records', () => { + let peerStore, ab + + describe('consumes successfully a valid peer record and stores its data', () => { + beforeEach(() => { + peerStore = new PeerStore() + ab = peerStore.addressBook + }) + + it('no previous data in AddressBook', async () => { + const multiaddrs = [addr1, addr2] + const peerRecord = new PeerRecord({ + peerId, + multiaddrs + }) + const envelope = await Envelope.seal(peerRecord, peerId) + + // consume peer record + const consumed = ab.consumePeerRecord(envelope) + expect(consumed).to.eql(true) + + // Validate stored envelope + const storedEnvelope = await ab.getPeerRecord(peerId) + expect(envelope.isEqual(storedEnvelope)).to.eql(true) + + // Validate AddressBook addresses + const addrs = ab.get(peerId) + expect(addrs).to.exist() + expect(addrs).to.have.lengthOf(multiaddrs.length) + addrs.forEach((addr, index) => { + expect(addr.isCertified).to.eql(true) + expect(multiaddrs[index].equals(addr.multiaddr)).to.eql(true) + }) + }) + + it('emits change:multiaddrs event when adding multiaddrs', async () => { + const defer = pDefer() + const multiaddrs = [addr1, addr2] + const peerRecord = new PeerRecord({ + peerId, + multiaddrs + }) + const envelope = await Envelope.seal(peerRecord, peerId) + + peerStore.once('change:multiaddrs', ({ peerId, multiaddrs }) => { + expect(peerId).to.exist() + expect(multiaddrs).to.eql(multiaddrs) + defer.resolve() + }) + + // consume peer record + const consumed = ab.consumePeerRecord(envelope) + expect(consumed).to.eql(true) + + return defer.promise + }) + + it('with same data currently in AddressBook (not certified)', async () => { + const multiaddrs = [addr1, addr2] + + // Set addressBook data + ab.set(peerId, multiaddrs) + + // Validate data exists, but not certified + let addrs = ab.get(peerId) + expect(addrs).to.exist() + expect(addrs).to.have.lengthOf(multiaddrs.length) + + addrs.forEach((addr, index) => { + expect(addr.isCertified).to.eql(false) + expect(multiaddrs[index].equals(addr.multiaddr)).to.eql(true) + }) + + // Create peer record + const peerRecord = new PeerRecord({ + peerId, + multiaddrs + }) + const envelope = await Envelope.seal(peerRecord, peerId) + + // consume peer record + const consumed = ab.consumePeerRecord(envelope) + expect(consumed).to.eql(true) + + // Validate data exists and certified + addrs = ab.get(peerId) + expect(addrs).to.exist() + expect(addrs).to.have.lengthOf(multiaddrs.length) + addrs.forEach((addr, index) => { + expect(addr.isCertified).to.eql(true) + expect(multiaddrs[index].equals(addr.multiaddr)).to.eql(true) + }) + }) + + it('with previous partial data in AddressBook (not certified)', async () => { + const multiaddrs = [addr1, addr2] + + // Set addressBook data + ab.set(peerId, [addr1]) + + // Validate data exists, but not certified + let addrs = ab.get(peerId) + expect(addrs).to.exist() + expect(addrs).to.have.lengthOf(1) + expect(addrs[0].isCertified).to.eql(false) + expect(addrs[0].multiaddr.equals(addr1)).to.eql(true) + + // Create peer record + const peerRecord = new PeerRecord({ + peerId, + multiaddrs + }) + const envelope = await Envelope.seal(peerRecord, peerId) + + // consume peer record + const consumed = ab.consumePeerRecord(envelope) + expect(consumed).to.eql(true) + + // Validate data exists and certified + addrs = ab.get(peerId) + expect(addrs).to.exist() + expect(addrs).to.have.lengthOf(multiaddrs.length) + addrs.forEach((addr, index) => { + expect(addr.isCertified).to.eql(true) + expect(multiaddrs[index].equals(addr.multiaddr)).to.eql(true) + }) + }) + + it('with previous different data in AddressBook (not certified)', async () => { + const multiaddrsUncertified = [addr3] + const multiaddrsCertified = [addr1, addr2] + + // Set addressBook data + ab.set(peerId, multiaddrsUncertified) + + // Validate data exists, but not certified + let addrs = ab.get(peerId) + expect(addrs).to.exist() + expect(addrs).to.have.lengthOf(multiaddrsUncertified.length) + addrs.forEach((addr, index) => { + expect(addr.isCertified).to.eql(false) + expect(multiaddrsUncertified[index].equals(addr.multiaddr)).to.eql(true) + }) + + // Create peer record + const peerRecord = new PeerRecord({ + peerId, + multiaddrs: multiaddrsCertified + }) + const envelope = await Envelope.seal(peerRecord, peerId) + + // consume peer record + const consumed = ab.consumePeerRecord(envelope) + expect(consumed).to.eql(true) + + // Validate data exists and certified + addrs = ab.get(peerId) + expect(addrs).to.exist() + expect(addrs).to.have.lengthOf(multiaddrsCertified.length) + addrs.forEach((addr, index) => { + expect(addr.isCertified).to.eql(true) + expect(multiaddrsCertified[index].equals(addr.multiaddr)).to.eql(true) + }) + // TODO: should it has the older one? + }) + }) + + describe('fails to consume invalid peer records', () => { + beforeEach(() => { + peerStore = new PeerStore() + ab = peerStore.addressBook + }) + + it('invalid peer record', () => { + const invalidEnvelope = { + payload: Buffer.from('invalid-peerRecord') + } + + const consumed = ab.consumePeerRecord(invalidEnvelope) + expect(consumed).to.eql(false) + }) + + it('peer that created the envelope is not the same as the peer record', async () => { + const multiaddrs = [addr1, addr2] + + // Create peer record + const peerId2 = await PeerId.create() + const peerRecord = new PeerRecord({ + peerId: peerId2, + multiaddrs + }) + const envelope = await Envelope.seal(peerRecord, peerId) + + const consumed = ab.consumePeerRecord(envelope) + expect(consumed).to.eql(false) + }) + + it('does not store an outdated record', async () => { + const multiaddrs = [addr1, addr2] + const peerRecord1 = new PeerRecord({ + peerId, + multiaddrs, + seqNumber: Date.now() + }) + const peerRecord2 = new PeerRecord({ + peerId, + multiaddrs, + seqNumber: Date.now() - 1 + }) + const envelope1 = await Envelope.seal(peerRecord1, peerId) + const envelope2 = await Envelope.seal(peerRecord2, peerId) + + // Consume envelope1 (bigger seqNumber) + let consumed = ab.consumePeerRecord(envelope1) + expect(consumed).to.eql(true) + + consumed = ab.consumePeerRecord(envelope2) + expect(consumed).to.eql(false) + }) + + it('empty multiaddrs', async () => { + const peerRecord = new PeerRecord({ + peerId, + multiaddrs: [] + }) + const envelope = await Envelope.seal(peerRecord, peerId) + + const consumed = ab.consumePeerRecord(envelope) + expect(consumed).to.eql(false) + }) + }) + }) }) diff --git a/test/peer-store/persisted-peer-store.spec.js b/test/peer-store/persisted-peer-store.spec.js index 23f68bc3d1..43b761a40e 100644 --- a/test/peer-store/persisted-peer-store.spec.js +++ b/test/peer-store/persisted-peer-store.spec.js @@ -210,6 +210,8 @@ describe('Persisted PeerStore', () => { throw new Error('Datastore should be empty') } }) + + // TODO: certified? }) describe('setup with content not stored per change (threshold 2)', () => {