diff --git a/src/agent/service/nodesClosestLocalNodesGet.ts b/src/agent/service/nodesClosestLocalNodesGet.ts index 5c2c0e204..844aa0253 100644 --- a/src/agent/service/nodesClosestLocalNodesGet.ts +++ b/src/agent/service/nodesClosestLocalNodesGet.ts @@ -1,6 +1,6 @@ import type * as grpc from '@grpc/grpc-js'; +import type { NodeGraph } from '../../nodes'; import type { DB } from '@matrixai/db'; -import type NodeConnectionManager from '../../nodes/NodeConnectionManager'; import type { NodeId } from '../../nodes/types'; import type Logger from '@matrixai/logger'; import * as grpcUtils from '../../grpc/utils'; @@ -16,11 +16,11 @@ import * as agentUtils from '../utils'; * to some provided node ID. */ function nodesClosestLocalNodesGet({ - nodeConnectionManager, + nodeGraph, db, logger, }: { - nodeConnectionManager: NodeConnectionManager; + nodeGraph: NodeGraph; db: DB; logger: Logger; }) { @@ -47,21 +47,16 @@ function nodesClosestLocalNodesGet({ ); // Get all local nodes that are closest to the target node from the request const closestNodes = await db.withTransactionF( - async (tran) => - await nodeConnectionManager.getClosestLocalNodes( - nodeId, - undefined, - tran, - ), + async (tran) => await nodeGraph.getClosestNodes(nodeId, tran), ); - for (const node of closestNodes) { + for (const [nodeId, nodeData] of closestNodes) { const addressMessage = new nodesPB.Address(); - addressMessage.setHost(node.address.host); - addressMessage.setPort(node.address.port); + addressMessage.setHost(nodeData.address.host); + addressMessage.setPort(nodeData.address.port); // Add the node to the response's map (mapping of node ID -> node address) response .getNodeTableMap() - .set(nodesUtils.encodeNodeId(node.id), addressMessage); + .set(nodesUtils.encodeNodeId(nodeId), addressMessage); } callback(null, response); return; diff --git a/src/nodes/NodeConnectionManager.ts b/src/nodes/NodeConnectionManager.ts index 5c1b34cb7..fc5c99ff8 100644 --- a/src/nodes/NodeConnectionManager.ts +++ b/src/nodes/NodeConnectionManager.ts @@ -383,49 +383,7 @@ class NodeConnectionManager { return address; } - /** - * Finds the set of nodes (of size k) known by the current node (i.e. in its - * bucket's database) that have the smallest distance to the target node (i.e. - * are closest to the target node). - * i.e. FIND_NODE RPC from Kademlia spec - * - * Used by the RPC service. - * - * @param targetNodeId the node ID to find other nodes closest to it - * @param numClosest the number of the closest nodes to return (by default, returns - * according to the maximum number of nodes per bucket) - * @param tran - * @returns a mapping containing exactly k nodeIds -> nodeAddresses (unless the - * current node has less than k nodes in all of its buckets, in which case it - * returns all nodes it has knowledge of) - */ - @ready(new nodesErrors.ErrorNodeConnectionManagerNotRunning()) - public async getClosestLocalNodes( - targetNodeId: NodeId, - numClosest: number = this.nodeGraph.maxNodesPerBucket, - tran?: DBTransaction, - ): Promise> { - // Retrieve all nodes from buckets in database - const buckets = await this.nodeGraph.getAllBuckets(tran); - // Iterate over all the nodes in each bucket - const distanceToNodes: Array = []; - buckets.forEach(function (bucket) { - for (const nodeIdString of Object.keys(bucket)) { - // Compute the distance from the node, and add it to the array - const nodeId = IdInternal.fromString(nodeIdString); - distanceToNodes.push({ - id: nodeId, - address: bucket[nodeId].address, - distance: nodesUtils.calculateDistance(nodeId, targetNodeId), - }); - } - }); - // Sort the array (based on the distance at index 1) - distanceToNodes.sort(nodesUtils.sortByDistance); - // Return the closest k nodes (i.e. the first k), or all nodes if < k in array - return distanceToNodes.slice(0, numClosest); - } - + // FIXME: getClosestNodes was moved to NodeGraph? that needs to be updated. /** * Attempts to locate a target node in the network (using Kademlia). * Adds all discovered, active nodes to the current node's database (up to k @@ -447,7 +405,7 @@ class NodeConnectionManager { // Let foundTarget: boolean = false; let foundAddress: NodeAddress | undefined = undefined; // Get the closest alpha nodes to the target node (set as shortlist) - const shortlist: Array = await this.getClosestLocalNodes( + const shortlist = await this.nodeGraph.getClosestNodes( targetNodeId, this.initialClosestNodes, ); @@ -470,8 +428,9 @@ class NodeConnectionManager { if (nextNode == null) { break; } + const [nextNodeId, nextNodeAddress] = nextNode; // Skip if the node has already been contacted - if (contacted[nextNode.id]) { + if (contacted[nextNodeId]) { continue; } // Connect to the node (check if pre-existing connection exists, otherwise @@ -479,16 +438,16 @@ class NodeConnectionManager { try { // Add the node to the database so that we can find its address in // call to getConnectionToNode - await this.nodeGraph.setNode(nextNode.id, nextNode.address); - await this.getConnection(nextNode.id); + await this.nodeGraph.setNode(nextNodeId, nextNodeAddress.address); + await this.getConnection(nextNodeId); } catch (e) { // If we can't connect to the node, then skip it continue; } - contacted[nextNode.id] = true; + contacted[nextNodeId] = true; // Ask the node to get their own closest nodes to the target const foundClosest = await this.getRemoteNodeClosestNodes( - nextNode.id, + nextNodeId, targetNodeId, ); // Check to see if any of these are the target node. At the same time, add diff --git a/src/nodes/NodeGraph.ts b/src/nodes/NodeGraph.ts index 9fa404896..34fa5c56a 100644 --- a/src/nodes/NodeGraph.ts +++ b/src/nodes/NodeGraph.ts @@ -105,7 +105,7 @@ class NodeGraph { // Bucket metadata sublevel: `!meta!! -> value` this.nodeGraphMetaDbPath = [...this.nodeGraphDbPath, 'meta' + space]; // Bucket sublevel: `!buckets!! -> NodeData` - // The BucketIndex can range from 0 to NodeId bitsize minus 1 + // The BucketIndex can range from 0 to NodeId bit-size minus 1 // So 256 bits means 256 buckets of 0 to 255 this.nodeGraphBucketsDbPath = [...this.nodeGraphDbPath, 'buckets' + space]; // Last updated sublevel: `!lastUpdated!!- -> NodeId` @@ -166,7 +166,7 @@ class NodeGraph { } /** - * Get all nodes + * Get all nodes. * Nodes are always sorted by `NodeBucketIndex` first * Then secondly by the node IDs * The `order` parameter applies to both, for example possible sorts: @@ -340,7 +340,7 @@ class NodeGraph { } /** - * Gets all buckets + * Gets all buckets. * Buckets are always sorted by `NodeBucketIndex` first * Then secondly by the `sort` parameter * The `order` parameter applies to both, for example possible sorts: @@ -582,6 +582,136 @@ class NodeGraph { return value; } + /** + * Finds the set of nodes (of size k) known by the current node (i.e. in its + * buckets' database) that have the smallest distance to the target node (i.e. + * are closest to the target node). + * i.e. FIND_NODE RPC from Kademlia spec + * + * Used by the RPC service. + * + * @param nodeId the node ID to find other nodes closest to it + * @param limit the number of the closest nodes to return (by default, returns + * according to the maximum number of nodes per bucket) + * @param tran + * @returns a mapping containing exactly k nodeIds -> nodeAddresses (unless the + * current node has less than k nodes in all of its buckets, in which case it + * returns all nodes it has knowledge of) + */ + @ready(new nodesErrors.ErrorNodeGraphNotRunning()) + public async getClosestNodes( + nodeId: NodeId, + limit: number = this.nodeBucketLimit, + tran: DBTransaction, + ): Promise { + // Buckets map to the target node in the following way; + // 1. 0, 1, ..., T-1 -> T + // 2. T -> 0, 1, ..., T-1 + // 3. T+1, T+2, ..., 255 are unchanged + // We need to obtain nodes in the following bucket order + // 1. T + // 2. iterate over 0 ---> T-1 + // 3. iterate over T+1 ---> K + // Need to work out the relevant bucket to start from + const startingBucket = nodesUtils.bucketIndex( + this.keyManager.getNodeId(), + nodeId, + ); + // Getting the whole target's bucket first + const nodeIds: NodeBucket = await this.getBucket( + startingBucket, + undefined, + undefined, + tran, + ); + // We need to iterate over the key stream + // When streaming we want all nodes in the starting bucket + // The keys takes the form `!(lexpack bucketId)!(nodeId)` + // We can just use `!(lexpack bucketId)` to start from + // Less than `!(bucketId 101)!` gets us buckets 100 and lower + // greater than `!(bucketId 99)!` gets up buckets 100 and greater + const prefix = Buffer.from([33]); // Code for `!` prefix + if (nodeIds.length < limit) { + // Just before target bucket + const bucketId = Buffer.from(nodesUtils.bucketKey(startingBucket)); + const endKeyLower = Buffer.concat([prefix, bucketId, prefix]); + const remainingLimit = limit - nodeIds.length; + // Iterate over lower buckets + tran.iterator( + { + lt: endKeyLower, + limit: remainingLimit, + valueAsBuffer: false, + }, + this.nodeGraphBucketsDbPath, + ); + for await (const [key, nodeData] of tran.iterator( + { + lt: endKeyLower, + limit: remainingLimit, + valueAsBuffer: false, + }, + this.nodeGraphBucketsDbPath, + )) { + const info = nodesUtils.parseBucketsDbKey(key as unknown as Buffer); + nodeIds.push([info.nodeId, nodeData]); + } + } + if (nodeIds.length < limit) { + // Just after target bucket + const bucketId = Buffer.from(nodesUtils.bucketKey(startingBucket + 1)); + const startKeyUpper = Buffer.concat([prefix, bucketId, prefix]); + const remainingLimit = limit - nodeIds.length; + // Iterate over ids further away + tran.iterator( + { + gt: startKeyUpper, + limit: remainingLimit, + }, + this.nodeGraphBucketsDbPath, + ); + for await (const [key, nodeData] of tran.iterator( + { + gt: startKeyUpper, + limit: remainingLimit, + valueAsBuffer: false, + }, + this.nodeGraphBucketsDbPath, + )) { + const info = nodesUtils.parseBucketsDbKey(key as unknown as Buffer); + nodeIds.push([info.nodeId, nodeData]); + } + } + // If no nodes were found, return nothing + if (nodeIds.length === 0) return []; + // Need to get the whole of the last bucket + const lastBucketIndex = nodesUtils.bucketIndex( + this.keyManager.getNodeId(), + nodeIds[nodeIds.length - 1][0], + ); + const lastBucket = await this.getBucket( + lastBucketIndex, + undefined, + undefined, + tran, + ); + // Pop off elements of the same bucket to avoid duplicates + let element = nodeIds.pop(); + while ( + element != null && + nodesUtils.bucketIndex(this.keyManager.getNodeId(), element[0]) === + lastBucketIndex + ) { + element = nodeIds.pop(); + } + if (element != null) nodeIds.push(element); + // Adding last bucket to the list + nodeIds.push(...lastBucket); + + nodesUtils.bucketSortByDistance(nodeIds, nodeId, 'asc'); + return nodeIds.slice(0, limit); + } + /** * Sets a bucket meta property * This is protected because users cannot directly manipulate bucket meta diff --git a/tests/nodes/NodeConnectionManager.general.test.ts b/tests/nodes/NodeConnectionManager.general.test.ts index d21be106b..24986923b 100644 --- a/tests/nodes/NodeConnectionManager.general.test.ts +++ b/tests/nodes/NodeConnectionManager.general.test.ts @@ -74,7 +74,6 @@ describe(`${NodeConnectionManager.name} general test`, () => { let keyManager: KeyManager; let db: DB; let proxy: Proxy; - let nodeGraph: NodeGraph; let remoteNode1: PolykeyAgent; @@ -336,129 +335,6 @@ describe(`${NodeConnectionManager.name} general test`, () => { }, global.failedConnectionTimeout * 2, ); - test('finds a single closest node', async () => { - // NodeConnectionManager under test - const nodeConnectionManager = new NodeConnectionManager({ - keyManager, - nodeGraph, - proxy, - logger: nodeConnectionManagerLogger, - }); - await nodeConnectionManager.start(); - try { - // New node added - const newNode2Id = nodeId1; - const newNode2Address = { host: '227.1.1.1', port: 4567 } as NodeAddress; - await nodeGraph.setNode(newNode2Id, newNode2Address); - - // Find the closest nodes to some node, NODEID3 - const closest = await nodeConnectionManager.getClosestLocalNodes(nodeId3); - expect(closest).toContainEqual({ - id: newNode2Id, - distance: 121n, - address: { host: '227.1.1.1', port: 4567 }, - }); - } finally { - await nodeConnectionManager.stop(); - } - }); - test('finds 3 closest nodes', async () => { - const nodeConnectionManager = new NodeConnectionManager({ - keyManager, - nodeGraph, - proxy, - logger: nodeConnectionManagerLogger, - }); - await nodeConnectionManager.start(); - try { - // Add 3 nodes - await nodeGraph.setNode(nodeId1, { - host: '2.2.2.2', - port: 2222, - } as NodeAddress); - await nodeGraph.setNode(nodeId2, { - host: '3.3.3.3', - port: 3333, - } as NodeAddress); - await nodeGraph.setNode(nodeId3, { - host: '4.4.4.4', - port: 4444, - } as NodeAddress); - - // Find the closest nodes to some node, NODEID4 - const closest = await nodeConnectionManager.getClosestLocalNodes(nodeId3); - expect(closest.length).toBe(5); - expect(closest).toContainEqual({ - id: nodeId3, - distance: 0n, - address: { host: '4.4.4.4', port: 4444 }, - }); - expect(closest).toContainEqual({ - id: nodeId2, - distance: 116n, - address: { host: '3.3.3.3', port: 3333 }, - }); - expect(closest).toContainEqual({ - id: nodeId1, - distance: 121n, - address: { host: '2.2.2.2', port: 2222 }, - }); - } finally { - await nodeConnectionManager.stop(); - } - }); - test('finds the 20 closest nodes', async () => { - const nodeConnectionManager = new NodeConnectionManager({ - keyManager, - nodeGraph, - proxy, - logger: nodeConnectionManagerLogger, - }); - await nodeConnectionManager.start(); - try { - // Generate the node ID to find the closest nodes to (in bucket 100) - const nodeId = keyManager.getNodeId(); - const nodeIdToFind = testNodesUtils.generateNodeIdForBucket(nodeId, 100); - // Now generate and add 20 nodes that will be close to this node ID - const addedClosestNodes: NodeData[] = []; - for (let i = 1; i < 101; i += 5) { - const closeNodeId = testNodesUtils.generateNodeIdForBucket( - nodeIdToFind, - i, - ); - const nodeAddress = { - host: (i + '.' + i + '.' + i + '.' + i) as Host, - port: i as Port, - }; - await nodeGraph.setNode(closeNodeId, nodeAddress); - addedClosestNodes.push({ - id: closeNodeId, - address: nodeAddress, - distance: nodesUtils.calculateDistance(nodeIdToFind, closeNodeId), - }); - } - // Now create and add 10 more nodes that are far away from this node - for (let i = 1; i <= 10; i++) { - const farNodeId = nodeIdGenerator(i); - const nodeAddress = { - host: `${i}.${i}.${i}.${i}` as Host, - port: i as Port, - }; - await nodeGraph.setNode(farNodeId, nodeAddress); - } - - // Find the closest nodes to the original generated node ID - const closest = await nodeConnectionManager.getClosestLocalNodes( - nodeIdToFind, - ); - // We should always only receive k nodes - expect(closest.length).toBe(nodeGraph.maxNodesPerBucket); - // Retrieved closest nodes should be exactly the same as the ones we added - expect(closest).toEqual(addedClosestNodes); - } finally { - await nodeConnectionManager.stop(); - } - }); test('receives 20 closest local nodes from connected target', async () => { let serverPKAgent: PolykeyAgent | undefined; let nodeConnectionManager: NodeConnectionManager | undefined; diff --git a/tests/nodes/NodeGraph.test.ts b/tests/nodes/NodeGraph.test.ts index 6ea350cad..abf5534cd 100644 --- a/tests/nodes/NodeGraph.test.ts +++ b/tests/nodes/NodeGraph.test.ts @@ -172,7 +172,7 @@ describe(`${NodeGraph.name} test`, () => { (nodeId) => !nodeId.equals(keyManager.getNodeId()), ); let bucketIndexes: Array; - let nodes: Array<[NodeId, NodeData]>; + let nodes: NodeBucket; nodes = await utils.asyncIterableArray(nodeGraph.getNodes()); expect(nodes).toHaveLength(0); for (const nodeId of nodeIds) { @@ -715,4 +715,343 @@ describe(`${NodeGraph.name} test`, () => { expect(buckets2).not.toStrictEqual(buckets1); await nodeGraph.stop(); }); + test('get closest nodes, 40 nodes lower than target, take 20', async () => { + const nodeGraph = await NodeGraph.createNodeGraph({ + db, + keyManager, + logger, + }); + const baseNodeId = keyManager.getNodeId(); + const nodeIds: NodeBucket = []; + // Add 1 node to each bucket + for (let i = 0; i < 40; i++) { + const nodeId = testNodesUtils.generateNodeIdForBucket( + baseNodeId, + 50 + i, + i, + ); + nodeIds.push([nodeId, {} as NodeData]); + await nodeGraph.setNode(nodeId, { + host: '127.0.0.1', + port: utils.getRandomInt(0, 2 ** 16), + } as NodeAddress); + } + const targetNodeId = testNodesUtils.generateNodeIdForBucket( + baseNodeId, + 100, + 2, + ); + const result = await nodeGraph.getClosestNodes(targetNodeId, 20); + nodesUtils.bucketSortByDistance(nodeIds, targetNodeId); + const a = nodeIds.map((a) => nodesUtils.encodeNodeId(a[0])); + const b = result.map((a) => nodesUtils.encodeNodeId(a[0])); + // Are the closest nodes out of all of the nodes + expect(a.slice(0, b.length)).toEqual(b); + + // Check that the list is strictly ascending + const closestNodeDistances = result.map(([nodeId]) => + nodesUtils.nodeDistance(targetNodeId, nodeId), + ); + expect( + closestNodeDistances.slice(1).every((distance, i) => { + return closestNodeDistances[i] < distance; + }), + ).toBe(true); + await nodeGraph.stop(); + }); + test('get closest nodes, 15 nodes lower than target, take 20', async () => { + const nodeGraph = await NodeGraph.createNodeGraph({ + db, + keyManager, + logger, + }); + const baseNodeId = keyManager.getNodeId(); + const nodeIds: NodeBucket = []; + // Add 1 node to each bucket + for (let i = 0; i < 15; i++) { + const nodeId = testNodesUtils.generateNodeIdForBucket( + baseNodeId, + 50 + i, + i, + ); + nodeIds.push([nodeId, {} as NodeData]); + await nodeGraph.setNode(nodeId, { + host: '127.0.0.1', + port: utils.getRandomInt(0, 2 ** 16), + } as NodeAddress); + } + const targetNodeId = testNodesUtils.generateNodeIdForBucket( + baseNodeId, + 100, + 2, + ); + const result = await nodeGraph.getClosestNodes(targetNodeId); + nodesUtils.bucketSortByDistance(nodeIds, targetNodeId); + const a = nodeIds.map((a) => nodesUtils.encodeNodeId(a[0])); + const b = result.map((a) => nodesUtils.encodeNodeId(a[0])); + // Are the closest nodes out of all of the nodes + expect(a.slice(0, b.length)).toEqual(b); + + // Check that the list is strictly ascending + const closestNodeDistances = result.map(([nodeId]) => + nodesUtils.nodeDistance(targetNodeId, nodeId), + ); + expect( + closestNodeDistances.slice(1).every((distance, i) => { + return closestNodeDistances[i] < distance; + }), + ).toBe(true); + await nodeGraph.stop(); + }); + test('get closest nodes, 10 nodes lower than target, 30 nodes above, take 20', async () => { + const nodeGraph = await NodeGraph.createNodeGraph({ + db, + keyManager, + logger, + }); + const baseNodeId = keyManager.getNodeId(); + const nodeIds: NodeBucket = []; + // Add 1 node to each bucket + for (let i = 0; i < 40; i++) { + const nodeId = testNodesUtils.generateNodeIdForBucket( + baseNodeId, + 90 + i, + i, + ); + nodeIds.push([nodeId, {} as NodeData]); + await nodeGraph.setNode(nodeId, { + host: '127.0.0.1', + port: utils.getRandomInt(0, 2 ** 16), + } as NodeAddress); + } + const targetNodeId = testNodesUtils.generateNodeIdForBucket( + baseNodeId, + 100, + 2, + ); + const result = await nodeGraph.getClosestNodes(targetNodeId); + nodesUtils.bucketSortByDistance(nodeIds, targetNodeId); + const a = nodeIds.map((a) => nodesUtils.encodeNodeId(a[0])); + const b = result.map((a) => nodesUtils.encodeNodeId(a[0])); + // Are the closest nodes out of all of the nodes + expect(a.slice(0, b.length)).toEqual(b); + + // Check that the list is strictly ascending + const closestNodeDistances = result.map(([nodeId]) => + nodesUtils.nodeDistance(targetNodeId, nodeId), + ); + expect( + closestNodeDistances.slice(1).every((distance, i) => { + return closestNodeDistances[i] < distance; + }), + ).toBe(true); + await nodeGraph.stop(); + }); + test('get closest nodes, 10 nodes lower than target, 30 nodes above, take 5', async () => { + const nodeGraph = await NodeGraph.createNodeGraph({ + db, + keyManager, + logger, + }); + const baseNodeId = keyManager.getNodeId(); + const nodeIds: NodeBucket = []; + // Add 1 node to each bucket + for (let i = 0; i < 40; i++) { + const nodeId = testNodesUtils.generateNodeIdForBucket( + baseNodeId, + 90 + i, + i, + ); + nodeIds.push([nodeId, {} as NodeData]); + await nodeGraph.setNode(nodeId, { + host: '127.0.0.1', + port: utils.getRandomInt(0, 2 ** 16), + } as NodeAddress); + } + const targetNodeId = testNodesUtils.generateNodeIdForBucket( + baseNodeId, + 100, + 2, + ); + const result = await nodeGraph.getClosestNodes(targetNodeId, 5); + nodesUtils.bucketSortByDistance(nodeIds, targetNodeId); + const a = nodeIds.map((a) => nodesUtils.encodeNodeId(a[0])); + const b = result.map((a) => nodesUtils.encodeNodeId(a[0])); + // Are the closest nodes out of all of the nodes + expect(a.slice(0, b.length)).toEqual(b); + + // Check that the list is strictly ascending + const closestNodeDistances = result.map(([nodeId]) => + nodesUtils.nodeDistance(targetNodeId, nodeId), + ); + expect( + closestNodeDistances.slice(1).every((distance, i) => { + return closestNodeDistances[i] < distance; + }), + ).toBe(true); + await nodeGraph.stop(); + }); + test('get closest nodes, 5 nodes lower than target, 10 nodes above, take 20', async () => { + const nodeGraph = await NodeGraph.createNodeGraph({ + db, + keyManager, + logger, + }); + const baseNodeId = keyManager.getNodeId(); + const nodeIds: NodeBucket = []; + // Add 1 node to each bucket + for (let i = 0; i < 15; i++) { + const nodeId = testNodesUtils.generateNodeIdForBucket( + baseNodeId, + 95 + i, + i, + ); + nodeIds.push([nodeId, {} as NodeData]); + await nodeGraph.setNode(nodeId, { + host: '127.0.0.1', + port: utils.getRandomInt(0, 2 ** 16), + } as NodeAddress); + } + const targetNodeId = testNodesUtils.generateNodeIdForBucket( + baseNodeId, + 100, + 2, + ); + const result = await nodeGraph.getClosestNodes(targetNodeId); + nodesUtils.bucketSortByDistance(nodeIds, targetNodeId); + const a = nodeIds.map((a) => nodesUtils.encodeNodeId(a[0])); + const b = result.map((a) => nodesUtils.encodeNodeId(a[0])); + // Are the closest nodes out of all of the nodes + expect(a.slice(0, b.length)).toEqual(b); + + // Check that the list is strictly ascending + const closestNodeDistances = result.map(([nodeId]) => + nodesUtils.nodeDistance(targetNodeId, nodeId), + ); + expect( + closestNodeDistances.slice(1).every((distance, i) => { + return closestNodeDistances[i] < distance; + }), + ).toBe(true); + await nodeGraph.stop(); + }); + test('get closest nodes, 40 nodes above target, take 20', async () => { + const nodeGraph = await NodeGraph.createNodeGraph({ + db, + keyManager, + logger, + }); + const baseNodeId = keyManager.getNodeId(); + const nodeIds: NodeBucket = []; + // Add 1 node to each bucket + for (let i = 0; i < 40; i++) { + const nodeId = testNodesUtils.generateNodeIdForBucket( + baseNodeId, + 101 + i, + i, + ); + nodeIds.push([nodeId, {} as NodeData]); + await nodeGraph.setNode(nodeId, { + host: '127.0.0.1', + port: utils.getRandomInt(0, 2 ** 16), + } as NodeAddress); + } + const targetNodeId = testNodesUtils.generateNodeIdForBucket( + baseNodeId, + 100, + 2, + ); + const result = await nodeGraph.getClosestNodes(targetNodeId); + nodesUtils.bucketSortByDistance(nodeIds, targetNodeId); + const a = nodeIds.map((a) => nodesUtils.encodeNodeId(a[0])); + const b = result.map((a) => nodesUtils.encodeNodeId(a[0])); + // Are the closest nodes out of all of the nodes + expect(a.slice(0, b.length)).toEqual(b); + + // Check that the list is strictly ascending + const closestNodeDistances = result.map(([nodeId]) => + nodesUtils.nodeDistance(targetNodeId, nodeId), + ); + expect( + closestNodeDistances.slice(1).every((distance, i) => { + return closestNodeDistances[i] < distance; + }), + ).toBe(true); + await nodeGraph.stop(); + }); + test('get closest nodes, 15 nodes above target, take 20', async () => { + const nodeGraph = await NodeGraph.createNodeGraph({ + db, + keyManager, + logger, + }); + const baseNodeId = keyManager.getNodeId(); + const nodeIds: NodeBucket = []; + // Add 1 node to each bucket + for (let i = 0; i < 15; i++) { + const nodeId = testNodesUtils.generateNodeIdForBucket( + baseNodeId, + 101 + i, + i, + ); + nodeIds.push([nodeId, {} as NodeData]); + await nodeGraph.setNode(nodeId, { + host: '127.0.0.1', + port: utils.getRandomInt(0, 2 ** 16), + } as NodeAddress); + } + const targetNodeId = testNodesUtils.generateNodeIdForBucket( + baseNodeId, + 100, + 2, + ); + const result = await nodeGraph.getClosestNodes(targetNodeId); + nodesUtils.bucketSortByDistance(nodeIds, targetNodeId); + const a = nodeIds.map((a) => nodesUtils.encodeNodeId(a[0])); + const b = result.map((a) => nodesUtils.encodeNodeId(a[0])); + // Are the closest nodes out of all of the nodes + expect(a.slice(0, b.length)).toEqual(b); + + // Check that the list is strictly ascending + const closestNodeDistances = result.map(([nodeId]) => + nodesUtils.nodeDistance(targetNodeId, nodeId), + ); + expect( + closestNodeDistances.slice(1).every((distance, i) => { + return closestNodeDistances[i] < distance; + }), + ).toBe(true); + await nodeGraph.stop(); + }); + test('get closest nodes, no nodes, take 20', async () => { + const nodeGraph = await NodeGraph.createNodeGraph({ + db, + keyManager, + logger, + }); + const baseNodeId = keyManager.getNodeId(); + const nodeIds: NodeBucket = []; + const targetNodeId = testNodesUtils.generateNodeIdForBucket( + baseNodeId, + 100, + 2, + ); + const result = await nodeGraph.getClosestNodes(targetNodeId); + nodesUtils.bucketSortByDistance(nodeIds, targetNodeId); + const a = nodeIds.map((a) => nodesUtils.encodeNodeId(a[0])); + const b = result.map((a) => nodesUtils.encodeNodeId(a[0])); + // Are the closest nodes out of all of the nodes + expect(a.slice(0, b.length)).toEqual(b); + + // Check that the list is strictly ascending + const closestNodeDistances = result.map(([nodeId]) => + nodesUtils.nodeDistance(targetNodeId, nodeId), + ); + expect( + closestNodeDistances.slice(1).every((distance, i) => { + return closestNodeDistances[i] < distance; + }), + ).toBe(true); + await nodeGraph.stop(); + }); });