diff --git a/src/peer-store/address-book.js b/src/peer-store/address-book.js
index 5ace70b8..4cd5bd0c 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 0cf54c0d..c6df7b5d 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 e507d05d..18a51f3e 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 8320f0f0..a55d1fae 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 f1c4139d..eb77bc74 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 23f68bc3..43b761a4 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)', () => {