netus-k-bucket
Version:
Kademlia DHT k-bucket implementation
466 lines (430 loc) • 16.4 kB
JavaScript
var test = require('test-kit')('tape')
var KBucket = require('.')
function str2id (s) {
return Buffer.from(s, 'hex')
}
function id2str (id) {
return id && id.toString('hex').toUpperCase()
}
test('count', function (t) {
t.table_assert([
[ 'root_id', 'ids', 'exp' ],
[ '0A', null, 0 ],
[ null, '0A', 1 ],
[ null, '0A,0A', 1 ],
[ null, '0A,0A,FF,11,FF', 3 ],
[ null, '0A,0A,FF,11,FE,FF,FE,00', 5 ],
], function (root_id, ids) {
var opt = {}
if (root_id) { opt.root_id = str2id(root_id) }
var kb = new KBucket(opt)
if (ids) {
ids.split(',').forEach(function (id) {kb.add({id: str2id(id)})})
}
return kb.count()
})
})
test('get', function (t) {
t.table_assert([
[ 'opt', 'contracts', 'get_id', 'exp' ],
[ null, null, 'A1', null ],
[ null, [ {id: 'A1'} ], 'A1', { id: 'A1' } ],
'# retrieve highest vtime (default arbiter)',
[ null, [ {id: '08', x: 1, vtime: 0}, {id: '08', x: 0, vtime: 1} ], '08', { id: '08', x: 0, vtime: 1 } ],
[ { root_id: '00' }, [ {id: '08', x: 1, vtime: 1}, {id: '08', x: 1, vtime: 0} ], '08', { id: '08', x: 1, vtime: 1 } ],
], function (opt, contracts, get_id) {
var kb = new KBucket(opt)
if (contracts) {
contracts.forEach(function (c) { c.id = str2id(c.id); kb.add(c) })
}
var ret = kb.get(str2id(get_id))
if (ret) {
ret.id = id2str(ret.id)
}
return ret
})
})
test('get retrieves contact from nested leaf node', function (t) {
var kBucket = new KBucket({localNodeId: Buffer.from([ 0x00, 0x00 ])})
for (var i = 0; i < kBucket.numberOfNodesPerKBucket; ++i) {
kBucket.add({ id: Buffer.from([ 0x80, i ]) }) // make sure all go into "far away" bucket
}
// cause a split to happen
kBucket.add({ id: Buffer.from([ 0x00, i ]), find: 'me' })
t.same(kBucket.get(Buffer.from([ 0x00, i ])).find, 'me')
t.end()
})
test('all_contacts should return empty array if no contacts', function (t) {
var kBucket = new KBucket()
t.same(kBucket.all_contacts().length, 0)
t.end()
})
test('all_contacts should return all contacts in an array arranged from low to high buckets', function (t) {
t.plan(22)
var kBucket = new KBucket({ localNodeId: Buffer.from([ 0x00, 0x00 ]) })
var expectedIds = []
for (var i = 0; i < kBucket.bucket_len; ++i) {
kBucket.add({ id: Buffer.from([ 0x80, i ]) }) // make sure all go into "far away" bucket
expectedIds.push(0x80 * 256 + i)
}
// cause a split to happen
kBucket.add({ id: Buffer.from([ 0x00, 0x80, i - 1 ]) })
// console.log(require('util').inspect(kBucket, {depth: null}))
var contacts = kBucket.all_contacts()
// console.log(require('util').inspect(contacts, {depth: null}))
t.same(contacts.length, kBucket.bucket_len + 1)
t.same(parseInt(contacts[0].id.toString('hex'), 16), 0x80 * 256 + i - 1)
contacts.shift() // get rid of low bucket contact
for (i = 0; i < kBucket.bucket_len; ++i) {
t.same(parseInt(contacts[i].id.toString('hex'), 16), expectedIds[i])
}
t.end()
})
test('closest nodes are returned', function (t) {
var kBucket = new KBucket()
for (var i = 0; i < 0x12; ++i) kBucket.add({ id: Buffer.from([ i ]) })
var contact = { id: Buffer.from([ 0x15 ]) } // 00010101
var contacts = kBucket.closest(contact.id, 3)
t.same(contacts.length, 3)
t.same(contacts[0].id, Buffer.from([ 0x11 ])) // distance: 00000100
t.same(contacts[1].id, Buffer.from([ 0x10 ])) // distance: 00000101
t.same(contacts[2].id, Buffer.from([ 0x05 ])) // distance: 00010000
t.end()
})
test('n is Infinity by default', function (t) {
var kBucket = new KBucket({ localNodeId: Buffer.from([ 0x00, 0x00 ]) })
for (var i = 0; i < 1e3; ++i) kBucket.add({ id: Buffer.from([ ~~(i / 256), i % 256 ]) })
t.true(kBucket.closest(Buffer.from([ 0x80, 0x80 ])).length > 100)
t.end()
})
test('closest nodes are returned (including exact match)', function (t) {
var kBucket = new KBucket()
for (var i = 0; i < 0x12; ++i) kBucket.add({ id: Buffer.from([ i ]) })
var contact = { id: Buffer.from([ 0x11 ]) } // 00010001
var contacts = kBucket.closest(contact.id, 3)
t.same(contacts[0].id, Buffer.from([ 0x11 ])) // distance: 00000000
t.same(contacts[1].id, Buffer.from([ 0x10 ])) // distance: 00000001
t.same(contacts[2].id, Buffer.from([ 0x01 ])) // distance: 00010000
t.end()
})
test('closest nodes are returned even if there isn\'t enough in one bucket', function (t) {
var kBucket = new KBucket({ localNodeId: Buffer.from([ 0x00, 0x00 ]) })
for (var i = 0; i < kBucket.bucket_len; i++) {
kBucket.add({ id: Buffer.from([ 0x80, i ]) })
kBucket.add({ id: Buffer.from([ 0x01, i ]) })
}
kBucket.add({ id: Buffer.from([ 0x00, 0x01 ]) })
var contact = { id: Buffer.from([ 0x00, 0x03 ]) } // 0000000000000011
var ids = kBucket.closest(contact.id, 22).map(function (c) { return [ id2str(c.id), c._dist.toString(16) ] })
t.same(ids, [
// expected:
// id, distance from '0003' (hex)
[ '0001', '2' ],
[ '0103', '100' ],
[ '0102', '101' ],
[ '0101', '102' ],
[ '0100', '103' ],
[ '0107', '104' ],
[ '0106', '105' ],
[ '0105', '106' ],
[ '0104', '107' ],
[ '010B', '108' ],
[ '010A', '109' ],
[ '0109', '10a' ],
[ '0108', '10b' ],
[ '010F', '10c' ],
[ '010E', '10d' ],
[ '010D', '10e' ],
[ '010C', '10f' ],
[ '0113', '110' ],
[ '0112', '111' ],
[ '0111', '112' ],
[ '0110', '113' ],
[ '8003', '8000' ]
])
// console.log(require('util').inspect(kBucket, false, null))
t.end()
})
test('equal vtime results in contact marked as most recent', function (t) {
var kBucket = new KBucket()
var contact = { id: Buffer.from('a'), vtime: 3 }
kBucket.add(contact)
kBucket.add({ id: Buffer.from('b') })
kBucket.add(contact)
t.same(kBucket.root.contacts[1], contact)
t.end()
})
test('more recent vtime results in contact update and contact being marked as most recent', function (t) {
var kBucket = new KBucket()
var contact = { id: Buffer.from('a'), old: 'property', vtime: 3 }
kBucket.add(contact)
kBucket.add({ id: Buffer.from('b') })
kBucket.add({ id: Buffer.from('a'), newer: 'property', vtime: 4 })
t.same(kBucket.root.contacts[1].id, contact.id)
t.same(kBucket.root.contacts[1].vtime, 4)
t.same(kBucket.root.contacts[1].old, undefined)
t.same(kBucket.root.contacts[1].newer, 'property')
t.end()
})
test('should generate "updated"', function (t) {
t.plan(2)
var kBucket = new KBucket()
var contact1 = { id: Buffer.from('a'), vtime: 1 }
var contact2 = { id: Buffer.from('a'), vtime: 2 }
kBucket.on('updated', function (oldContact, newContact) {
t.same(oldContact, contact1)
t.same(newContact, contact2)
t.end()
})
kBucket.add(contact1)
kBucket.add(contact2)
})
test('should generate event "updated" when updating a split node', function (t) {
t.plan(3)
var kBucket = new KBucket({
localNodeId: Buffer.from('') // need non-random localNodeId for deterministic splits
})
for (var i = 0; i < kBucket.bucket_len + 1; ++i) {
kBucket.add({ id: Buffer.from('' + i) })
}
t.false(kBucket.bucket)
var contact1 = { id: Buffer.from('a'), vtime: 1 }
var contact2 = { id: Buffer.from('a'), vtime: 2 }
kBucket.on('updated', function (oldContact, newContact) {
t.same(oldContact, contact1)
t.same(newContact, contact2)
t.end()
})
kBucket.add(contact1)
kBucket.add(contact2)
})
test('add', function (t) {
t.table_assert([
[ 'lid', 'npb', 'addIds', 'exp' ],
'# add existing contact',
[ '80', 4, '80', { b: '80' } ],
'# add single contact',
[ '80', 4, 'C0', { b: 'C0' } ],
'# add no split',
[ '80', 4, '80,C0,E0,F0', { b: '80,C0,E0,F0' } ],
'# add with splits',
[ '80', 4, '80,C0,E0,F0,F8', { r: {l: {b:'80'}, r: {b:'!C0,E0,F0,F8'}} } ],
[ '80', 4, '80,C0,E0,F0,00', { l: {b: '!00'}, r: {b: '80,C0,E0,F0'} } ],
[ '80', 4, '80,C0,E0,F0,00,81', { l: {b: '!00'}, r: {l: {b:'80,81'}, r: {b:'!C0,E0,F0'}} } ],
[ '80', 4, '80,A0,00,70,71', { l: {b: '!00,70,71'}, r: {b: '80,A0'} } ],
[ '80', 4, '81,82,83,84,85', { r: {l: {l:{l:{l:{l:{b:'81,82,83'},r:{b:'!84,85'}}}}}} } ],
], function (lid, npb, addIds) {
var kBucket = new KBucket({ localNodeId: Buffer.from(lid, 'hex'), numberOfNodesPerKBucket: npb })
addIds.split(',').forEach(function (id) { kBucket.add({ id: Buffer.from(id, 'hex') }) })
console.log(JSON.stringify(kBucket.to_obj()))
return kBucket.to_obj()
})
})
test('adding a contact places it in root node', function (t) {
var kBucket = new KBucket()
var contact = { id: Buffer.from('a') }
kBucket.add(contact)
t.same(kBucket.root.contacts, [ contact ])
t.end()
})
test('adding an existing contact does not increase number of contacts in root node', function (t) {
var kBucket = new KBucket()
var contact = { id: Buffer.from('a') }
kBucket.add(contact)
kBucket.add({ id: Buffer.from('a') })
t.same(kBucket.root.contacts.length, 1)
t.end()
})
test('adding same contact moves it to the end of the root node (most-recently-contacted end)', function (t) {
var kBucket = new KBucket()
var contact = { id: Buffer.from('a') }
kBucket.add(contact)
t.same(kBucket.root.contacts.length, 1)
kBucket.add({ id: Buffer.from('b') })
t.same(kBucket.root.contacts.length, 2)
t.true(kBucket.root.contacts[0] === contact) // least-recently-contacted end
kBucket.add(contact)
t.same(kBucket.root.contacts.length, 2)
t.true(kBucket.root.contacts[1] === contact) // most-recently-contacted end
t.end()
})
test('adding contact to bucket that can\'t be split results in calling "ping" callback', function (t) {
t.plan(3 /* num_to_ping */ + 2)
var kBucket = new KBucket({ localNodeId: Buffer.from([ 0x00, 0x00 ]) })
kBucket.on('ping', function (contacts, replacement) {
t.same(contacts.length, kBucket.num_to_ping)
// console.dir(kBucket.root.right.contacts[0])
for (var i = 0; i < kBucket.num_to_ping; ++i) {
// the least recently contacted end of the node should be pinged
t.true(contacts[i] === kBucket.root.right.contacts[i])
}
t.same(replacement, { id: Buffer.from([ 0x80, j ]) })
t.end()
})
for (var j = 0; j < kBucket.bucket_len + 1; ++j) {
kBucket.add({ id: Buffer.from([ 0x80, j ]) }) // make sure all go into "far away" node
}
})
test('should generate event "added" once', function (t) {
t.plan(1)
var kBucket = new KBucket()
var contact = { id: Buffer.from('a') }
kBucket.on('added', function (newContact) {
t.same(newContact, contact)
})
kBucket.add(contact)
kBucket.add(contact)
t.end()
})
test('should generate event "added" when adding to a split node', function (t) {
t.plan(2)
var kBucket = new KBucket({
localNodeId: Buffer.from('') // need non-random root_id for deterministic splits
})
for (var i = 0; i < kBucket.bucket_len + 1; ++i) {
kBucket.add({ id: Buffer.from('' + i) })
}
t.same(kBucket.root.contacts, null)
var contact = { id: Buffer.from('a') }
kBucket.on('added', function (newContact) {
t.same(newContact, contact)
})
kBucket.add(contact)
t.end()
})
test('distance', function (t) {
var kb = new KBucket()
t.table_assert([
[ 'id1', 'id2', 'exp' ],
[ '00', '00', '0'],
[ '00', '01', '1'],
[ '01', '00', '1'],
[ '01', '02', '3'],
[ '02', '01', '3'],
[ '00', '02', '2'],
[ '15', '11', '4'],
[ '00', '0000', '0'],
[ '00', '00FF', 'FF'],
[ '00', '7AFF', '7AFF'],
[ '01', '24', '25'],
[ '24', '01', '25'],
], function (id1, id2) {
return KBucket.distance( str2id(id1), str2id(id2)).toString(16).toUpperCase()
})
})
test('removing a contact should remove contact from nested buckets', function (t) {
var kBucket = new KBucket({ localNodeId: Buffer.from([ 0x00, 0x00 ]) })
for (var i = 0; i < kBucket.bucket_len; ++i) {
kBucket.add({ id: Buffer.from([ 0x80, i ]) }) // make sure all go into "far away" bucket
}
// cause a split to happen
kBucket.add({ id: Buffer.from([ 0x00, i ]) })
// console.log(require('util').inspect(kBucket, false, null))
var contactToDelete = { id: Buffer.from([ 0x80, 0x00 ]) }
t.same(kBucket.root.right.index_of(contactToDelete.id), 0)
kBucket.remove(Buffer.from([ 0x80, 0x00 ]))
t.same(kBucket.root.right.index_of(contactToDelete.id), -1)
t.end()
})
test('should generate "removed"', function (t) {
t.plan(1)
var kBucket = new KBucket()
var contact = { id: Buffer.from('a') }
kBucket.on('removed', function (removedContact) {
t.same(removedContact, contact)
t.end()
})
kBucket.add(contact)
kBucket.remove(contact.id)
})
test('should generate event "removed" when removing from a split bucket', function (t) {
t.plan(2)
var kBucket = new KBucket({
localNodeId: Buffer.from('') // need non-random localNodeId for deterministic splits
})
for (var i = 0; i < kBucket.bucket_len + 1; ++i) {
kBucket.add({ id: Buffer.from('' + i) })
}
t.false(kBucket.bucket)
var contact = { id: Buffer.from('a') }
kBucket.on('removed', function (removedContact) {
t.same(removedContact, contact)
t.end()
})
kBucket.add(contact)
kBucket.remove(contact.id)
})
test('adding a contact does not split node', function (t) {
var kBucket = new KBucket()
kBucket.add({ id: Buffer.from('a') })
t.same(kBucket.root.left, null)
t.same(kBucket.root.right, null)
t.notSame(kBucket.root.contacts, null)
t.end()
})
test('adding maximum number of contacts (per node) [20] into node does not split node', function (t) {
var kBucket = new KBucket()
for (var i = 0; i < kBucket.bucket_len; ++i) {
kBucket.add({ id: Buffer.from('' + i) })
}
t.same(kBucket.root.left, null)
t.same(kBucket.root.right, null)
t.notSame(kBucket.root.contacts, null)
t.end()
})
test('adding maximum number of contacts (per node) + 1 [21] into node splits the node', function (t) {
var kBucket = new KBucket()
for (var i = 0; i < kBucket.bucket_len + 1; ++i) {
kBucket.add({ id: Buffer.from('' + i) })
}
t.notSame(kBucket.root.left, null)
t.notSame(kBucket.root.right, null)
t.same(kBucket.root.contacts, null)
t.end()
})
test('split nodes contain all added contacts', function (t) {
t.plan(20 /* bucket_len */ + 2)
var kBucket = new KBucket({ localNodeId: Buffer.from([ 0x00 ]) })
var foundContact = {}
for (var i = 0; i < kBucket.bucket_len + 1; ++i) {
kBucket.add({ id: Buffer.from([ i ]) })
foundContact[i] = false
}
var traverse = function (node) {
if (node.contacts === null) {
traverse(node.left)
traverse(node.right)
} else {
node.contacts.forEach(function (contact) {
foundContact[parseInt(contact.id.toString('hex'), 16)] = true
})
}
}
traverse(kBucket.root)
Object.keys(foundContact).forEach(function (key) { t.true(foundContact[key], key) })
t.same(kBucket.root.contacts, null)
t.end()
})
test('when splitting nodes the "far away" node should be marked to prevent splitting "far away" node', function (t) {
t.plan(5)
var kBucket = new KBucket({ localNodeId: Buffer.from([ 0x00 ]) })
for (var i = 0; i < kBucket.bucket_len + 1; ++i) {
kBucket.add({ id: Buffer.from([ i ]) })
}
// above algorithm will split left node 4 times and put 0x00 through 0x0f
// in the left node, and put 0x10 through 0x14 in right node
// since root_id is 0x00, we expect every right node to be "far" and
// therefore marked as "far = true"
// there will be one "left" node and four "right" nodes (t.expect(5))
var traverse = function (node, dontSplit) {
if (node.contacts === null) {
traverse(node.left, false)
traverse(node.right, true)
} else {
if (dontSplit) t.true(node.far)
else t.false(node.far)
}
}
traverse(kBucket.root)
t.end()
})