diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js index 7d120ff4e..98f078473 100644 --- a/js/modules/loki_snode_api.js +++ b/js/modules/loki_snode_api.js @@ -756,73 +756,127 @@ class LokiSnodeAPI { } } - async getLnsMapping(lnsName) { + async getLnsMapping(lnsName, timeout) { + // Returns { pubkey, error } + // pubkey is: + // null when there is confirmed to be no LNS mapping + // undefined when unconfirmee + // string when found + // timeout parameter optional (ms) + + // How many nodes to fetch data from? + const numRequests = 5; + + // How many nodes must have the same response value? + const numRequiredConfirms = 3; + + let ciphertextHex; + let pubkey; + let error; + const _ = window.Lodash; const input = Buffer.from(lnsName); - const output = await window.blake2b(input); - const nameHash = dcodeIO.ByteBuffer.wrap(output).toString('base64'); + // Return value of null represents a timeout + const timeoutResponse = { timedOut: true }; + const timeoutPromise = (cb, interval) => () => new Promise(resolve => setTimeout(() => cb(resolve), interval)); + const onTimeout = timeoutPromise(resolve => resolve(timeoutResponse), timeout || Number.MAX_SAFE_INTEGER); + // Get nodes capable of doing LNS - const lnsNodes = this.getNodesMinVersion('2.0.3'); - // randomPool should already be shuffled - // lnsNodes = _.shuffle(lnsNodes); + let lnsNodes = await this.getNodesMinVersion(window.CONSTANTS.LNS_CAPABLE_NODES_VERSION); + lnsNodes = _.shuffle(lnsNodes); - // Loop until 3 confirmations + // Enough nodes? + if (lnsNodes.length < numRequiredConfirms) { + error = window.i18n('lnsTooFewNodes'); + return {pubkey, error}; + } - // We don't trust any single node, so we accumulate - // answers here and select a dominating answer - const allResults = []; - let ciphertextHex = null; + const confirmedNodes = []; - while (!ciphertextHex) { - if (lnsNodes.length < 3) { - log.error('Not enough nodes for lns lookup'); - return false; - } - - // extract 3 and make requests in parallel - const nodes = lnsNodes.splice(0, 3); + let cipherResolve; + // eslint-disable-next-line no-unused-vars + const cipherPromise = () => new Promise((resolve, _reject) => { + cipherResolve = resolve; + }); - // eslint-disable-next-line no-await-in-loop - const results = await Promise.all( - nodes.map(node => this._requestLnsMapping(node, nameHash)) + const decryptHex = async cipherHex => { + const ciphertext = new Uint8Array( + StringView.hexToArrayBuffer(cipherHex) ); - results.forEach(res => { - if ( - res && - res.result && - res.result.status === 'OK' && - res.result.entries && - res.result.entries.length > 0 - ) { - allResults.push(results[0].result.entries[0].encrypted_value); - } - }); + const res = await window.decryptLnsEntry(lnsName, ciphertext); + const pubicKey = StringView.arrayBufferToHex(res); - const [winner, count] = _.maxBy( - _.entries(_.countBy(allResults)), - x => x[1] - ); + return pubicKey; + } + + const fetchFromNode = async node => { + const res = await this._requestLnsMapping(node, nameHash); + + // Do validation + if ( + res && + res.result && + res.result.status === 'OK' + ) { + const hasMapping = res.result.entries && res.result.entries.length > 0; + + const resValue = hasMapping + ? res.result.entries[0].encrypted_value + : null; + + confirmedNodes.push(resValue); + + if (confirmedNodes.length >= numRequiredConfirms) { + if (ciphertextHex){ + // result already found, dont worry + return; + } + + const [winner, count] = _.maxBy( + _.entries(_.countBy(confirmedNodes)), + x => x[1] + ); + + if (count >= numRequiredConfirms) { + ciphertextHex = winner === String(null) + ? null + : winner; - if (count >= 3) { - // eslint-disable-next-lint prefer-destructuring - ciphertextHex = winner; + // null represents no LNS mapping + if (ciphertextHex === null){ + error = window.i18n('lnsMappingNotFound'); + } + + cipherResolve({ciphertextHex}); + } + } } } - const ciphertext = new Uint8Array( - StringView.hexToArrayBuffer(ciphertextHex) - ); + const nodes = lnsNodes.splice(0, numRequests); + + // Start fetching from nodes + Promise.resolve(nodes.map(async node => fetchFromNode(node))); - const res = await window.decryptLnsEntry(lnsName, ciphertext); + // Timeouts (optional parameter) + // Wait for cipher to be found; race against timeout + const { timedOut } = await Promise.race([cipherPromise, onTimeout].map(f => f())); - const pubkey = StringView.arrayBufferToHex(res); + if (timedOut) { + error = window.i18n('lnsLookupTimeout'); + return { pubkey, error }; + } - return pubkey; + pubkey = ciphertextHex === null + ? null + : await decryptHex(ciphertextHex); + + return {pubkey, error}; } // get snodes for pubkey from random snode