diff --git a/emscripten/Makefile b/emscripten/Makefile index 52ac0a29c8..66e1bf4923 100644 --- a/emscripten/Makefile +++ b/emscripten/Makefile @@ -1,15 +1,19 @@ EMFLAGS = \ - -O2 --closure 1 \ + -Os --closure 1 \ --memory-init-file 0 \ --pre-js pre.js --post-js post.js \ - -s EXPORTED_FUNCTIONS='["_hdnode_public_ckd", "_ecdsa_get_address"]' \ + -s EXPORTED_FUNCTIONS='["_hdnode_public_ckd", "_ecdsa_get_address"]' SRC = ../bignum.c ../ecdsa.c ../secp256k1.c ../hmac.c ../bip32.c \ ../base58.c ../ripemd160.c ../sha2.c ../rand.c -test: node_modules trezor-crypto.js +test-node: node_modules trezor-crypto.js test.js node test.js +test-browserify.js: node_modules trezor-crypto.js test.js + browserify test.js -o $@ --noparse=`pwd`/trezor-crypto.js + @echo "open test.html in your favourite browser" + trezor-crypto.js: $(SRC) emcc $(EMFLAGS) -o $@ $^ @@ -17,4 +21,4 @@ node_modules: npm install clean: - rm -f trezor-crypto.js + rm -f trezor-crypto.js test-browserify.js diff --git a/emscripten/post.js b/emscripten/post.js index d5b37bd2f4..638121abc5 100644 --- a/emscripten/post.js +++ b/emscripten/post.js @@ -1 +1,113 @@ -module.exports = Module; +/* + typedef struct { + uint32_t depth; + uint32_t fingerprint; + uint32_t child_num; + uint8_t chain_code[32]; + uint8_t private_key[32]; + uint8_t public_key[33]; + } HDNode; + */ + +var HEAPU8 = Module['HEAPU8']; +var _malloc = Module['_malloc']; +var _hdnode_public_ckd = Module['_hdnode_public_ckd']; +var _ecdsa_get_address = Module['_ecdsa_get_address']; +var Pointer_stringify = Module['Pointer_stringify']; + +// HDNode struct global +var HDNODE_SIZE = 4 + 4 + 4 + 32 + 32 + 33; +var _hdnode = _malloc(HDNODE_SIZE); + +// address string global +var ADDRESS_SIZE = 40; // maximum size +var _address = _malloc(ADDRESS_SIZE); + +/* + * public library interface + */ + +/** + * @param {HDNode} node HDNode struct, see the definition above + * @return {Uint8Array} + */ +function serializeNode(node) { + var b = new ArrayBuffer(HDNODE_SIZE); + + var u32 = new Uint32Array(b, 0, 12); + u32[0] = node['depth']; + u32[1] = node['fingerprint']; + u32[2] = node['child_num']; + + var u8 = new Uint8Array(b, 0, HDNODE_SIZE); + u8.set(node['chain_code'], 12); + u8.set(node['public_key'], 12 + 32 + 32); + + return u8; +} + +/** + * @param {Uint8Array} sn serialized node, see `serializeNode` + * @param {Number} index BIP32 index of the address + * @param {Number} version address version byte + * @return {String} + */ +function deriveAddress(sn, index, version) { + HEAPU8.set(sn, _hdnode); + _hdnode_public_ckd(_hdnode, index); + _ecdsa_get_address(_hdnode + 12 + 32 + 32, version, _address, ADDRESS_SIZE); + return Pointer_stringify(_address); +} + +/** + * @param {HDNode} node HDNode struct, see the definition above + * @param {Number} from index of the first address + * @param {Number} to index of the last address + * @param {Number} version address version byte + * @return {Array} + */ +function deriveAddressRange(node, from, to, version) { + var addresses = []; + var sn = serializeNode(node); + var i; + for (i = from; i <= to; i++) { + addresses.push(deriveAddress(sn, i, version)); + } + return addresses; +} + +if (typeof module !== 'undefined') { + module['exports'] = { + 'serializeNode': serializeNode, + 'deriveAddress': deriveAddress, + 'deriveAddressRange': deriveAddressRange + }; +} + +/* + * Web worker processing + */ + +function processMessage(event) { + var data = event['data']; + var type = data['type']; + + switch (type) { + case 'deriveAddressRange': + var response = deriveAddressRange( + data['node'], + data['from'], + data['to'], + data['version'] + ); + postMessage(response); + break; + + default: + throw new Error('Unknown message type: ' + type); + } +} + +if (ENVIRONMENT_IS_WORKER) { + this['onmessage'] = processMessage; +} diff --git a/emscripten/pre.js b/emscripten/pre.js index e69de29bb2..f1b364afcb 100644 --- a/emscripten/pre.js +++ b/emscripten/pre.js @@ -0,0 +1,9 @@ +// stub importScripts for the faulty detection of web worker env +if (typeof importScripts === 'undefined' + && typeof WorkerGlobalScope !== 'undefined' + && this instanceof WorkerGlobalScope + ) { + this.importScripts = function () { + throw new Error('importScripts is a stub'); + }; + } diff --git a/emscripten/test.html b/emscripten/test.html new file mode 100644 index 0000000000..9c764a230f --- /dev/null +++ b/emscripten/test.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/emscripten/test.js b/emscripten/test.js index 0bc809c9a3..31d58f2c3b 100644 --- a/emscripten/test.js +++ b/emscripten/test.js @@ -1,67 +1,98 @@ var crypto = require('./trezor-crypto'); - -/* typedef struct { - uint32_t depth; - uint32_t fingerprint; - uint32_t child_num; - uint8_t chain_code[32]; - uint8_t private_key[32]; - uint8_t public_key[33]; - } HDNode; -*/ - -var HDNODE_SIZE = 4 + 4 + 4 + 32 + 32 + 33; -var hdnode = crypto._malloc(HDNODE_SIZE); - -var ADDRESS_SIZE = 40; // maximum size -var address = crypto._malloc(ADDRESS_SIZE); - -function prepareNode(n) { - var b = new ArrayBuffer(HDNODE_SIZE); - var u8 = new Uint8Array(b, 0, HDNODE_SIZE); - var u32 = new Uint32Array(b, 0, 12); - - u32[0] = n.depth; - u32[1] = n.parentFingerprint; - u32[2] = n.index; - u8.set(n.chainCode, 12); - u8.set(n.pubKey.toBuffer(), 12 + 32 + 32); - - return u8; -} - -function deriveAddress(pn, i, version) { - crypto.HEAPU8.set(pn, hdnode); - crypto._hdnode_public_ckd(hdnode, i); - crypto._ecdsa_get_address(hdnode + 12 + 32 + 32, version, address, ADDRESS_SIZE); - return crypto.Pointer_stringify(address); -} - -// benching code - var bitcoin = require('bitcoinjs-lib'); -var node = bitcoin.HDNode.fromBase58( +var XPUB = 'xpub6AHA9hZDN11k2ijHMeS5QqHx2KP9aMBRhTDqANMnwVtdyw2TDYRm' + - 'F8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU' -).derive(0); + 'F8PjpvwUFcL1Et8Hj59S3gTSMcUQ5gAqTz3Wd8EsMTmF3DChhqPQBnU'; +var node = bitcoin.HDNode.fromBase58(XPUB).derive(0); -timeBitcoinjs(node); -timeTrezorCrypto(node); +var nodeStruct = { + depth: node.depth, + child_num: node.index, + fingerprint: node.parentFingerprint, + chain_code: node.chainCode, + public_key: node.pubKey.toBuffer() +}; +var nodeSerialized = crypto.serializeNode(nodeStruct); -function timeBitcoinjs(n) { - console.time('bitcoinjs') - for (var i = 0; i < 1000; i++) { - n.derive(i).getAddress() - } - console.timeEnd('bitcoinjs') +var suite; +var worker; + +if (typeof Worker !== 'undefined') { + console.log('enabling web worker benchmark'); + worker = new Worker('./trezor-crypto.js'); + worker.onerror = function (error) { + console.error('worker:', error); + }; + suite = [ + // benchBitcoinJS, + // benchBrowserify, + benchWorker + ]; +} else { + suite = [ + benchBitcoinJS, + benchBrowserify + ]; } -function timeTrezorCrypto(n) { - var nP = prepareNode(n); - console.time('trezor-crypto') - for (var i = 0; i < 1000; i++) { - deriveAddress(nP, i, 0); - } - console.timeEnd('trezor-crypto') +benchmark(suite, 1000, 1000); + +function benchmark(suite, delay, ops) { + (function cycle(i) { + setTimeout(function () { + var benchmark = suite[i]; + runBenchmark(benchmark, ops, function (runtime) { + printResult(benchmark, ops, runtime); + cycle(i+1 < suite.length ? i+1 : 0); + }); + }, delay); + }(0)); +} + +function benchBitcoinJS(ops, fn) { + var i; + for (i = 0; i < ops; i++) { + node.derive(i).getAddress(); + } + fn(); +} + +function benchBrowserify(ops, fn) { + var i; + for (i = 0; i < ops; i++) { + crypto.deriveAddress(nodeSerialized, i, 0); + } + fn(); +} + +function benchWorker(ops, fn) { + worker.onmessage = function (event) { + fn(); + }; + worker.postMessage({ + type: 'deriveAddressRange', + node: nodeStruct, + from: 0, + to: ops - 1, + version: 0 + }); +} + +function runBenchmark(benchmark, ops, fn) { + var start = new Date(); + benchmark(ops, function () { + var end = new Date(); + fn(end - start); + }); +} + +function printResult(benchmark, ops, runtime) { + var opssec = (ops / runtime) * 1000; + console.log( + benchmark.name, + 'ops #', ops, + 'runtime', runtime / 1000, + 'sec, ops/sec', opssec + ); }