feat: cerified addressbook

This commit is contained in:
Vasco Santos 2020-06-21 19:33:58 +02:00 committed by Jacob Heun
parent b0a36ccbc8
commit 8f2e69048f
6 changed files with 404 additions and 21 deletions

View File

@ -9,10 +9,12 @@ const multiaddr = require('multiaddr')
const PeerId = require('peer-id') const PeerId = require('peer-id')
const Book = require('./book') const Book = require('./book')
const PeerRecord = require('../record/peer-record')
const { const {
codes: { ERR_INVALID_PARAMETERS } codes: { ERR_INVALID_PARAMETERS }
} = require('../errors') } = require('../errors')
const Envelope = require('../record/envelope')
/** /**
* The AddressBook is responsible for keeping the known multiaddrs * The AddressBook is responsible for keeping the known multiaddrs
@ -23,8 +25,23 @@ class AddressBook extends Book {
* Address object * Address object
* @typedef {Object} Address * @typedef {Object} Address
* @property {Multiaddr} multiaddr peer multiaddr. * @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<Address>} addresses peer Addresses.
* @property {CertifiedRecord} record certified peer record.
*/
/** /**
* @constructor * @constructor
* @param {PeerStore} peerStore * @param {PeerStore} peerStore
@ -39,16 +56,95 @@ class AddressBook extends Book {
peerStore, peerStore,
eventName: 'change:multiaddrs', eventName: 'change:multiaddrs',
eventProperty: '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. * Map known peers to their known Address Entries.
* @type {Map<string, Array<Address>>} * @type {Map<string, Array<Entry>>}
*/ */
this.data = new 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<Envelope>}
*/
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. * Set known multiaddrs of a provided peer.
* @override * @override
@ -64,7 +160,8 @@ class AddressBook extends Book {
const addresses = this._toAddresses(multiaddrs) const addresses = this._toAddresses(multiaddrs)
const id = peerId.toB58String() const id = peerId.toB58String()
const rec = this.data.get(id) const entry = this.data.get(id) || {}
const rec = entry.addresses
// Not replace multiaddrs // Not replace multiaddrs
if (!addresses.length) { 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}`) log(`stored provided multiaddrs for ${id}`)
// Notify the existance of a new peer // Notify the existance of a new peer
@ -109,7 +209,9 @@ class AddressBook extends Book {
const addresses = this._toAddresses(multiaddrs) const addresses = this._toAddresses(multiaddrs)
const id = peerId.toB58String() 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) // Add recorded uniquely to the new array (Union)
rec && rec.forEach((mi) => { rec && rec.forEach((mi) => {
@ -125,7 +227,10 @@ class AddressBook extends Book {
return this return this
} }
this._setData(peerId, addresses) this._setData(peerId, {
addresses,
record: entry.record
})
log(`added provided multiaddrs for ${id}`) log(`added provided multiaddrs for ${id}`)
@ -137,13 +242,31 @@ class AddressBook extends Book {
return this return this
} }
/**
* Get the known data of a provided peer.
* @override
* @param {PeerId} peerId
* @returns {Array<data>}
*/
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. * Transforms received multiaddrs into Address.
* @private * @private
* @param {Array<Multiaddr>} multiaddrs * @param {Array<Multiaddr>} multiaddrs
* @param {boolean} [isCertified]
* @returns {Array<Address>} * @returns {Array<Address>}
*/ */
_toAddresses (multiaddrs) { _toAddresses (multiaddrs, isCertified = false) {
if (!multiaddrs) { if (!multiaddrs) {
log.error('multiaddrs must be provided to store data') log.error('multiaddrs must be provided to store data')
throw errcode(new Error('multiaddrs must be provided'), ERR_INVALID_PARAMETERS) throw errcode(new Error('multiaddrs must be provided'), ERR_INVALID_PARAMETERS)
@ -158,7 +281,8 @@ class AddressBook extends Book {
} }
addresses.push({ 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) 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 undefined
} }
return record.map((address) => { return entry.addresses.map((address) => {
const multiaddr = address.multiaddr const multiaddr = address.multiaddr
const idString = multiaddr.getPeerId() const idString = multiaddr.getPeerId()

View File

@ -296,9 +296,11 @@ class PersistentPeerStore extends PeerStore {
this.addressBook._setData( this.addressBook._setData(
peerId, peerId,
decoded.addrs.map((address) => ({ {
multiaddr: multiaddr(address.multiaddr) addresses: decoded.addrs.map((address) => ({
})), multiaddr: multiaddr(address.multiaddr)
}))
},
{ emit: false }) { emit: false })
break break
case 'keys': case 'keys':

View File

@ -4,11 +4,26 @@ const protons = require('protons')
const message = ` const message = `
message Addresses { message Addresses {
// Address represents a single multiaddr.
message Address { message Address {
required bytes multiaddr = 1; 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; repeated Address addrs = 1;
// The most recently received signed PeerRecord.
CertifiedRecord certified_record = 2;
} }
` `

View File

@ -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 unmarshalEnvelope = async (data) => {
const envelopeData = Protobuf.decode(data) const envelopeData = Protobuf.decode(data)
const peerId = await PeerId.createFromPubKey(envelopeData.public_key) 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>}
*/
Envelope.createFromProtobuf = unmarshalEnvelope
/** /**
* Seal marshals the given Record, places the marshaled bytes inside an Envelope * Seal marshals the given Record, places the marshaled bytes inside an Envelope
* and signs it with the given peerId's private key. * and signs it with the given peerId's private key.

View File

@ -1,15 +1,20 @@
'use strict' 'use strict'
/* eslint-env mocha */ /* eslint-env mocha */
/* eslint max-nested-callbacks: ["error", 6] */
const chai = require('chai') const chai = require('chai')
chai.use(require('dirty-chai')) chai.use(require('dirty-chai'))
const { expect } = chai const { expect } = chai
const pDefer = require('p-defer') const { Buffer } = require('buffer')
const multiaddr = require('multiaddr') const multiaddr = require('multiaddr')
const arrayEquals = require('libp2p-utils/src/array-equals') 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 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 peerUtils = require('../utils/creators/peer')
const { const {
@ -396,4 +401,237 @@ describe('addressBook', () => {
return defer.promise 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)
})
})
})
}) })

View File

@ -210,6 +210,8 @@ describe('Persisted PeerStore', () => {
throw new Error('Datastore should be empty') throw new Error('Datastore should be empty')
} }
}) })
// TODO: certified?
}) })
describe('setup with content not stored per change (threshold 2)', () => { describe('setup with content not stored per change (threshold 2)', () => {