From bfbb7633ec54018a6cbe0fce43dc25b747390db7 Mon Sep 17 00:00:00 2001 From: Arthur Britto Date: Fri, 1 Feb 2013 13:28:27 -0800 Subject: [PATCH 1/6] JS: Make transaction subscription robust. --- src/js/remote.js | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/js/remote.js b/src/js/remote.js index 1b1f67591..5832cced3 100644 --- a/src/js/remote.js +++ b/src/js/remote.js @@ -205,7 +205,7 @@ var Remote = function (opts, trace) { this._testnet = undefined; this._transaction_subs = 0; this.online_target = false; - this.online_state = 'closed'; // 'open', 'closed', 'connecting', 'closing' + this._online_state = 'closed'; // 'open', 'closed', 'connecting', 'closing' this.state = 'offline'; // 'online', 'offline' this.retry_timer = undefined; this.retry = undefined; @@ -245,7 +245,7 @@ var Remote = function (opts, trace) { this.on('newListener', function (type, listener) { if ('transaction' === type) { - if (!self._transaction_subs) + if (!self._transaction_subs && 'open' === self._online_state) { self.request_subscribe([ 'transactions' ]) .request(); @@ -260,7 +260,7 @@ var Remote = function (opts, trace) { { self._transaction_subs -= 1; - if (!self._transaction_subs) + if (!self._transaction_subs && 'open' === self._online_state) { self.request_unsubscribe([ 'transactions' ]) .request(); @@ -327,12 +327,12 @@ Remote.prototype._set_state = function (state) { switch (state) { case 'online': - this.online_state = 'open'; + this._online_state = 'open'; this.emit('connected'); break; case 'offline': - this.online_state = 'closed'; + this._online_state = 'closed'; this.emit('disconnected'); break; } @@ -353,7 +353,7 @@ Remote.prototype.connect = function (online) { this.online_target = target; // If we were in a stable state, go dynamic. - switch (this.online_state) { + switch (this._online_state) { case 'open': if (!target) this._connect_stop(); break; @@ -390,9 +390,9 @@ Remote.prototype._connect_retry = function () { // Do not continue trying to connect. this._set_state('offline'); } - else if ('connecting' !== this.online_state) { + else if ('connecting' !== this._online_state) { // New to connecting state. - this.online_state = 'connecting'; + this._online_state = 'connecting'; this.retry = 0; this._set_state('offline'); // Report newly offline. @@ -915,7 +915,12 @@ Remote.prototype.submit = function (transaction) { Remote.prototype._server_subscribe = function () { var self = this; - this.request_subscribe([ 'ledger', 'server' ]) + var feeds = [ 'ledger', 'server' ]; + + if (this._transaction_subs) + feeds.push('transactions'); + + this.request_subscribe(feeds) .on('success', function (message) { self._stand_alone = !!message.stand_alone; self._testnet = !!message.testnet; From 37cc88ccf246954a7a081653a96c7d4aa669858e Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 1 Feb 2013 01:41:36 +0100 Subject: [PATCH 2/6] Move custom SJCL extensions from client over to Ripple library. --- grunt.js | 5 +- src/js/README.md | 3 + src/js/sjcl-custom/sjcl-extramath.js | 61 ++++++++ src/js/sjcl-custom/sjcl-ripemd160.js | 207 +++++++++++++++++++++++++++ src/js/sjcl-custom/sjcl-secp256k1.js | 72 ++++++++++ 5 files changed, 347 insertions(+), 1 deletion(-) create mode 100644 src/js/README.md create mode 100755 src/js/sjcl-custom/sjcl-extramath.js create mode 100755 src/js/sjcl-custom/sjcl-ripemd160.js create mode 100755 src/js/sjcl-custom/sjcl-secp256k1.js diff --git a/grunt.js b/grunt.js index 152cef351..fdb94be2b 100644 --- a/grunt.js +++ b/grunt.js @@ -32,7 +32,10 @@ module.exports = function(grunt) { "src/js/sjcl/core/convenience.js", "src/js/sjcl/core/bn.js", "src/js/sjcl/core/ecc.js", - "src/js/sjcl/core/srp.js" + "src/js/sjcl/core/srp.js", + "src/js/sjcl-custom/sjcl-secp256k1.js", + "src/js/sjcl-custom/sjcl-ripemd160.js", + "src/js/sjcl-custom/sjcl-extramath.js" ], dest: 'build/sjcl.js' } diff --git a/src/js/README.md b/src/js/README.md new file mode 100644 index 000000000..7b357e08b --- /dev/null +++ b/src/js/README.md @@ -0,0 +1,3 @@ +# Ripple JavaScript library + +This library lets you connect to a ripple server via websockets. diff --git a/src/js/sjcl-custom/sjcl-extramath.js b/src/js/sjcl-custom/sjcl-extramath.js new file mode 100755 index 000000000..4dd5f326a --- /dev/null +++ b/src/js/sjcl-custom/sjcl-extramath.js @@ -0,0 +1,61 @@ +sjcl.bn.ZERO = new sjcl.bn(0); + +/** [ this / that , this % that ] */ +sjcl.bn.prototype.divRem = function (that) { + if (typeof(that) !== "object") { that = new this._class(that); } + var thisa = this.abs(), thata = that.abs(), quot = new this._class(0), + ci = 0; + if (!thisa.greaterEquals(thata)) { + this.initWith(0); + return this; + } else if (thisa.equals(thata)) { + this.initWith(sign); + return this; + } + + for (; thisa.greaterEquals(thata); ci++) { + thata.doubleM(); + } + for (; ci > 0; ci--) { + quot.doubleM(); + thata.halveM(); + if (thisa.greaterEquals(thata)) { + quot.addM(1); + thisa.subM(that).normalize(); + } + } + return [quot, thisa]; +}; + +/** this /= that (rounded to nearest int) */ +sjcl.bn.prototype.divRound = function (that) { + var dr = this.divRem(that), quot = dr[0], rem = dr[1]; + + if (rem.doubleM().greaterEquals(that)) { + quot.addM(1); + } + + return quot; +}; + +/** this /= that (rounded down) */ +sjcl.bn.prototype.div = function (that) { + var dr = this.divRem(that); + return dr[0]; +}; + +sjcl.bn.prototype.sign = function () { + return this.greaterEquals(sjcl.bn.ZERO) ? 1 : -1; + }; + +/** -this */ +sjcl.bn.prototype.neg = function () { + return sjcl.bn.ZERO.sub(this); +}; + +/** |this| */ +sjcl.bn.prototype.abs = function () { + if (this.sign() === -1) { + return this.neg(); + } else return this; +}; diff --git a/src/js/sjcl-custom/sjcl-ripemd160.js b/src/js/sjcl-custom/sjcl-ripemd160.js new file mode 100755 index 000000000..4ee7b9246 --- /dev/null +++ b/src/js/sjcl-custom/sjcl-ripemd160.js @@ -0,0 +1,207 @@ +/** @fileOverview Javascript RIPEMD-160 implementation. + * + * @author Artem S Vybornov + */ +(function() { + +/** + * Context for a RIPEMD-160 operation in progress. + * @constructor + * @class RIPEMD, 160 bits. + */ +sjcl.hash.ripemd160 = function (hash) { + if (hash) { + this._h = hash._h.slice(0); + this._buffer = hash._buffer.slice(0); + this._length = hash._length; + } else { + this.reset(); + } +}; + +/** + * Hash a string or an array of words. + * @static + * @param {bitArray|String} data the data to hash. + * @return {bitArray} The hash value, an array of 5 big-endian words. + */ +sjcl.hash.ripemd160.hash = function (data) { + return (new sjcl.hash.ripemd160()).update(data).finalize(); +}; + +sjcl.hash.ripemd160.prototype = { + /** + * Reset the hash state. + * @return this + */ + reset: function () { + this._h = _h0.slice(0); + this._buffer = []; + this._length = 0; + return this; + }, + + /** + * Reset the hash state. + * @param {bitArray|String} data the data to hash. + * @return this + */ + update: function (data) { + if ( typeof data === "string" ) + data = sjcl.codec.utf8String.toBits(data); + + var i, b = this._buffer = sjcl.bitArray.concat(this._buffer, data), + ol = this._length, + nl = this._length = ol + sjcl.bitArray.bitLength(data); + for (i = 512+ol & -512; i <= nl; i+= 512) { + var words = b.splice(0,16); + for ( var w = 0; w < 16; ++w ) + words[w] = _cvt(words[w]); + + _block.call( this, words ); + } + + return this; + }, + + /** + * Complete hashing and output the hash value. + * @return {bitArray} The hash value, an array of 5 big-endian words. + */ + finalize: function () { + var b = sjcl.bitArray.concat( this._buffer, [ sjcl.bitArray.partial(1,1) ] ), + l = ( this._length + 1 ) % 512, + z = ( l > 448 ? 512 : 448 ) - l % 448, + zp = z % 32; + + if ( zp > 0 ) + b = sjcl.bitArray.concat( b, [ sjcl.bitArray.partial(zp,0) ] ) + for ( ; z >= 32; z -= 32 ) + b.push(0); + + b.push( _cvt( this._length | 0 ) ); + b.push( _cvt( Math.floor(this._length / 0x100000000) ) ); + + while ( b.length ) { + var words = b.splice(0,16); + for ( var w = 0; w < 16; ++w ) + words[w] = _cvt(words[w]); + + _block.call( this, words ); + } + + var h = this._h; + this.reset(); + + for ( var w = 0; w < 5; ++w ) + h[w] = _cvt(h[w]); + + return h; + } +}; + +var _h0 = [ 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0 ]; + +var _k1 = [ 0x00000000, 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xa953fd4e ]; +var _k2 = [ 0x50a28be6, 0x5c4dd124, 0x6d703ef3, 0x7a6d76e9, 0x00000000 ]; +for ( var i = 4; i >= 0; --i ) { + for ( var j = 1; j < 16; ++j ) { + _k1.splice(i,0,_k1[i]); + _k2.splice(i,0,_k2[i]); + } +} + +var _r1 = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, + 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, + 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, + 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13 ]; +var _r2 = [ 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, + 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, + 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, + 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, + 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11 ]; + +var _s1 = [ 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, + 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, + 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, + 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, + 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6 ]; +var _s2 = [ 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, + 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, + 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, + 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, + 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11 ]; + +function _f0(x,y,z) { + return x ^ y ^ z; +}; + +function _f1(x,y,z) { + return (x & y) | (~x & z); +}; + +function _f2(x,y,z) { + return (x | ~y) ^ z; +}; + +function _f3(x,y,z) { + return (x & z) | (y & ~z); +}; + +function _f4(x,y,z) { + return x ^ (y | ~z); +}; + +function _rol(n,l) { + return (n << l) | (n >>> (32-l)); +} + +function _cvt(n) { + return ( (n & 0xff << 0) << 24 ) + | ( (n & 0xff << 8) << 8 ) + | ( (n & 0xff << 16) >>> 8 ) + | ( (n & 0xff << 24) >>> 24 ); +} + +function _block(X) { + var A1 = this._h[0], B1 = this._h[1], C1 = this._h[2], D1 = this._h[3], E1 = this._h[4], + A2 = this._h[0], B2 = this._h[1], C2 = this._h[2], D2 = this._h[3], E2 = this._h[4]; + + var j = 0, T; + + for ( ; j < 16; ++j ) { + T = _rol( A1 + _f0(B1,C1,D1) + X[_r1[j]] + _k1[j], _s1[j] ) + E1; + A1 = E1; E1 = D1; D1 = _rol(C1,10); C1 = B1; B1 = T; + T = _rol( A2 + _f4(B2,C2,D2) + X[_r2[j]] + _k2[j], _s2[j] ) + E2; + A2 = E2; E2 = D2; D2 = _rol(C2,10); C2 = B2; B2 = T; } + for ( ; j < 32; ++j ) { + T = _rol( A1 + _f1(B1,C1,D1) + X[_r1[j]] + _k1[j], _s1[j] ) + E1; + A1 = E1; E1 = D1; D1 = _rol(C1,10); C1 = B1; B1 = T; + T = _rol( A2 + _f3(B2,C2,D2) + X[_r2[j]] + _k2[j], _s2[j] ) + E2; + A2 = E2; E2 = D2; D2 = _rol(C2,10); C2 = B2; B2 = T; } + for ( ; j < 48; ++j ) { + T = _rol( A1 + _f2(B1,C1,D1) + X[_r1[j]] + _k1[j], _s1[j] ) + E1; + A1 = E1; E1 = D1; D1 = _rol(C1,10); C1 = B1; B1 = T; + T = _rol( A2 + _f2(B2,C2,D2) + X[_r2[j]] + _k2[j], _s2[j] ) + E2; + A2 = E2; E2 = D2; D2 = _rol(C2,10); C2 = B2; B2 = T; } + for ( ; j < 64; ++j ) { + T = _rol( A1 + _f3(B1,C1,D1) + X[_r1[j]] + _k1[j], _s1[j] ) + E1; + A1 = E1; E1 = D1; D1 = _rol(C1,10); C1 = B1; B1 = T; + T = _rol( A2 + _f1(B2,C2,D2) + X[_r2[j]] + _k2[j], _s2[j] ) + E2; + A2 = E2; E2 = D2; D2 = _rol(C2,10); C2 = B2; B2 = T; } + for ( ; j < 80; ++j ) { + T = _rol( A1 + _f4(B1,C1,D1) + X[_r1[j]] + _k1[j], _s1[j] ) + E1; + A1 = E1; E1 = D1; D1 = _rol(C1,10); C1 = B1; B1 = T; + T = _rol( A2 + _f0(B2,C2,D2) + X[_r2[j]] + _k2[j], _s2[j] ) + E2; + A2 = E2; E2 = D2; D2 = _rol(C2,10); C2 = B2; B2 = T; } + + T = this._h[1] + C1 + D2; + this._h[1] = this._h[2] + D1 + E2; + this._h[2] = this._h[3] + E1 + A2; + this._h[3] = this._h[4] + A1 + B2; + this._h[4] = this._h[0] + B1 + C2; + this._h[0] = T; +} + +})(); diff --git a/src/js/sjcl-custom/sjcl-secp256k1.js b/src/js/sjcl-custom/sjcl-secp256k1.js new file mode 100755 index 000000000..4d34db7f1 --- /dev/null +++ b/src/js/sjcl-custom/sjcl-secp256k1.js @@ -0,0 +1,72 @@ +// ----- for secp256k1 ------ + +// Overwrite NIST-P256 with secp256k1 so we're on even footing +sjcl.ecc.curves.c256 = new sjcl.ecc.curve( + sjcl.bn.pseudoMersennePrime(256, [[0,-1],[4,-1],[6,-1],[7,-1],[8,-1],[9,-1],[32,-1]]), + "0x14551231950b75fc4402da1722fc9baee", + 0, + 7, + "0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + "0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8" +); + +// Replace point addition and doubling algorithms +// NIST-P256 is a=-3, we need algorithms for a=0 +sjcl.ecc.pointJac.prototype.add = function(T) { + var S = this; + if (S.curve !== T.curve) { + throw("sjcl.ecc.add(): Points must be on the same curve to add them!"); + } + + if (S.isIdentity) { + return T.toJac(); + } else if (T.isIdentity) { + return S; + } + + var z1z1 = S.z.square(); + var h = T.x.mul(z1z1).subM(S.x); + var s2 = T.y.mul(S.z).mul(z1z1); + + if (h.equals(0)) { + if (S.y.equals(T.y.mul(z1z1.mul(S.z)))) { + // same point + return S.doubl(); + } else { + // inverses + return new sjcl.ecc.pointJac(S.curve); + } + } + + var hh = h.square(); + var i = hh.copy().doubleM().doubleM(); + var j = h.mul(i); + var r = s2.sub(S.y).doubleM(); + var v = S.x.mul(i); + + var x = r.square().subM(j).subM(v.copy().doubleM()); + var y = r.mul(v.sub(x)).subM(S.y.mul(j).doubleM()); + var z = S.z.add(h).square().subM(z1z1).subM(hh); + + return new sjcl.ecc.pointJac(this.curve,x,y,z); +}; + +sjcl.ecc.pointJac.prototype.doubl = function () { + if (this.isIdentity) { return this; } + + var a = this.x.square(); + var b = this.y.square(); + var c = b.square(); + var d = this.x.add(b).square().subM(a).subM(c).doubleM(); + var e = a.mul(3); + var f = e.square(); + var x = f.sub(d.copy().doubleM()); + var y = e.mul(d.sub(x)).subM(c.doubleM().doubleM().doubleM()); + var z = this.y.mul(this.z).doubleM(); + return new sjcl.ecc.pointJac(this.curve, x, y, z); +}; + +sjcl.ecc.point.prototype.toBytesCompressed = function () { + var header = this.y.mod(2).toString() == "0x0" ? 0x02 : 0x03; + return [header].concat(sjcl.codec.bytes.fromBits(this.x.toBits())) +}; From 24dac24d2937a109322b3ddb39e12bbdd7c1153b Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 1 Feb 2013 15:13:09 +0100 Subject: [PATCH 3/6] Refactor JavaScript library. - Separate classes in separate modules (files) - Constants should be associated with a class - Replace exports.config scheme with a config singleton - Refactor base58 functions as a static class --- src/js/amount.js | 442 +---------------------------------------- src/js/base.js | 136 +++++++++++++ src/js/config.js | 3 + src/js/currency.js | 81 ++++++++ src/js/remote.js | 8 +- src/js/seed.js | 89 +++++++++ src/js/uint160.js | 156 +++++++++++++++ test/amount-test.js | 9 +- test/jsonrpc-test.js | 4 +- test/monitor-test.js | 4 +- test/offer-test.js | 4 +- test/path-test.js | 4 +- test/remote-test.js | 4 +- test/send-test.js | 4 +- test/testutils.js | 4 +- test/websocket-test.js | 3 +- 16 files changed, 500 insertions(+), 455 deletions(-) create mode 100644 src/js/base.js create mode 100644 src/js/config.js create mode 100644 src/js/currency.js create mode 100644 src/js/seed.js create mode 100644 src/js/uint160.js diff --git a/src/js/amount.js b/src/js/amount.js index b24ff0397..7614e4872 100644 --- a/src/js/amount.js +++ b/src/js/amount.js @@ -3,27 +3,18 @@ var sjcl = require('../../build/sjcl'); var bn = sjcl.bn; -var utils = require('./utils.js'); -var jsbn = require('./jsbn.js'); +var utils = require('./utils'); +var jsbn = require('./jsbn'); -var BigInteger = jsbn.BigInteger; -var nbi = jsbn.nbi; +var BigInteger = jsbn.BigInteger; -var alphabets = { - 'ripple' : "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz", - 'tipple' : "RPShNAF39wBUDnEGHJKLM4pQrsT7VWXYZ2bcdeCg65jkm8ofqi1tuvaxyz", - 'bitcoin' : "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" -}; +var UInt160 = require('./uint160').UInt160, + Seed = require('./seed').Seed, + Currency = require('./currency').Currency; var consts = exports.consts = { - 'address_xns' : "rrrrrrrrrrrrrrrrrrrrrhoLvTp", - 'address_one' : "rrrrrrrrrrrrrrrrrrrrBZbvji", 'currency_xns' : 0, 'currency_one' : 1, - 'uint160_xns' : utils.hexToString("0000000000000000000000000000000000000000"), - 'uint160_one' : utils.hexToString("0000000000000000000000000000000000000001"), - 'hex_xns' : "0000000000000000000000000000000000000000", - 'hex_one' : "0000000000000000000000000000000000000001", 'xns_precision' : 6, // BigInteger values prefixed with bi_. @@ -42,421 +33,8 @@ var consts = exports.consts = { 'cMinOffset' : -96, 'cMaxOffset' : 80, - - 'VER_NONE' : 1, - 'VER_NODE_PUBLIC' : 28, - 'VER_NODE_PRIVATE' : 32, - 'VER_ACCOUNT_ID' : 0, - 'VER_ACCOUNT_PUBLIC' : 35, - 'VER_ACCOUNT_PRIVATE' : 34, - 'VER_FAMILY_GENERATOR' : 41, - 'VER_FAMILY_SEED' : 33, }; -// --> input: big-endian array of bytes. -// <-- string at least as long as input. -var encode_base = function (input, alphabet) { - var alphabet = alphabets[alphabet || 'ripple']; - var bi_base = new BigInteger(String(alphabet.length)); - var bi_q = nbi(); - var bi_r = nbi(); - var bi_value = new BigInteger(input); - var buffer = []; - - while (bi_value.compareTo(BigInteger.ZERO) > 0) - { - bi_value.divRemTo(bi_base, bi_q, bi_r); - bi_q.copyTo(bi_value); - - buffer.push(alphabet[bi_r.intValue()]); - } - - var i; - - for (i = 0; i != input.length && !input[i]; i += 1) { - buffer.push(alphabet[0]); - } - - return buffer.reverse().join(""); -}; - -// --> input: String -// <-- array of bytes or undefined. -var decode_base = function (input, alphabet) { - var alphabet = alphabets[alphabet || 'ripple']; - var bi_base = new BigInteger(String(alphabet.length)); - var bi_value = nbi(); - var i; - - for (i = 0; i != input.length && input[i] === alphabet[0]; i += 1) - ; - - for (; i != input.length; i += 1) { - var v = alphabet.indexOf(input[i]); - - if (v < 0) - return undefined; - - var r = nbi(); - - r.fromInt(v); - - bi_value = bi_value.multiply(bi_base).add(r); - } - - // toByteArray: - // - Returns leading zeros! - // - Returns signed bytes! - var bytes = bi_value.toByteArray().map(function (b) { return b ? b < 0 ? 256+b : b : 0}); - var extra = 0; - - while (extra != bytes.length && !bytes[extra]) - extra += 1; - - if (extra) - bytes = bytes.slice(extra); - - var zeros = 0; - - while (zeros !== input.length && input[zeros] === alphabet[0]) - zeros += 1; - - return [].concat(utils.arraySet(zeros, 0), bytes); -}; - -var sha256 = function (bytes) { - return sjcl.codec.bytes.fromBits(sjcl.hash.sha256.hash(sjcl.codec.bytes.toBits(bytes))); -}; - -var sha256hash = function (bytes) { - return sha256(sha256(bytes)); -}; - -// --> input: Array -// <-- String -var encode_base_check = function (version, input, alphabet) { - var buffer = [].concat(version, input); - var check = sha256(sha256(buffer)).slice(0, 4); - - return encode_base([].concat(buffer, check), alphabet); -} - -// --> input : String -// <-- NaN || BigInteger -var decode_base_check = function (version, input, alphabet) { - var buffer = decode_base(input, alphabet); - - if (!buffer || buffer[0] !== version || buffer.length < 5) - return NaN; - - var computed = sha256hash(buffer.slice(0, -4)).slice(0, 4); - var checksum = buffer.slice(-4); - var i; - - for (i = 0; i != 4; i += 1) - if (computed[i] !== checksum[i]) - return NaN; - - return new BigInteger(buffer.slice(1, -4)); -} - -// -// Seed support -// - -var Seed = function () { - // Internal form: NaN or BigInteger - this._value = NaN; -}; - -Seed.json_rewrite = function (j) { - return Seed.from_json(j).to_json(); -}; - -// Return a new Seed from j. -Seed.from_json = function (j) { - return 'string' === typeof j - ? (new Seed()).parse_json(j) - : j.clone(); -}; - -Seed.is_valid = function (j) { - return Seed.from_json(j).is_valid(); -}; - -Seed.prototype.clone = function () { - return this.copyTo(new Seed()); -}; - -// Returns copy. -Seed.prototype.copyTo = function (d) { - d._value = this._value; - - return d; -}; - -Seed.prototype.equals = function (d) { - return this._value instanceof BigInteger && d._value instanceof BigInteger && this._value.equals(d._value); -}; - -Seed.prototype.is_valid = function () { - return this._value instanceof BigInteger; -}; - -// value = NaN on error. -// One day this will support rfc1751 too. -Seed.prototype.parse_json = function (j) { - if ('string' !== typeof j) { - this._value = NaN; - } - else if (j[0] === "s") { - this._value = decode_base_check(consts.VER_FAMILY_SEED, j); - } - else if (16 === j.length) { - this._value = new BigInteger(utils.stringToArray(j), 128); - } - else { - this._value = NaN; - } - - return this; -}; - -// Convert from internal form. -Seed.prototype.to_json = function () { - if (!(this._value instanceof BigInteger)) - return NaN; - - var bytes = this._value.toByteArray().map(function (b) { return b ? b < 0 ? 256+b : b : 0}); - var target = 20; - - // XXX Make sure only trim off leading zeros. - var array = bytes.length < target - ? bytes.length - ? [].concat(utils.arraySet(target - bytes.length, 0), bytes) - : utils.arraySet(target, 0) - : bytes.slice(target - bytes.length); - var output = encode_base_check(consts.VER_FAMILY_SEED, array); - - return output; -}; - -// -// UInt160 support -// - -var UInt160 = function () { - // Internal form: NaN or BigInteger - this._value = NaN; -}; - -UInt160.json_rewrite = function (j) { - return UInt160.from_json(j).to_json(); -}; - -// Return a new UInt160 from j. -UInt160.from_generic = function (j) { - return 'string' === typeof j - ? (new UInt160()).parse_generic(j) - : j.clone(); -}; - -// Return a new UInt160 from j. -UInt160.from_json = function (j) { - if ('string' === typeof j) { - return (new UInt160()).parse_json(j); - } else if (j instanceof UInt160) { - return j.clone(); - } else { - return new UInt160(); - } -}; - -UInt160.is_valid = function (j) { - return UInt160.from_json(j).is_valid(); -}; - -UInt160.prototype.clone = function () { - return this.copyTo(new UInt160()); -}; - -// Returns copy. -UInt160.prototype.copyTo = function (d) { - d._value = this._value; - - return d; -}; - -UInt160.prototype.equals = function (d) { - return this._value instanceof BigInteger && d._value instanceof BigInteger && this._value.equals(d._value); -}; - -UInt160.prototype.is_valid = function () { - return this._value instanceof BigInteger; -}; - -// value = NaN on error. -UInt160.prototype.parse_generic = function (j) { - // Canonicalize and validate - if (exports.config.accounts && j in exports.config.accounts) - j = exports.config.accounts[j].account; - - switch (j) { - case undefined: - case "0": - case consts.address_xns: - case consts.uint160_xns: - case consts.hex_xns: - this._value = nbi(); - break; - - case "1": - case consts.address_one: - case consts.uint160_one: - case consts.hex_one: - this._value = new BigInteger([1]); - - break; - - default: - if ('string' !== typeof j) { - this._value = NaN; - } - else if (j[0] === "r") { - this._value = decode_base_check(consts.VER_ACCOUNT_ID, j); - } - else if (20 === j.length) { - this._value = new BigInteger(utils.stringToArray(j), 256); - } - else if (40 === j.length) { - // XXX Check char set! - this._value = new BigInteger(j, 16); - } - else { - this._value = NaN; - } - } - - return this; -}; - -// value = NaN on error. -UInt160.prototype.parse_json = function (j) { - // Canonicalize and validate - if (exports.config.accounts && j in exports.config.accounts) - j = exports.config.accounts[j].account; - - if ('string' !== typeof j) { - this._value = NaN; - } - else if (j[0] === "r") { - this._value = decode_base_check(consts.VER_ACCOUNT_ID, j); - } - else { - this._value = NaN; - } - - return this; -}; - -// Convert from internal form. -// XXX Json form should allow 0 and 1, C++ doesn't currently allow it. -UInt160.prototype.to_json = function () { - if (!(this._value instanceof BigInteger)) - return NaN; - - var bytes = this._value.toByteArray().map(function (b) { return b ? b < 0 ? 256+b : b : 0}); - var target = 20; - - // XXX Make sure only trim off leading zeros. - var array = bytes.length < target - ? bytes.length - ? [].concat(utils.arraySet(target - bytes.length, 0), bytes) - : utils.arraySet(target, 0) - : bytes.slice(target - bytes.length); - var output = encode_base_check(consts.VER_ACCOUNT_ID, array); - - return output; -}; - -// -// Currency support -// - -// XXX Internal form should be UInt160. -var Currency = function () { - // Internal form: 0 = XRP. 3 letter-code. - // XXX Internal should be 0 or hex with three letter annotation when valid. - - // Json form: - // '', 'XRP', '0': 0 - // 3-letter code: ... - // XXX Should support hex, C++ doesn't currently allow it. - - this._value = NaN; -} - -// Given "USD" return the json. -Currency.json_rewrite = function (j) { - return Currency.from_json(j).to_json(); -}; - -Currency.from_json = function (j) { - return 'string' === typeof j - ? (new Currency()).parse_json(j) - : j.clone(); -}; - -Currency.is_valid = function (j) { - return currency.from_json(j).is_valid(); -}; - -Currency.prototype.clone = function() { - return this.copyTo(new Currency()); -}; - -// Returns copy. -Currency.prototype.copyTo = function (d) { - d._value = this._value; - - return d; -}; - -Currency.prototype.equals = function (d) { - return ('string' !== typeof this._value && isNaN(this._value)) - || ('string' !== typeof d._value && isNaN(d._value)) ? false : this._value === d._value; -}; - -// this._value = NaN on error. -Currency.prototype.parse_json = function (j) { - if ("" === j || "0" === j || "XRP" === j) { - this._value = 0; - } - else if ('string' != typeof j || 3 !== j.length) { - this._value = NaN; - } - else { - this._value = j; - } - - return this; -}; - -Currency.prototype.is_native = function () { - return !isNaN(this._value) && !this._value; -}; - -Currency.prototype.is_valid = function () { - return !isNaN(this._value); -}; - -Currency.prototype.to_json = function () { - return this._value ? this._value : "XRP"; -}; - -Currency.prototype.to_human = function () { - return this._value ? this._value : "XRP"; -}; // // Amount class in the style of Java's BigInteger class @@ -505,9 +83,9 @@ Amount.is_valid_full = function (j) { Amount.NaN = function () { var result = new Amount(); - + result._value = NaN; - + return result; }; @@ -1305,10 +883,10 @@ Amount.prototype.not_equals_why = function (d) { }; exports.Amount = Amount; + +// DEPRECATED: Include the corresponding files instead. exports.Currency = Currency; exports.Seed = Seed; exports.UInt160 = UInt160; -exports.config = {}; - // vim:sw=2:sts=2:ts=8:et diff --git a/src/js/base.js b/src/js/base.js new file mode 100644 index 000000000..47ef551ed --- /dev/null +++ b/src/js/base.js @@ -0,0 +1,136 @@ + +var sjcl = require('../../build/sjcl'); +var utils = require('./utils'); +var jsbn = require('./jsbn'); +var extend = require('extend'); + +var BigInteger = jsbn.BigInteger; +var nbi = jsbn.nbi; + +var Base = {}; + +var alphabets = Base.alphabets = { + 'ripple' : "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz", + 'tipple' : "RPShNAF39wBUDnEGHJKLM4pQrsT7VWXYZ2bcdeCg65jkm8ofqi1tuvaxyz", + 'bitcoin' : "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +}; + +extend(Base, { + 'VER_NONE' : 1, + 'VER_NODE_PUBLIC' : 28, + 'VER_NODE_PRIVATE' : 32, + 'VER_ACCOUNT_ID' : 0, + 'VER_ACCOUNT_PUBLIC' : 35, + 'VER_ACCOUNT_PRIVATE' : 34, + 'VER_FAMILY_GENERATOR' : 41, + 'VER_FAMILY_SEED' : 33 +}); + +var sha256 = function (bytes) { + return sjcl.codec.bytes.fromBits(sjcl.hash.sha256.hash(sjcl.codec.bytes.toBits(bytes))); +}; + +var sha256hash = function (bytes) { + return sha256(sha256(bytes)); +}; + +// --> input: big-endian array of bytes. +// <-- string at least as long as input. +Base.encode = function (input, alpha) { + var alphabet = alphabets[alpha || 'ripple']; + var bi_base = new BigInteger(String(alphabet.length)); + var bi_q = nbi(); + var bi_r = nbi(); + var bi_value = new BigInteger(input); + var buffer = []; + + while (bi_value.compareTo(BigInteger.ZERO) > 0) + { + bi_value.divRemTo(bi_base, bi_q, bi_r); + bi_q.copyTo(bi_value); + + buffer.push(alphabet[bi_r.intValue()]); + } + + var i; + + for (i = 0; i != input.length && !input[i]; i += 1) { + buffer.push(alphabet[0]); + } + + return buffer.reverse().join(""); +}; + +// --> input: String +// <-- array of bytes or undefined. +Base.decode = function (input, alpha) { + var alphabet = alphabets[alpha || 'ripple']; + var bi_base = new BigInteger(String(alphabet.length)); + var bi_value = nbi(); + var i; + + for (i = 0; i != input.length && input[i] === alphabet[0]; i += 1) + ; + + for (; i != input.length; i += 1) { + var v = alphabet.indexOf(input[i]); + + if (v < 0) + return undefined; + + var r = nbi(); + + r.fromInt(v); + + bi_value = bi_value.multiply(bi_base).add(r); + } + + // toByteArray: + // - Returns leading zeros! + // - Returns signed bytes! + var bytes = bi_value.toByteArray().map(function (b) { return b ? b < 0 ? 256+b : b : 0; }); + var extra = 0; + + while (extra != bytes.length && !bytes[extra]) + extra += 1; + + if (extra) + bytes = bytes.slice(extra); + + var zeros = 0; + + while (zeros !== input.length && input[zeros] === alphabet[0]) + zeros += 1; + + return [].concat(utils.arraySet(zeros, 0), bytes); +}; + +// --> input: Array +// <-- String +Base.encode_check = function (version, input, alphabet) { + var buffer = [].concat(version, input); + var check = sha256(sha256(buffer)).slice(0, 4); + + return Base.encode([].concat(buffer, check), alphabet); +} + +// --> input : String +// <-- NaN || BigInteger +Base.decode_check = function (version, input, alphabet) { + var buffer = Base.decode(input, alphabet); + + if (!buffer || buffer[0] !== version || buffer.length < 5) + return NaN; + + var computed = sha256hash(buffer.slice(0, -4)).slice(0, 4); + var checksum = buffer.slice(-4); + var i; + + for (i = 0; i != 4; i += 1) + if (computed[i] !== checksum[i]) + return NaN; + + return new BigInteger(buffer.slice(1, -4)); +} + +exports.Base = Base; diff --git a/src/js/config.js b/src/js/config.js new file mode 100644 index 000000000..372b7d261 --- /dev/null +++ b/src/js/config.js @@ -0,0 +1,3 @@ +// This object serves as a singleton to store config options + +module.exports = {}; diff --git a/src/js/currency.js b/src/js/currency.js new file mode 100644 index 000000000..14e902159 --- /dev/null +++ b/src/js/currency.js @@ -0,0 +1,81 @@ + +// +// Currency support +// + +// XXX Internal form should be UInt160. +var Currency = function () { + // Internal form: 0 = XRP. 3 letter-code. + // XXX Internal should be 0 or hex with three letter annotation when valid. + + // Json form: + // '', 'XRP', '0': 0 + // 3-letter code: ... + // XXX Should support hex, C++ doesn't currently allow it. + + this._value = NaN; +} + +// Given "USD" return the json. +Currency.json_rewrite = function (j) { + return Currency.from_json(j).to_json(); +}; + +Currency.from_json = function (j) { + return 'string' === typeof j + ? (new Currency()).parse_json(j) + : j.clone(); +}; + +Currency.is_valid = function (j) { + return Currency.from_json(j).is_valid(); +}; + +Currency.prototype.clone = function() { + return this.copyTo(new Currency()); +}; + +// Returns copy. +Currency.prototype.copyTo = function (d) { + d._value = this._value; + + return d; +}; + +Currency.prototype.equals = function (d) { + return ('string' !== typeof this._value && isNaN(this._value)) + || ('string' !== typeof d._value && isNaN(d._value)) ? false : this._value === d._value; +}; + +// this._value = NaN on error. +Currency.prototype.parse_json = function (j) { + if ("" === j || "0" === j || "XRP" === j) { + this._value = 0; + } + else if ('string' != typeof j || 3 !== j.length) { + this._value = NaN; + } + else { + this._value = j; + } + + return this; +}; + +Currency.prototype.is_native = function () { + return !isNaN(this._value) && !this._value; +}; + +Currency.prototype.is_valid = function () { + return !isNaN(this._value); +}; + +Currency.prototype.to_json = function () { + return this._value ? this._value : "XRP"; +}; + +Currency.prototype.to_human = function () { + return this._value ? this._value : "XRP"; +}; + +exports.Currency = Currency; diff --git a/src/js/remote.js b/src/js/remote.js index 5832cced3..f34ed3287 100644 --- a/src/js/remote.js +++ b/src/js/remote.js @@ -22,6 +22,7 @@ var UInt160 = require('./amount').UInt160; var Transaction = require('./transaction').Transaction; var utils = require('./utils'); +var config = require('./config'); // Request events emitted: // 'success' : Request successful. @@ -272,12 +273,12 @@ var Remote = function (opts, trace) { Remote.prototype = new EventEmitter; Remote.from_config = function (obj, trace) { - var serverConfig = 'string' === typeof obj ? exports.config.servers[obj] : obj; + var serverConfig = 'string' === typeof obj ? config.servers[obj] : obj; var remote = new Remote(serverConfig, trace); - for (var account in exports.config.accounts) { - var accountInfo = exports.config.accounts[account]; + for (var account in config.accounts) { + var accountInfo = config.accounts[account]; if ("object" === typeof accountInfo) { if (accountInfo.secret) { // Index by nickname ... @@ -1191,7 +1192,6 @@ Remote.prototype.transaction = function () { return new Transaction(this); }; -exports.config = {}; exports.Remote = Remote; // vim:sw=2:sts=2:ts=8:et diff --git a/src/js/seed.js b/src/js/seed.js new file mode 100644 index 000000000..b26f2e2c6 --- /dev/null +++ b/src/js/seed.js @@ -0,0 +1,89 @@ +// +// Seed support +// + +var utils = require('./utils'); +var jsbn = require('./jsbn'); + +var BigInteger = jsbn.BigInteger; + +var Base = require('./base').Base; + +var Seed = function () { + // Internal form: NaN or BigInteger + this._value = NaN; +}; + +Seed.json_rewrite = function (j) { + return Seed.from_json(j).to_json(); +}; + +// Return a new Seed from j. +Seed.from_json = function (j) { + return 'string' === typeof j + ? (new Seed()).parse_json(j) + : j.clone(); +}; + +Seed.is_valid = function (j) { + return Seed.from_json(j).is_valid(); +}; + +Seed.prototype.clone = function () { + return this.copyTo(new Seed()); +}; + +// Returns copy. +Seed.prototype.copyTo = function (d) { + d._value = this._value; + + return d; +}; + +Seed.prototype.equals = function (d) { + return this._value instanceof BigInteger && d._value instanceof BigInteger && this._value.equals(d._value); +}; + +Seed.prototype.is_valid = function () { + return this._value instanceof BigInteger; +}; + +// value = NaN on error. +// One day this will support rfc1751 too. +Seed.prototype.parse_json = function (j) { + if ('string' !== typeof j) { + this._value = NaN; + } + else if (j[0] === "s") { + this._value = Base.decode_check(Base.VER_FAMILY_SEED, j); + } + else if (16 === j.length) { + this._value = new BigInteger(utils.stringToArray(j), 128); + } + else { + this._value = NaN; + } + + return this; +}; + +// Convert from internal form. +Seed.prototype.to_json = function () { + if (!(this._value instanceof BigInteger)) + return NaN; + + var bytes = this._value.toByteArray().map(function (b) { return b ? b < 0 ? 256+b : b : 0; }); + var target = 20; + + // XXX Make sure only trim off leading zeros. + var array = bytes.length < target + ? bytes.length + ? [].concat(utils.arraySet(target - bytes.length, 0), bytes) + : utils.arraySet(target, 0) + : bytes.slice(target - bytes.length); + var output = Base.encode_check(Base.VER_FAMILY_SEED, array); + + return output; +}; + +exports.Seed = Seed; diff --git a/src/js/uint160.js b/src/js/uint160.js new file mode 100644 index 000000000..befe2f101 --- /dev/null +++ b/src/js/uint160.js @@ -0,0 +1,156 @@ + +var utils = require('./utils'); +var config = require('./config'); +var jsbn = require('./jsbn'); + +var BigInteger = jsbn.BigInteger; +var nbi = jsbn.nbi; + +var Base = require('./base').Base; + +// +// UInt160 support +// + +var UInt160 = function () { + // Internal form: NaN or BigInteger + this._value = NaN; +}; + +UInt160.ZERO = utils.hexToString("0000000000000000000000000000000000000000"); +UInt160.ONE = utils.hexToString("0000000000000000000000000000000000000001"); +var ADDRESS_ZERO = UInt160.ADDRESS_ZERO = "rrrrrrrrrrrrrrrrrrrrrhoLvTp"; +var ADDRESS_ONE = UInt160.ADDRESS_ONE = "rrrrrrrrrrrrrrrrrrrrBZbvji"; +var HEX_ZERO = UInt160.HEX_ZERO = "0000000000000000000000000000000000000000"; +var HEX_ONE = UInt160.HEX_ONE = "0000000000000000000000000000000000000001"; + +UInt160.json_rewrite = function (j) { + return UInt160.from_json(j).to_json(); +}; + +// Return a new UInt160 from j. +UInt160.from_generic = function (j) { + return 'string' === typeof j + ? (new UInt160()).parse_generic(j) + : j.clone(); +}; + +// Return a new UInt160 from j. +UInt160.from_json = function (j) { + if ('string' === typeof j) { + return (new UInt160()).parse_json(j); + } else if (j instanceof UInt160) { + return j.clone(); + } else { + return new UInt160(); + } +}; + +UInt160.is_valid = function (j) { + return UInt160.from_json(j).is_valid(); +}; + +UInt160.prototype.clone = function () { + return this.copyTo(new UInt160()); +}; + +// Returns copy. +UInt160.prototype.copyTo = function (d) { + d._value = this._value; + + return d; +}; + +UInt160.prototype.equals = function (d) { + return this._value instanceof BigInteger && d._value instanceof BigInteger && this._value.equals(d._value); +}; + +UInt160.prototype.is_valid = function () { + return this._value instanceof BigInteger; +}; + +// value = NaN on error. +UInt160.prototype.parse_generic = function (j) { + // Canonicalize and validate + if (config.accounts && j in config.accounts) + j = config.accounts[j].account; + + switch (j) { + case undefined: + case "0": + case UInt160.ZERO: + case ADDRESS_ZERO: + case HEX_ZERO: + this._value = nbi(); + break; + + case "1": + case UInt160.ONE: + case ADDRESS_ONE: + case HEX_ONE: + this._value = new BigInteger([1]); + + break; + + default: + if ('string' !== typeof j) { + this._value = NaN; + } + else if (j[0] === "r") { + this._value = Base.decode_check(Base.VER_ACCOUNT_ID, j); + } + else if (20 === j.length) { + this._value = new BigInteger(utils.stringToArray(j), 256); + } + else if (40 === j.length) { + // XXX Check char set! + this._value = new BigInteger(j, 16); + } + else { + this._value = NaN; + } + } + + return this; +}; + +// value = NaN on error. +UInt160.prototype.parse_json = function (j) { + // Canonicalize and validate + if (config.accounts && j in config.accounts) + j = config.accounts[j].account; + + if ('string' !== typeof j) { + this._value = NaN; + } + else if (j[0] === "r") { + this._value = Base.decode_check(Base.VER_ACCOUNT_ID, j); + } + else { + this._value = NaN; + } + + return this; +}; + +// Convert from internal form. +// XXX Json form should allow 0 and 1, C++ doesn't currently allow it. +UInt160.prototype.to_json = function () { + if (!(this._value instanceof BigInteger)) + return NaN; + + var bytes = this._value.toByteArray().map(function (b) { return b ? b < 0 ? 256+b : b : 0}); + var target = 20; + + // XXX Make sure only trim off leading zeros. + var array = bytes.length < target + ? bytes.length + ? [].concat(utils.arraySet(target - bytes.length, 0), bytes) + : utils.arraySet(target, 0) + : bytes.slice(target - bytes.length); + var output = Base.encode_check(Base.VER_ACCOUNT_ID, array); + + return output; +}; + +exports.UInt160 = UInt160; diff --git a/test/amount-test.js b/test/amount-test.js index 71279cbee..e5bec8e00 100644 --- a/test/amount-test.js +++ b/test/amount-test.js @@ -8,7 +8,8 @@ var amount = require("../src/js/amount.js"); var Amount = require("../src/js/amount.js").Amount; var UInt160 = require("../src/js/amount.js").UInt160; -require("../src/js/amount.js").config = require("./config.js"); +var extend = require('extend'); +extend(require('../src/js/config'), require('./config')); var config = require('./config.js'); @@ -20,16 +21,16 @@ buster.testCase("Amount", { buster.assert.equals(nbi(), UInt160.from_generic("0")._value); }, "Parse 0 export" : function () { - buster.assert.equals(amount.consts.address_xns, UInt160.from_generic("0").to_json()); + buster.assert.equals(UInt160.ADDRESS_ZERO, UInt160.from_generic("0").to_json()); }, "Parse 1" : function () { buster.assert.equals(new BigInteger([1]), UInt160.from_generic("1")._value); }, "Parse rrrrrrrrrrrrrrrrrrrrrhoLvTp export" : function () { - buster.assert.equals(amount.consts.address_xns, UInt160.from_json("rrrrrrrrrrrrrrrrrrrrrhoLvTp").to_json()); + buster.assert.equals(UInt160.ADDRESS_ZERO, UInt160.from_json("rrrrrrrrrrrrrrrrrrrrrhoLvTp").to_json()); }, "Parse rrrrrrrrrrrrrrrrrrrrBZbvji export" : function () { - buster.assert.equals(amount.consts.address_one, UInt160.from_json("rrrrrrrrrrrrrrrrrrrrBZbvji").to_json()); + buster.assert.equals(UInt160.ADDRESS_ONE, UInt160.from_json("rrrrrrrrrrrrrrrrrrrrBZbvji").to_json()); }, "Parse mtgox export" : function () { buster.assert.equals(config.accounts["mtgox"].account, UInt160.from_json("mtgox").to_json()); diff --git a/test/jsonrpc-test.js b/test/jsonrpc-test.js index ffad70ba6..c1569ba5d 100644 --- a/test/jsonrpc-test.js +++ b/test/jsonrpc-test.js @@ -12,8 +12,8 @@ var testutils = require("./testutils.js"); var config = require("./config.js"); -require("../src/js/amount.js").config = require("./config.js"); -require("../src/js/remote.js").config = require("./config.js"); +var extend = require('extend'); +extend(require('../src/js/config'), require('./config')); // How long to wait for server to start. var serverDelay = 1500; diff --git a/test/monitor-test.js b/test/monitor-test.js index daef55a65..861f4c15d 100644 --- a/test/monitor-test.js +++ b/test/monitor-test.js @@ -7,8 +7,8 @@ var Server = require("./server.js").Server; var testutils = require("./testutils.js"); -require("../src/js/amount.js").config = require("./config.js"); -require("../src/js/remote.js").config = require("./config.js"); +var extend = require('extend'); +extend(require('../src/js/config'), require('./config')); buster.testRunner.timeout = 5000; diff --git a/test/offer-test.js b/test/offer-test.js index de87c7d10..07122d1d8 100644 --- a/test/offer-test.js +++ b/test/offer-test.js @@ -9,8 +9,8 @@ var Server = require("./server").Server; var testutils = require("./testutils"); -require("../src/js/amount").config = require("./config"); -require("../src/js/remote").config = require("./config"); +var extend = require('extend'); +extend(require('../src/js/config'), require('./config')); buster.testRunner.timeout = 5000; diff --git a/test/path-test.js b/test/path-test.js index 9cc0d7f82..dbc72ef89 100644 --- a/test/path-test.js +++ b/test/path-test.js @@ -7,8 +7,8 @@ var Server = require("./server.js").Server; var testutils = require("./testutils.js"); -require("../src/js/amount.js").config = require("./config.js"); -require("../src/js/remote.js").config = require("./config.js"); +var extend = require('extend'); +extend(require('../src/js/config'), require('./config')); buster.testRunner.timeout = 5000; diff --git a/test/remote-test.js b/test/remote-test.js index 75ade8be3..e54afde22 100644 --- a/test/remote-test.js +++ b/test/remote-test.js @@ -6,8 +6,8 @@ var Server = require("./server.js").Server; var testutils = require("./testutils.js"); -require("../src/js/amount.js").config = require("./config.js"); -require("../src/js/remote.js").config = require("./config.js"); +var extend = require('extend'); +extend(require('../src/js/config'), require('./config')); // How long to wait for server to start. var serverDelay = 1500; // XXX Not implemented. diff --git a/test/send-test.js b/test/send-test.js index d2fe9c7f3..524841e86 100644 --- a/test/send-test.js +++ b/test/send-test.js @@ -7,8 +7,8 @@ var Server = require("./server.js").Server; var testutils = require("./testutils.js"); -require("../src/js/amount.js").config = require("./config.js"); -require("../src/js/remote.js").config = require("./config.js"); +var extend = require('extend'); +extend(require('../src/js/config'), require('./config')); // How long to wait for server to start. var serverDelay = 1500; diff --git a/test/testutils.js b/test/testutils.js index deddc3009..3ef6d6354 100644 --- a/test/testutils.js +++ b/test/testutils.js @@ -4,8 +4,8 @@ var Amount = require("../src/js/amount.js").Amount; var Remote = require("../src/js/remote.js").Remote; var Server = require("./server.js").Server; -require("../src/js/amount.js").config = require("./config.js"); -require("../src/js/remote.js").config = require("./config.js"); +var extend = require('extend'); +extend(require('../src/js/config'), require('./config')); var config = require("./config.js"); diff --git a/test/websocket-test.js b/test/websocket-test.js index 02d7e59f9..5f15135cc 100644 --- a/test/websocket-test.js +++ b/test/websocket-test.js @@ -4,7 +4,8 @@ var Server = require("./server.js").Server; var Remote = require("../src/js/remote.js").Remote; var config = require("./config.js"); -require("../src/js/remote.js").config = require("./config.js"); +var extend = require('extend'); +extend(require('../src/js/config'), require('./config')); buster.testRunner.timeout = 5000; From eeb9598b12c629220aebd684a318d0431bfb9531 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 1 Feb 2013 22:35:50 +0100 Subject: [PATCH 4/6] Add debugging parameter to sign and submit RPC calls. --- src/cpp/ripple/RPCHandler.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/cpp/ripple/RPCHandler.cpp b/src/cpp/ripple/RPCHandler.cpp index e6a245aad..65bb7996b 100644 --- a/src/cpp/ripple/RPCHandler.cpp +++ b/src/cpp/ripple/RPCHandler.cpp @@ -296,6 +296,11 @@ Json::Value RPCHandler::transactionSign(Json::Value jvRequest, bool bSubmit) return jvResult; } + if (jvRequest.isMember("debug_signing")) { + jvResult["tx_unsigned"] = strHex(stpTrans->getSerializer().peekData()); + jvResult["tx_signing_hash"] = stpTrans->getSigningHash().ToString(); + } + // FIXME: For performance, transactions should not be signed in this code path. stpTrans->sign(naAccountPrivate); From 54f4edf5ef50fc02d956690f9acc20dea6eb7495 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 1 Feb 2013 22:38:37 +0100 Subject: [PATCH 5/6] Create abstract UInt class for UInt160, UInt256, ... --- src/js/uint.js | 221 ++++++++++++++++++++++++++++++++++++++++++++++ src/js/uint160.js | 119 +++---------------------- src/js/uint256.js | 37 ++++++++ 3 files changed, 271 insertions(+), 106 deletions(-) create mode 100644 src/js/uint.js create mode 100644 src/js/uint256.js diff --git a/src/js/uint.js b/src/js/uint.js new file mode 100644 index 000000000..b989b7e84 --- /dev/null +++ b/src/js/uint.js @@ -0,0 +1,221 @@ + +var sjcl = require('../../build/sjcl'); +var utils = require('./utils'); +var config = require('./config'); +var jsbn = require('./jsbn'); + +var BigInteger = jsbn.BigInteger; +var nbi = jsbn.nbi; + +var Base = require('./base').Base; + +// +// Abstract UInt class +// +// Base class for UInt??? classes +// + +var UInt = function () { + // Internal form: NaN or BigInteger + this._value = NaN; +}; + +UInt.json_rewrite = function (j) { + return this.from_json(j).to_json(); +}; + +// Return a new UInt from j. +UInt.from_generic = function (j) { + if (j instanceof this) { + return j.clone(); + } else { + return (new this()).parse_generic(j); + } +}; + +// Return a new UInt from j. +UInt.from_hex = function (j) { + if (j instanceof this) { + return j.clone(); + } else { + return (new this()).parse_hex(j); + } +}; + +// Return a new UInt from j. +UInt.from_json = function (j) { + if (j instanceof this) { + return j.clone(); + } else { + return (new this()).parse_json(j); + } +}; + +// Return a new UInt from j. +UInt.from_bits = function (j) { + if (j instanceof this) { + return j.clone(); + } else { + return (new this()).parse_bits(j); + } +}; + +// Return a new UInt from j. +UInt.from_bn = function (j) { + if (j instanceof this) { + return j.clone(); + } else { + return (new this()).parse_bn(j); + } +}; + +UInt.is_valid = function (j) { + return this.from_json(j).is_valid(); +}; + +UInt.prototype.clone = function () { + return this.copyTo(new this.constructor()); +}; + +// Returns copy. +UInt.prototype.copyTo = function (d) { + d._value = this._value; + + return d; +}; + +UInt.prototype.equals = function (d) { + return this._value instanceof BigInteger && d._value instanceof BigInteger && this._value.equals(d._value); +}; + +UInt.prototype.is_valid = function () { + return this._value instanceof BigInteger; +}; + +// value = NaN on error. +UInt.prototype.parse_generic = function (j) { + // Canonicalize and validate + if (config.accounts && j in config.accounts) + j = config.accounts[j].account; + + switch (j) { + case undefined: + case "0": + case this.constructor.STR_ZERO: + case this.constructor.ADDRESS_ZERO: + case this.constructor.HEX_ZERO: + this._value = nbi(); + break; + + case "1": + case this.constructor.STR_ONE: + case this.constructor.ADDRESS_ONE: + case this.constructor.HEX_ONE: + this._value = new BigInteger([1]); + + break; + + default: + if ('string' !== typeof j) { + this._value = NaN; + } + else if (j[0] === "r") { + this._value = Base.decode_check(Base.VER_ACCOUNT_ID, j); + } + else if (this.constructor.width === j.length) { + this._value = new BigInteger(utils.stringToArray(j), 256); + } + else if ((this.constructor.width*2) === j.length) { + // XXX Check char set! + this._value = new BigInteger(j, 16); + } + else { + this._value = NaN; + } + } + + return this; +}; + +UInt.prototype.parse_hex = function (j) { + if ('string' === typeof j && + j.length === (this.constructor.width * 2)) { + this._value = new BigInteger(j, 16); + } else { + this._value = NaN; + } + + return this; +}; + +UInt.prototype.parse_bits = function (j) { + if (sjcl.bitArray.bitLength(j) !== this.constructor.width * 8) { + this._value = NaN; + } else { + var bytes = sjcl.codec.bytes.fromBits(j); + this._value = new BigInteger(bytes, 256); + } + + return this; +}; + +UInt.prototype.parse_json = UInt.prototype.parse_hex; + +UInt.prototype.parse_bn = function (j) { + if (j instanceof sjcl.bn && + j.bitLength() <= this.constructor.width * 8) { + var bytes = sjcl.codec.bytes.fromBits(j.toBits()); + this._value = new BigInteger(bytes, 256); + } else { + this._value = NaN; + } + + return this; +}; + +// Convert from internal form. +UInt.prototype.to_bytes = function () { + if (!(this._value instanceof BigInteger)) + return null; + + var bytes = this._value.toByteArray(); + bytes = bytes.map(function (b) { return (b+256) % 256; }); + var target = this.constructor.width; + + // XXX Make sure only trim off leading zeros. + bytes = bytes.slice(-target); + while (bytes.length < target) bytes.unshift(0); + + return bytes; +}; + +UInt.prototype.to_hex = function () { + if (!(this._value instanceof BigInteger)) + return null; + + var bytes = this.to_bytes(); + + return sjcl.codec.hex.fromBits(sjcl.codec.bytes.toBits(bytes)).toUpperCase(); +}; + +UInt.prototype.to_json = UInt.prototype.to_hex; + +UInt.prototype.to_bits = function () { + if (!(this._value instanceof BigInteger)) + return null; + + var bytes = this.to_bytes(); + + return sjcl.codec.bytes.toBits(bytes); +}; + +UInt.prototype.to_bn = function () { + if (!(this._value instanceof BigInteger)) + return null; + + var bits = this.to_bits(); + + return sjcl.bn.fromBits(bits); +}; + +exports.UInt = UInt; diff --git a/src/js/uint160.js b/src/js/uint160.js index befe2f101..b9165e34c 100644 --- a/src/js/uint160.js +++ b/src/js/uint160.js @@ -1,118 +1,35 @@ +var sjcl = require('../../build/sjcl'); var utils = require('./utils'); var config = require('./config'); var jsbn = require('./jsbn'); +var extend = require('extend'); var BigInteger = jsbn.BigInteger; var nbi = jsbn.nbi; -var Base = require('./base').Base; +var UInt = require('./uint').UInt, + Base = require('./base').Base; // // UInt160 support // -var UInt160 = function () { +var UInt160 = extend(function () { // Internal form: NaN or BigInteger this._value = NaN; -}; +}, UInt); + +UInt160.width = 20; +UInt160.prototype = extend({}, UInt.prototype); +UInt160.prototype.constructor = UInt160; -UInt160.ZERO = utils.hexToString("0000000000000000000000000000000000000000"); -UInt160.ONE = utils.hexToString("0000000000000000000000000000000000000001"); var ADDRESS_ZERO = UInt160.ADDRESS_ZERO = "rrrrrrrrrrrrrrrrrrrrrhoLvTp"; var ADDRESS_ONE = UInt160.ADDRESS_ONE = "rrrrrrrrrrrrrrrrrrrrBZbvji"; var HEX_ZERO = UInt160.HEX_ZERO = "0000000000000000000000000000000000000000"; var HEX_ONE = UInt160.HEX_ONE = "0000000000000000000000000000000000000001"; - -UInt160.json_rewrite = function (j) { - return UInt160.from_json(j).to_json(); -}; - -// Return a new UInt160 from j. -UInt160.from_generic = function (j) { - return 'string' === typeof j - ? (new UInt160()).parse_generic(j) - : j.clone(); -}; - -// Return a new UInt160 from j. -UInt160.from_json = function (j) { - if ('string' === typeof j) { - return (new UInt160()).parse_json(j); - } else if (j instanceof UInt160) { - return j.clone(); - } else { - return new UInt160(); - } -}; - -UInt160.is_valid = function (j) { - return UInt160.from_json(j).is_valid(); -}; - -UInt160.prototype.clone = function () { - return this.copyTo(new UInt160()); -}; - -// Returns copy. -UInt160.prototype.copyTo = function (d) { - d._value = this._value; - - return d; -}; - -UInt160.prototype.equals = function (d) { - return this._value instanceof BigInteger && d._value instanceof BigInteger && this._value.equals(d._value); -}; - -UInt160.prototype.is_valid = function () { - return this._value instanceof BigInteger; -}; - -// value = NaN on error. -UInt160.prototype.parse_generic = function (j) { - // Canonicalize and validate - if (config.accounts && j in config.accounts) - j = config.accounts[j].account; - - switch (j) { - case undefined: - case "0": - case UInt160.ZERO: - case ADDRESS_ZERO: - case HEX_ZERO: - this._value = nbi(); - break; - - case "1": - case UInt160.ONE: - case ADDRESS_ONE: - case HEX_ONE: - this._value = new BigInteger([1]); - - break; - - default: - if ('string' !== typeof j) { - this._value = NaN; - } - else if (j[0] === "r") { - this._value = Base.decode_check(Base.VER_ACCOUNT_ID, j); - } - else if (20 === j.length) { - this._value = new BigInteger(utils.stringToArray(j), 256); - } - else if (40 === j.length) { - // XXX Check char set! - this._value = new BigInteger(j, 16); - } - else { - this._value = NaN; - } - } - - return this; -}; +var STR_ZERO = UInt160.STR_ZERO = utils.hexToString(HEX_ZERO); +var STR_ONE = UInt160.STR_ONE = utils.hexToString(HEX_ONE); // value = NaN on error. UInt160.prototype.parse_json = function (j) { @@ -133,22 +50,12 @@ UInt160.prototype.parse_json = function (j) { return this; }; -// Convert from internal form. // XXX Json form should allow 0 and 1, C++ doesn't currently allow it. UInt160.prototype.to_json = function () { if (!(this._value instanceof BigInteger)) return NaN; - var bytes = this._value.toByteArray().map(function (b) { return b ? b < 0 ? 256+b : b : 0}); - var target = 20; - - // XXX Make sure only trim off leading zeros. - var array = bytes.length < target - ? bytes.length - ? [].concat(utils.arraySet(target - bytes.length, 0), bytes) - : utils.arraySet(target, 0) - : bytes.slice(target - bytes.length); - var output = Base.encode_check(Base.VER_ACCOUNT_ID, array); + var output = Base.encode_check(Base.VER_ACCOUNT_ID, this.to_bytes()); return output; }; diff --git a/src/js/uint256.js b/src/js/uint256.js new file mode 100644 index 000000000..ab5b6f70b --- /dev/null +++ b/src/js/uint256.js @@ -0,0 +1,37 @@ + +var sjcl = require('../../build/sjcl'); +var utils = require('./utils'); +var config = require('./config'); +var jsbn = require('./jsbn'); +var extend = require('extend'); + +var BigInteger = jsbn.BigInteger; +var nbi = jsbn.nbi; + +var UInt = require('./uint').UInt, + Base = require('./base').Base; + +// +// UInt256 support +// + +var UInt256 = extend(function () { + // Internal form: NaN or BigInteger + this._value = NaN; +}, UInt); + +UInt256.width = 32; +UInt256.prototype = extend({}, UInt.prototype); +UInt256.prototype.constructor = UInt256; + +// XXX Generate these constants (or remove them) +var ADDRESS_ZERO = UInt256.ADDRESS_ZERO = "XXX"; +var ADDRESS_ONE = UInt256.ADDRESS_ONE = "XXX"; +var HEX_ZERO = UInt256.HEX_ZERO = "00000000000000000000000000000000" + + "00000000000000000000000000000000"; +var HEX_ONE = UInt256.HEX_ONE = "00000000000000000000000000000000" + + "00000000000000000000000000000001"; +var STR_ZERO = UInt256.STR_ZERO = utils.hexToString(HEX_ZERO); +var STR_ONE = UInt256.STR_ONE = utils.hexToString(HEX_ONE); + +exports.UInt256 = UInt256; From 6820c6823d39fbadf9b8c8c105652fe572447bfc Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 1 Feb 2013 22:43:01 +0100 Subject: [PATCH 6/6] Add transaction signing basics. Most fields aren't supported yet and there aren't any nice external APIs, but my test script successfully serializes and signs an XRP Payment, so this seems like a good time to make a commit. --- grunt.js | 3 +- src/js/remote.js | 16 ++- src/js/seed.js | 48 ++++++++- src/js/serializedobject.js | 144 +++++++++++++++++++++++++++ src/js/serializedtypes.js | 124 +++++++++++++++++++++-- src/js/serializer.js | 44 -------- src/js/sjcl-custom/sjcl-ecdsa-der.js | 28 ++++++ src/js/transaction.js | 61 +++++++++--- src/js/utils.js | 6 +- 9 files changed, 401 insertions(+), 73 deletions(-) create mode 100644 src/js/serializedobject.js delete mode 100644 src/js/serializer.js create mode 100644 src/js/sjcl-custom/sjcl-ecdsa-der.js diff --git a/grunt.js b/grunt.js index fdb94be2b..dab1bcef5 100644 --- a/grunt.js +++ b/grunt.js @@ -35,7 +35,8 @@ module.exports = function(grunt) { "src/js/sjcl/core/srp.js", "src/js/sjcl-custom/sjcl-secp256k1.js", "src/js/sjcl-custom/sjcl-ripemd160.js", - "src/js/sjcl-custom/sjcl-extramath.js" + "src/js/sjcl-custom/sjcl-extramath.js", + "src/js/sjcl-custom/sjcl-ecdsa-der.js" ], dest: 'build/sjcl.js' } diff --git a/src/js/remote.js b/src/js/remote.js index f34ed3287..23c2296e8 100644 --- a/src/js/remote.js +++ b/src/js/remote.js @@ -851,6 +851,17 @@ Remote.prototype.request_wallet_accounts = function (seed) { return request; }; +Remote.prototype.request_sign = function (secret, tx_json) { + utils.assert(this.trusted); // Don't send secrets. + + var request = new Request(this, 'sign'); + + request.message.secret = secret; + request.message.tx_json = tx_json; + + return request; +}; + // Submit a transaction. Remote.prototype.submit = function (transaction) { var self = this; @@ -1134,7 +1145,9 @@ Remote.prototype.request_ripple_path_find = function (src_account, dst_account, request.message.source_account = UInt160.json_rewrite(src_account); request.message.destination_account = UInt160.json_rewrite(dst_account); request.message.destination_amount = Amount.json_rewrite(dst_amount); - request.message.source_currencies = source_currencies.map(function (ci) { + + if (source_currencies) { + request.message.source_currencies = source_currencies.map(function (ci) { var ci_new = {}; if ('issuer' in ci) @@ -1145,6 +1158,7 @@ Remote.prototype.request_ripple_path_find = function (src_account, dst_account, return ci_new; }); + } return request; }; diff --git a/src/js/seed.js b/src/js/seed.js index b26f2e2c6..ee519566c 100644 --- a/src/js/seed.js +++ b/src/js/seed.js @@ -2,12 +2,14 @@ // Seed support // +var sjcl = require('../../build/sjcl'); var utils = require('./utils'); var jsbn = require('./jsbn'); var BigInteger = jsbn.BigInteger; -var Base = require('./base').Base; +var Base = require('./base').Base, + UInt256 = require('./uint256').UInt256; var Seed = function () { // Internal form: NaN or BigInteger @@ -20,9 +22,9 @@ Seed.json_rewrite = function (j) { // Return a new Seed from j. Seed.from_json = function (j) { - return 'string' === typeof j - ? (new Seed()).parse_json(j) - : j.clone(); + return (j instanceof Seed) + ? j.clone() + : (new Seed()).parse_json(j); }; Seed.is_valid = function (j) { @@ -86,4 +88,42 @@ Seed.prototype.to_json = function () { return output; }; +function append_int(a, i) { + return [].concat(a, i >> 24, (i >> 16) & 0xff, (i >> 8) & 0xff, i & 0xff); +} + +function firstHalfOfSHA512(bytes) { + return sjcl.bitArray.bitSlice( + sjcl.hash.sha512.hash(sjcl.codec.bytes.toBits(bytes)), + 0, 256 + ); +} + +function SHA256_RIPEMD160(bits) { + return sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits)); +} + +Seed.prototype.generate_private = function (account_id) { + // XXX If account_id is given, should loop over keys until we find the right one + + var seq = 0; + + var private_gen, public_gen, i = 0; + do { + private_gen = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(this.seed, i))); + i++; + } while (!sjcl.ecc.curves.c256.r.greaterEquals(private_gen)); + + public_gen = sjcl.ecc.curves.c256.G.mult(private_gen); + + var sec; + i = 0; + do { + sec = sjcl.bn.fromBits(firstHalfOfSHA512(append_int(append_int(public_gen.toBytesCompressed(), seq), i))); + i++; + } while (!sjcl.ecc.curves.c256.r.greaterEquals(sec)); + + return UInt256.from_bn(sec); +}; + exports.Seed = Seed; diff --git a/src/js/serializedobject.js b/src/js/serializedobject.js new file mode 100644 index 000000000..e66a2001a --- /dev/null +++ b/src/js/serializedobject.js @@ -0,0 +1,144 @@ +var binformat = require('./binformat'), + sjcl = require('../../build/sjcl'), + extend = require('extend'), + stypes = require('./serializedtypes'); + +var UInt256 = require('./uint256').UInt256; + +var SerializedObject = function () { + this.buffer = []; + this.pointer = 0; +}; + +SerializedObject.from_json = function (obj) { + var typedef; + var so = new SerializedObject(); + + // Create a copy of the object so we don't modify it + obj = extend({}, obj); + + if ("number" === typeof obj.TransactionType) { + obj.TransactionType = SerializedObject.lookup_type_tx(obj.TransactionType); + + if (!obj.TransactionType) { + throw new Error("Transaction type ID is invalid."); + } + } + + if ("string" === typeof obj.TransactionType) { + typedef = binformat.tx[obj.TransactionType].slice(); + + obj.TransactionType = typedef.shift(); + } else if ("undefined" !== typeof obj.LedgerEntryType) { + // XXX: TODO + throw new Error("Ledger entry binary format not yet implemented."); + } else throw new Error("Object to be serialized must contain either " + + "TransactionType or LedgerEntryType."); + + so.serialize(typedef, obj); + + return so; +}; + +SerializedObject.prototype.append = function (bytes) { + this.buffer = this.buffer.concat(bytes); + this.pointer += bytes.length; +}; + +SerializedObject.prototype.to_bits = function () +{ + return sjcl.codec.bytes.toBits(this.buffer); +}; + +SerializedObject.prototype.to_hex = function () { + return sjcl.codec.hex.fromBits(this.to_bits()).toUpperCase(); +}; + +SerializedObject.prototype.serialize = function (typedef, obj) +{ + // Ensure canonical order + typedef = SerializedObject._sort_typedef(typedef.slice()); + + // Serialize fields + for (var i = 0, l = typedef.length; i < l; i++) { + var spec = typedef[i]; + this.serialize_field(spec, obj); + } +}; + +SerializedObject.prototype.signing_hash = function (prefix) +{ + var sign_buffer = new SerializedObject(); + stypes.Int32.serialize(sign_buffer, prefix); + sign_buffer.append(this.buffer); + return sign_buffer.hash_sha512_half(); +}; + +SerializedObject.prototype.hash_sha512_half = function () +{ + var bits = sjcl.codec.bytes.toBits(this.buffer), + hash = sjcl.bitArray.bitSlice(sjcl.hash.sha512.hash(bits), 0, 256); + + return UInt256.from_hex(sjcl.codec.hex.fromBits(hash)); +}; + +SerializedObject.prototype.serialize_field = function (spec, obj) +{ + spec = spec.slice(); + + var name = spec.shift(), + presence = spec.shift(), + field_id = spec.shift(), + Type = spec.shift(); + + if ("undefined" !== typeof obj[name]) { + console.log(name, Type.id, field_id); + this.append(SerializedObject.get_field_header(Type.id, field_id)); + + try { + Type.serialize(this, obj[name]); + } catch (e) { + // Add field name to message and rethrow + e.message = "Error serializing '"+name+"': "+e.message; + throw e; + } + } else if (presence === binformat.REQUIRED) { + throw new Error('Missing required field '+name); + } +}; + +SerializedObject.get_field_header = function (type_id, field_id) +{ + var buffer = [0]; + if (type_id > 0xf) buffer.push(type_id & 0xff); + else buffer[0] += (type_id & 0xf) << 4; + + if (field_id > 0xf) buffer.push(field_id & 0xff); + else buffer[0] += field_id & 0xf; + + return buffer; +}; + +function sort_field_compare(a, b) { + // Sort by type id first, then by field id + return a[3].id !== b[3].id ? + a[3].id - b[3].id : + a[2] - b[2]; +}; +SerializedObject._sort_typedef = function (typedef) { + return typedef.sort(sort_field_compare); +}; + +SerializedObject.lookup_type_tx = function (id) { + for (var i in binformat.tx) { + if (!binformat.tx.hasOwnProperty(i)) continue; + + if (binformat.tx[i][0] === id) { + return i; + } + } + + return null; +}; + +exports.SerializedObject = SerializedObject; diff --git a/src/js/serializedtypes.js b/src/js/serializedtypes.js index 11acd294f..2c61b64b3 100644 --- a/src/js/serializedtypes.js +++ b/src/js/serializedtypes.js @@ -1,10 +1,55 @@ -var SerializedType = function () { +/** + * Type definitions for binary format. + * + * This file should not be included directly. Instead, find the format you're + * trying to parse or serialize in binformat.js and pass that to + * SerializedObject.parse() or SerializedObject.serialize(). + */ +var extend = require('extend'), + utils = require('./utils'), + sjcl = require('../../build/sjcl'); + +var amount = require('./amount'), + UInt160 = amount.UInt160, + Amount = amount.Amount; + +// Shortcuts +var hex = sjcl.codec.hex, + bytes = sjcl.codec.bytes; + +var SerializedType = function (methods) { + extend(this, methods); +}; + +SerializedType.prototype.serialize_hex = function (so, hexData) { + var byteData = bytes.fromBits(hex.toBits(hexData)); + this.serialize_varint(so, byteData.length); + so.append(byteData); +}; + +SerializedType.prototype.serialize_varint = function (so, val) { + if (val < 0) { + throw new Error("Variable integers are unsigned."); + } + if (val <= 192) { + so.append([val]); + } else if (val <= 12,480) { + val -= 193; + so.append([193 + (val >>> 8), val & 0xff]); + } else if (val <= 918744) { + val -= 12481; + so.append([ + 241 + (val >>> 16), + val >>> 8 & 0xff, + val & 0xff + ]); + } else throw new Error("Variable integer overflow."); }; exports.Int8 = new SerializedType({ serialize: function (so, val) { - return so.append([val & 0xff]); + so.append([val & 0xff]); }, parse: function (so) { return so.read(1)[0]; @@ -13,7 +58,10 @@ exports.Int8 = new SerializedType({ exports.Int16 = new SerializedType({ serialize: function (so, val) { - // XXX + so.append([ + val >>> 8 & 0xff, + val & 0xff + ]); }, parse: function (so) { // XXX @@ -22,7 +70,12 @@ exports.Int16 = new SerializedType({ exports.Int32 = new SerializedType({ serialize: function (so, val) { - // XXX + so.append([ + val >>> 24 & 0xff, + val >>> 16 & 0xff, + val >>> 8 & 0xff, + val & 0xff + ]); }, parse: function (so) { // XXX @@ -67,7 +120,62 @@ exports.Hash160 = new SerializedType({ exports.Amount = new SerializedType({ serialize: function (so, val) { - // XXX + var amount = Amount.from_json(val); + if (!amount.is_valid()) { + throw new Error("Not a valid Amount object."); + } + + // Amount (64-bit integer) + if (amount.is_native()) { + var valueHex = amount._value.toString(16); + + // Enforce correct length (64 bits) + if (valueHex.length > 16) { + throw new Error('Value out of bounds'); + } + while (valueHex.length < 16) { + valueHex = "0" + valueHex; + } + + var valueBytes = bytes.fromBits(hex.toBits(valueHex)); + // Clear most significant two bits - these bits should already be 0 if + // Amount enforces the range correctly, but we'll clear them anyway just + // so this code can make certain guarantees about the encoded value. + valueBytes[0] &= 0x3f; + if (!amount.is_negative()) valueBytes[0] |= 0x40; + + so.append(valueBytes); + } else { + // XXX + throw new Error("Non-native amounts not implemented!"); + } + + if (!amount.is_native()) { + // Currency (160-bit hash) + var currency = amount.currency().to_json(); + if ("string" === typeof currency && currency.length === 3) { + var currencyCode = currency.toUpperCase(), + currencyData = utils.arraySet(20, 0); + + if (!/^[A-Z]{3}$/.test(currencyCode)) { + throw new Error('Invalid currency code'); + } + + currencyData[12] = currencyCode.charCodeAt(0) & 0xff; + currencyData[13] = currencyCode.charCodeAt(1) & 0xff; + currencyData[14] = currencyCode.charCodeAt(2) & 0xff; + + var currencyBits = bytes.toBits(currencyData), + currencyHash = sjcl.hash.ripemd160.hash(currencyBits); + + so.append(bytes.fromBits(currencyHash)); + } else { + throw new Error('Tried to serialize invalid/unimplemented currency type.'); + } + + // Issuer (160-bit hash) + // XXX + } }, parse: function (so) { // XXX @@ -76,7 +184,8 @@ exports.Amount = new SerializedType({ exports.VariableLength = new SerializedType({ serialize: function (so, val) { - // XXX + if ("string" === typeof val) this.serialize_hex(so, val); + else throw new Error("Unknown datatype."); }, parse: function (so) { // XXX @@ -85,7 +194,8 @@ exports.VariableLength = new SerializedType({ exports.Account = new SerializedType({ serialize: function (so, val) { - // XXX + var account = UInt160.from_json(val); + this.serialize_hex(so, account.to_hex()); }, parse: function (so) { // XXX diff --git a/src/js/serializer.js b/src/js/serializer.js deleted file mode 100644 index 6a75e84ef..000000000 --- a/src/js/serializer.js +++ /dev/null @@ -1,44 +0,0 @@ -// - -var serializer = {}; - -serializer.addUInt16 = function(value) { - switch (typeof value) { - case 'string': - addUInt16(value.charCodeAt(0)); - break; - - case 'integer': - for (i = 16/8; i; i -=1) { - raw.push(value & 255); - value >>= 8; - } - break; - - default: - throw 'UNEXPECTED_TYPE'; - } -}; - -serializer.addUInt160 = function(value) { - switch (typeof value) { - case 'array': - raw.concat(value); - break; - - case 'integer': - for (i = 160/8; i; i -=1) { - raw.push(value & 255); - value >>= 8; - } - break; - - default: - throw 'UNEXPECTED_TYPE'; - } -}; - -serializer.getSHA512Half = function() { -}; - -// vim:sw=2:sts=2:ts=8:et diff --git a/src/js/sjcl-custom/sjcl-ecdsa-der.js b/src/js/sjcl-custom/sjcl-ecdsa-der.js new file mode 100644 index 000000000..befddbf08 --- /dev/null +++ b/src/js/sjcl-custom/sjcl-ecdsa-der.js @@ -0,0 +1,28 @@ +sjcl.ecc.ecdsa.secretKey.prototype.signDER = function(hash, paranoia) { + return this.encodeDER(this.sign(hash, paranoia)); +}; + +sjcl.ecc.ecdsa.secretKey.prototype.encodeDER = function(rs) { + var w = sjcl.bitArray, + R = this._curve.r, + l = R.bitLength(), + r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)).toBits(), + s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)).toBits(); + + var rb = sjcl.codec.bytes.fromBits(r), + sb = sjcl.codec.bytes.fromBits(s); + + var buffer = [].concat( + 0x30, + 4 + rb.length + sb.length, + 0x02, + rb.length, + rb, + 0x02, + sb.length, + sb + ); + + return sjcl.codec.bytes.toBits(buffer); +}; + diff --git a/src/js/transaction.js b/src/js/transaction.js index 3fedb3dca..a2ad368a3 100644 --- a/src/js/transaction.js +++ b/src/js/transaction.js @@ -43,10 +43,16 @@ // - may or may not forward. // -var Amount = require('./amount').Amount; -var Currency = require('./amount').Currency; -var UInt160 = require('./amount').UInt160; -var EventEmitter = require('events').EventEmitter; +var sjcl = require('../../build/sjcl'); + +var Amount = require('./amount').Amount; +var Currency = require('./amount').Currency; +var UInt160 = require('./amount').UInt160; +var Seed = require('./seed').Seed; +var EventEmitter = require('events').EventEmitter; +var SerializedObject = require('./serializedobject').SerializedObject; + +var config = require('./config'); var SUBMIT_MISSING = 4; // Report missing. var SUBMIT_LOST = 8; // Give up tracking. @@ -112,6 +118,11 @@ Transaction.flags = { }, }; +Transaction.formats = require('./binformat').tx; + +Transaction.HASH_SIGN = 0x53545800; +Transaction.HASH_SIGN_TESTNET = 0x73747800; + Transaction.prototype.consts = { 'telLOCAL_ERROR' : -399, 'temMALFORMED' : -299, @@ -156,6 +167,30 @@ Transaction.prototype.set_state = function (state) { } }; +Transaction.prototype.serialize = function () { + return SerializedObject.from_json(this.tx_json); +}; + +Transaction.prototype.signing_hash = function () { + var prefix = config.testnet + ? Transaction.HASH_SIGN_TESTNET + : Transaction.HASH_SIGN; + + return SerializedObject.from_json(this.tx_json).signing_hash(prefix); +}; + +Transaction.prototype.sign = function () { + var seed = Seed.from_json(this._secret), + priv = seed.generate_private(this.tx_json.Account), + hash = this.signing_hash(); + + var key = new sjcl.ecc.ecdsa.secretKey(sjcl.ecc.curves['c256'], priv.to_bn()), + sig = key.signDER(hash.to_bits(), 0), + hex = sjcl.codec.hex.fromBits(sig).toUpperCase(); + + this.tx_json.TxnSignature = hex; +}; + // Submit a transaction to the network. // XXX Don't allow a submit without knowing ledger_index. // XXX Have a network canSubmit(), post events for following. @@ -355,21 +390,21 @@ Transaction.prototype.transfer_rate = function (rate) { // --> flags: undefined, _flag_, or [ _flags_ ] Transaction.prototype.set_flags = function (flags) { if (flags) { - var transaction_flags = Transaction.flags[this.tx_json.TransactionType]; + var transaction_flags = Transaction.flags[this.tx_json.TransactionType]; if (undefined == this.tx_json.Flags) // We plan to not define this field on new Transaction. this.tx_json.Flags = 0; - var flag_set = 'object' === typeof flags ? flags : [ flags ]; + var flag_set = 'object' === typeof flags ? flags : [ flags ]; - for (index in flag_set) { - var flag = flag_set[index]; + for (var index in flag_set) { + if (!flag_set.hasOwnProperty(index)) continue; - if (flag in transaction_flags) - { - this.tx_json.Flags += transaction_flags[flag]; - } - else { + var flag = flag_set[index]; + + if (flag in transaction_flags) { + this.tx_json.Flags += transaction_flags[flag]; + } else { // XXX Immediately report an error or mark it. } } diff --git a/src/js/utils.js b/src/js/utils.js index d337763a6..d1779faf9 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -27,11 +27,11 @@ var trace = function(comment, func) { }; var arraySet = function (count, value) { - var a = new Array(count); - var i; + var i, a = new Array(count); - for (i = 0; i != count; i += 1) + for (i = 0; i < count; i++) { a[i] = value; + } return a; };