You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			127 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			127 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			TypeScript
		
	
| import _, { intersectionWith, sampleSize } from 'lodash';
 | |
| import { SnodePool } from '.';
 | |
| import { Snode } from '../../../data/types';
 | |
| import { GetServiceNodesSubRequest } from './SnodeRequestTypes';
 | |
| import { doSnodeBatchRequest } from './batchRequest';
 | |
| import { GetNetworkTime } from './getNetworkTime';
 | |
| import { minSnodePoolCount, requiredSnodesForAgreement } from './snodePool';
 | |
| 
 | |
| function buildSnodeListRequests(): Array<GetServiceNodesSubRequest> {
 | |
|   const request: GetServiceNodesSubRequest = {
 | |
|     method: 'oxend_request',
 | |
|     params: {
 | |
|       endpoint: 'get_service_nodes',
 | |
|       params: {
 | |
|         active_only: true,
 | |
|         fields: {
 | |
|           public_ip: true,
 | |
|           storage_port: true,
 | |
|           pubkey_x25519: true,
 | |
|           pubkey_ed25519: true,
 | |
|           storage_server_version: true,
 | |
|         },
 | |
|       },
 | |
|     },
 | |
|   };
 | |
|   return [request];
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Returns a list of unique snodes got from the specified targetNode.
 | |
|  * This function won't try to rebuild a path if at some point we don't have enough snodes.
 | |
|  * This is exported for testing purpose only.
 | |
|  */
 | |
| async function getSnodePoolFromSnode(targetNode: Snode): Promise<Array<Snode>> {
 | |
|   const requests = buildSnodeListRequests();
 | |
|   const results = await doSnodeBatchRequest(requests, targetNode, 4000, null);
 | |
| 
 | |
|   const firstResult = results[0];
 | |
| 
 | |
|   if (!firstResult || firstResult.code !== 200) {
 | |
|     throw new Error('Invalid result');
 | |
|   }
 | |
| 
 | |
|   try {
 | |
|     const json = firstResult.body;
 | |
| 
 | |
|     if (!json || !json.result || !json.result.service_node_states?.length) {
 | |
|       window?.log?.error('getSnodePoolFromSnode - invalid result from snode', firstResult);
 | |
|       return [];
 | |
|     }
 | |
| 
 | |
|     // Filter 0.0.0.0 nodes which haven't submitted uptime proofs
 | |
|     const snodes: Array<Snode> = json.result.service_node_states
 | |
|       .filter((snode: any) => snode.public_ip !== '0.0.0.0')
 | |
|       .map((snode: any) => ({
 | |
|         ip: snode.public_ip,
 | |
|         port: snode.storage_port,
 | |
|         pubkey_x25519: snode.pubkey_x25519,
 | |
|         pubkey_ed25519: snode.pubkey_ed25519,
 | |
|         storage_server_version: snode.storage_server_version,
 | |
|       }));
 | |
|     GetNetworkTime.handleTimestampOffsetFromNetwork('get_service_nodes', json.t);
 | |
| 
 | |
|     // we the return list by the snode is already made of uniq snodes
 | |
|     return _.compact(snodes);
 | |
|   } catch (e) {
 | |
|     window?.log?.error('Invalid json response');
 | |
|     return [];
 | |
|   }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Try to fetch from 3 different snodes an updated list of snodes.
 | |
|  * If we get less than 24 common snodes in those result, we consider the request to failed and an exception is thrown.
 | |
|  * The three snode we make the request to is randomized.
 | |
|  * This function is to be called with a pRetry so that if one snode does not reply anything, another might be choose next time.
 | |
|  * Return the list of nodes all snodes agreed on.
 | |
|  */
 | |
| async function getSnodePoolFromSnodes() {
 | |
|   const existingSnodePool = await SnodePool.getSnodePoolFromDBOrFetchFromSeed();
 | |
|   if (existingSnodePool.length <= minSnodePoolCount) {
 | |
|     window?.log?.warn(
 | |
|       'getSnodePoolFromSnodes: Cannot get snodes list from snodes; not enough snodes',
 | |
|       existingSnodePool.length
 | |
|     );
 | |
|     throw new Error(
 | |
|       `Cannot get snodes list from snodes; not enough snodes even after refetching from seed', ${existingSnodePool.length}`
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   // Note intersectionWith only works with 3 at most array to find the common snodes.
 | |
|   const nodesToRequest = sampleSize(existingSnodePool, 3);
 | |
|   const results = await Promise.all(
 | |
|     nodesToRequest.map(async node => {
 | |
|       /**
 | |
|        * this call is already retried if the snode does not reply
 | |
|        * (at least when onion requests are enabled)
 | |
|        * this request might want to rebuild a path if the snode length gets < minSnodePoolCount during the
 | |
|        * retries, so we need to make sure this does not happen.
 | |
|        *
 | |
|        * Remember that here, we are trying to fetch from snodes the updated list of snodes to rebuild a path.
 | |
|        * If we don't disable rebuilding a path below, this gets to a chicken and egg problem.
 | |
|        */
 | |
|       return ServiceNodesList.getSnodePoolFromSnode(node);
 | |
|     })
 | |
|   );
 | |
| 
 | |
|   // we want those at least `requiredSnodesForAgreement` snodes common between all the result
 | |
|   const commonSnodes = intersectionWith(
 | |
|     results[0],
 | |
|     results[1],
 | |
|     results[2],
 | |
|     (s1: Snode, s2: Snode) => {
 | |
|       return s1.ip === s2.ip && s1.port === s2.port;
 | |
|     }
 | |
|   );
 | |
|   // We want the snodes to agree on at least this many snodes
 | |
|   if (commonSnodes.length < requiredSnodesForAgreement) {
 | |
|     throw new Error(
 | |
|       `Inconsistent snode pools. We did not get at least ${requiredSnodesForAgreement} in common`
 | |
|     );
 | |
|   }
 | |
|   return commonSnodes;
 | |
| }
 | |
| 
 | |
| export const ServiceNodesList = { getSnodePoolFromSnode, getSnodePoolFromSnodes };
 |