From 83060ee964c32db05c66207aa88a0543508fa901 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Sat, 13 May 2017 13:46:43 -0700 Subject: [PATCH] bech32: rewrite and improve perf. --- bench/bech32.js | 24 ++++ lib/utils/bech32.js | 271 ++++++++++++++++++++------------------------ 2 files changed, 148 insertions(+), 147 deletions(-) create mode 100644 bench/bech32.js diff --git a/bench/bech32.js b/bench/bech32.js new file mode 100644 index 00000000..dd7a1e08 --- /dev/null +++ b/bench/bech32.js @@ -0,0 +1,24 @@ +'use strict'; + +var Address = require('../lib/primitives/address'); +var crypto = require('../lib/crypto/crypto'); +var bench = require('./bench'); + +var i, end, addr; + +var addrs = []; + +end = bench('serialize'); +for (i = 0; i < 10000; i++) { + addr = Address.fromProgram(0, crypto.randomBytes(20)); + addrs.push(addr.toBech32()); +} +end(i); + +end = bench('parse'); +for (i = 0; i < 10000; i++) { + addr = addrs[i]; + addr = Address.fromBech32(addr); + addrs[i] = addr; +} +end(i); diff --git a/lib/utils/bech32.js b/lib/utils/bech32.js index 5f63f0b2..e1ee1ccd 100644 --- a/lib/utils/bech32.js +++ b/lib/utils/bech32.js @@ -34,117 +34,32 @@ */ var CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; -var TABLE = {}; -var GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; -var ZERO6 = new Buffer('000000000000', 'hex'); -var POOL6 = new Buffer(6); -var POOL10 = new Buffer(10); -var i; - -for (i = 0; i < CHARSET.length; i++) - TABLE[CHARSET[i]] = i; - -/** - * Allocate a buffer from the pool. - * @ignore - * @param {Number} size - * @returns {Buffer} - */ - -function alloc(size) { - if (size > 10) - return new Buffer(size); - return POOL10.slice(0, size); -} +var TABLE = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 +]; /** * Update checksum. * @ignore - * @param {Buffer} values * @param {Number} chk * @returns {Number} */ -function polymod(values, chk) { - var i, j, top; - - for (i = 0; i < values.length; i++) { - top = chk >> 25; - chk = (chk & 0x1ffffff) << 5 ^ values[i]; - - for (j = 0; j < 5; j++) { - if ((top >> j) & 1) - chk ^= GENERATOR[j]; - } - } - - return chk; -} - -/** - * Expand human readable part. - * @ignore - * @param {String} hrp - * @returns {Buffer} - */ - -function expand(hrp) { - var ret = alloc(hrp.length * 2 + 1); - var p = 0; - var i; - - for (i = 0; i < hrp.length; i++) - ret[p++] = hrp.charCodeAt(i) >> 5; - - ret[p++] = 0; - - for (i = 0; i < hrp.length; i++) - ret[p++] = hrp.charCodeAt(i) & 31; - - return ret; -} - -/** - * Verify checksum against hrp and data. - * @ignore - * @param {String} hrp - * @param {Buffer} data - * @returns {Boolean} - */ - -function verify(hrp, data) { - var chk = 1; - - chk = polymod(expand(hrp), chk); - chk = polymod(data, chk); - - return chk === 1; -} - -/** - * Create checksum from hrp and data. - * @ignore - * @param {String} hrp - * @param {Buffer} data - * @returns {Buffer} - */ - -function checksum(hrp, data) { - var chk = 1; - var ret = POOL6; - var p = 0; - var i, mod; - - chk = polymod(expand(hrp), chk); - chk = polymod(data, chk); - chk = polymod(ZERO6, chk); - - mod = chk ^ 1; - - for (i = 0; i < 6; i++) - ret[p++] = (mod >> 5 * (5 - i)) & 31; - - return ret; +function polymod(pre) { + var b = pre >>> 25; + return ((pre & 0x1ffffff) << 5) + ^ (-((b >> 0) & 1) & 0x3b6a57b2) + ^ (-((b >> 1) & 1) & 0x26508e6d) + ^ (-((b >> 2) & 1) & 0x1ea119fa) + ^ (-((b >> 3) & 1) & 0x3d4233dd) + ^ (-((b >> 4) & 1) & 0x2a1462b3); } /** @@ -156,15 +71,49 @@ function checksum(hrp, data) { */ function encode(hrp, data) { - var chk = checksum(hrp, data); - var str = hrp + '1'; - var i; + var str = ''; + var chk = 1; + var i, ch; - for (i = 0; i < data.length; i++) - str += CHARSET[data[i]]; + for (i = 0; i < hrp.length; i++) { + ch = hrp.charCodeAt(i); - for (i = 0; i < chk.length; i++) - str += CHARSET[chk[i]]; + if ((ch >> 5) === 0) + throw new Error('Invalid bech32 character.'); + + chk = polymod(chk) ^ (ch >> 5); + } + + if (i + 7 + data.length > 90) + throw new Error('Invalid bech32 data length.'); + + chk = polymod(chk); + + for (i = 0; i < hrp.length; i++) { + ch = hrp.charCodeAt(i); + chk = polymod(chk) ^ (ch & 0x1f); + str += hrp[i]; + } + + str += '1'; + + for (i = 0; i < data.length; i++) { + ch = data[i]; + + if ((ch >> 5) !== 0) + throw new Error('Invalid bech32 value.'); + + chk = polymod(chk) ^ ch; + str += CHARSET[ch]; + } + + for (i = 0; i < 6; i++) + chk = polymod(chk); + + chk ^= 1; + + for (i = 0; i < 6; i++) + str += CHARSET[(chk >>> ((5 - i) * 5)) & 0x1f]; return str; } @@ -176,50 +125,78 @@ function encode(hrp, data) { */ function decode(str) { + var chk = 1; var lower = false; var upper = false; - var p = 0; - var i, ch, pos, hrp, data; + var hrp = ''; + var dlen = 0; + var i, hlen, ch, v, data; - for (i = 0; i < str.length; i++) { + if (str.length < 8 || str.length > 90) + throw new Error('Invalid bech32 string length.'); + + while (dlen < str.length && str[(str.length - 1) - dlen] !== '1') + dlen++; + + hlen = str.length - (1 + dlen); + + if (hlen < 1 || dlen < 6) + throw new Error('Invalid bech32 data length.'); + + dlen -= 6; + data = new Buffer(dlen); + + for (i = 0; i < hlen; i++) { ch = str.charCodeAt(i); - if (ch < 33 || ch > 126) - throw new Error('Bech32 character out of range.'); + if (ch < 0x21 || ch > 0x7e) + throw new Error('Invalid bech32 character.'); - if (ch >= 97 && ch <= 122) + if (ch >= 0x61 && ch <= 0x7a) { lower = true; - - if (ch >= 65 && ch <= 90) + } else if (ch >= 0x41 && ch <= 0x5a) { upper = true; + ch = (ch - 0x41) + 0x61; + } + + hrp += String.fromCharCode(ch); + chk = polymod(chk) ^ (ch >> 5); + } + + chk = polymod(chk); + + for (i = 0; i < hlen; i++) + chk = polymod(chk) ^ (str.charCodeAt(i) & 0x1f); + + i++; + + while (i < str.length) { + ch = str.charCodeAt(i); + v = (ch & 0x80) ? -1 : TABLE[ch]; + + if (ch >= 0x61 && ch <= 0x7a) + lower = true; + else if (ch >= 0x41 && ch <= 0x5a) + upper = true; + + if (v === -1) + throw new Error('Invalid bech32 character.'); + + chk = polymod(chk) ^ v; + + if (i + 6 < str.length) + data[i - (1 + hlen)] = v; + + i++; } if (lower && upper) throw new Error('Invalid bech32 casing.'); - str = str.toLowerCase(); - - pos = str.lastIndexOf('1'); - - if (pos < 1 || pos + 7 > str.length || str.length > 90) - throw new Error('Invalid bech32 data section.'); - - hrp = str.substring(0, pos); - data = new Buffer(str.length - (pos + 1)); - - for (i = pos + 1; i < str.length; i++) { - ch = TABLE[str[i]]; - - if (ch == null) - throw new Error('Invalid bech32 character.'); - - data[p++] = ch; - } - - if (!verify(hrp, data)) + if (chk !== 1) throw new Error('Invalid bech32 checksum.'); - return new Bech32Result(hrp, data.slice(0, -6)); + return new Bech32Result(hrp, data.slice(0, dlen)); } /** @@ -238,37 +215,37 @@ function bitsify(data, size, frombits, tobits, pad, off) { var acc = 0; var bits = 0; var maxv = (1 << tobits) - 1; - var ret = new Buffer(size); - var p = 0; + var output = new Buffer(size); + var j = 0; var i, value; if (pad !== -1) - ret[p++] = pad; + output[j++] = pad; for (i = off; i < data.length; i++) { value = data[i]; if ((value >> frombits) !== 0) - throw new Error('Invalid value in bech32 bits.'); + throw new Error('Invalid bech32 bits.'); acc = (acc << frombits) | value; bits += frombits; while (bits >= tobits) { bits -= tobits; - ret[p++] = (acc >> bits) & maxv; + output[j++] = (acc >>> bits) & maxv; } } if (pad !== -1) { if (bits > 0) - ret[p++] = (acc << (tobits - bits)) & maxv; + output[j++] = (acc << (tobits - bits)) & maxv; } else { if (bits >= frombits || ((acc << (tobits - bits)) & maxv)) - throw new Error('Bad bech32 bits.'); + throw new Error('Invalid bech32 bits.'); } - return ret.slice(0, p); + return output.slice(0, j); } /**