diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index e608f8b4d..283f86bae 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -17,10 +17,10 @@ function initialize({ url }) { function connect() { return { - sendMessage + sendMessage, }; - function getPoWNonce(timestamp, ttl, pub_key, data) { + function getPoWNonce(timestamp, ttl, pubKey, data) { return new Promise((resolve, reject) => { // Create forked node process to calculate PoW without blocking main process const child = fork('./libloki/proof-of-work.js'); @@ -29,8 +29,8 @@ function initialize({ url }) { child.send({ timestamp, ttl, - pub_key, - data + pubKey, + data: Array.from(data), }); // Handle child process error (should never happen) @@ -49,14 +49,14 @@ function initialize({ url }) { }); }); - }; + } - async function sendMessage(pub_key, data, ttl) { + async function sendMessage(pubKey, data, ttl) { const timestamp = Math.floor(Date.now() / 1000); // Nonce is returned as a base64 string to include in header let nonce; try { - nonce = await getPoWNonce(timestamp, ttl, pub_key, data); + nonce = await getPoWNonce(timestamp, ttl, pubKey, data); } catch(err) { // Something went horribly wrong // TODO: Handle gracefully @@ -67,7 +67,7 @@ function initialize({ url }) { url: `${url}/send_message`, type: 'POST', responseType: undefined, - timeout: undefined + timeout: undefined, }; log.info(options.type, options.url); @@ -79,7 +79,7 @@ function initialize({ url }) { 'X-Loki-pow-nonce': nonce, 'X-Loki-timestamp': timestamp.toString(), 'X-Loki-ttl': ttl.toString(), - 'X-Loki-recipient': pub_key, + 'X-Loki-recipient': pubKey, 'Content-Length': data.byteLength, }, timeout: options.timeout, @@ -133,4 +133,4 @@ function HTTPError(message, providedCode, response, stack) { e.response = response; } return e; -} \ No newline at end of file +} diff --git a/libloki/proof-of-work.js b/libloki/proof-of-work.js index e423efce1..fd1057d6a 100644 --- a/libloki/proof-of-work.js +++ b/libloki/proof-of-work.js @@ -1,67 +1,104 @@ const hash = require('js-sha512'); const bb = require('bytebuffer'); +const BigInteger = require('jsbn').BigInteger; + +const NONCE_LEN = 8; +// Modify this value for difficulty scaling +const NONCE_TRIALS = 1000; // Increment Uint8Array nonce by 1 with carrying function incrementNonce(nonce) { - let idx = nonce.length - 1; - nonce[idx] += 1; - // Nonce will just reset to 0 if all values are 255 causing infinite loop, should never happen - while (nonce[idx] == 0 && idx >= 0) { - nonce[--idx] += 1; + let idx = NONCE_LEN - 1; + const newNonce = new Uint8Array(nonce); + newNonce[idx] += 1; + // Nonce will just reset to 0 if all values are 255 causing infinite loop + while (newNonce[idx] === 0 && idx > 0) { + idx -= 1; + newNonce[idx] += 1; } - return nonce; + return newNonce; } // Convert a Uint8Array to a base64 string function bufferToBase64(buf) { - let binstr = Array.prototype.map.call(buf, function (ch) { + function mapFn(ch) { return String.fromCharCode(ch); - }).join(''); - return bb.btoa(binstr); + }; + const binaryString = Array.prototype.map.call(buf, mapFn).join(''); + return bb.btoa(binaryString); } -// Convert javascript number to Uint8Array of length 8 -function numberToUintArr(numberVal) { - let arr = new Uint8Array(8); - for (let idx = 7; idx >= 0; idx--) { - let n = 8 - (idx + 1); +// Convert BigInteger to Uint8Array of length NONCE_LEN +function bigIntToUint8Array(bigInt) { + const arr = new Uint8Array(NONCE_LEN); + let n; + for (let idx = NONCE_LEN - 1; idx >= 0; idx -= 1) { + n = NONCE_LEN - (idx + 1); // 256 ** n is the value of one bit in arr[idx], modulus to carry over - arr[idx] = (numberVal / 256**n) % 256; + // (bigInt / 256**n) % 256; + const uint8Val = (bigInt.divide((new BigInteger('256')).pow(n))).mod(new BigInteger('256')); + arr[idx] = uint8Val.intValue(); } return arr; } +// Compare two Uint8Arrays, return true if arr1 is > arr2 +function greaterThan(arr1, arr2) { + // Early exit if lengths are not equal. Should never happen + if (arr1.length !== arr2.length) + return false; + + for (let i = 0, len = arr1.length; i < len; i += 1) { + if (arr1[i] > arr2[i]) + return true; + if (arr1[i] < arr2[i]) + return false; + } + return false; +} + // Return nonce that hashes together with payload lower than the target -function calcPoW(timestamp, ttl, pub_key, data) { - const leadingString = timestamp.toString() + ttl.toString() + pub_key; +function calcPoW(timestamp, ttl, pubKey, data) { + const leadingString = timestamp.toString() + ttl.toString() + pubKey; const leadingArray = new Uint8Array(bb.wrap(leadingString, 'binary').toArrayBuffer()); - // Payload constructed from concatenating timestamp, ttl and pubkey strings, converting to Uint8Array - // and then appending to the message data array - const payload = leadingArray + data; - const nonceLen = 8; - // Modify this value for difficulty scaling - // TODO: Have more informed reason for setting this to 100 - const nonceTrialsPerByte = 1000; - let nonce = new Uint8Array(nonceLen); - let trialValue = numberToUintArr(Number.MAX_SAFE_INTEGER); - // Target is converter to Uint8Array for simple comparison with trialValue - const target = numberToUintArr(Math.floor(Math.pow(2, 64) / ( - nonceTrialsPerByte * ( - payload.length + nonceLen + ( - (ttl * ( payload.length + nonceLen )) - / Math.pow(2, 16) - ) - ) - ))); + // Payload constructed from concatenating timestamp, ttl and pubkey strings, + // converting to Uint8Array and then appending to the message data array + const payload = new Uint8Array(leadingArray.length + data.length); + payload.set(leadingArray); + payload.set(data, leadingArray.length); + + // payloadLength + NONCE_LEN + const totalLen = (new BigInteger(payload.length.toString())).add(new BigInteger(NONCE_LEN.toString())); + // ttl * totalLen + const ttlMult = (new BigInteger(ttl.toString())).multiply(totalLen); + // ttlMult / (2^16 - 1) + const innerFrac = ttlMult.divide((new BigInteger('2').pow(16)).subtract(new BigInteger('1'))); + // totalLen + innerFrac + const lenPlusInnerFrac = totalLen.add(innerFrac); + // NONCE_TRIALS * lenPlusInnerFrac + const denominator = (new BigInteger(NONCE_TRIALS.toString())).multiply(lenPlusInnerFrac); + // 2^64 - 1 + const two64 = (new BigInteger('2').pow(64)).subtract(new BigInteger('1')); + // two64 / denominator + const targetNum = two64.divide(denominator); + const target = bigIntToUint8Array(targetNum); + + let nonce = new Uint8Array(NONCE_LEN); + let trialValue = bigIntToUint8Array(new BigInteger(Number.MAX_SAFE_INTEGER.toString())); const initialHash = new Uint8Array(bb.wrap(hash(payload), 'hex').toArrayBuffer()); - while (trialValue > target) { + const innerPayload = new Uint8Array(initialHash.length + NONCE_LEN); + innerPayload.set(initialHash, NONCE_LEN); + let resultHash; + while (greaterThan(trialValue, target)) { nonce = incrementNonce(nonce); - trialValue = (new Uint8Array(bb.wrap(hash(nonce + initialHash), 'hex').toArrayBuffer())).slice(0, 8); + innerPayload.set(nonce); + resultHash = hash(innerPayload); + trialValue = (new Uint8Array(bb.wrap(resultHash, 'hex').toArrayBuffer())).slice(0, NONCE_LEN); } return bufferToBase64(nonce); } // Start calculation in child process when main process sends message data process.on('message', (msg) => { - process.send({nonce: calcPoW(msg.timestamp, msg.ttl, msg.pub_key, msg.data)}); -}); \ No newline at end of file + process.send({nonce: calcPoW(msg.timestamp, msg.ttl, msg.pubKey, new Uint8Array(msg.data))}); +});