From 8c799f4e919dafcb31acc05792602e07f9f5f857 Mon Sep 17 00:00:00 2001 From: Kagami Hiiragi Date: Fri, 9 Jan 2015 16:12:23 +0300 Subject: [PATCH] Do a POW (Browser) --- .gitignore | 1 + .jshintignore | 9 ++--- .npmignore | 11 +++--- karma.conf.js | 8 +++-- lib/platform.browser.js | 79 +++++++++++++++++++++++++++++++++++++++++ lib/pow.js | 20 ++++++++++- lib/worker.browser.js | 65 +++++++++++++++++++++++++++++++++ package.json | 16 +++++---- test.js | 12 ++++++- 9 files changed, 200 insertions(+), 21 deletions(-) create mode 100644 lib/worker.browser.js diff --git a/.gitignore b/.gitignore index 9546ce4..840ae7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /node_modules/ /npm-debug.log /docs/ +/worker.browserify.js diff --git a/.jshintignore b/.jshintignore index 165b077..07a6d50 100644 --- a/.jshintignore +++ b/.jshintignore @@ -1,4 +1,5 @@ -node_modules -test* -karma* -docs +/node_modules/ +/test* +/karma* +/docs/ +/worker.browserify.js diff --git a/.npmignore b/.npmignore index 0c11bf6..c633857 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,6 @@ -.travis.yml -.jshint* -karma* -docs -jsdoc.json +/.travis.yml +/.jshint* +/karma* +/docs/ +/jsdoc.json +/worker.browserify.js diff --git a/karma.conf.js b/karma.conf.js index b931c5a..c2e44d9 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,9 +1,11 @@ process.env.CHROME_BIN = "chromium-browser"; var allTests = !!process.env.ALL_TESTS; var files = ["test.js"]; -// Kludgy way to pass a variable to `test.js`. if (allTests) { + // Kludgy way to pass a variable to `test.js`. files.unshift("karma-all-tests.js"); + // Worker code. + files.push({pattern: "worker.browserify.js", included: false}); }; module.exports = function(config) { @@ -15,7 +17,7 @@ module.exports = function(config) { // frameworks to use // available frameworks: https://npmjs.org/browse/keyword/karma-adapter - frameworks: ["mocha", "browserify"], + frameworks: ["browserify", "mocha"], // list of files / patterns to load in the browser @@ -73,6 +75,6 @@ module.exports = function(config) { singleRun: true, - browserNoActivityTimeout: allTests ? 60000 : 10000, + browserNoActivityTimeout: allTests ? 120000 : 10000, }); }; diff --git a/lib/platform.browser.js b/lib/platform.browser.js index c96d2f9..620b682 100644 --- a/lib/platform.browser.js +++ b/lib/platform.browser.js @@ -41,3 +41,82 @@ exports.getTarget = function(opts) { assert(target <= 9007199254740991, "Unsafe target"); return target; }; + +var FAILBACK_POOL_SIZE = 8; + +// NOTE(Kagami): We don't use promise shim in Browser implementation +// because it's supported natively in new browsers (see +// ) and we can use only new browsers +// because of the WebCryptoAPI (see +// ). +exports.doPOW = function(opts) { + // Try to get native cores count then fallback to more or less + // reasonable value. See for + // details. + var poolSize = opts.poolSize || navigator.hardwareConcurrency; + poolSize = poolSize || FAILBACK_POOL_SIZE; + + // Check all input params prematurely to not let promise executor or + // worker to fail because of it. + assert(poolSize > 0, "Bad pool size"); + assert(opts.workerUrl, "Bad worker URL"); + assert(typeof opts.target === "number", "Bad target"); + assert(Buffer.isBuffer(opts.initialHash), "Bad initial hash"); + + var cancel; + var promise = new Promise(function(resolve, reject) { + function terminateAll() { + while (workers.length) { + workers.shift().terminate(); + } + } + + function onmessage(e) { + terminateAll(); + if (e.data >= 0) { + resolve(e.data); + } else { + // It's very unlikely that execution will ever reach this place. + // Currently the only reason why Worker may return value less + // than zero is a 32-bit nonce overflow (see worker + // implementation). It's more than 4G double hashes. + reject(); + } + } + + function onerror(e) { + // XXX(Kagami): `onerror` events fires in Chrome even after all + // workers were terminated. It doesn't cause wrong behaviour but + // beware that this function may be executed several times. + terminateAll(); + reject(e); + } + + var workers = []; + var worker; + for (var i = 0; i < poolSize; i++) { + worker = new Worker(opts.workerUrl); + workers.push(worker); + // NOTE(Kagami): There is no race condition here. `onmessage` can + // only be called _after_ this for-loop finishes. See + // for details. + worker.onmessage = onmessage; + worker.onerror = onerror; + worker.postMessage({ + num: i, + poolSize: poolSize, + target: opts.target, + initialHash: opts.initialHash, + }); + } + + cancel = function(e) { + terminateAll(); + reject(e); + }; + }); + // Allow to stop POW via custom function added to the Promise + // instance. + promise.cancel = cancel; + return promise; +}; diff --git a/lib/pow.js b/lib/pow.js index a05ceb0..eb6b347 100644 --- a/lib/pow.js +++ b/lib/pow.js @@ -3,9 +3,11 @@ * @see {@link https://bitmessage.org/wiki/Proof_of_work} * @module bitmessage/pow */ +// TODO(Kagami): Find a way how to document object params properly. "use strict"; +var objectAssign = Object.assign || require("object-assign"); var bmcrypto = require("./crypto"); var platform = require("./platform"); @@ -17,7 +19,6 @@ var DEFAULT_EXTRA_BYTES = 1000; * @param {Object} opts - Target options * @return {number} Target. */ -// TODO(Kagami): Find a way how to document object params properly. // Just a wrapper around platform-specific implementation. exports.getTarget = function(opts) { var payloadLength = opts.payloadLength || opts.payload.length; @@ -75,3 +76,20 @@ exports.check = function(opts) { return trialLo <= targetLo; } }; + +/** + * Do a POW. + * @param {Object} opts - Proof of work options + * @return {Promise.} A promise that contains computed nonce for + * the given target when fulfilled. + */ +exports.do = function(opts) { + var initialHash; + if (opts.payload) { + initialHash = bmcrypto.sha512(opts.payload); + } else { + initialHash = opts.initialHash; + } + opts = objectAssign({}, opts, {initialHash: initialHash}); + return platform.doPOW(opts); +}; diff --git a/lib/worker.browser.js b/lib/worker.browser.js new file mode 100644 index 0000000..29ff5c0 --- /dev/null +++ b/lib/worker.browser.js @@ -0,0 +1,65 @@ +/** + * Web Worker routines for Browser platform. + */ + +"use strict"; + +// NOTE(Kagami): In order to use it you need to create separate +// browserify bundle for this file, place it somewhere in your HTTP +// server assets path (under the same origin with your application code) +// and then pass appropriate `workerUrl` value to the function that +// spawns workers. +// You may also try to pass object URL instead. See +// , +// for details. + +// XXX(Kagami): This is rather unpleasent that we use different SHA-2 +// implementations for main library code and for worker code (we use +// `sha.js` here because it's faster). Though worker code lays in +// separate file so it shouldn't result in any download overhead. +var createHash = require("sha.js"); + +function sha512(buf) { + return createHash("sha512").update(buf).digest(); +} + +function pow(opts) { + var nonce = opts.num; + var poolSize = opts.poolSize; + var message = new Buffer(72); + message.fill(0); + Buffer(opts.initialHash).copy(message, 8); + var targetHi = Math.floor(opts.target / 4294967296); + var targetLo = opts.target % 4294967296; + var digest, trialHi, trialLo; + + while (true) { + // uint32_t overflow. There is no much need to fix it since 4G + // double hashes would we computed too long anyway in a Browser. + if (nonce > 4294967295) { + return -1; + } + + message.writeUInt32BE(nonce, 4, true); + digest = sha512(sha512(message)); + trialHi = digest.readUInt32BE(0, true); + + if (trialHi > targetHi) { + nonce += poolSize; + } else if (trialHi === targetHi) { + trialLo = digest.readUInt32BE(4, true); + if (trialLo > targetLo) { + nonce += poolSize; + } else { + return nonce; + } + } else { + return nonce; + } + } +} + +onmessage = function(e) { // jshint ignore:line + var nonce = pow(e.data); + postMessage(nonce); // jshint ignore:line +}; diff --git a/package.json b/package.json index e007065..1d77bed 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,12 @@ "./lib/platform.js": "./lib/platform.browser.js" }, "scripts": { - "test": "ALL_TESTS=1 mocha && ALL_TESTS=1 xvfb-run -a karma start && jshint .", + "test": "ALL_TESTS=1 mocha && ALL_TESTS=1 npm run -s kc && ALL_TESTS=1 npm run -s kf && jshint .", "m": "mocha", - "k": "xvfb-run -a karma start", - "kc": "xvfb-run -a karma start --browsers Chromium", - "kf": "xvfb-run -a karma start --browsers Firefox", + "k": "npm run -s w && xvfb-run -a karma start", + "kc": "npm run -s w && xvfb-run -a karma start --browsers Chromium", + "kf": "npm run -s w && xvfb-run -a karma start --browsers Firefox", + "w": "browserify lib/worker.browser.js > worker.browserify.js", "j": "jshint .", "d": "jsdoc -c jsdoc.json", "mv-docs": "rm -rf docs && jsdoc -c jsdoc.json && D=`mktemp -d` && mv docs \"$D\" && git checkout gh-pages && rm -rf docs && mv \"$D/docs\" . && rm -rf \"$D\"" @@ -33,14 +34,15 @@ }, "homepage": "https://github.com/bitchan/bitmessage", "devDependencies": { + "browserify": "^8.1.0", "chai": "*", "jsdoc": "^3.3.0-alpha13", "jshint": "*", - "karma": "^0.12.28", - "karma-browserify": "^1.0.1", + "karma": "^0.12.31", + "karma-browserify": "^2.0.0", "karma-chrome-launcher": "^0.1.7", "karma-cli": "~0.0.4", - "karma-firefox-launcher": "^0.1.3", + "karma-firefox-launcher": "^0.1.4", "karma-mocha": "^0.1.10", "karma-mocha-reporter": "^0.3.1", "mocha": "*" diff --git a/test.js b/test.js index 7c65395..971e568 100644 --- a/test.js +++ b/test.js @@ -278,6 +278,16 @@ describe("POW", function() { expect(POW.check({nonce: 3122437, target: 4864647698763, initialHash: Buffer("8ff2d685db89a0af2e3dbfd3f700ae96ef4d9a1eac72fd778bbb368c7510cddda349e03207e1c4965bd95c6f7265e8f1a481a08afab3874eaafb9ade09a10880", "hex")})).to.be.true; expect(POW.check({nonce: 3122436, target: 4864647698763, initialHash: Buffer("8ff2d685db89a0af2e3dbfd3f700ae96ef4d9a1eac72fd778bbb368c7510cddda349e03207e1c4965bd95c6f7265e8f1a481a08afab3874eaafb9ade09a10880", "hex")})).to.be.false; }); + + if (allTests && typeof window !== "undefined") { + it("should do a POW", function() { + this.timeout(120000); + return POW.do({workerUrl: "/base/worker.browserify.js", target: 10693764680411, initialHash: Buffer("8ff2d685db89a0af2e3dbfd3f700ae96ef4d9a1eac72fd778bbb368c7510cddda349e03207e1c4965bd95c6f7265e8f1a481a08afab3874eaafb9ade09a10880", "hex")}) + .then(function(nonce) { + expect(nonce).to.equal(2373146); + }); + }); + } }); describe("High-level classes", function() { @@ -317,7 +327,7 @@ describe("High-level classes", function() { // very slow. This need to be fixed. if (allTests && typeof window === "undefined") { it("should allow to generate shorter address", function() { - this.timeout(60000); + this.timeout(120000); var addr = Address.fromRandom({ripelen: 18}); var ripe = addr.getRipe({short: true}); expect(ripe.length).to.be.at.most(18);