From 2301bac7e18f11ffadf62fdcc7568e21e6e350f4 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Fri, 8 Apr 2022 18:28:22 +1000 Subject: [PATCH] feat: implemented `NodeManager.refreshBucket()` This method preforms the kademlia `refreshBucket` operation. It selects a random node within the bucket and preforms a search for that node. The process exchanges node information with any nodes it connects to. #345 --- src/nodes/NodeManager.ts | 28 ++++++++++++++++++++++++-- src/nodes/utils.ts | 42 +++++++++++++++++++++++++++++++++++++++ tests/nodes/utils.test.ts | 18 +++++++++++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) diff --git a/src/nodes/NodeManager.ts b/src/nodes/NodeManager.ts index f94c5315da..61fac98389 100644 --- a/src/nodes/NodeManager.ts +++ b/src/nodes/NodeManager.ts @@ -5,11 +5,17 @@ import type KeyManager from '../keys/KeyManager'; import type { PublicKeyPem } from '../keys/types'; import type Sigchain from '../sigchain/Sigchain'; import type { ChainData, ChainDataEncoded } from '../sigchain/types'; -import type { NodeId, NodeAddress, NodeBucket } from '../nodes/types'; +import type { + NodeId, + NodeAddress, + NodeBucket, + NodeBucketIndex, +} from '../nodes/types'; import type { ClaimEncoded } from '../claims/types'; import type { Timer } from '../types'; import Logger from '@matrixai/logger'; import { StartStop, ready } from '@matrixai/async-init/dist/StartStop'; +import { IdInternal } from '@matrixai/id'; import * as nodesErrors from './errors'; import * as nodesUtils from './utils'; import * as networkUtils from '../network/utils'; @@ -514,7 +520,7 @@ class NodeManager { // return await this.nodeGraph.getAllBuckets(tran); // } - // FIXME + // FIXME potentially confusing name, should we rename this to renewBuckets? /** * To be called on key renewal. Re-orders all nodes in all buckets with respect * to the new node ID. @@ -609,6 +615,24 @@ class NodeManager { public async queueDrained(): Promise { await this.setNodeQueueEmpty; } + + /** + * Kademlia refresh bucket operation. + * It picks a random node within a bucket and does a search for that node. + * Connections during the search will will share node information with other + * nodes. + * @param bucketIndex + */ + private async refreshBucket(bucketIndex: NodeBucketIndex) { + // We need to generate a random nodeId for this bucket + const nodeId = this.keyManager.getNodeId(); + const bucketRandomNodeId = nodesUtils.generateRandomNodeIdForBucket( + nodeId, + bucketIndex, + ); + // We then need to start a findNode procedure + await this.nodeConnectionManager.findNode(bucketRandomNodeId); + } } export default NodeManager; diff --git a/src/nodes/utils.ts b/src/nodes/utils.ts index 76bb4058a2..c61a6cd586 100644 --- a/src/nodes/utils.ts +++ b/src/nodes/utils.ts @@ -7,6 +7,7 @@ import type { import { IdInternal } from '@matrixai/id'; import lexi from 'lexicographic-integer'; import { bytes2BigInt, bufferSplit } from '../utils'; +import * as keysUtils from '../keys/utils'; // FIXME: const prefixBuffer = Buffer.from([33]); @@ -283,6 +284,44 @@ function bucketSortByDistance( } } +function generateRandomDistanceForBucket(bucketIndex: NodeBucketIndex): NodeId { + const buffer = keysUtils.getRandomBytesSync(32); + // Calculate the most significant byte for bucket + const base = bucketIndex / 8; + const mSigByte = Math.floor(base); + const mSigBit = (base - mSigByte) * 8 + 1; + const mSigByteIndex = buffer.length - mSigByte - 1; + // Creating masks + // AND mask should look like 0b00011111 + // OR mask should look like 0b00010000 + const shift = 8 - mSigBit; + const andMask = 0b11111111 >>> shift; + const orMask = 0b10000000 >>> shift; + let byte = buffer[mSigByteIndex]; + byte = byte & andMask; // Forces 0 for bits above bucket bit + byte = byte | orMask; // Forces 1 in the desired bucket bit + buffer[mSigByteIndex] = byte; + // Zero out byte 'above' mSigByte + for (let byteIndex = 0; byteIndex < mSigByteIndex; byteIndex++) { + buffer[byteIndex] = 0; + } + return IdInternal.fromBuffer(buffer); +} + +function xOrNodeId(node1: NodeId, node2: NodeId): NodeId { + const xOrNodeArray = node1.map((byte, i) => byte ^ node2[i]); + const xOrNodeBuffer = Buffer.from(xOrNodeArray); + return IdInternal.fromBuffer(xOrNodeBuffer); +} + +function generateRandomNodeIdForBucket( + nodeId: NodeId, + bucket: NodeBucketIndex, +): NodeId { + const randomDistanceForBucket = generateRandomDistanceForBucket(bucket); + return xOrNodeId(nodeId, randomDistanceForBucket); +} + export { prefixBuffer, encodeNodeId, @@ -299,4 +338,7 @@ export { parseLastUpdatedBucketDbKey, nodeDistance, bucketSortByDistance, + generateRandomDistanceForBucket, + xOrNodeId, + generateRandomNodeIdForBucket, }; diff --git a/tests/nodes/utils.test.ts b/tests/nodes/utils.test.ts index 59d565812d..c87a82f263 100644 --- a/tests/nodes/utils.test.ts +++ b/tests/nodes/utils.test.ts @@ -171,4 +171,22 @@ describe('nodes/utils', () => { i++; } }); + test('should generate random distance for a bucket', async () => { + // Const baseNodeId = testNodesUtils.generateRandomNodeId(); + const zeroNodeId = IdInternal.fromBuffer(Buffer.alloc(32, 0)); + for (let i = 0; i < 255; i++) { + const randomDistance = nodesUtils.generateRandomDistanceForBucket(i); + expect(nodesUtils.bucketIndex(zeroNodeId, randomDistance)).toEqual(i); + } + }); + test('should generate random NodeId for a bucket', async () => { + const baseNodeId = testNodesUtils.generateRandomNodeId(); + for (let i = 0; i < 255; i++) { + const randomDistance = nodesUtils.generateRandomNodeIdForBucket( + baseNodeId, + i, + ); + expect(nodesUtils.bucketIndex(baseNodeId, randomDistance)).toEqual(i); + } + }); });