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/data';
|
|
import { doSnodeBatchRequest } from './batchRequest';
|
|
import { GetNetworkTime } from './getNetworkTime';
|
|
import { minSnodePoolCount, requiredSnodesForAgreement } from './snodePool';
|
|
import { GetServiceNodesSubRequest } from './SnodeRequestTypes';
|
|
|
|
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 };
|