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 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<Address>} 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<string, Array<Address>>}
* Map known peers to their known Address Entries.
* @type {Map<string, Array<Entry>>}
*/
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.
* @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<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.
* @private
* @param {Array<Multiaddr>} multiaddrs
* @param {boolean} [isCertified]
* @returns {Array<Address>}
*/
_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()

View File

@ -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':

View File

@ -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;
}
`

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 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>}
*/
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.

View File

@ -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)
})
})
})
})

View File

@ -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)', () => {