Merge pull request #104 from BeaudanBrown/pow-refactor
Refactor PoW calculation to use webworkers Fixes #100pull/107/head
commit
76cdf61ef9
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,124 @@
|
||||
/* global Worker, window, setTimeout */
|
||||
|
||||
const WORKER_TIMEOUT = 60 * 1000; // one minute
|
||||
|
||||
class TimedOutError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
if (typeof Error.captureStackTrace === 'function') {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
} else {
|
||||
this.stack = (new Error(message)).stack;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WorkerInterface {
|
||||
constructor(path) {
|
||||
this._utilWorker = new Worker(path);
|
||||
this._jobs = Object.create(null);
|
||||
this._DEBUG = false;
|
||||
this._jobCounter = 0;
|
||||
|
||||
this._utilWorker.onmessage = e => {
|
||||
const [jobId, errorForDisplay, result] = e.data;
|
||||
|
||||
const job = this._getJob(jobId);
|
||||
if (!job) {
|
||||
throw new Error(
|
||||
`Received worker reply to job ${jobId}, but did not have it in our registry!`
|
||||
);
|
||||
}
|
||||
|
||||
const { resolve, reject, fnName } = job;
|
||||
|
||||
if (errorForDisplay) {
|
||||
return reject(
|
||||
new Error(
|
||||
`Error received from worker job ${jobId} (${fnName}): ${errorForDisplay}`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return resolve(result);
|
||||
};
|
||||
}
|
||||
|
||||
_makeJob (fnName) {
|
||||
this._jobCounter += 1;
|
||||
const id = this._jobCounter;
|
||||
|
||||
if (this._DEBUG) {
|
||||
window.log.info(`Worker job ${id} (${fnName}) started`);
|
||||
}
|
||||
this._jobs[id] = {
|
||||
fnName,
|
||||
start: Date.now(),
|
||||
};
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
_updateJob(id, data) {
|
||||
const { resolve, reject } = data;
|
||||
const { fnName, start } = this._jobs[id];
|
||||
|
||||
this._jobs[id] = {
|
||||
...this._jobs[id],
|
||||
...data,
|
||||
resolve: value => {
|
||||
this._removeJob(id);
|
||||
const end = Date.now();
|
||||
window.log.info(
|
||||
`Worker job ${id} (${fnName}) succeeded in ${end - start}ms`
|
||||
);
|
||||
return resolve(value);
|
||||
},
|
||||
reject: error => {
|
||||
this._removeJob(id);
|
||||
const end = Date.now();
|
||||
window.log.info(
|
||||
`Worker job ${id} (${fnName}) failed in ${end - start}ms`
|
||||
);
|
||||
return reject(error);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
_removeJob(id) {
|
||||
if (this._DEBUG) {
|
||||
this._jobs[id].complete = true;
|
||||
} else {
|
||||
delete this._jobs[id];
|
||||
}
|
||||
}
|
||||
|
||||
_getJob(id) {
|
||||
return this._jobs[id];
|
||||
};
|
||||
|
||||
callWorker(fnName, ...args) {
|
||||
const jobId = this._makeJob(fnName);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this._utilWorker.postMessage([jobId, fnName, ...args]);
|
||||
|
||||
this._updateJob(jobId, {
|
||||
resolve,
|
||||
reject,
|
||||
args: this._DEBUG ? args : null,
|
||||
});
|
||||
|
||||
setTimeout(
|
||||
() => reject(new TimedOutError(`Worker job ${jobId} (${fnName}) timed out`)),
|
||||
WORKER_TIMEOUT
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
WorkerInterface,
|
||||
TimedOutError,
|
||||
};
|
@ -1,134 +1,132 @@
|
||||
const hash = require('js-sha512');
|
||||
const bb = require('bytebuffer');
|
||||
const { BigInteger } = require('jsbn');
|
||||
|
||||
module.exports = {
|
||||
calcTarget,
|
||||
incrementNonce,
|
||||
bufferToBase64,
|
||||
bigIntToUint8Array,
|
||||
greaterThan,
|
||||
};
|
||||
|
||||
/* global dcodeIO, crypto, JSBI */
|
||||
const NONCE_LEN = 8;
|
||||
// Modify this value for difficulty scaling
|
||||
const DEV_NONCE_TRIALS = 10;
|
||||
const PROD_NONCE_TRIALS = 1000;
|
||||
let development = true;
|
||||
|
||||
// Increment Uint8Array nonce by 1 with carrying
|
||||
function incrementNonce(nonce) {
|
||||
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;
|
||||
const pow = {
|
||||
// Increment Uint8Array nonce by 1 with carrying
|
||||
incrementNonce(nonce) {
|
||||
let idx = NONCE_LEN - 1;
|
||||
const newNonce = new Uint8Array(nonce);
|
||||
newNonce[idx] += 1;
|
||||
}
|
||||
return newNonce;
|
||||
}
|
||||
// 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 newNonce;
|
||||
},
|
||||
|
||||
// Convert a Uint8Array to a base64 string
|
||||
function bufferToBase64(buf) {
|
||||
function mapFn(ch) {
|
||||
return String.fromCharCode(ch);
|
||||
}
|
||||
const binaryString = Array.prototype.map.call(buf, mapFn).join('');
|
||||
return bb.btoa(binaryString);
|
||||
}
|
||||
// Convert a Uint8Array to a base64 string
|
||||
bufferToBase64(buf) {
|
||||
function mapFn(ch) {
|
||||
return String.fromCharCode(ch);
|
||||
}
|
||||
const binaryString = Array.prototype.map.call(buf, mapFn).join('');
|
||||
return dcodeIO.ByteBuffer.btoa(binaryString);
|
||||
},
|
||||
|
||||
// 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
|
||||
// (bigInt / 256**n) % 256;
|
||||
const uint8Val = bigInt
|
||||
.divide(new BigInteger('256').pow(n))
|
||||
.mod(new BigInteger('256'));
|
||||
arr[idx] = uint8Val.intValue();
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
// Convert BigInteger to Uint8Array of length NONCE_LEN
|
||||
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
|
||||
// (bigInt / 256**n) % 256;
|
||||
const denominator = JSBI.exponentiate(
|
||||
JSBI.BigInt('256'),
|
||||
JSBI.BigInt(n)
|
||||
);
|
||||
const fraction = JSBI.divide(bigInt, denominator);
|
||||
const uint8Val = JSBI.remainder(fraction, JSBI.BigInt(256));
|
||||
arr[idx] = JSBI.toNumber(uint8Val);
|
||||
}
|
||||
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;
|
||||
// Compare two Uint8Arrays, return true if arr1 is > arr2
|
||||
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;
|
||||
}
|
||||
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, pubKey, data) {
|
||||
const payload = new Uint8Array(
|
||||
bb.wrap(timestamp.toString() + ttl.toString() + pubKey + data, 'binary').toArrayBuffer()
|
||||
);
|
||||
// Return nonce that hashes together with payload lower than the target
|
||||
async calcPoW(timestamp, ttl, pubKey, data, development = false) {
|
||||
const payload = new Uint8Array(
|
||||
dcodeIO.ByteBuffer.wrap(
|
||||
timestamp.toString() + ttl.toString() + pubKey + data,
|
||||
'binary'
|
||||
).toArrayBuffer()
|
||||
);
|
||||
|
||||
const target = calcTarget(ttl, payload.length);
|
||||
const nonceTrials = development ? DEV_NONCE_TRIALS : PROD_NONCE_TRIALS;
|
||||
const target = pow.calcTarget(ttl, payload.length, nonceTrials);
|
||||
|
||||
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()
|
||||
);
|
||||
const innerPayload = new Uint8Array(initialHash.length + NONCE_LEN);
|
||||
innerPayload.set(initialHash, NONCE_LEN);
|
||||
let resultHash;
|
||||
while (greaterThan(trialValue, target)) {
|
||||
nonce = incrementNonce(nonce);
|
||||
innerPayload.set(nonce);
|
||||
resultHash = hash(innerPayload);
|
||||
trialValue = new Uint8Array(
|
||||
bb.wrap(resultHash, 'hex').toArrayBuffer()
|
||||
).slice(0, NONCE_LEN);
|
||||
}
|
||||
return bufferToBase64(nonce);
|
||||
}
|
||||
let nonce = new Uint8Array(NONCE_LEN);
|
||||
let trialValue = pow.bigIntToUint8Array(
|
||||
JSBI.BigInt(Number.MAX_SAFE_INTEGER)
|
||||
);
|
||||
const initialHash = new Uint8Array(
|
||||
await crypto.subtle.digest('SHA-512', payload)
|
||||
);
|
||||
const innerPayload = new Uint8Array(initialHash.length + NONCE_LEN);
|
||||
innerPayload.set(initialHash, NONCE_LEN);
|
||||
let resultHash;
|
||||
while (pow.greaterThan(trialValue, target)) {
|
||||
nonce = pow.incrementNonce(nonce);
|
||||
innerPayload.set(nonce);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
resultHash = await crypto.subtle.digest('SHA-512', innerPayload);
|
||||
trialValue = new Uint8Array(
|
||||
dcodeIO.ByteBuffer.wrap(resultHash, 'hex').toArrayBuffer()
|
||||
).slice(0, NONCE_LEN);
|
||||
}
|
||||
return pow.bufferToBase64(nonce);
|
||||
},
|
||||
|
||||
function calcTarget(ttl, payloadLen) {
|
||||
// payloadLength + NONCE_LEN
|
||||
const totalLen = new BigInteger(payloadLen.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);
|
||||
// nonceTrials * lenPlusInnerFrac
|
||||
const nonceTrials = development ? DEV_NONCE_TRIALS : PROD_NONCE_TRIALS;
|
||||
const denominator = new BigInteger(nonceTrials.toString()).multiply(
|
||||
lenPlusInnerFrac
|
||||
);
|
||||
// 2^64 - 1
|
||||
const two64 = new BigInteger('2').pow(64).subtract(new BigInteger('1'));
|
||||
// two64 / denominator
|
||||
const targetNum = two64.divide(denominator);
|
||||
return bigIntToUint8Array(targetNum);
|
||||
}
|
||||
|
||||
// Start calculation in child process when main process sends message data
|
||||
process.on('message', msg => {
|
||||
({ development } = msg);
|
||||
process.send({
|
||||
nonce: calcPoW(
|
||||
msg.timestamp,
|
||||
msg.ttl,
|
||||
msg.pubKey,
|
||||
msg.data
|
||||
),
|
||||
});
|
||||
});
|
||||
calcTarget(ttl, payloadLen, nonceTrials = PROD_NONCE_TRIALS) {
|
||||
// payloadLength + NONCE_LEN
|
||||
const totalLen = JSBI.add(
|
||||
JSBI.BigInt(payloadLen),
|
||||
JSBI.BigInt(NONCE_LEN)
|
||||
);
|
||||
// ttl * totalLen
|
||||
const ttlMult = JSBI.multiply(
|
||||
JSBI.BigInt(ttl),
|
||||
JSBI.BigInt(totalLen)
|
||||
);
|
||||
// 2^16 - 1
|
||||
const two16 = JSBI.subtract(
|
||||
JSBI.exponentiate(JSBI.BigInt(2), JSBI.BigInt(16)), // 2^16
|
||||
JSBI.BigInt(1)
|
||||
);
|
||||
// ttlMult / two16
|
||||
const innerFrac = JSBI.divide(
|
||||
ttlMult,
|
||||
two16
|
||||
);
|
||||
// totalLen + innerFrac
|
||||
const lenPlusInnerFrac = JSBI.add(totalLen, innerFrac);
|
||||
// nonceTrials * lenPlusInnerFrac
|
||||
const denominator = JSBI.multiply(
|
||||
JSBI.BigInt(nonceTrials),
|
||||
lenPlusInnerFrac
|
||||
);
|
||||
// 2^64 - 1
|
||||
const two64 = JSBI.subtract(
|
||||
JSBI.exponentiate(JSBI.BigInt(2), JSBI.BigInt(64)), // 2^64
|
||||
JSBI.BigInt(1)
|
||||
);
|
||||
// two64 / denominator
|
||||
const targetNum = JSBI.divide(two64, denominator);
|
||||
return pow.bigIntToUint8Array(targetNum);
|
||||
},
|
||||
};
|
||||
|
Loading…
Reference in New Issue