From 9d6ccdcab1fc237dbcfae41fc9e0ca1d2b7565ca Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Thu, 20 Mar 2014 05:09:34 -0700 Subject: [PATCH 01/40] [CHORE] Enable signature canonicalization. --- Gruntfile.js | 1 + src/js/ripple/keypair.js | 6 +++-- src/js/ripple/serializedtypes.js | 2 ++ src/js/ripple/transaction.js | 22 ++++++++++------- src/js/sjcl-custom/sjcl-ecdsa-canonical.js | 17 +++++++++++++ test/sjcl-ecdsa-canonical-test.js | 28 ++++++++++++++++++++++ 6 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 src/js/sjcl-custom/sjcl-ecdsa-canonical.js create mode 100644 test/sjcl-ecdsa-canonical-test.js diff --git a/Gruntfile.js b/Gruntfile.js index 462a485f..a1b1bf5e 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -41,6 +41,7 @@ module.exports = function(grunt) { "src/js/sjcl-custom/sjcl-extramath.js", "src/js/sjcl-custom/sjcl-montgomery.js", "src/js/sjcl-custom/sjcl-validecc.js", + "src/js/sjcl-custom/sjcl-ecdsa-canonical.js", "src/js/sjcl-custom/sjcl-ecdsa-der.js", "src/js/sjcl-custom/sjcl-jacobi.js" ], diff --git a/src/js/ripple/keypair.js b/src/js/ripple/keypair.js index db82e79d..518ae70d 100644 --- a/src/js/ripple/keypair.js +++ b/src/js/ripple/keypair.js @@ -89,8 +89,10 @@ KeyPair.prototype.get_address = function () { }; KeyPair.prototype.sign = function (hash) { - var hash = UInt256.from_json(hash); - return this._secret.signDER(hash.to_bits(), 0); + hash = UInt256.from_json(hash); + var sig = this._secret.sign(hash.to_bits(), 0); + sig = this._secret.canonicalizeSignature(sig); + return this._secret.encodeDER(sig); }; exports.KeyPair = KeyPair; diff --git a/src/js/ripple/serializedtypes.js b/src/js/ripple/serializedtypes.js index a79d3535..cdca9fe6 100644 --- a/src/js/ripple/serializedtypes.js +++ b/src/js/ripple/serializedtypes.js @@ -120,6 +120,8 @@ SerializedType.prototype.parse_varint = function (so) { * The result is appended to the serialized object ('so'). */ function append_byte_array(so, val, bytes) { + val = val >>> 0; + if (!isNumber(val)) { throw new Error('Value is not a number'); } diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index 58dcc131..da02827e 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -62,7 +62,9 @@ function Transaction(remote) { this.remote = remote; // Transaction data - this.tx_json = { Flags: 0 }; + this.tx_json = { + Flags: Transaction.defaultFlags + }; this._secret = void(0); this._build_path = false; @@ -80,15 +82,10 @@ function Transaction(remote) { this.submittedIDs = [ ] function finalize(message) { - if (self.result) { - self.result.ledger_index = message.ledger_index; - self.result.ledger_hash = message.ledger_hash; - } else { - self.result = message; - self.result.tx_json = self.tx_json; + if (!self.finalized) { + self.finalized = true; + self.emit('cleanup', message); } - - self.emit('cleanup', message); }; this.once('success', function(message) { @@ -120,6 +117,11 @@ Transaction.fee_units = { }; Transaction.flags = { + // Universal flags can apply to any transaction type + Universal: { + FullyCanonicalSig: 0x80000000 + }, + AccountSet: { RequireDestTag: 0x00010000, OptionalDestTag: 0x00020000, @@ -149,6 +151,8 @@ Transaction.flags = { } }; +Transaction.defaultFlags = 0 | Transaction.flags.Universal.FullyCanonicalSig; + Transaction.formats = require('./binformat').tx; Transaction.prototype.consts = { diff --git a/src/js/sjcl-custom/sjcl-ecdsa-canonical.js b/src/js/sjcl-custom/sjcl-ecdsa-canonical.js new file mode 100644 index 00000000..d56c5114 --- /dev/null +++ b/src/js/sjcl-custom/sjcl-ecdsa-canonical.js @@ -0,0 +1,17 @@ +sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature = function(rs) { + var w = sjcl.bitArray, + R = this._curve.r, + l = R.bitLength(); + + var r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)), + s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)); + + // For a canonical signature we want the lower of two possible values for s + // 0 < s <= n/2 + if (!R.copy().halveM().greaterEquals(s)) { + s = R.sub(s); + } + + return w.concat(r.toBits(l), s.toBits(l)); +}; + diff --git a/test/sjcl-ecdsa-canonical-test.js b/test/sjcl-ecdsa-canonical-test.js new file mode 100644 index 00000000..d0a270b7 --- /dev/null +++ b/test/sjcl-ecdsa-canonical-test.js @@ -0,0 +1,28 @@ +var assert = require('assert'); +var utils = require('./testutils'); +var sjcl = require('../build/sjcl'); +var Seed = require('../src/js/ripple/seed').Seed; + +describe('SJCL ECDSA Canonicalization', function() { + describe('canonicalizeSignature', function() { + it('should canonicalize non-canonical signatures', function () { + var seed = Seed.from_json('saESc82Vun7Ta5EJRzGJbrXb5HNYk'); + var key = seed.get_key('rBZ4j6MsoctipM6GEyHSjQKzXG3yambDnZ'); + + var rs = sjcl.codec.hex.toBits("27ce1b914045ba7e8c11a2f2882cb6e07a19d4017513f12e3e363d71dc3fff0fb0a0747ecc7b4ca46e45b3b32b6b2a066aa0249c027ef11e5bce93dab756549c"); + rs = sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature.call(key._secret, rs); + assert.strictEqual(sjcl.codec.hex.fromBits(rs), "27ce1b914045ba7e8c11a2f2882cb6e07a19d4017513f12e3e363d71dc3fff0f4f5f8b813384b35b91ba4c4cd494d5f8500eb84aacc9af1d6403cab218dfeca5"); + }); + + it('should not touch canonical signatures', function () { + var seed = Seed.from_json('saESc82Vun7Ta5EJRzGJbrXb5HNYk'); + var key = seed.get_key('rBZ4j6MsoctipM6GEyHSjQKzXG3yambDnZ'); + + var rs = sjcl.codec.hex.toBits("5c32bc2b4d34e27af9fb66eeea0f47f6afb3d433658af0f649ebae7b872471ab7d23860688aaf9d8131f84cfffa6c56bf9c32fd8b315b2ef9d6bcb243f7a686c"); + rs = sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature.call(key._secret, rs); + assert.strictEqual(sjcl.codec.hex.fromBits(rs), "5c32bc2b4d34e27af9fb66eeea0f47f6afb3d433658af0f649ebae7b872471ab7d23860688aaf9d8131f84cfffa6c56bf9c32fd8b315b2ef9d6bcb243f7a686c"); + }); + }); +}); + +// vim:sw=2:sts=2:ts=8:et From 11540f8cd9a8dc40710f6247890c0bc62f520da7 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Thu, 20 Mar 2014 17:21:32 -0700 Subject: [PATCH 02/40] [CHORE] Allow integer strings for server "port" setting. --- src/js/ripple/server.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/js/ripple/server.js b/src/js/ripple/server.js index 1b393503..f7846e86 100644 --- a/src/js/ripple/server.js +++ b/src/js/ripple/server.js @@ -36,8 +36,17 @@ function Server(remote, opts) { throw new Error('Server host is malformed, use "host" and "port" server configuration'); } - if (typeof opts.port !== 'number') { - throw new TypeError('Server configuration "port" is not a Number'); + // We want to allow integer strings as valid port numbers for backward + // compatibility. + if (typeof opts.port === 'string') { + opts.port = parseFloat(opts.port); + } + + if (typeof opts.port !== 'number' || + opts.port >>> 0 !== parseFloat(opts.port) || // is integer? + opts.port < 1 || + opts.port > 65535) { + throw new TypeError('Server "port" must be an integer in range 1-65535'); } if (typeof opts.secure !== 'boolean') { From 5f677a86a7811596ee2c5b693de2e9591d4ec377 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Thu, 20 Mar 2014 17:22:10 -0700 Subject: [PATCH 03/40] [CHORE] Update SJCL. --- build/sjcl.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/build/sjcl.js b/build/sjcl.js index 8e2c9a2a..b2c436dc 100644 --- a/build/sjcl.js +++ b/build/sjcl.js @@ -4043,6 +4043,24 @@ sjcl.ecc.ecdsa.publicKey.prototype = { } }; +sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature = function(rs) { + var w = sjcl.bitArray, + R = this._curve.r, + l = R.bitLength(); + + var r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)), + s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)); + + // For a canonical signature we want the lower of two possible values for s + // 0 < s <= n/2 + if (!R.copy().halveM().greaterEquals(s)) { + s = R.sub(s); + } + + return w.concat(r.toBits(l), s.toBits(l)); +}; + + sjcl.ecc.ecdsa.secretKey.prototype.signDER = function(hash, paranoia) { return this.encodeDER(this.sign(hash, paranoia)); }; From c808cb0a1cbc5f16d2bcd7eb00c6c1ded4ff0a54 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 21 Mar 2014 18:43:22 -0700 Subject: [PATCH 04/40] [CHORE] Add ability to apply demurrage at the time of product/ratio calculation. --- src/js/ripple/amount.js | 80 +++++++++++++++++++++++++++++++--------- test/amount-test.js | 81 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 17 deletions(-) diff --git a/src/js/ripple/amount.js b/src/js/ripple/amount.js index d5dfc5ba..8d4fd3eb 100644 --- a/src/js/ripple/amount.js +++ b/src/js/ripple/amount.js @@ -349,9 +349,14 @@ Amount.prototype.divide = function (d) { * * @this {Amount} The numerator (top half) of the fraction. * @param {Amount} denominator The denominator (bottom half) of the fraction. + * @param opts Options for the calculation. + * @param opts.reference_date {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. * @return {Amount} The resulting ratio. Unit will be the same as numerator. */ -Amount.prototype.ratio_human = function (denominator) { +Amount.prototype.ratio_human = function (denominator, opts) { + opts = opts || {}; + if (typeof denominator === 'number' && parseInt(denominator, 10) === denominator) { // Special handling of integer arguments denominator = Amount.from_json('' + denominator + '.0'); @@ -367,6 +372,14 @@ Amount.prototype.ratio_human = function (denominator) { return Amount.NaN(); } + // Apply interest/demurrage + // + // We only need to apply it to the second factor, because the currency unit of + // the first factor will carry over into the result. + if (opts.reference_date) { + denominator = denominator.applyInterest(opts.reference_date); + } + // Special case: The denominator is a native (XRP) amount. // // In that case, it's going to be expressed as base units (1 XRP = @@ -402,9 +415,14 @@ Amount.prototype.ratio_human = function (denominator) { * * @this {Amount} The first factor of the product. * @param {Amount} factor The second factor of the product. + * @param opts Options for the calculation. + * @param opts.reference_date {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. * @return {Amount} The product. Unit will be the same as the first factor. */ -Amount.prototype.product_human = function (factor) { +Amount.prototype.product_human = function (factor, opts) { + opts = opts || {}; + if (typeof factor === 'number' && parseInt(factor, 10) === factor) { // Special handling of integer arguments factor = Amount.from_json(String(factor) + '.0'); @@ -417,6 +435,14 @@ Amount.prototype.product_human = function (factor) { return Amount.NaN(); } + // Apply interest/demurrage + // + // We only need to apply it to the second factor, because the currency unit of + // the first factor will carry over into the result. + if (opts.reference_date) { + factor = factor.applyInterest(opts.reference_date); + } + var product = this.multiply(factor); // Special case: The second factor is a native (XRP) amount expressed as base @@ -850,6 +876,39 @@ Amount.prototype.to_text = function (allow_nan) { return result; }; +/** + * Calculate present value based on currency and a reference date. + * + * This only affects demurraging and interest-bearing currencies. + * + * User should not store amount objects after the interest is applied. This is + * intended by display functions such as toHuman(). + * + * @param referenceDate {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. + * @return {Amount} The amount with interest applied. + */ +Amount.prototype.applyInterest = function (referenceDate) { + if (this._currency.has_interest()) { + var interest = this._currency.get_interest_at(referenceDate); + + // XXX Because the Amount parsing routines don't support some of the things + // that JavaScript can output when casting a float to a string, the + // following call sometimes does not produce a valid Amount. + // + // The correct way to solve this is probably to switch to a proper + // BigDecimal for our internal representation and then use that across + // the board instead of instantiating these dummy Amount objects. + var interestTempAmount = Amount.from_json(""+interest+"/1/1"); + + if (interestTempAmount.is_valid()) { + return this.multiply(interestTempAmount); + } + } else { + return this; + } +}; + /** * Format only value in a human-readable format. * @@ -885,21 +944,8 @@ Amount.prototype.to_human = function (opts) { // Apply demurrage/interest var ref = this; - if (opts.reference_date && this._currency.has_interest()) { - var interest = this._currency.get_interest_at(opts.reference_date); - - // XXX Because the Amount parsing routines don't support some of the things - // that JavaScript can output when casting a float to a string, the - // following call sometimes does not produce a valid Amount. - // - // The correct way to solve this is probably to switch to a proper - // BigDecimal for our internal representation and then use that across - // the board instead of instantiating these dummy Amount objects. - var interestTempAmount = Amount.from_json(""+interest+"/1/1"); - - if (interestTempAmount.is_valid()) { - ref = this.multiply(interestTempAmount); - } + if (opts.reference_date) { + ref = this.applyInterest(opts.reference_date); } var order = ref._is_native ? consts.xns_precision : -ref._offset; diff --git a/test/amount-test.js b/test/amount-test.js index 51c40282..7ac14854 100644 --- a/test/amount-test.js +++ b/test/amount-test.js @@ -431,4 +431,85 @@ describe('Amount', function() { assert.strictEqual(a.not_equals_why(b), 'Native mismatch.'); }); }); + + describe('product_human', function() { + it('Multiply 0 XRP with 0 XRP', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 0 USD with 0 XRP', function () { + assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 0 XRP with 0 USD', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply 1 XRP with 0 XRP', function () { + assert.strictEqual('0/XRP', Amount.from_json('1').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 1 USD with 0 XRP', function () { + assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('0')).to_text_full()); + }); + it('Multiply 1 XRP with 0 USD', function () { + assert.strictEqual('0/XRP', Amount.from_json('1').product_human(Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply 0 XRP with 1 XRP', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('1')).to_text_full()); + }); + it('Multiply 0 USD with 1 XRP', function () { + assert.strictEqual('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('0/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('1')).to_text_full()); + }); + it('Multiply 0 XRP with 1 USD', function () { + assert.strictEqual('0/XRP', Amount.from_json('0').product_human(Amount.from_json('1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD', function () { + assert.equal('0.002/XRP', Amount.from_json('200').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD', function () { + assert.strictEqual('0.2/XRP', Amount.from_json('20000').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD', function () { + assert.strictEqual('20/XRP', Amount.from_json('2000000').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD, neg', function () { + assert.strictEqual('-0.002/XRP', Amount.from_json('200').product_human(Amount.from_json('-10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply XRP with USD, neg, frac', function () { + assert.strictEqual('-0.222/XRP', Amount.from_json('-6000').product_human(Amount.from_json('37/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply USD with USD', function () { + assert.strictEqual('20000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('10/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply USD with USD', function () { + assert.strictEqual('200000000000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('2000000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('100000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply EUR with USD, result < 1', function () { + assert.strictEqual('100000/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh', Amount.from_json('100/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('1000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full()); + }); + it('Multiply EUR with USD, neg', function () { + assert.strictEqual(Amount.from_json('-24000/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full(), '-48000000/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with USD, neg, <1', function () { + assert.strictEqual(Amount.from_json('0.1/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('-1000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh')).to_text_full(), '-100/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with XRP, factor < 1', function () { + assert.strictEqual(Amount.from_json('0.05/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('2000')).to_text_full(), '0.0001/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with XRP, neg', function () { + assert.strictEqual(Amount.from_json('-100/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('5')).to_text_full(), '-0.0005/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply EUR with XRP, neg, <1', function () { + assert.strictEqual(Amount.from_json('-0.05/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('2000')).to_text_full(), '-0.0001/EUR/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Multiply XRP with XRP', function () { + assert.strictEqual(Amount.from_json('10000000').product_human(Amount.from_json('10')).to_text_full(), '0.0001/XRP'); + }); + it('Multiply USD with XAU (dem)', function () { + assert.strictEqual(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').product_human(Amount.from_json('10/015841551A748AD2C1F76FF6ECB0CCCD00000000/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'), {reference_date: 443845330 + 31535000}).to_text_full(), '19900.00316303882/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + }); + + describe('ratio_human', function() { + it('Divide USD by XAU (dem)', function () { + assert.strictEqual(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').ratio_human(Amount.from_json('10/015841551A748AD2C1F76FF6ECB0CCCD00000000/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'), {reference_date: 443845330 + 31535000}).to_text_full(), '201.0049931765529/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + }); }); From 6f5cf8506fa1076c7266f8df2fafd1a18555816b Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 21 Mar 2014 23:56:13 -0700 Subject: [PATCH 05/40] [CHORE] Better variable names in Amount#parse_quality. --- src/js/ripple/amount.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/js/ripple/amount.js b/src/js/ripple/amount.js index 8d4fd3eb..05d3d12c 100644 --- a/src/js/ripple/amount.js +++ b/src/js/ripple/amount.js @@ -71,8 +71,8 @@ Amount.from_json = function (j) { return (new Amount()).parse_json(j); }; -Amount.from_quality = function (q, c, i) { - return (new Amount()).parse_quality(q, c, i); +Amount.from_quality = function (quality, currency, issuer) { + return (new Amount()).parse_quality(quality, currency, issuer); }; Amount.from_human = function (j, opts) { @@ -645,12 +645,12 @@ Amount.prototype.parse_issuer = function (issuer) { }; // --> h: 8 hex bytes quality or 32 hex bytes directory index. -Amount.prototype.parse_quality = function (q, c, i) { +Amount.prototype.parse_quality = function (quality, currency, issuer) { this._is_negative = false; - this._value = new BigInteger(q.substring(q.length-14), 16); - this._offset = parseInt(q.substring(q.length-16, q.length-14), 16)-100; - this._currency = Currency.from_json(c); - this._issuer = UInt160.from_json(i); + this._value = new BigInteger(quality.substring(quality.length-14), 16); + this._offset = parseInt(quality.substring(quality.length-16, quality.length-14), 16)-100; + this._currency = Currency.from_json(currency); + this._issuer = UInt160.from_json(issuer); this._is_native = this._currency.is_native(); this.canonicalize(); From 893fc4c168c0543ae28301a90af1f191402ccd81 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Sat, 22 Mar 2014 00:47:41 -0700 Subject: [PATCH 06/40] [CHORE] Add Amount#invert mathematical utility function. --- src/js/ripple/amount.js | 23 +++++++++++++++++++++++ test/amount-test.js | 12 ++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/js/ripple/amount.js b/src/js/ripple/amount.js index 05d3d12c..23b2e995 100644 --- a/src/js/ripple/amount.js +++ b/src/js/ripple/amount.js @@ -159,6 +159,29 @@ Amount.prototype.add = function (v) { return result; }; +/** + * Turn this amount into its inverse. + * + * @private + */ +Amount.prototype._invert = function () { + this._value = consts.bi_1e32.divide(this._value); + this._offset = -32 - this._offset; + this.canonicalize(); + + return this; +}; + +/** + * Return the inverse of this amount. + * + * @return {Amount} New Amount object with same currency and issuer, but the + * inverse of the value. + */ +Amount.prototype.invert = function () { + return this.copy()._invert(); +}; + Amount.prototype.canonicalize = function () { if (!(this._value instanceof BigInteger)) { // NaN. diff --git a/test/amount-test.js b/test/amount-test.js index 7ac14854..f4b73f9e 100644 --- a/test/amount-test.js +++ b/test/amount-test.js @@ -512,4 +512,16 @@ describe('Amount', function() { assert.strictEqual(Amount.from_json('2000/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').ratio_human(Amount.from_json('10/015841551A748AD2C1F76FF6ECB0CCCD00000000/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'), {reference_date: 443845330 + 31535000}).to_text_full(), '201.0049931765529/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); }); }); + + describe('_invert', function() { + it('Invert 1', function () { + assert.strictEqual(Amount.from_json('1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').invert().to_text_full(), '1/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Invert 20', function () { + assert.strictEqual(Amount.from_json('20/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').invert().to_text_full(), '0.05/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + it('Invert 0.02', function () { + assert.strictEqual(Amount.from_json('0.02/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').invert().to_text_full(), '50/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); + }); + }); }); From 716fd0b938db9f34f7bac709d39315bdd1093874 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Sat, 22 Mar 2014 02:38:03 -0700 Subject: [PATCH 07/40] [CHORE] Improved Amount#parse_quality w/ demurrage support, drops->XRP, etc. Amount#parse_quality is made currency-aware. This allows it to adjust for XRP as the base currency, as well as for interest-bearing or demurring base currencies. --- src/js/ripple/amount.js | 76 +++++++++++++++++++++++++++++++++++--- src/js/ripple/orderbook.js | 5 ++- test/amount-test.js | 33 +++++++++++++++++ 3 files changed, 106 insertions(+), 8 deletions(-) diff --git a/src/js/ripple/amount.js b/src/js/ripple/amount.js index 23b2e995..1d4440ad 100644 --- a/src/js/ripple/amount.js +++ b/src/js/ripple/amount.js @@ -71,8 +71,8 @@ Amount.from_json = function (j) { return (new Amount()).parse_json(j); }; -Amount.from_quality = function (quality, currency, issuer) { - return (new Amount()).parse_quality(quality, currency, issuer); +Amount.from_quality = function (quality, currency, issuer, opts) { + return (new Amount()).parse_quality(quality, currency, issuer, opts); }; Amount.from_human = function (j, opts) { @@ -667,17 +667,81 @@ Amount.prototype.parse_issuer = function (issuer) { return this; }; -// --> h: 8 hex bytes quality or 32 hex bytes directory index. -Amount.prototype.parse_quality = function (quality, currency, issuer) { +/** + * Decode a price from a BookDirectory index. + * + * BookDirectory ledger entries each encode the offer price in their index. This + * method can decode that information and populate an Amount object with it. + * + * It is possible not to provide a currency or issuer, but be aware that Amount + * objects behave differently based on the currency, so you may get incorrect + * results. + * + * Prices involving demurraging currencies are tricky, since they depend on the + * base and counter currencies. + * + * @param quality {String} 8 hex bytes quality or 32 hex bytes BookDirectory + * index. + * @param counterCurrency {Currency|String} Currency of the resulting Amount + * object. + * @param counterIssuer {Issuer|String} Issuer of the resulting Amount object. + * @param opts Additional options + * @param opts.inverse {Boolean} If true, return the inverse of the price + * encoded in the quality. + * @param opts.base_currency {Currency|String} The other currency. This plays a + * role with interest-bearing or demurrage currencies. In that case the + * demurrage has to be applied when the quality is decoded, otherwise the + * price will be false. + * @param opts.reference_date {Date|Number} Date based on which demurrage/interest + * should be applied. Can be given as JavaScript Date or int for Ripple epoch. + * @param opts.xrp_as_drops {Boolean} Whether XRP amount should be treated as + * drops. When the base currency is XRP, the quality is calculated in drops. + * For human use however, we want to think of 1000000 drops as 1 XRP and + * prices as per-XRP instead of per-drop. + */ +Amount.prototype.parse_quality = function (quality, counterCurrency, counterIssuer, opts) +{ + opts = opts || {}; + + var baseCurrency = Currency.from_json(opts.base_currency); + this._is_negative = false; this._value = new BigInteger(quality.substring(quality.length-14), 16); this._offset = parseInt(quality.substring(quality.length-16, quality.length-14), 16)-100; - this._currency = Currency.from_json(currency); - this._issuer = UInt160.from_json(issuer); + this._currency = Currency.from_json(counterCurrency); + this._issuer = UInt160.from_json(counterIssuer); this._is_native = this._currency.is_native(); + // Correct offset if xrp_as_drops option is not set and base currency is XRP + if (!opts.xrp_as_drops && + baseCurrency.is_valid() && + baseCurrency.is_native()) { + if (opts.inverse) { + this._offset -= 6; + } else { + this._offset += 6; + } + } + + if (opts.inverse) { + this._invert(); + } + this.canonicalize(); + if (opts.reference_date && baseCurrency.is_valid() && baseCurrency.has_interest()) { + var interest = baseCurrency.get_interest_at(opts.reference_date); + + // XXX If we had better math utilities, we wouldn't need this hack. + var interestTempAmount = Amount.from_json(""+interest+"/1/1"); + + if (interestTempAmount.is_valid()) { + var v = this.divide(interestTempAmount); + this._value = v._value; + this._offset = v._offset; + } + } + return this; } diff --git a/src/js/ripple/orderbook.js b/src/js/ripple/orderbook.js index d51f3e72..d667289b 100644 --- a/src/js/ripple/orderbook.js +++ b/src/js/ripple/orderbook.js @@ -225,11 +225,12 @@ OrderBook.prototype.notify = function (message) { break; case 'CreatedNode': - var price = Amount.from_json(an.fields.TakerPays).ratio_human(an.fields.TakerGets); + // XXX Should use Amount#from_quality + var price = Amount.from_json(an.fields.TakerPays).ratio_human(an.fields.TakerGets, {reference_date: new Date()}); for (i = 0, l = self._offers.length; i < l; i++) { offer = self._offers[i]; - var priceItem = Amount.from_json(offer.TakerPays).ratio_human(offer.TakerGets); + var priceItem = Amount.from_json(offer.TakerPays).ratio_human(offer.TakerGets, {reference_date: new Date()}); if (price.compareTo(priceItem) <= 0) { var obj = an.fields; diff --git a/test/amount-test.js b/test/amount-test.js index f4b73f9e..0630470c 100644 --- a/test/amount-test.js +++ b/test/amount-test.js @@ -524,4 +524,37 @@ describe('Amount', function() { assert.strictEqual(Amount.from_json('0.02/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh').invert().to_text_full(), '50/USD/rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh'); }); }); + + describe('from_quality', function() { + it('BTC/XRP', function () { + assert.strictEqual(Amount.from_quality('7B73A610A009249B0CC0D4311E8BA7927B5A34D86634581C5F0FF9FF678E1000', 'XRP', NaN, {base_currency: 'BTC'}).to_text_full(), '44,970/XRP'); + }); + it('BTC/XRP inverse', function () { + assert.strictEqual(Amount.from_quality('37AAC93D336021AE94310D0430FFA090F7137C97D473488C4A0918D0DEF8624E', 'XRP', NaN, {inverse: true, base_currency: 'BTC'}).to_text_full(), '39,053.954453/XRP'); + }); + it('XRP/USD', function () { + assert.strictEqual(Amount.from_quality('DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4D05DCAA8FE12000', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {base_currency: 'XRP'}).to_text_full(), '0.0165/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'); + }); + it('XRP/USD inverse', function () { + assert.strictEqual(Amount.from_quality('4627DFFCFF8B5A265EDBD8AE8C14A52325DBFEDAF4F5C32E5C22A840E27DCA9B', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {inverse: true, base_currency: 'XRP'}).to_text_full(), '0.010251/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'); + }); + it('BTC/USD', function () { + assert.strictEqual(Amount.from_quality('6EAB7C172DEFA430DBFAD120FDC373B5F5AF8B191649EC9858038D7EA4C68000', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {base_currency: 'BTC'}).to_text_full(), '1000/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'); + }); + it('BTC/USD inverse', function () { + assert.strictEqual(Amount.from_quality('20294C923E80A51B487EB9547B3835FD483748B170D2D0A455071AFD498D0000', 'USD', 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', {inverse: true, base_currency: 'BTC'}).to_text_full(), '0.5/USD/rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B'); + }); + it('XAU(dem)/XRP', function () { + assert.strictEqual(Amount.from_quality('587322CCBDE0ABD01704769A73A077C32FB39057D813D4165F1FF973CAF997EF', 'XRP', NaN, {base_currency: '015841551A748AD2C1F76FF6ECB0CCCD00000000', reference_date: 443845330 + 31535000}).to_text_full(), '90,452.246928/XRP'); + }); + it('XAU(dem)/XRP inverse', function () { + assert.strictEqual(Amount.from_quality('F72C7A9EAE4A45ED1FB547AD037D07B9B965C6E662BEBAFA4A03F2A976804235', 'XRP', NaN, {inverse: true, base_currency: '015841551A748AD2C1F76FF6ECB0CCCD00000000', reference_date: 443845330 + 31535000}).to_text_full(), '90,442.196677/XRP'); + }); + it('USD/XAU(dem)', function () { + assert.strictEqual(Amount.from_quality('4743E58E44974B325D42FD2BB683A6E36950F350EE46DD3A521B644B99782F5F', '015841551A748AD2C1F76FF6ECB0CCCD00000000', 'rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN', {base_currency: 'USD', reference_date: 443845330 + 31535000}).to_text_full(), '0.007710100231303007/015841551A748AD2C1F76FF6ECB0CCCD00000000/rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN'); + }); + it('USD/XAU(dem) inverse', function () { + assert.strictEqual(Amount.from_quality('CDFD3AFB2F8C5DBEF75B081F7C957FF5509563266F28F36C5704A0FB0BAD8800', '015841551A748AD2C1F76FF6ECB0CCCD00000000', 'rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN', {inverse: true, base_currency: 'USD', reference_date: 443845330 + 31535000}).to_text_full(), '0.007675186123263489/015841551A748AD2C1F76FF6ECB0CCCD00000000/rUyPiNcSFFj6uMR2gEaD8jUerQ59G1qvwN'); + }); + }); }); From 87ba2abc9a2726ab18fd4f0de5a4ddb17ea77f6a Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Sat, 22 Mar 2014 03:18:11 -0700 Subject: [PATCH 08/40] [BUG] Undo previous commit making append_byte_array too loose. This commit introduces an alternative way of setting the canonical signature flag, without compromising the strictness of append_byte_array input sanitizing. --- src/js/ripple/serializedtypes.js | 2 -- src/js/ripple/transaction.js | 19 +++++++++++++------ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/js/ripple/serializedtypes.js b/src/js/ripple/serializedtypes.js index cdca9fe6..a79d3535 100644 --- a/src/js/ripple/serializedtypes.js +++ b/src/js/ripple/serializedtypes.js @@ -120,8 +120,6 @@ SerializedType.prototype.parse_varint = function (so) { * The result is appended to the serialized object ('so'). */ function append_byte_array(so, val, bytes) { - val = val >>> 0; - if (!isNumber(val)) { throw new Error('Value is not a number'); } diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index da02827e..f50b74ab 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -62,9 +62,7 @@ function Transaction(remote) { this.remote = remote; // Transaction data - this.tx_json = { - Flags: Transaction.defaultFlags - }; + this.tx_json = { Flags: 0 }; this._secret = void(0); this._build_path = false; @@ -75,11 +73,13 @@ function Transaction(remote) { // Index at which transaction was submitted this.submitIndex = void(0); + this.canonical = true; + // We aren't clever enough to eschew preventative measures so we keep an array // of all submitted transactionIDs (which can change due to load_factor // effecting the Fee amount). This should be populated with a transactionID // any time it goes on the network - this.submittedIDs = [ ] + this.submittedIDs = [ ]; function finalize(message) { if (!self.finalized) { @@ -151,8 +151,6 @@ Transaction.flags = { } }; -Transaction.defaultFlags = 0 | Transaction.flags.Universal.FullyCanonicalSig; - Transaction.formats = require('./binformat').tx; Transaction.prototype.consts = { @@ -282,6 +280,15 @@ Transaction.prototype.complete = function() { this.tx_json.SigningPubKey = key.to_hex_pub(); } + // Set canonical flag - this enables canonicalized signature checking + if (this.canonical) { + this.tx_json.Flags |= Transaction.flags.Universal.FullyCanonicalSig; + + // JavaScript converts operands to 32-bit signed ints before doing bitwise + // operations. We need to convert it back to an unsigned int. + this.tx_json.Flags = this.tx_json.Flags >>> 0; + } + return this.tx_json; }; From 58afce517a7311bec990eed786e0ca824f982ce0 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Mon, 24 Mar 2014 04:47:37 +0100 Subject: [PATCH 09/40] [DOCS] Add npm badge to README. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9eb999f4..695bee1b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ #The Ripple JavaScript Library +[![NPM](https://nodei.co/npm/ripple-lib.png)](https://www.npmjs.org/package/ripple-lib) + `ripple-lib` connects to the Ripple network via the WebSocket protocol and runs in Node.js as well as in the browser. **Use ripple-lib for** From 0de7d84862a9bd165365527e089bb5289c44e26e Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Tue, 25 Mar 2014 17:23:28 -0700 Subject: [PATCH 10/40] Use LRU cache API to prevent multiple transaction events for the same transaction --- src/js/ripple/remote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/ripple/remote.js b/src/js/ripple/remote.js index afe54293..eee720a2 100644 --- a/src/js/ripple/remote.js +++ b/src/js/ripple/remote.js @@ -529,7 +529,7 @@ Remote.prototype._handleMessage = function(message, server) { // De-duplicate transactions that are immediately following each other var hash = message.transaction.hash; - if (this._received_tx.hasOwnProperty(hash)) { + if (this._received_tx.get(hash)) { break; } From be33b1be6003c46f52e9882eb388ed811e565259 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Thu, 10 Apr 2014 08:15:52 -0700 Subject: [PATCH 11/40] [BUG] Don't set canonical flag when remote signing. --- src/js/ripple/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index f50b74ab..154bec8a 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -281,7 +281,7 @@ Transaction.prototype.complete = function() { } // Set canonical flag - this enables canonicalized signature checking - if (this.canonical) { + if (this.remote.local_signing && this.canonical) { this.tx_json.Flags |= Transaction.flags.Universal.FullyCanonicalSig; // JavaScript converts operands to 32-bit signed ints before doing bitwise From b14fab8aa72c345bd74647278062427ce54afc74 Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Thu, 10 Apr 2014 23:29:16 -0700 Subject: [PATCH 12/40] Check remote exists in transaction.complete --- src/js/ripple/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index 154bec8a..fb00bec3 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -281,7 +281,7 @@ Transaction.prototype.complete = function() { } // Set canonical flag - this enables canonicalized signature checking - if (this.remote.local_signing && this.canonical) { + if (this.remote && this.remote.local_signing && this.canonical) { this.tx_json.Flags |= Transaction.flags.Universal.FullyCanonicalSig; // JavaScript converts operands to 32-bit signed ints before doing bitwise From 1e3c96b14ff676cf1dbea760f61218a8d8fa6537 Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Tue, 15 Apr 2014 12:27:33 -0700 Subject: [PATCH 13/40] Fix transaction finalize --- src/js/ripple/transaction.js | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index fb00bec3..4a9cc1d6 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -81,23 +81,16 @@ function Transaction(remote) { // any time it goes on the network this.submittedIDs = [ ]; - function finalize(message) { - if (!self.finalized) { - self.finalized = true; - self.emit('cleanup', message); - } - }; - this.once('success', function(message) { - self.finalized = true; self.setState('validated'); - finalize(message); + self.finalize(message); + self.emit('cleanup', message); }); this.once('error', function(message) { - self.finalized = true; self.setState('failed'); - finalize(message); + self.finalize(message); + self.emit('cleanup', message); }); this.once('submitted', function() { @@ -207,6 +200,20 @@ Transaction.prototype.setState = function(state) { } }; +Transaction.prototype.finalize = function(message) { + this.finalized = true; + + if (this.result) { + this.result.ledger_index = message.ledger_index; + this.result.ledger_hash = message.ledger_hash; + } else { + this.result = message; + this.result.tx_json = this.tx_json; + } + + return this; +}; + Transaction.prototype._accountSecret = function(account) { return this.remote.secrets[account]; }; From 282ac6d8ab664ce0a30c43116d6d3c6e45f3e514 Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Tue, 15 Apr 2014 12:38:57 -0700 Subject: [PATCH 14/40] Fix transaction constructor --- src/js/ripple/remote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/ripple/remote.js b/src/js/ripple/remote.js index eee720a2..ef853827 100644 --- a/src/js/ripple/remote.js +++ b/src/js/ripple/remote.js @@ -1666,7 +1666,7 @@ Remote.prototype.transaction = function(source, options, callback) { break; case 'string': - transactionType = source.toLowerCase(); + transactionType = transactionTypes[source.toLowerCase()]; if (!transactionType) { throw new Error('Invalid transaction type: ' + transactionType); From 969873441ec42f1790b3b601f04e46d9493a64d8 Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Tue, 15 Apr 2014 12:47:21 -0700 Subject: [PATCH 15/40] Recognize account option as equivalent to source in transaction construction --- src/js/ripple/transaction.js | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index 4a9cc1d6..b656142b 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -529,7 +529,7 @@ Transaction.prototype.setFlags = function(flags) { Transaction.prototype.accountSet = function(src) { if (typeof src === 'object') { var options = src; - src = options.source || options.from; + src = options.source || options.from || options.account; } if (!UInt160.is_valid(src)) { @@ -538,6 +538,7 @@ Transaction.prototype.accountSet = function(src) { this.tx_json.TransactionType = 'AccountSet'; this.tx_json.Account = UInt160.json_rewrite(src); + return this; }; @@ -547,7 +548,7 @@ Transaction.prototype.claim = function(src, generator, public_key, signature) { signature = options.signature; public_key = options.public_key; generator = options.generator; - src = options.source || options.from; + src = options.source || options.from || options.account; } this.tx_json.TransactionType = 'Claim'; @@ -561,7 +562,7 @@ Transaction.prototype.offerCancel = function(src, sequence) { if (typeof src === 'object') { var options = src; sequence = options.sequence; - src = options.source || options.from; + src = options.source || options.from || options.account; } if (!UInt160.is_valid(src)) { @@ -571,6 +572,7 @@ Transaction.prototype.offerCancel = function(src, sequence) { this.tx_json.TransactionType = 'OfferCancel'; this.tx_json.Account = UInt160.json_rewrite(src); this.tx_json.OfferSequence = Number(sequence); + return this; }; @@ -585,7 +587,7 @@ Transaction.prototype.offerCreate = function(src, taker_pays, taker_gets, expira expiration = options.expiration; taker_gets = options.taker_gets || options.sell; taker_pays = options.taker_pays || options.buy; - src = options.source || options.from; + src = options.source || options.from || options.account; } if (!UInt160.is_valid(src)) { @@ -629,6 +631,7 @@ Transaction.prototype.passwordFund = function(src, dst) { this.tx_json.TransactionType = 'PasswordFund'; this.tx_json.Destination = UInt160.json_rewrite(dst); + return this; }; @@ -639,7 +642,7 @@ Transaction.prototype.passwordSet = function(src, authorized_key, generator, pub public_key = options.public_key; generator = options.generator; authorized_key = options.authorized_key; - src = options.source || options.from; + src = options.source || options.from || options.account; } if (!UInt160.is_valid(src)) { @@ -651,6 +654,7 @@ Transaction.prototype.passwordSet = function(src, authorized_key, generator, pub this.tx_json.Generator = generator; this.tx_json.PublicKey = public_key; this.tx_json.Signature = signature; + return this; }; @@ -676,10 +680,11 @@ Transaction.prototype.payment = function(src, dst, amount) { var options = src; amount = options.amount; dst = options.destination || options.to; - src = options.source || options.from; - if (options.invoiceID) { - this.invoiceID(options.invoiceID); - } + src = options.source || options.from || options.account; + } + + if (options.invoiceID) { + this.invoiceID(options.invoiceID); } if (!UInt160.is_valid(src)) { @@ -709,7 +714,7 @@ Transaction.prototype.rippleLineSet = function(src, limit, quality_in, quality_o quality_out = options.quality_out; quality_in = options.quality_in; limit = options.limit; - src = options.source || options.from; + src = options.source || options.from || options.account; } if (!UInt160.is_valid(src)) { @@ -743,7 +748,7 @@ Transaction.prototype.walletAdd = function(src, amount, authorized_key, public_k public_key = options.public_key; authorized_key = options.authorized_key; amount = options.amount; - src = options.source || options.from; + src = options.source || options.from || options.account; } if (!UInt160.is_valid(src)) { @@ -755,6 +760,7 @@ Transaction.prototype.walletAdd = function(src, amount, authorized_key, public_k this.tx_json.RegularKey = authorized_key; this.tx_json.PublicKey = public_key; this.tx_json.Signature = signature; + return this; }; From 8ffd0b13a3836e57d69ee74510671e16688da8f3 Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Thu, 17 Apr 2014 15:19:08 -0700 Subject: [PATCH 16/40] Cleanup --- src/js/ripple/remote.js | 89 +++++++++++++++++----------------- src/js/ripple/request.js | 92 ++++++++++++++++++++---------------- src/js/ripple/transaction.js | 4 +- 3 files changed, 95 insertions(+), 90 deletions(-) diff --git a/src/js/ripple/remote.js b/src/js/ripple/remote.js index ef853827..b069e634 100644 --- a/src/js/ripple/remote.js +++ b/src/js/ripple/remote.js @@ -239,20 +239,20 @@ function Remote(opts, trace) { self.storage.getPendingTransactions(function(err, transactions) { if (err || !Array.isArray(transactions)) return; - var properties = [ - 'submittedIDs', - 'clientID', - 'submitIndex' - ]; - function resubmitTransaction(tx) { var transaction = self.transaction(); transaction.parseJson(tx.tx_json); - properties.forEach(function(prop) { - if (typeof tx[prop] !== 'undefined') { - transaction[prop] = tx[prop]; + + Object.keys(tx).forEach(function(prop) { + switch (prop) { + case 'submittedIDs': + case 'clientID': + case 'submitIndex': + transaction[prop] = tx[prop]; + break; } }); + transaction.submit(); }; @@ -344,16 +344,16 @@ Remote.prototype.addServer = function(opts) { server.on('message', serverMessage); function serverConnect() { + self._connection_count += 1; + if (opts.primary || !self._primary_server) { self._setPrimaryServer(server); } - switch (++self._connection_count) { - case 1: - self._setState('online'); - break; - case self._servers.length: - self.emit('ready'); - break; + if (self._connection_count === 1) { + self._setState('online'); + } + if (self._connection_count === self._servers.length) { + self.emit('ready'); } }; @@ -699,20 +699,18 @@ Remote.prototype.requestLedger = function(ledger, options, callback) { request.message.ledger = ledger; } - var requestFields = [ - 'full', - 'expand', - 'transactions', - 'accounts' - ]; - switch (typeof options) { case 'object': - for (var key in options) { - if (~requestFields.indexOf(key)) { - request.message[key] = true; + Object.keys(options).forEach(function(o) { + switch (o) { + case 'full': + case 'expand': + case 'transactions': + case 'accounts': + request.message[o] = true; + break; } - } + }, options); break; case 'function': @@ -732,7 +730,7 @@ Remote.prototype.requestLedger = function(ledger, options, callback) { return request; }; -// Only for unit testing. +Remote.prototype.requestLedgerClosed = Remote.prototype.requestLedgerHash = function(callback) { //utils.assert(this.trusted); // If not trusted, need to check proof. return new Request(this, 'ledger_closed').callback(callback); @@ -975,26 +973,24 @@ Remote.prototype.requestAccountTx = function(options, callback) { var request = new Request(this, 'account_tx'); - var requestFields = [ - 'account', - 'ledger_index_min', //earliest - 'ledger_index_max', //latest - 'binary', //false - 'count', //false - 'descending', //false - 'offset', //0 - 'limit', + Object.keys(options).forEach(function(o) { + switch (o) { + case 'account': + case 'ledger_index_min': //earliest + case 'ledger_index_max': //latest + case 'binary': //false + case 'count': //false + case 'descending': //false + case 'offset': //0 + case 'limit': - //extended account_tx - 'forward', //false - 'marker' - ]; - - for (var key in options) { - if (~requestFields.indexOf(key)) { - request.message[key] = options[key]; + //extended account_tx + case 'forward': //false + case 'marker': + request.message[o] = this[o]; + break; } - } + }, options); function propertiesFilter(obj, transaction) { var properties = Object.keys(obj); @@ -1259,6 +1255,7 @@ Remote.accountRootRequest = function(type, responseFilter, account, ledger, call } var request = this.requestLedgerEntry('account_root'); + request.accountRoot(account); request.ledgerChoose(ledger); diff --git a/src/js/ripple/request.js b/src/js/ripple/request.js index 6aacb966..c7261bc6 100644 --- a/src/js/ripple/request.js +++ b/src/js/ripple/request.js @@ -7,6 +7,7 @@ var Account = require('./account').Account; var Meta = require('./meta').Meta; var OrderBook = require('./orderbook').OrderBook; var RippleError = require('./rippleerror').RippleError; +var Server = require('./server').Server; // Request events emitted: // 'success' : Request successful. @@ -17,12 +18,9 @@ var RippleError = require('./rippleerror').RippleError; function Request(remote, command) { EventEmitter.call(this); - this.remote = remote; - this.requested = false; - this.message = { - command : command, - id : void(0) - }; + this.remote = remote; + this.requested = false; + this.message = { command: command, id: void(0) }; }; util.inherits(Request, EventEmitter); @@ -37,6 +35,7 @@ Request.prototype.request = function(remote) { if (this.requested) return; this.requested = true; + this.on('error', new Function); this.emit('request', remote); @@ -44,7 +43,7 @@ Request.prototype.request = function(remote) { this.remote._servers.forEach(function(server) { this.setServer(server); this.remote.request(this); - }, this ); + }, this); } else { this.remote.request(this); } @@ -53,37 +52,40 @@ Request.prototype.request = function(remote) { }; Request.prototype.callback = function(callback, successEvent, errorEvent) { - if (callback && typeof callback === 'function') { - var self = this; + var self = this; - function request_success(message) { - callback.call(self, null, message); - } - - function request_error(error) { - if (!(error instanceof RippleError)) { - error = new RippleError(error); - } - callback.call(self, error); - } - - this.once(successEvent || 'success', request_success); - this.once(errorEvent || 'error' , request_error); - this.request(); + if (this.requestsed || typeof callback !== 'function') { + return this; } + function requestSuccess(message) { + callback.call(self, null, message); + }; + + function requestError(error) { + if (!(error instanceof RippleError)) { + error = new RippleError(error); + } + callback.call(self, error); + }; + + this.once(successEvent || 'success', requestSuccess); + this.once(errorEvent || 'error' , requestError); + this.request(); + return this; }; Request.prototype.timeout = function(duration, callback) { var self = this; + function requested() { + self.timeout(duration, callback); + }; + if (!this.requested) { - function requested() { - self.timeout(duration, callback); - } - this.once('request', requested); - return; + // Defer until requested + return this.once('request', requested); } var emit = this.emit; @@ -112,8 +114,11 @@ Request.prototype.setServer = function(server) { case 'object': selected = server; break; + case 'string': + // Find server with hostname string var servers = this.remote._servers; + for (var i=0, s; s=servers[i]; i++) { if (s._host === server) { selected = s; @@ -123,18 +128,19 @@ Request.prototype.setServer = function(server) { break; }; - this.server = selected; + if (selected instanceof Server) { + this.server = selected; + } return this; }; Request.prototype.buildPath = function(build) { - if (this.remote.local_signing) { throw new Error( - '`build_path` is completely ignored when doing local signing as ' + - '`Paths` is a component of the signed blob. The `tx_blob` is signed,' + - 'sealed and delivered, and the txn unmodified after' ); + '`build_path` is completely ignored when doing local signing as ' + + '`Paths` is a component of the signed blob. The `tx_blob` is signed,' + + 'sealed and delivered, and the txn unmodified after' ); } if (build) { @@ -144,6 +150,7 @@ Request.prototype.buildPath = function(build) { // value being `truthy` delete this.message.build_path } + return this; }; @@ -153,6 +160,7 @@ Request.prototype.ledgerChoose = function(current) { } else { this.message.ledger_hash = this.remote._ledger_hash; } + return this; }; @@ -171,8 +179,8 @@ Request.prototype.ledgerIndex = function(ledger_index) { return this; }; -Request.prototype.ledgerSelect = function(ledger_spec) { - switch (ledger_spec) { +Request.prototype.ledgerSelect = function(ledger) { + switch (ledger) { case 'current': case 'closed': case 'verified': @@ -180,10 +188,10 @@ Request.prototype.ledgerSelect = function(ledger_spec) { break; default: - if (Number(ledger_spec)) { - this.message.ledger_index = ledger_spec; - } else { - this.message.ledger_hash = ledger_spec; + if (isNaN(ledger)) { + this.message.ledger_hash = ledger; + } else if (ledger = Number(ledger)) { + this.message.ledger_index = ledger; } break; } @@ -196,8 +204,8 @@ Request.prototype.accountRoot = function(account) { return this; }; -Request.prototype.index = function(hash) { - this.message.index = hash; +Request.prototype.index = function(index) { + this.message.index = index; return this; }; @@ -305,7 +313,7 @@ Request.prototype.books = function(books, snapshot) { Request.prototype.addBook = function (book, snapshot) { if (!Array.isArray(this.message.books)) { - this.message.books = []; + this.message.books = [ ]; } var json = { }; diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index b656142b..f1f4fc90 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -683,8 +683,8 @@ Transaction.prototype.payment = function(src, dst, amount) { src = options.source || options.from || options.account; } - if (options.invoiceID) { - this.invoiceID(options.invoiceID); + if (src.invoiceID) { + this.invoiceID(src.invoiceID); } if (!UInt160.is_valid(src)) { From 14f409ff5656445625e3259187d3b19d29848b6e Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Fri, 18 Apr 2014 23:08:19 -0700 Subject: [PATCH 17/40] Properly convert JS time to Ripple time in OfferCreate transactions --- src/js/ripple/transaction.js | 12 +++--------- src/js/ripple/utils.js | 7 ++++++- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index f1f4fc90..9165d0fa 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -44,6 +44,7 @@ var EventEmitter = require('events').EventEmitter; var util = require('util'); +var utils = require('./utils'); var sjcl = require('./utils').sjcl; var Amount = require('./amount').Amount; var Currency = require('./amount').Currency; @@ -555,6 +556,7 @@ Transaction.prototype.claim = function(src, generator, public_key, signature) { this.tx_json.Generator = generator; this.tx_json.PublicKey = public_key; this.tx_json.Signature = signature; + return this; }; @@ -600,15 +602,7 @@ Transaction.prototype.offerCreate = function(src, taker_pays, taker_gets, expira this.tx_json.TakerGets = Amount.json_rewrite(taker_gets); if (expiration) { - switch (expiration.constructor) { - case Date: - //offset = (new Date(2000, 0, 1).getTime()) - (new Date(1970, 0, 1).getTime()); - this.tx_json.Expiration = expiration.getTime() - 946684800000; - break; - case Number: - this.tx_json.Expiration = expiration; - break; - } + this.tx_json.Expiration = utils.time.toRipple(expiration); } if (cancel_sequence) { diff --git a/src/js/ripple/utils.js b/src/js/ripple/utils.js index 4304f661..a678feab 100644 --- a/src/js/ripple/utils.js +++ b/src/js/ripple/utils.js @@ -135,7 +135,12 @@ function fromTimestamp(rpepoch) { rpepoch = rpepoch.getTime(); } - return Math.round(rpepoch/1000) - 0x386D4380; + return Math.round(rpepoch / 1000) - 0x386D4380; +}; + +exports.time = { + fromRipple: toTimestamp, + toRipple: fromTimestamp }; exports.trace = trace; From 7f59fb917c575c38035a9518403686eb12c43e4d Mon Sep 17 00:00:00 2001 From: Evan Schwartz Date: Tue, 22 Apr 2014 14:53:35 -0700 Subject: [PATCH 18/40] [FEATURE] Added setRegularKey transaction and more accountSet flags --- src/js/ripple/transaction.js | 72 +++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index 9165d0fa..2d74612d 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -145,6 +145,18 @@ Transaction.flags = { } }; +// The following are integer (as opposed to bit) flags +// that can be set for particular transactions in the +// SetFlag or ClearFlag field +Transaction.set_clear_flags = { + AccountSet: { + asfRequireDest: 1, + asfRequireAuth: 2, + asfDisallowXRP: 3, + asfDisableMaster: 4 + } +}; + Transaction.formats = require('./binformat').tx; Transaction.prototype.consts = { @@ -527,10 +539,30 @@ Transaction.prototype.setFlags = function(flags) { // .transfer_rate() // .wallet_locator() NYI // .wallet_size() NYI -Transaction.prototype.accountSet = function(src) { + +/** + * Construct an 'AccountSet' transaction. + * + * Note that bit flags can be set using the .setFlags() method + * but for 'AccountSet' transactions there is an additional way to + * modify AccountRoot flags. The values available for the SetFlag + * and ClearFlag are as follows: + * + * "asfRequireDest" + * Require a destination tag + * "asfRequireAuth" + * Authorization is required to extend trust + * "asfDisallowXRP" + * XRP should not be sent to this account + * "asfDisableMaster" + * Disallow use of the master key + */ +Transaction.prototype.accountSet = function(src, set_flag, clear_flag) { if (typeof src === 'object') { var options = src; src = options.source || options.from || options.account; + set_flag = options.set_flag; + clear_flag = options.clear_flag; } if (!UInt160.is_valid(src)) { @@ -539,6 +571,16 @@ Transaction.prototype.accountSet = function(src) { this.tx_json.TransactionType = 'AccountSet'; this.tx_json.Account = UInt160.json_rewrite(src); + if (set_flag && typeof set_flag === 'number') { + this.tx_json.SetFlag = set_flag; + } else if (set_flag && typeof set_flag === 'string') { + this.tx_json.SetFlag = Transaction.set_clear_flags.AccountSet[set_flag]; + } + if (clear_flag && typeof clear_flag === 'number') { + this.tx_json.ClearFlag = clear_flag; + } else if (clear_flag && typeof clear_flag === 'string') { + this.tx_json.ClearFlag = Transaction.set_clear_flags.AccountSet[clear_flag]; + } return this; }; @@ -652,6 +694,34 @@ Transaction.prototype.passwordSet = function(src, authorized_key, generator, pub return this; }; + +/** + * Construct a 'SetRegularKey' transaction. + * If the RegularKey is set, the private key that corresponds to + * it can be used to sign transactions instead of the master key + * + * The RegularKey must be a valid Ripple Address, or a Hash160 of + * the public key corresponding to the new private signing key. + */ +Transaction.prototype.setRegularKey = function(src, regular_key) { + if (typeof src === 'object') { + var options = src; + src = options.address || options.account || options.from; + regular_key = options.regular_key; + } + + if (!UInt160.is_valid(src)) { + throw new Error('Source address invalid'); + } + if (!UInt160.is_valid(regular_key)) { + throw new Error('RegularKey must be a valid Ripple Address (a Hash160 of the public key)'); + } + + this.tx_json.TransactionType = 'SetRegularKey'; + this.tx_json.Account = UInt160.json_rewrite(src); + this.tx_json.RegularKey = UInt160.json_rewrite(regular_key); +}; + // Construct a 'payment' transaction. // // When a transaction is submitted: From 18efa5d74251222cbccd6eae33063de2bd46261e Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Wed, 23 Apr 2014 03:56:36 -0700 Subject: [PATCH 19/40] Emit an error on invalid secret, cleanup --- src/js/ripple/transaction.js | 55 ++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index 2d74612d..9cf5537e 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -153,7 +153,7 @@ Transaction.set_clear_flags = { asfRequireDest: 1, asfRequireAuth: 2, asfDisallowXRP: 3, - asfDisableMaster: 4 + asfDisableMaster: 4 } }; @@ -295,9 +295,13 @@ Transaction.prototype.complete = function() { } if (typeof this.tx_json.SigningPubKey === 'undefined') { - var seed = Seed.from_json(this._secret); - var key = seed.get_key(this.tx_json.Account); - this.tx_json.SigningPubKey = key.to_hex_pub(); + try { + var seed = Seed.from_json(this._secret); + var key = seed.get_key(this.tx_json.Account); + this.tx_json.SigningPubKey = key.to_hex_pub(); + } catch(e) { + return this.emit('error', new RippleError('tejSecretInvalid', 'Invalid secret')); + } } // Set canonical flag - this enables canonicalized signature checking @@ -512,20 +516,16 @@ Transaction.prototype.transferRate = function(rate) { Transaction.prototype.setFlags = function(flags) { if (!flags) return this; - var transaction_flags = Transaction.flags[this.tx_json.TransactionType]; var flag_set = Array.isArray(flags) ? flags : Array.prototype.slice.call(arguments); - - // We plan to not define this field on new Transaction. - if (this.tx_json.Flags === void(0)) { - this.tx_json.Flags = 0; - } + var transaction_flags = Transaction.flags[this.tx_json.TransactionType] || { }; for (var i=0, l=flag_set.length; i Date: Wed, 23 Apr 2014 18:23:25 +0700 Subject: [PATCH 20/40] Fix README 1000th --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 695bee1b..d1758976 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ `ripple-lib` connects to the Ripple network via the WebSocket protocol and runs in Node.js as well as in the browser. -**Use ripple-lib for** +###Use ripple-lib for: + Connecting to a local or remote rippled in JavaScript (Node.js or browser) + Issuing [rippled API](https://ripple.com/wiki/JSON_Messages) requests From 52e1665e72790358e8694bc56940ead0ff6b7b40 Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Wed, 23 Apr 2014 14:19:02 -0700 Subject: [PATCH 21/40] Fixes for per-transaction defined secret and offline errors --- src/js/ripple/transaction.js | 32 +++++++++++------ src/js/ripple/transactionmanager.js | 56 +++++++++++++---------------- src/js/ripple/transactionqueue.js | 47 ++++-------------------- 3 files changed, 54 insertions(+), 81 deletions(-) diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index 9cf5537e..fcc0012c 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -281,8 +281,25 @@ Transaction.prototype._getServer = function() { Transaction.prototype.complete = function() { // Try to auto-fill the secret - if (!this._secret && !(this._secret = this._account_secret(this.tx_json.Account))) { - return this.emit('error', new RippleError('tejSecretUnknown', 'Missing secret')); + if (!this._secret && !(this._secret = this._accountSecret(this.tx_json.Account))) { + this.emit('error', new RippleError('tejSecretUnknown', 'Missing secret')); + return false; + } + + if (typeof this.tx_json.SigningPubKey === 'undefined') { + try { + var seed = Seed.from_json(this._secret); + var key = seed.get_key(this.tx_json.Account); + this.tx_json.SigningPubKey = key.to_hex_pub(); + } catch(e) { + this.emit('error', new RippleError('tejSecretInvalid', 'Invalid secret')); + return false; + } + } + + if (!this.remote.trusted && !this.remote.local_signing) { + this.emit('error', new RippleError('tejServerUntrusted', 'Attempt to give secret to untrusted server')); + return false; } // If the Fee hasn't been set, one needs to be computed by @@ -294,14 +311,9 @@ Transaction.prototype.complete = function() { } } - if (typeof this.tx_json.SigningPubKey === 'undefined') { - try { - var seed = Seed.from_json(this._secret); - var key = seed.get_key(this.tx_json.Account); - this.tx_json.SigningPubKey = key.to_hex_pub(); - } catch(e) { - return this.emit('error', new RippleError('tejSecretInvalid', 'Invalid secret')); - } + if (Number(this.tx_json.Fee) > this._maxFee) { + tx.emit('error', new RippleError('tejMaxFeeExceeded', 'Max fee exceeded')); + return false; } // Set canonical flag - this enables canonicalized signature checking diff --git a/src/js/ripple/transactionmanager.js b/src/js/ripple/transactionmanager.js index 4e484486..57637b29 100644 --- a/src/js/ripple/transactionmanager.js +++ b/src/js/ripple/transactionmanager.js @@ -184,24 +184,20 @@ TransactionManager.prototype._fillSequence = function(tx, callback) { fill.account_set(self._accountID); fill.tx_json.Sequence = sequence; fill.once('submitted', callback); + + // Secrets may be set on a per-transaction basis + if (tx._secret) { + fill.secret(tx._secret); + } + fill.submit(); }; function sequenceLoaded(err, sequence) { if (typeof sequence !== 'number') { callback(new Error('Failed to fetch account transaction sequence')); - return; - } - - var sequenceDif = tx.tx_json.Sequence - sequence; - var submitted = 0; - - for (var i=sequence; i this._maxFee) { - tx.emit('error', new RippleError('tejMaxFeeExceeded', 'Max fee exceeded')); - } else { - // ND: this is the ONLY place we put the tx into the queue. The - // TransactionQueue queue is merely a list, so any mutations to tx._hash - // will cause subsequent look ups (eg. inside 'transaction-outbound' - // validated transaction clearing) to fail. - this._pending.push(tx); - this._request(tx); - } + // ND: this is the ONLY place we put the tx into the queue. The + // TransactionQueue queue is merely a list, so any mutations to tx._hash + // will cause subsequent look ups (eg. inside 'transaction-outbound' + // validated transaction clearing) to fail. + this._pending.push(tx); + this._request(tx); }; exports.TransactionManager = TransactionManager; diff --git a/src/js/ripple/transactionqueue.js b/src/js/ripple/transactionqueue.js index 4b3758d3..b9968dd9 100644 --- a/src/js/ripple/transactionqueue.js +++ b/src/js/ripple/transactionqueue.js @@ -4,46 +4,17 @@ */ var Transaction = require('./transaction').Transaction; +var LRU = require('lru-cache'); function TransactionQueue() { var self = this; this._queue = [ ]; - this._idCache = { }; - this._sequenceCache = { }; + this._idCache = LRU({ max: 100 }); + this._sequenceCache = LRU({ max: 100 }); this._save = void(0); }; -TransactionQueue.prototype.clearCache = function() { - this._idCache = { }; - this._sequenceCache = { }; -}; - -TransactionQueue.prototype.getMinLedger = function() { - var minLedger = Infinity; - - for (var i=0; i Date: Wed, 23 Apr 2014 14:20:41 -0700 Subject: [PATCH 22/40] Remove logging --- src/js/ripple/transactionmanager.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/js/ripple/transactionmanager.js b/src/js/ripple/transactionmanager.js index 57637b29..1e261ac5 100644 --- a/src/js/ripple/transactionmanager.js +++ b/src/js/ripple/transactionmanager.js @@ -375,10 +375,7 @@ TransactionManager.prototype._request = function(tx) { function transactionRetry(message) { if (tx.finalized) return; - console.log('TER, submitting fill'); - self._fillSequence(tx, function() { - console.log('FILL COMPLETE, resubmitting'); self._resubmit(1, tx); }); }; From 693e2aaae74d26b06eb001ab7bbacc218690d0ba Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Wed, 23 Apr 2014 14:39:30 -0700 Subject: [PATCH 23/40] Decrement transaction sequence if transaction.complete fails --- src/js/ripple/transactionmanager.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/js/ripple/transactionmanager.js b/src/js/ripple/transactionmanager.js index 1e261ac5..22d7bcb7 100644 --- a/src/js/ripple/transactionmanager.js +++ b/src/js/ripple/transactionmanager.js @@ -195,10 +195,23 @@ TransactionManager.prototype._fillSequence = function(tx, callback) { function sequenceLoaded(err, sequence) { if (typeof sequence !== 'number') { - callback(new Error('Failed to fetch account transaction sequence')); - } else { - submitFill(sequence, callback); + return callback(new Error('Failed to fetch account transaction sequence')); } + + var sequenceDif = tx.tx_json.Sequence - sequence; + var submitted = 0; + + ;(function nextFill(sequence) { + if (sequence >= tx.tx_json.Sequence) return; + + submitFill(sequence, function() { + if (++submitted === sequenceDif) { + callback(); + } else { + nextFill(++sequence); + } + }); + })(sequence); }; this._loadSequence(sequenceLoaded); @@ -581,6 +594,7 @@ TransactionManager.prototype.submit = function(tx) { // If the transaction can't complete, decrement sequence so that // subsequent transactions if (!tx.complete()) { + this._nextSequence--; return; } From 5a04ce96296102aec8d8edb7817214e28668cb05 Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Wed, 23 Apr 2014 14:42:29 -0700 Subject: [PATCH 24/40] Use binary sequence increment --- src/js/ripple/transactionmanager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/ripple/transactionmanager.js b/src/js/ripple/transactionmanager.js index 22d7bcb7..3616f5cb 100644 --- a/src/js/ripple/transactionmanager.js +++ b/src/js/ripple/transactionmanager.js @@ -208,7 +208,7 @@ TransactionManager.prototype._fillSequence = function(tx, callback) { if (++submitted === sequenceDif) { callback(); } else { - nextFill(++sequence); + nextFill(sequence + 1); } }); })(sequence); From fbdef6eea03c2ae4553ba1a4ea8d27490ff37878 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Thu, 24 Apr 2014 09:00:57 -0700 Subject: [PATCH 25/40] [BUG] UInt#parse_number should support zero. Add tests. --- src/js/ripple/uint.js | 2 +- test/uint-test.js | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 test/uint-test.js diff --git a/src/js/ripple/uint.js b/src/js/ripple/uint.js index 4f2338be..e44f7fbc 100644 --- a/src/js/ripple/uint.js +++ b/src/js/ripple/uint.js @@ -234,7 +234,7 @@ UInt.prototype.parse_number = function (j) { if ("number" === typeof j && j === +j && - j > 0) { + j >= 0) { // XXX Better, faster way to get BigInteger from JS int? this._value = new BigInteger(""+j); } diff --git a/test/uint-test.js b/test/uint-test.js new file mode 100644 index 00000000..9075f51e --- /dev/null +++ b/test/uint-test.js @@ -0,0 +1,25 @@ +var assert = require('assert'); +var utils = require('./testutils'); +var UInt128 = utils.load_module('uint128').UInt128; +var config = require('./testutils').get_config(); + +describe('UInt', function() { + describe('128', function() { + describe('#parse_number', function () { + it('should create 00000000000000000000000000000000 when called with 0', function () { + var val = UInt128.from_number(0); + assert.strictEqual(val.to_hex(), '00000000000000000000000000000000'); + }); + it('should create 00000000000000000000000000000001 when called with 1', function () { + var val = UInt128.from_number(0); + assert.strictEqual(val.to_hex(), '00000000000000000000000000000000'); + }); + it('should create 000000000000000000000000FFFFFFFF when called with 0xFFFFFFFF', function () { + var val = UInt128.from_number(0xFFFFFFFF); + assert.strictEqual(val.to_hex(), '000000000000000000000000FFFFFFFF'); + }); + }); + }); +}); + +// vim:sw=2:sts=2:ts=8:et From 30fd0e7bff311027d34e683e8ca98ae299c1e160 Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Fri, 25 Apr 2014 00:08:26 -0700 Subject: [PATCH 26/40] Finalize before setting final transaction state --- src/js/ripple/transaction.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index fcc0012c..f354768c 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -83,14 +83,14 @@ function Transaction(remote) { this.submittedIDs = [ ]; this.once('success', function(message) { - self.setState('validated'); self.finalize(message); + self.setState('validated'); self.emit('cleanup', message); }); this.once('error', function(message) { - self.setState('failed'); self.finalize(message); + self.setState('failed'); self.emit('cleanup', message); }); From 903e480130fa24001e27b48b07ef3171f2fcc9e5 Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Fri, 25 Apr 2014 01:38:01 -0700 Subject: [PATCH 27/40] Fix for missing transaction.remote --- src/js/ripple/transaction.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index f354768c..7a8284eb 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -280,6 +280,13 @@ Transaction.prototype._getServer = function() { */ Transaction.prototype.complete = function() { + if (this.remote) { + if (!this.remote.trusted && !this.remote.local_signing) { + this.emit('error', new RippleError('tejServerUntrusted', 'Attempt to give secret to untrusted server')); + return false; + } + } + // Try to auto-fill the secret if (!this._secret && !(this._secret = this._accountSecret(this.tx_json.Account))) { this.emit('error', new RippleError('tejSecretUnknown', 'Missing secret')); @@ -297,11 +304,6 @@ Transaction.prototype.complete = function() { } } - if (!this.remote.trusted && !this.remote.local_signing) { - this.emit('error', new RippleError('tejServerUntrusted', 'Attempt to give secret to untrusted server')); - return false; - } - // If the Fee hasn't been set, one needs to be computed by // an assigned server if (this.remote && typeof this.tx_json.Fee === 'undefined') { From 8275e036c9104f26fc36146d611e2e90f77fb3bb Mon Sep 17 00:00:00 2001 From: Evan Schwartz Date: Wed, 23 Apr 2014 10:59:15 -0700 Subject: [PATCH 28/40] [CHORE] Modified functions not to overwrite entire prototype --- src/js/sjcl-custom/sjcl-validecc.js | 44 +++++++++++++---------------- 1 file changed, 20 insertions(+), 24 deletions(-) diff --git a/src/js/sjcl-custom/sjcl-validecc.js b/src/js/sjcl-custom/sjcl-validecc.js index 7d24fd3b..536f2bf8 100644 --- a/src/js/sjcl-custom/sjcl-validecc.js +++ b/src/js/sjcl-custom/sjcl-validecc.js @@ -1,30 +1,26 @@ -sjcl.ecc.ecdsa.secretKey.prototype = { - sign: function(hash, paranoia) { - var R = this._curve.r, - l = R.bitLength(), - k = sjcl.bn.random(R.sub(1), paranoia).add(1), - r = this._curve.G.mult(k).x.mod(R), - s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R); +sjcl.ecc.ecdsa.secretKey.prototype.sign = function(hash, paranoia) { + var R = this._curve.r, + l = R.bitLength(), + k = sjcl.bn.random(R.sub(1), paranoia).add(1), + r = this._curve.G.mult(k).x.mod(R), + s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R); - return sjcl.bitArray.concat(r.toBits(l), s.toBits(l)); - } + return sjcl.bitArray.concat(r.toBits(l), s.toBits(l)); }; -sjcl.ecc.ecdsa.publicKey.prototype = { - verify: function(hash, rs) { - var w = sjcl.bitArray, - R = this._curve.r, - l = R.bitLength(), - r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)), - s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)), - sInv = s.inverseMod(R), - hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R), - hA = r.mul(sInv).mod(R), - r2 = this._curve.G.mult2(hG, hA, this._point).x; +sjcl.ecc.ecdsa.publicKey.prototype.verify = function(hash, rs) { + var w = sjcl.bitArray, + R = this._curve.r, + l = R.bitLength(), + r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)), + s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)), + sInv = s.inverseMod(R), + hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R), + hA = r.mul(sInv).mod(R), + r2 = this._curve.G.mult2(hG, hA, this._point).x; - if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) { - throw (new sjcl.exception.corrupt("signature didn't check out")); - } - return true; + if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) { + throw (new sjcl.exception.corrupt("signature didn't check out")); } + return true; }; From f56a20d6972b64667f02a6e5c64fa22213d3b99b Mon Sep 17 00:00:00 2001 From: Evan Schwartz Date: Wed, 30 Apr 2014 17:47:12 -0700 Subject: [PATCH 29/40] [FIX] Replaced Account.is_valid() with Account.isValid() --- src/js/ripple/remote.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/ripple/remote.js b/src/js/ripple/remote.js index b069e634..a842b219 100644 --- a/src/js/ripple/remote.js +++ b/src/js/ripple/remote.js @@ -1311,7 +1311,7 @@ Remote.prototype.getAccount = function(accountID) { Remote.prototype.addAccount = function(accountID) { var account = new Account(this, accountID); - if (account.is_valid()) { + if (account.isValid()) { this._accounts[accountID] = account; } From 904082a86cf89b4b301a3e4570b0582288ef5b5a Mon Sep 17 00:00:00 2001 From: Evan Schwartz Date: Tue, 22 Apr 2014 17:21:42 -0700 Subject: [PATCH 30/40] [FEATURE] New Message class for sigs on arbitrary data This includes supporting files that can sign arbitrary data with a signature that enables public key recovery. It also includes the PublicKeyValidator class that can verify whether a given public key is active for an account by looking in its AccountRoot. --- Gruntfile.js | 2 + src/js/ripple/index.js | 1 + src/js/ripple/message.js | 194 +++++++++++ src/js/ripple/pubkeyvalidator.js | 104 ++++++ src/js/sjcl-custom/sjcl-ecc-pointextras.js | 83 +++++ .../sjcl-ecdsa-recoverablepublickey.js | 308 ++++++++++++++++++ src/js/sjcl-custom/sjcl-secp256k1.js | 2 +- src/js/sjcl-custom/sjcl-validecc.js | 22 +- test/pubkeyvalidator-test.js | 157 +++++++++ test/sjcl-ecdsa-recoverablepublickey-test.js | 245 ++++++++++++++ 10 files changed, 1112 insertions(+), 6 deletions(-) create mode 100644 src/js/ripple/message.js create mode 100644 src/js/ripple/pubkeyvalidator.js create mode 100644 src/js/sjcl-custom/sjcl-ecc-pointextras.js create mode 100644 src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js create mode 100644 test/pubkeyvalidator-test.js create mode 100644 test/sjcl-ecdsa-recoverablepublickey-test.js diff --git a/Gruntfile.js b/Gruntfile.js index a1b1bf5e..7c537060 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -36,6 +36,7 @@ module.exports = function(grunt) { "src/js/sjcl/core/bn.js", "src/js/sjcl/core/ecc.js", "src/js/sjcl/core/srp.js", + "src/js/sjcl-custom/sjcl-ecc-pointextras.js", "src/js/sjcl-custom/sjcl-secp256k1.js", "src/js/sjcl-custom/sjcl-ripemd160.js", "src/js/sjcl-custom/sjcl-extramath.js", @@ -43,6 +44,7 @@ module.exports = function(grunt) { "src/js/sjcl-custom/sjcl-validecc.js", "src/js/sjcl-custom/sjcl-ecdsa-canonical.js", "src/js/sjcl-custom/sjcl-ecdsa-der.js", + "src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js", "src/js/sjcl-custom/sjcl-jacobi.js" ], dest: 'build/sjcl.js' diff --git a/src/js/ripple/index.js b/src/js/ripple/index.js index 92ae258a..a6cfa2ce 100644 --- a/src/js/ripple/index.js +++ b/src/js/ripple/index.js @@ -11,6 +11,7 @@ exports.Seed = require('./seed').Seed; exports.Meta = require('./meta').Meta; exports.SerializedObject = require('./serializedobject').SerializedObject; exports.RippleError = require('./rippleerror').RippleError; +exports.Message = require('./message'); exports.binformat = require('./binformat'); exports.utils = require('./utils'); diff --git a/src/js/ripple/message.js b/src/js/ripple/message.js new file mode 100644 index 00000000..e92b2724 --- /dev/null +++ b/src/js/ripple/message.js @@ -0,0 +1,194 @@ +var async = require('async'); +var crypto = require('crypto'); +var sjcl = require('./utils').sjcl; +var Remote = require('./remote').Remote; +var Seed = require('./seed').Seed; +var KeyPair = require('./keypair').KeyPair; +var PublicKeyValidator = require('./pubkeyvalidator'); +var UInt160 = require('./uint160').UInt160; + +// Message class (static) +var Message = {}; + +Message.HASH_FUNCTION = sjcl.hash.sha512.hash; +Message.MAGIC_BYTES = 'Ripple Signed Message:\n'; + +var REGEX_HEX = /^[0-9a-fA-F]+$/; +var REGEX_BASE64 = /^([A-Za-z0-9\+]{4})*([A-Za-z0-9\+]{2}==)|([A-Za-z0-9\+]{3}=)?$/; + +/** + * Produce a Base64-encoded signature on the given message with + * the string 'Ripple Signed Message:\n' prepended. + * + * Note that this signature uses the signing function that includes + * a recovery_factor to be able to extract the public key from the signature + * without having to pass the public key along with the signature. + * + * @static + * + * @param {String} message + * @param {sjcl.ecc.ecdsa.secretKey|Any format accepted by Seed.from_json} secret_key + * @returns {Base64-encoded String} signature + */ +Message.signMessage = function(message, secret_key) { + + return Message.signHash(Message.HASH_FUNCTION(Message.MAGIC_BYTES + message), secret_key); + +}; + +/** + * Produce a Base64-encoded signature on the given hex-encoded hash. + * + * Note that this signature uses the signing function that includes + * a recovery_factor to be able to extract the public key from the signature + * without having to pass the public key along with the signature. + * + * @static + * + * @param {bitArray|Hex-encoded String} hash + * @param {sjcl.ecc.ecdsa.secretKey|Any format accepted by Seed.from_json} secret_key + * @returns {Base64-encoded String} signature + */ +Message.signHash = function(hash, secret_key) { + + if (typeof hash === 'string' && /^[0-9a-fA-F]+$/.test(hash)) { + hash = sjcl.codec.hex.toBits(hash); + } + + if (typeof hash !== 'object' || hash.length <= 0 || typeof hash[0] !== 'number') { + throw new Error('Hash must be a bitArray or hex-encoded string'); + } + + if (!(secret_key instanceof sjcl.ecc.ecdsa.secretKey)) { + secret_key = Seed.from_json(secret_key).get_key()._secret; + } + + var signature_bits = secret_key.signWithRecoverablePublicKey(hash); + var signature_base64 = sjcl.codec.base64.fromBits(signature_bits); + + return signature_base64; + +}; + + +/** + * Verify the signature on a given message. + * + * Note that this function is asynchronous. + * The ripple-lib remote is used to check that the public + * key extracted from the signature corresponds to one that is currently + * active for the given account. + * + * @static + * + * @param {String} data.message + * @param {RippleAddress} data.account + * @param {Base64-encoded String} data.signature + * @param {ripple-lib Remote} remote + * @param {Function} callback + * + * @callback callback + * @param {Error} error + * @param {boolean} is_valid true if the signature is valid, false otherwise + */ +Message.verifyMessageSignature = function(data, remote, callback) { + + if (typeof data.message === 'string') { + data.hash = Message.HASH_FUNCTION(Message.MAGIC_BYTES + data.message); + } else { + return callback(new Error('Data object must contain message field to verify signature')); + } + + return Message.verifyHashSignature(data, remote, callback); + +}; + + +/** + * Verify the signature on a given hash. + * + * Note that this function is asynchronous. + * The ripple-lib remote is used to check that the public + * key extracted from the signature corresponds to one that is currently + * active for the given account. + * + * @static + * + * @param {bitArray|Hex-encoded String} data.hash + * @param {RippleAddress} data.account + * @param {Base64-encoded String} data.signature + * @param {ripple-lib Remote} remote + * @param {Function} callback + * + * @callback callback + * @param {Error} error + * @param {boolean} is_valid true if the signature is valid, false otherwise + */ +Message.verifyHashSignature = function(data, remote, callback) { + + var hash, + account, + signature; + + if(typeof callback !== 'function') { + throw new Error('Must supply callback function'); + } + + hash = data.hash; + if (hash && typeof hash === 'string' && REGEX_HEX.test(hash)) { + hash = sjcl.codec.hex.toBits(hash); + } + + if (typeof hash !== 'object' || hash.length <= 0 || typeof hash[0] !== 'number') { + return callback(new Error('Hash must be a bitArray or hex-encoded string')); + } + + account = data.account || data.address; + if (!account || !UInt160.from_json(account).is_valid()) { + return callback(new Error('Account must be a valid ripple address')); + } + + signature = data.signature; + if (typeof signature !== 'string' || !REGEX_BASE64.test(signature)) { + return callback(new Error('Signature must be a Base64-encoded string')); + } + signature = sjcl.codec.base64.toBits(signature); + + if (!(remote instanceof Remote) || remote.state !== 'online') { + return callback(new Error('Must supply connected Remote to verify signature')); + } + + function recoverPublicKey (async_callback) { + + var public_key; + try { + public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature); + } catch (err) { + return async_callback(err); + } + async_callback(null, public_key); + + }; + + function checkPublicKeyIsValid (public_key, async_callback) { + + // Get hex-encoded public key + var key_pair = new KeyPair(); + key_pair._pubkey = public_key; + var public_key_hex = key_pair.to_hex_pub(); + + var public_key_validator = new PublicKeyValidator(remote); + public_key_validator.validate(account, public_key_hex, async_callback); + + }; + + var steps = [ + recoverPublicKey, + checkPublicKeyIsValid + ]; + + async.waterfall(steps, callback); + +}; + +module.exports = Message; diff --git a/src/js/ripple/pubkeyvalidator.js b/src/js/ripple/pubkeyvalidator.js new file mode 100644 index 00000000..d26e5857 --- /dev/null +++ b/src/js/ripple/pubkeyvalidator.js @@ -0,0 +1,104 @@ +var async = require('async'); +var UInt160 = require('./uint160').UInt160; +var sjcl = require('./utils').sjcl; +var Base = require('./base').Base; + + +/** + * @constructor PubKeyValidator + * @param {Remote} remote + */ + +function PubKeyValidator(remote) { + + var self = this; + + if (remote) { + self._remote = remote; + } else { + throw(new Error('Must instantiate the PubKeyValidator with a ripple-lib Remote')); + } + + // Convert hex string to UInt160 + self._parsePublicKey = function(public_key) { + + // Based on functions in /src/js/ripple/keypair.js + function hexToUInt160(public_key) { + var bits = sjcl.codec.hex.toBits(public_key); + var hash = sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits)); + var address = UInt160.from_bits(hash); + address.set_version(Base.VER_ACCOUNT_ID); + return address.to_json(); + } + + if (UInt160.is_valid(public_key)) { + return public_key; + } else if (/^[0-9a-fA-F]+$/.test(public_key)) { + return hexToUInt160(public_key); + } else { + throw(new Error('Public key is invalid. Must be a UInt160 or a hex string')); + } + }; + +} + +/** + * Check whether the public key is valid for the specified address. + * + * @param {String} address + * @param {String} public_key + * @param {Function} callback + * + * callback function is called with (err, is_valid), where is_valid + * is a boolean indicating whether the public_key supplied is active + */ + +PubKeyValidator.prototype.validate = function(address, public_key, callback) { + + var self = this; + + var public_key_as_uint160; + try { + public_key_as_uint160 = self._parsePublicKey(public_key); + } catch (e) { + return callback(e); + } + + + function getAccountInfo(async_callback) { + self._remote.account(address).getInfo(async_callback); + }; + + function publicKeyIsValid(account_info_res, async_callback) { + var account_info = account_info_res.account_data; + + // Respond with true if the RegularKey is set and matches the given public key or + // if the public key matches the account address and the lsfDisableMaster is not set + if (account_info.RegularKey && + account_info.RegularKey === public_key_as_uint160) { + + async_callback(null, true); + + } else if (account_info.Account === public_key_as_uint160 && + ((account_info.Flags & 0x00100000) === 0)) { + + async_callback(null, true); + + } else { + + async_callback(null, false); + + } + + }; + + var steps = [ + getAccountInfo, + publicKeyIsValid + ]; + + async.waterfall(steps, callback); + +}; + +module.exports = PubKeyValidator; diff --git a/src/js/sjcl-custom/sjcl-ecc-pointextras.js b/src/js/sjcl-custom/sjcl-ecc-pointextras.js new file mode 100644 index 00000000..e840ce0b --- /dev/null +++ b/src/js/sjcl-custom/sjcl-ecc-pointextras.js @@ -0,0 +1,83 @@ +/** + * Check that the point is valid based on the method described in + * SEC 1: Elliptic Curve Cryptography, section 3.2.2.1: + * Elliptic Curve Public Key Validation Primitive + * http://www.secg.org/download/aid-780/sec1-v2.pdf + * + * @returns {Boolean} + */ +sjcl.ecc.point.prototype.isValidPoint = function() { + + var self = this; + + var field_modulus = self.curve.field.modulus; + + if (self.isIdentity) { + return false; + } + + // Check that coordinatres are in bounds + // Return false if x < 1 or x > (field_modulus - 1) + if (((new sjcl.bn(1).greaterEquals(self.x)) && + !self.x.equals(1)) || + (self.x.greaterEquals(field_modulus.sub(1))) && + !self.x.equals(1)) { + + return false; + } + + // Return false if y < 1 or y > (field_modulus - 1) + if (((new sjcl.bn(1).greaterEquals(self.y)) && + !self.y.equals(1)) || + (self.y.greaterEquals(field_modulus.sub(1))) && + !self.y.equals(1)) { + + return false; + } + + if (!self.isOnCurve()) { + return false; + } + + // TODO check to make sure point is a scalar multiple of base_point + + return true; + +}; + +/** + * Check that the point is on the curve + * + * @returns {Boolean} + */ +sjcl.ecc.point.prototype.isOnCurve = function() { + + var self = this; + + var field_order = self.curve.r; + var component_a = self.curve.a; + var component_b = self.curve.b; + var field_modulus = self.curve.field.modulus; + + var y_squared_mod_field_order = self.y.mul(self.y).mod(field_modulus); + var x_cubed_plus_ax_plus_b = self.x.mul(self.x).mul(self.x).add(component_a.mul(self.x)).add(component_b).mod(field_modulus); + + return y_squared_mod_field_order.equals(x_cubed_plus_ax_plus_b); + +}; + + +sjcl.ecc.point.prototype.toString = function() { + return '(' + + this.x.toString() + ', ' + + this.y.toString() + + ')'; +}; + +sjcl.ecc.pointJac.prototype.toString = function() { + return '(' + + this.x.toString() + ', ' + + this.y.toString() + ', ' + + this.z.toString() + + ')'; +}; diff --git a/src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js b/src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js new file mode 100644 index 00000000..f8f1326e --- /dev/null +++ b/src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js @@ -0,0 +1,308 @@ +/** + * This module uses the public key recovery method + * described in SEC 1: Elliptic Curve Cryptography, + * section 4.1.6, "Public Key Recovery Operation". + * http://www.secg.org/download/aid-780/sec1-v2.pdf + * + * Implementation based on: + * https://github.com/bitcoinjs/bitcoinjs-lib/blob/89cf731ac7309b4f98994e3b4b67b7226020181f/src/ecdsa.js + */ + +// Defined here so that this value only needs to be calculated once +var FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR; + +/** + * Sign the given hash such that the public key, prepending an extra byte + * so that the public key will be recoverable from the signature + * + * @param {bitArray} hash + * @param {Number} paranoia + * @returns {bitArray} Signature formatted as bitArray + */ +sjcl.ecc.ecdsa.secretKey.prototype.signWithRecoverablePublicKey = function(hash, paranoia, k_for_testing) { + + var self = this; + + // Convert hash to bits and determine encoding for output + var hash_bits; + if (typeof hash === 'object' && hash.length > 0 && typeof hash[0] === 'number') { + hash_bits = hash; + } else { + throw new sjcl.exception.invalid('hash. Must be a bitArray'); + } + + // Sign hash with standard, canonicalized method + var standard_signature = self.sign(hash_bits, paranoia, k_for_testing); + var canonical_signature = self.canonicalizeSignature(standard_signature); + + // Extract r and s signature components from canonical signature + var r_and_s = getRandSFromSignature(self._curve, canonical_signature); + + // Rederive public key + var public_key = self._curve.G.mult(sjcl.bn.fromBits(self.get())); + + // Determine recovery factor based on which possible value + // returns the correct public key + var recovery_factor = calculateRecoveryFactor(self._curve, r_and_s.r, r_and_s.s, hash_bits, public_key); + + // Prepend recovery_factor to signature and encode in DER + // The value_to_prepend should be 4 bytes total + var value_to_prepend = recovery_factor + 27; + + var final_signature_bits = sjcl.bitArray.concat([value_to_prepend], canonical_signature); + + // Return value in bits + return final_signature_bits; + +}; + + +/** + * Recover the public key from a signature created with the + * signWithRecoverablePublicKey method in this module + * + * @static + * + * @param {bitArray} hash + * @param {bitArray} signature + * @param {sjcl.ecc.curve} [sjcl.ecc.curves['c256']] curve + * @returns {sjcl.ecc.ecdsa.publicKey} Public key + */ +sjcl.ecc.ecdsa.publicKey.recoverFromSignature = function(hash, signature, curve) { + + var self = this; + + if (!signature || signature instanceof sjcl.ecc.curve) { + throw new sjcl.exception.invalid('must supply hash and signature to recover public key'); + } + + if (!curve) { + curve = sjcl.ecc.curves['c256']; + } + + // Convert hash to bits and determine encoding for output + var hash_bits; + if (typeof hash === 'object' && hash.length > 0 && typeof hash[0] === 'number') { + hash_bits = hash; + } else { + throw new sjcl.exception.invalid('hash. Must be a bitArray'); + } + + var signature_bits; + if (typeof signature === 'object' && signature.length > 0 && typeof signature[0] === 'number') { + signature_bits = signature; + } else { + throw new sjcl.exception.invalid('signature. Must be a bitArray'); + } + + // Extract recovery_factor from first 4 bytes + var recovery_factor = signature_bits[0] - 27; + + if (recovery_factor < 0 || recovery_factor > 3) { + throw new sjcl.exception.invalid('signature. Signature must be generated with algorithm ' + + 'that prepends the recovery factor in order to recover the public key'); + } + + // Separate r and s values + var r_and_s = getRandSFromSignature(curve, signature_bits.slice(1)); + var signature_r = r_and_s.r; + var signature_s = r_and_s.s; + + // Recover public key using recovery_factor + var recovered_public_key_point = recoverPublicKeyPointFromSignature(curve, signature_r, signature_s, hash_bits, recovery_factor); + var recovered_public_key = new sjcl.ecc.ecdsa.publicKey(curve, recovered_public_key_point); + + return recovered_public_key; + +}; + + +/** + * Retrieve the r and s components of a signature + * + * @param {sjcl.ecc.curve} curve + * @param {bitArray} signature + * @returns {Object} Object with 'r' and 's' fields each as an sjcl.bn + */ +function getRandSFromSignature(curve, signature) { + + var r_length = curve.r.bitLength(); + + return { + r: sjcl.bn.fromBits(sjcl.bitArray.bitSlice(signature, 0, r_length)), + s: sjcl.bn.fromBits(sjcl.bitArray.bitSlice(signature, r_length, sjcl.bitArray.bitLength(signature))) + }; +}; + + +/** + * Determine the recovery factor by trying all four + * possibilities and figuring out which results in the + * correct public key + * + * @param {sjcl.ecc.curve} curve + * @param {sjcl.bn} r + * @param {sjcl.bn} s + * @param {bitArray} hash_bits + * @param {sjcl.ecc.point} original_public_key_point + * @returns {Number, 0-3} Recovery factor + */ +function calculateRecoveryFactor(curve, r, s, hash_bits, original_public_key_point) { + + var original_public_key_point_bits = original_public_key_point.toBits(); + + // TODO: verify that it is possible for the recovery_factor to be 2 or 3, + // we may only need 1 bit because the canonical signature might remove the + // possibility of us needing to "use the second candidate key" + for (var possible_factor = 0; possible_factor < 4; possible_factor++) { + + var resulting_public_key_point; + try { + resulting_public_key_point = recoverPublicKeyPointFromSignature(curve, r, s, hash_bits, possible_factor); + } catch (err) { + // console.log(err, err.stack); + continue; + } + + if (sjcl.bitArray.equal(resulting_public_key_point.toBits(), original_public_key_point_bits)) { + return possible_factor; + } + + } + + throw new sjcl.exception.bug('unable to calculate recovery factor from signature'); + +}; + + +/** + * Recover the public key from the signature. + * + * @param {sjcl.ecc.curve} curve + * @param {sjcl.bn} r + * @param {sjcl.bn} s + * @param {bitArray} hash_bits + * @param {Number, 0-3} recovery_factor + * @returns {sjcl.point} Public key corresponding to signature + */ +function recoverPublicKeyPointFromSignature(curve, signature_r, signature_s, hash_bits, recovery_factor) { + + var field_order = curve.r; + var field_modulus = curve.field.modulus; + + // Reduce the recovery_factor to the two bits used + recovery_factor = recovery_factor & 3; + + // The less significant bit specifies whether the y coordinate + // of the compressed point is even or not. + var compressed_point_y_coord_is_even = recovery_factor & 1; + + // The more significant bit specifies whether we should use the + // first or second candidate key. + var use_second_candidate_key = recovery_factor >> 1; + + // Calculate (field_order + 1) / 4 + if (!FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR) { + FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR = field_modulus.add(1).div(4); + } + + // In the paper they write "1. For j from 0 to h do the following..." + // That is not necessary here because we are given the recovery_factor + // step 1.1 Let x = r + jn + // Here "j" is either 0 or 1 + var x; + if (use_second_candidate_key) { + x = signature_r.add(field_order); + } else { + x = signature_r; + } + + // step 1.2 and 1.3 convert x to an elliptic curve point + // Following formula in section 2.3.4 Octet-String-to-Elliptic-Curve-Point Conversion + var alpha = x.mul(x).mul(x).add(curve.a.mul(x)).add(curve.b).mod(field_modulus); + var beta = alpha.powermodMontgomery(FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR, field_modulus); + + // If beta is even but y isn't or + // if beta is odd and y is even + // then subtract beta from the field_modulus + var y; + var beta_is_even = beta.mod(2).equals(0); + if (beta_is_even && !compressed_point_y_coord_is_even || + !beta_is_even && compressed_point_y_coord_is_even) { + y = beta; + } else { + y = field_modulus.sub(beta); + } + + // generated_point_R is the point generated from x and y + var generated_point_R = new sjcl.ecc.point(curve, x, y); + + // step 1.4 check that R is valid and R x field_order !== infinity + // TODO: add check for R x field_order === infinity + if (!generated_point_R.isValidPoint()) { + throw new sjcl.exception.corrupt('point R. Not a valid point on the curve. Cannot recover public key'); + } + + // step 1.5 Compute e from M + var message_e = sjcl.bn.fromBits(hash_bits); + var message_e_neg = new sjcl.bn(0).sub(message_e).mod(field_order); + + // step 1.6 Compute Q = r^-1 (sR - eG) + // console.log('r: ', signature_r); + var signature_r_inv = signature_r.inverseMod(field_order); + var public_key_point = generated_point_R.mult2(signature_s, message_e_neg, curve.G).mult(signature_r_inv); + + // Validate public key point + if (!public_key_point.isValidPoint()) { + throw new sjcl.exception.corrupt('public_key_point. Not a valid point on the curve. Cannot recover public key'); + } + + // Verify that this public key matches the signature + if (!verify_raw(curve, message_e, signature_r, signature_s, public_key_point)) { + throw new sjcl.exception.corrupt('cannot recover public key'); + } + + return public_key_point; + +}; + + +/** + * Verify a signature given the raw components + * using method defined in section 4.1.5: + * "Alternative Verifying Operation" + * + * @param {sjcl.ecc.curve} curve + * @param {sjcl.bn} e + * @param {sjcl.bn} r + * @param {sjcl.bn} s + * @param {sjcl.ecc.point} public_key_point + * @returns {Boolean} + */ +function verify_raw(curve, e, r, s, public_key_point) { + + var field_order = curve.r; + + // Return false if r is out of bounds + if ((new sjcl.bn(1)).greaterEquals(r) || r.greaterEquals(new sjcl.bn(field_order))) { + return false; + } + + // Return false if s is out of bounds + if ((new sjcl.bn(1)).greaterEquals(s) || s.greaterEquals(new sjcl.bn(field_order))) { + return false; + } + + // Check that r = (u1 + u2)G + // u1 = e x s^-1 (mod field_order) + // u2 = r x s^-1 (mod field_order) + var s_mod_inverse_field_order = s.inverseMod(field_order); + var u1 = e.mul(s_mod_inverse_field_order).mod(field_order); + var u2 = r.mul(s_mod_inverse_field_order).mod(field_order); + + var point_computed = curve.G.mult2(u1, u2, public_key_point); + + return r.equals(point_computed.x.mod(field_order)); + +}; + diff --git a/src/js/sjcl-custom/sjcl-secp256k1.js b/src/js/sjcl-custom/sjcl-secp256k1.js index d87bcc6f..a17eccf0 100755 --- a/src/js/sjcl-custom/sjcl-secp256k1.js +++ b/src/js/sjcl-custom/sjcl-secp256k1.js @@ -62,7 +62,7 @@ sjcl.ecc.pointJac.prototype.doubl = function () { 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(); + var z = this.z.mul(this.y).doubleM(); return new sjcl.ecc.pointJac(this.curve, x, y, z); }; diff --git a/src/js/sjcl-custom/sjcl-validecc.js b/src/js/sjcl-custom/sjcl-validecc.js index 536f2bf8..f894dd65 100644 --- a/src/js/sjcl-custom/sjcl-validecc.js +++ b/src/js/sjcl-custom/sjcl-validecc.js @@ -1,9 +1,21 @@ -sjcl.ecc.ecdsa.secretKey.prototype.sign = function(hash, paranoia) { +sjcl.ecc.ecdsa.secretKey.prototype.sign = function(hash, paranoia, k_for_testing) { var R = this._curve.r, - l = R.bitLength(), - k = sjcl.bn.random(R.sub(1), paranoia).add(1), - r = this._curve.G.mult(k).x.mod(R), - s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R); + l = R.bitLength(); + + // k_for_testing should ONLY BE SPECIFIED FOR TESTING + // specifying it will make the signature INSECURE + var k; + if (typeof k_for_testing === 'object' && k_for_testing.length > 0 && typeof k_for_testing[0] === 'number') { + k = k_for_testing; + } else if (typeof k_for_testing === 'string' && /^[0-9a-fA-F]+$/.test(k_for_testing)) { + k = sjcl.bn.fromBits(sjcl.codec.hex.toBits(k_for_testing)); + } else { + // This is the only option that should be used in production + k = sjcl.bn.random(R.sub(1), paranoia).add(1); + } + + var r = this._curve.G.mult(k).x.mod(R); + var s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R); return sjcl.bitArray.concat(r.toBits(l), s.toBits(l)); }; diff --git a/test/pubkeyvalidator-test.js b/test/pubkeyvalidator-test.js new file mode 100644 index 00000000..16a50251 --- /dev/null +++ b/test/pubkeyvalidator-test.js @@ -0,0 +1,157 @@ +var assert = require('assert'); +var PubKeyValidator = require('../src/js/ripple/pubkeyvalidator'); + +describe('PubKeyValidator', function(){ + + describe('._parsePublicKey()', function(){ + + var pkv = new PubKeyValidator({}); + + it('should throw an error if the key is invalid', function(){ + try { + pkv._parsePublicKey('not a real key'); + } catch (e) { + assert(e); + } + }); + + it('should return unchanged a valid UINT160', function(){ + assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === pkv._parsePublicKey('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz')); + }); + + it('should parse a hex-encoded public key as a UINT160', function(){ + assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === pkv._parsePublicKey('025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332')); + + assert('rLpq5RcRzA8FU1yUqEPW4xfsdwon7casuM' === pkv._parsePublicKey('03BFA879C00D58CF55F2B5975FF9B5293008FF49BEFB3EE6BEE2814247BF561A23')); + + assert('rP4yWwjoDGF2iZSBdAQAgpC449YDezEbT1' === pkv._parsePublicKey('02DF0AB18930B6410CA9F55CB37541F1FED891B8EDF8AB1D01D8F23018A4B204A7')); + }); + + }); + + describe('.validate()', function(){ + + it('should respond true if the public key corresponds to the account address and the master key IS NOT disabled', function(){ + + var pkv = new PubKeyValidator({ + account: function(address){ + return { + getInfo: function(callback) { + if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') { + callback(null, { account_data: { + Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', + Flags: 65536, + LedgerEntryType: 'AccountRoot' + }}); + } + } + } + } + }); + pkv.validate('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', '025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332', function(err, is_valid){ + assert(err === null); + assert(is_valid === true); + }); + + }); + + it('should respond false if the public key corresponds to the account address and the master key IS disabled', function(){ + + var pkv = new PubKeyValidator({ + account: function(address){ + return { + getInfo: function(callback) { + if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') { + callback(null, { account_data: { + Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', + Flags: parseInt(65536 | 0x00100000), + LedgerEntryType: 'AccountRoot' + }}); + } + } + } + } + }); + pkv.validate('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', '025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332', function(err, is_valid){ + assert(err === null); + assert(is_valid === false); + }); + + }); + + it('should respond true if the public key corresponds to the regular key', function(){ + + var pkv = new PubKeyValidator({ + account: function(address){ + return { + getInfo: function(callback) { + if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') { + callback(null, { account_data: { + Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', + Flags: parseInt(65536 | 0x00100000), + LedgerEntryType: 'AccountRoot', + RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r' + }}); + } + } + } + } + }); + pkv.validate('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', '02BE53B7ACBB0900E0BB7729C9CAC1033A0137993B17800BD1191BBD1B29D96A8C', function(err, is_valid){ + assert(err === null); + assert(is_valid === true); + }); + + }); + + it('should respond false if the public key does not correspond to an active public key for the account', function(){ + + var pkv = new PubKeyValidator({ + account: function(address){ + return { + getInfo: function(callback) { + if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') { + callback(null, { account_data: { + Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', + Flags: parseInt(65536 | 0x00100000), + LedgerEntryType: 'AccountRoot', + RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r' + }}); + } + } + } + } + }); + pkv.validate('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', '032ECDA93970BC7E8872EF6582CB52A5557F117244A949EB4FA8AC7688CF24FBC8', function(err, is_valid){ + assert(err === null); + assert(is_valid === false); + }); + + }); + + it('should respond false if the public key is invalid', function(){ + + var pkv = new PubKeyValidator({ + account: function(address){ + return { + getInfo: function(callback) { + if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') { + callback(null, { account_data: { + Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', + Flags: parseInt(65536 | 0x00100000), + LedgerEntryType: 'AccountRoot', + RegularKey: 'rNw4ozCG514KEjPs5cDrqEcdsi31Jtfm5r' + }}); + } + } + } + } + }); + pkv.validate('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', 'not a real public key', function(err, is_valid){ + assert(err); + }); + + }); + + }); +}); \ No newline at end of file diff --git a/test/sjcl-ecdsa-recoverablepublickey-test.js b/test/sjcl-ecdsa-recoverablepublickey-test.js new file mode 100644 index 00000000..03590d16 --- /dev/null +++ b/test/sjcl-ecdsa-recoverablepublickey-test.js @@ -0,0 +1,245 @@ +var assert = require('assert'); +var utils = require('./testutils'); +var sjcl = require('../build/sjcl'); + +describe('ECDSA signing with recoverable public key', function(){ + + describe('Sign and recover public key from signature', function(){ + + it('should recover public keys from signatures it generates', function(){ + + var messages = [{ + message: 'Hello world!', + secret_hex: '9931c08f61f127d5735fa3c60e702212ce7ed9a2ac90d5dbade99c689728cd9b', + random_value: '5473a3dbdc13ec9efbad7f7f929fbbea404af556a48041dd9d41d29fdbc989ad', + hash_function: sjcl.hash.sha512.hash + // signature: 'AAAAGzFa1pYjhssCpDFZgFSnYQ8qCnMkLaZrg0mXZyNQ2NxgMQ8z9U3ngYerxSZCEt3Q4raMIpt03db7jDNGbfmHy8I=' + }, { + // Correct recovery value for this one is 0 + message: 'ua5pdcG0I1JuhSr9Fwai2UoZ9ll5leUtHE5NzSSNnPkw8nSPH5mT1gE1fe0sn', + secret_hex: '84814318ffe6e612694ad59b9084b7b66d68b6979567c619171a67b05e2b654b', + random_value: '14261d30b319709c10ab13cabe595313b99dd2d5c76b8b38d7eb445f0b81cc9a', + hash_function: sjcl.hash.sha512.hash + // signature: 'AAAAHGjpBM7wnTHbPGo0TXsxKbr+d7KvACuJ/eGQsp3ZJfOOQHszaciRo3ClenwKixcquFcBlaVfHlOc3JWOZq1RjpQ=' + }, { + // Correct recovery value for this one is 1 + message: 'rxc76UnmVTp', + secret_hex: '37eac47c212be8ea8372f506b11673c281cd9ea29a035c2c9e90d027c3dbecc6', + random_value: '61b53ca6de0543f911765ae216a3a4d851918a0733fba9ac80cf29de5bec8032', + hash_function: sjcl.hash.sha256.hash + // signature: 'AAAAG8L/yOA3nNqK4aOiQWJmOaWvkvr3NoTk6wCdX97U3qowdgFd98UK3evWV16qO3RHgFMEnUW/Vt4+kcidqW6hMo0=' + }]; + + var curve = sjcl.ecc.curves['c256']; + + for (var m = 0; m < messages.length; m++) { + + var message = messages[m].message; + var secret_hex = messages[m].secret_hex; + var random_value = messages[m].random_value; + var hash_function = messages[m].hash_function; + + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + + var pub_val_point = secret_key._curve.G.mult(secret_key._exponent); + var public_key = new sjcl.ecc.ecdsa.publicKey(curve, pub_val_point); + var hash = hash_function(message); + + var recoverable_signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + var recovered_public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, recoverable_signature); + + assert.deepEqual(public_key.get().x, recovered_public_key.get().x, 'The x value for the recovered public key did not match for message: ' + message + '. Expected: ' + public_key.get().x.toString() + '. Actual: ' + recovered_public_key.get().x.toString()); + assert.deepEqual(public_key.get().y, recovered_public_key.get().y, 'The y value for the recovered public key did not match for message: ' + message + '. Expected: ' + public_key.get().y.toString() + '. Actual: ' + recovered_public_key.get().y.toString()); + + } + + }); + + }); + + describe('signWithRecoverablePublicKey', function(){ + + // it('should produce the same values as bitcoinjs-lib\'s implementation', function(){ + + // // TODO: figure out why bitcoinjs-lib and this produce different signature values + + // var curve = sjcl.ecc.curves['c256']; + + // var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + // var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + // var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + + // // var public_key = '0217b9f5b3ba8d550f19fdfb5233818cd27d19aaea029b667f547f5918c307ed3b'; + // var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + // var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + + // var bitcoin_signature_base64 = 'IJPzXewhO1CORRx14FROzZC8ne4v0Me94UZoBKH15e4pcSgeYiYeKZ4PJOBI/D5yqUOhemO+rKKHhE0HL66kAcM='; + + // var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + // var signature_base64 = sjcl.codec.base64.fromBits(signature); + + // assert.equal(signature_base64, bitcoin_signature_base64); + + // }); + + it('should produce an error if the hash is not given as a bitArray', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'; + + assert.throws(function(){ + secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + }, /(?=.*hash)(?=.*bitArray).+/); + + }); + + it('should return a bitArray', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + + var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + assert(typeof signature === 'object' && signature.length > 0 && typeof signature[0] === 'number'); + + }); + + it('should return a bitArray where the first word contains the recovery factor', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + + var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + var recovery_factor = signature[0] - 27; + + assert(recovery_factor >= 0 && recovery_factor < 4); + + }); + + }); + + describe('recoverFromSignature', function(){ + + // it('should be able to recover public keys from bitcoinjs-lib\'s implementation', function(){ + + // // TODO: figure out why bitcoinjs-lib and this produce different signature values + + // var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + // var signature = sjcl.codec.base64.toBits('IJPzXewhO1CORRx14FROzZC8ne4v0Me94UZoBKH15e4pcSgeYiYeKZ4PJOBI/D5yqUOhemO+rKKHhE0HL66kAcM='); + + // var public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature); + + // }); + + it('should produce an error if the signature given does not have the recovery factor prefix', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + + var signature = secret_key.sign(hash, 0, random_value); + + assert.throws(function(){ + sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature); + }, /(?=.*signature)(?=.*recovery factor)(?=.*public key).*/); + + }); + + it('should produce an error if it is not given both the hash and the signature', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + + var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + + assert.throws(function(){ + sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash); + }, /(?=.*hash\ and\ signature)(?=.*recover\ public\ key).*/); + + assert.throws(function(){ + sjcl.ecc.ecdsa.publicKey.recoverFromSignature(signature); + }, /(?=.*hash\ and\ signature)(?=.*recover\ public\ key).*/); + + }); + + it('should produce an error if it cannot generate a valid public key from the the signature', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + + var signature = sjcl.codec.base64.toBits('IJPzXewhO1CORRx14FROzZC8ne4v0Me94UZoBKH15e4pcSgeYiYeKZ4PJOBI/D5yqUOhemO+rKKHhE0HL66kAcM='); + signature[0] = 27; + + signature[3] = 0 - signature[3]; + + assert.throws(function(){ + sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature); + }, /(?=.*Cannot\ recover\ public\ key).*/); + + }); + + it('should return a publicKey object', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + + var key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature); + + assert(key instanceof sjcl.ecc.ecdsa.publicKey); + + }); + + it('tampering with the signature should produce a different public key, if it produces a valid one at all', function(){ + + var curve = sjcl.ecc.curves['c256']; + var secret_hex = '9e623166ac44d4e75fa842f3443485b9c8380551132a8ffaa898b5c93bb18b7d'; + var secret_bn = sjcl.bn.fromBits(sjcl.codec.hex.toBits(secret_hex)); + var secret_key = new sjcl.ecc.ecdsa.secretKey(curve, secret_bn); + var random_value = 'c3aa71cecb965bbbc96083d868b4955d77adb4e02ce229fe60869f745dfcd4e4a4d0f17a15a353d7592dca1baba2824e45c8e7a8f9faad3ce2c2d3792799f27a'; + var hash = sjcl.codec.hex.toBits('e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'); + + var signature = secret_key.signWithRecoverablePublicKey(hash, 0, random_value); + + signature[3]++; + + var original_public_key = new sjcl.ecc.ecdsa.publicKey(curve, curve.G.mult(secret_key._exponent)); + var recovered_public_key = sjcl.ecc.ecdsa.publicKey.recoverFromSignature(hash, signature); + + assert.notDeepEqual(original_public_key.get().x, recovered_public_key.get().x); + assert.notDeepEqual(original_public_key.get().y, recovered_public_key.get().y); + + }); + + }); + +}); + + From c32216c9e59b2b1a708e5a3d615612455e2423f5 Mon Sep 17 00:00:00 2001 From: Evan Schwartz Date: Thu, 1 May 2014 12:08:14 -0700 Subject: [PATCH 31/40] [CHORE] Added account param to signing functions --- src/js/ripple/message.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/js/ripple/message.js b/src/js/ripple/message.js index e92b2724..5614eea3 100644 --- a/src/js/ripple/message.js +++ b/src/js/ripple/message.js @@ -28,11 +28,13 @@ var REGEX_BASE64 = /^([A-Za-z0-9\+]{4})*([A-Za-z0-9\+]{2}==)|([A-Za-z0-9\+]{3}=) * * @param {String} message * @param {sjcl.ecc.ecdsa.secretKey|Any format accepted by Seed.from_json} secret_key + * @param {RippleAddress} [The first key] account Field to specify the signing account. + * If this is omitted the first account produced by the secret generator will be used. * @returns {Base64-encoded String} signature */ -Message.signMessage = function(message, secret_key) { +Message.signMessage = function(message, secret_key, account) { - return Message.signHash(Message.HASH_FUNCTION(Message.MAGIC_BYTES + message), secret_key); + return Message.signHash(Message.HASH_FUNCTION(Message.MAGIC_BYTES + message), secret_key, account); }; @@ -47,9 +49,11 @@ Message.signMessage = function(message, secret_key) { * * @param {bitArray|Hex-encoded String} hash * @param {sjcl.ecc.ecdsa.secretKey|Any format accepted by Seed.from_json} secret_key + * @param {RippleAddress} [The first key] account Field to specify the signing account. + * If this is omitted the first account produced by the secret generator will be used. * @returns {Base64-encoded String} signature */ -Message.signHash = function(hash, secret_key) { +Message.signHash = function(hash, secret_key, account) { if (typeof hash === 'string' && /^[0-9a-fA-F]+$/.test(hash)) { hash = sjcl.codec.hex.toBits(hash); @@ -60,7 +64,7 @@ Message.signHash = function(hash, secret_key) { } if (!(secret_key instanceof sjcl.ecc.ecdsa.secretKey)) { - secret_key = Seed.from_json(secret_key).get_key()._secret; + secret_key = Seed.from_json(secret_key).get_key(account)._secret; } var signature_bits = secret_key.signWithRecoverablePublicKey(hash); From e19be192bde32dc48fa511a9745174541d7e8979 Mon Sep 17 00:00:00 2001 From: Evan Schwartz Date: Thu, 1 May 2014 13:14:55 -0700 Subject: [PATCH 32/40] [FIX] Point coordinates should be converted to psuedo mersenne primes --- src/js/sjcl/core/ecc.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/js/sjcl/core/ecc.js b/src/js/sjcl/core/ecc.js index b36e7221..9a5a9b88 100644 --- a/src/js/sjcl/core/ecc.js +++ b/src/js/sjcl/core/ecc.js @@ -11,8 +11,16 @@ sjcl.ecc.point = function(curve,x,y) { if (x === undefined) { this.isIdentity = true; } else { + if (x instanceof sjcl.bn) { + x = new curve.field(x); + } + if (y instanceof sjcl.bn) { + y = new curve.field(y); + } + this.x = x; this.y = y; + this.isIdentity = false; } this.curve = curve; From 13a6a2c3355f49cba1626d2ab5f5656d59808e04 Mon Sep 17 00:00:00 2001 From: Evan Schwartz Date: Thu, 1 May 2014 16:29:23 -0700 Subject: [PATCH 33/40] [CHORE] Added pre-built sjcl with additional functions included --- build/sjcl.js | 455 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 431 insertions(+), 24 deletions(-) diff --git a/build/sjcl.js b/build/sjcl.js index b2c436dc..5b4baf99 100644 --- a/build/sjcl.js +++ b/build/sjcl.js @@ -2914,8 +2914,16 @@ sjcl.ecc.point = function(curve,x,y) { if (x === undefined) { this.isIdentity = true; } else { + if (x instanceof sjcl.bn) { + x = new curve.field(x); + } + if (y instanceof sjcl.bn) { + y = new curve.field(y); + } + this.x = x; this.y = y; + this.isIdentity = false; } this.curve = curve; @@ -3428,6 +3436,90 @@ sjcl.keyexchange.srp = { }; +/** + * Check that the point is valid based on the method described in + * SEC 1: Elliptic Curve Cryptography, section 3.2.2.1: + * Elliptic Curve Public Key Validation Primitive + * http://www.secg.org/download/aid-780/sec1-v2.pdf + * + * @returns {Boolean} + */ +sjcl.ecc.point.prototype.isValidPoint = function() { + + var self = this; + + var field_modulus = self.curve.field.modulus; + + if (self.isIdentity) { + return false; + } + + // Check that coordinatres are in bounds + // Return false if x < 1 or x > (field_modulus - 1) + if (((new sjcl.bn(1).greaterEquals(self.x)) && + !self.x.equals(1)) || + (self.x.greaterEquals(field_modulus.sub(1))) && + !self.x.equals(1)) { + + return false; + } + + // Return false if y < 1 or y > (field_modulus - 1) + if (((new sjcl.bn(1).greaterEquals(self.y)) && + !self.y.equals(1)) || + (self.y.greaterEquals(field_modulus.sub(1))) && + !self.y.equals(1)) { + + return false; + } + + if (!self.isOnCurve()) { + return false; + } + + // TODO check to make sure point is a scalar multiple of base_point + + return true; + +}; + +/** + * Check that the point is on the curve + * + * @returns {Boolean} + */ +sjcl.ecc.point.prototype.isOnCurve = function() { + + var self = this; + + var field_order = self.curve.r; + var component_a = self.curve.a; + var component_b = self.curve.b; + var field_modulus = self.curve.field.modulus; + + var y_squared_mod_field_order = self.y.mul(self.y).mod(field_modulus); + var x_cubed_plus_ax_plus_b = self.x.mul(self.x).mul(self.x).add(component_a.mul(self.x)).add(component_b).mod(field_modulus); + + return y_squared_mod_field_order.equals(x_cubed_plus_ax_plus_b); + +}; + + +sjcl.ecc.point.prototype.toString = function() { + return '(' + + this.x.toString() + ', ' + + this.y.toString() + + ')'; +}; + +sjcl.ecc.pointJac.prototype.toString = function() { + return '(' + + this.x.toString() + ', ' + + this.y.toString() + ', ' + + this.z.toString() + + ')'; +}; + // ----- for secp256k1 ------ // Overwrite NIST-P256 with secp256k1 @@ -3492,7 +3584,7 @@ sjcl.ecc.pointJac.prototype.doubl = function () { 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(); + var z = this.z.mul(this.y).doubleM(); return new sjcl.ecc.pointJac(this.curve, x, y, z); }; @@ -4012,35 +4104,43 @@ sjcl.bn.prototype.powermodMontgomery = function (e, m) return z.revert(r); } -sjcl.ecc.ecdsa.secretKey.prototype = { - sign: function(hash, paranoia) { - var R = this._curve.r, - l = R.bitLength(), - k = sjcl.bn.random(R.sub(1), paranoia).add(1), - r = this._curve.G.mult(k).x.mod(R), - s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R); +sjcl.ecc.ecdsa.secretKey.prototype.sign = function(hash, paranoia, k_for_testing) { + var R = this._curve.r, + l = R.bitLength(); - return sjcl.bitArray.concat(r.toBits(l), s.toBits(l)); + // k_for_testing should ONLY BE SPECIFIED FOR TESTING + // specifying it will make the signature INSECURE + var k; + if (typeof k_for_testing === 'object' && k_for_testing.length > 0 && typeof k_for_testing[0] === 'number') { + k = k_for_testing; + } else if (typeof k_for_testing === 'string' && /^[0-9a-fA-F]+$/.test(k_for_testing)) { + k = sjcl.bn.fromBits(sjcl.codec.hex.toBits(k_for_testing)); + } else { + // This is the only option that should be used in production + k = sjcl.bn.random(R.sub(1), paranoia).add(1); } + + var r = this._curve.G.mult(k).x.mod(R); + var s = sjcl.bn.fromBits(hash).add(r.mul(this._exponent)).mul(k.inverseMod(R)).mod(R); + + return sjcl.bitArray.concat(r.toBits(l), s.toBits(l)); }; -sjcl.ecc.ecdsa.publicKey.prototype = { - verify: function(hash, rs) { - var w = sjcl.bitArray, - R = this._curve.r, - l = R.bitLength(), - r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)), - s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)), - sInv = s.inverseMod(R), - hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R), - hA = r.mul(sInv).mod(R), - r2 = this._curve.G.mult2(hG, hA, this._point).x; +sjcl.ecc.ecdsa.publicKey.prototype.verify = function(hash, rs) { + var w = sjcl.bitArray, + R = this._curve.r, + l = R.bitLength(), + r = sjcl.bn.fromBits(w.bitSlice(rs,0,l)), + s = sjcl.bn.fromBits(w.bitSlice(rs,l,2*l)), + sInv = s.inverseMod(R), + hG = sjcl.bn.fromBits(hash).mul(sInv).mod(R), + hA = r.mul(sInv).mod(R), + r2 = this._curve.G.mult2(hG, hA, this._point).x; - if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) { - throw (new sjcl.exception.corrupt("signature didn't check out")); - } - return true; + if (r.equals(0) || s.equals(0) || r.greaterEquals(R) || s.greaterEquals(R) || !r2.equals(r)) { + throw (new sjcl.exception.corrupt("signature didn't check out")); } + return true; }; sjcl.ecc.ecdsa.secretKey.prototype.canonicalizeSignature = function(rs) { @@ -4096,6 +4196,313 @@ sjcl.ecc.ecdsa.secretKey.prototype.encodeDER = function(rs) { }; +/** + * This module uses the public key recovery method + * described in SEC 1: Elliptic Curve Cryptography, + * section 4.1.6, "Public Key Recovery Operation". + * http://www.secg.org/download/aid-780/sec1-v2.pdf + * + * Implementation based on: + * https://github.com/bitcoinjs/bitcoinjs-lib/blob/89cf731ac7309b4f98994e3b4b67b7226020181f/src/ecdsa.js + */ + +// Defined here so that this value only needs to be calculated once +var FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR; + +/** + * Sign the given hash such that the public key, prepending an extra byte + * so that the public key will be recoverable from the signature + * + * @param {bitArray} hash + * @param {Number} paranoia + * @returns {bitArray} Signature formatted as bitArray + */ +sjcl.ecc.ecdsa.secretKey.prototype.signWithRecoverablePublicKey = function(hash, paranoia, k_for_testing) { + + var self = this; + + // Convert hash to bits and determine encoding for output + var hash_bits; + if (typeof hash === 'object' && hash.length > 0 && typeof hash[0] === 'number') { + hash_bits = hash; + } else { + throw new sjcl.exception.invalid('hash. Must be a bitArray'); + } + + // Sign hash with standard, canonicalized method + var standard_signature = self.sign(hash_bits, paranoia, k_for_testing); + var canonical_signature = self.canonicalizeSignature(standard_signature); + + // Extract r and s signature components from canonical signature + var r_and_s = getRandSFromSignature(self._curve, canonical_signature); + + // Rederive public key + var public_key = self._curve.G.mult(sjcl.bn.fromBits(self.get())); + + // Determine recovery factor based on which possible value + // returns the correct public key + var recovery_factor = calculateRecoveryFactor(self._curve, r_and_s.r, r_and_s.s, hash_bits, public_key); + + // Prepend recovery_factor to signature and encode in DER + // The value_to_prepend should be 4 bytes total + var value_to_prepend = recovery_factor + 27; + + var final_signature_bits = sjcl.bitArray.concat([value_to_prepend], canonical_signature); + + // Return value in bits + return final_signature_bits; + +}; + + +/** + * Recover the public key from a signature created with the + * signWithRecoverablePublicKey method in this module + * + * @static + * + * @param {bitArray} hash + * @param {bitArray} signature + * @param {sjcl.ecc.curve} [sjcl.ecc.curves['c256']] curve + * @returns {sjcl.ecc.ecdsa.publicKey} Public key + */ +sjcl.ecc.ecdsa.publicKey.recoverFromSignature = function(hash, signature, curve) { + + if (!signature || signature instanceof sjcl.ecc.curve) { + throw new sjcl.exception.invalid('must supply hash and signature to recover public key'); + } + + if (!curve) { + curve = sjcl.ecc.curves['c256']; + } + + // Convert hash to bits and determine encoding for output + var hash_bits; + if (typeof hash === 'object' && hash.length > 0 && typeof hash[0] === 'number') { + hash_bits = hash; + } else { + throw new sjcl.exception.invalid('hash. Must be a bitArray'); + } + + var signature_bits; + if (typeof signature === 'object' && signature.length > 0 && typeof signature[0] === 'number') { + signature_bits = signature; + } else { + throw new sjcl.exception.invalid('signature. Must be a bitArray'); + } + + // Extract recovery_factor from first 4 bytes + var recovery_factor = signature_bits[0] - 27; + + if (recovery_factor < 0 || recovery_factor > 3) { + throw new sjcl.exception.invalid('signature. Signature must be generated with algorithm ' + + 'that prepends the recovery factor in order to recover the public key'); + } + + // Separate r and s values + var r_and_s = getRandSFromSignature(curve, signature_bits.slice(1)); + var signature_r = r_and_s.r; + var signature_s = r_and_s.s; + + // Recover public key using recovery_factor + var recovered_public_key_point = recoverPublicKeyPointFromSignature(curve, signature_r, signature_s, hash_bits, recovery_factor); + var recovered_public_key = new sjcl.ecc.ecdsa.publicKey(curve, recovered_public_key_point); + + return recovered_public_key; + +}; + + +/** + * Retrieve the r and s components of a signature + * + * @param {sjcl.ecc.curve} curve + * @param {bitArray} signature + * @returns {Object} Object with 'r' and 's' fields each as an sjcl.bn + */ +function getRandSFromSignature(curve, signature) { + + var r_length = curve.r.bitLength(); + + return { + r: sjcl.bn.fromBits(sjcl.bitArray.bitSlice(signature, 0, r_length)), + s: sjcl.bn.fromBits(sjcl.bitArray.bitSlice(signature, r_length, sjcl.bitArray.bitLength(signature))) + }; +}; + + +/** + * Determine the recovery factor by trying all four + * possibilities and figuring out which results in the + * correct public key + * + * @param {sjcl.ecc.curve} curve + * @param {sjcl.bn} r + * @param {sjcl.bn} s + * @param {bitArray} hash_bits + * @param {sjcl.ecc.point} original_public_key_point + * @returns {Number, 0-3} Recovery factor + */ +function calculateRecoveryFactor(curve, r, s, hash_bits, original_public_key_point) { + + var original_public_key_point_bits = original_public_key_point.toBits(); + + // TODO: verify that it is possible for the recovery_factor to be 2 or 3, + // we may only need 1 bit because the canonical signature might remove the + // possibility of us needing to "use the second candidate key" + for (var possible_factor = 0; possible_factor < 4; possible_factor++) { + + var resulting_public_key_point; + try { + resulting_public_key_point = recoverPublicKeyPointFromSignature(curve, r, s, hash_bits, possible_factor); + } catch (err) { + // console.log(err, err.stack); + continue; + } + + if (sjcl.bitArray.equal(resulting_public_key_point.toBits(), original_public_key_point_bits)) { + return possible_factor; + } + + } + + throw new sjcl.exception.bug('unable to calculate recovery factor from signature'); + +}; + + +/** + * Recover the public key from the signature. + * + * @param {sjcl.ecc.curve} curve + * @param {sjcl.bn} r + * @param {sjcl.bn} s + * @param {bitArray} hash_bits + * @param {Number, 0-3} recovery_factor + * @returns {sjcl.point} Public key corresponding to signature + */ +function recoverPublicKeyPointFromSignature(curve, signature_r, signature_s, hash_bits, recovery_factor) { + + var field_order = curve.r; + var field_modulus = curve.field.modulus; + + // Reduce the recovery_factor to the two bits used + recovery_factor = recovery_factor & 3; + + // The less significant bit specifies whether the y coordinate + // of the compressed point is even or not. + var compressed_point_y_coord_is_even = recovery_factor & 1; + + // The more significant bit specifies whether we should use the + // first or second candidate key. + var use_second_candidate_key = recovery_factor >> 1; + + // Calculate (field_order + 1) / 4 + if (!FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR) { + FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR = field_modulus.add(1).div(4); + } + + // In the paper they write "1. For j from 0 to h do the following..." + // That is not necessary here because we are given the recovery_factor + // step 1.1 Let x = r + jn + // Here "j" is either 0 or 1 + var x; + if (use_second_candidate_key) { + x = signature_r.add(field_order); + } else { + x = signature_r; + } + + // step 1.2 and 1.3 convert x to an elliptic curve point + // Following formula in section 2.3.4 Octet-String-to-Elliptic-Curve-Point Conversion + var alpha = x.mul(x).mul(x).add(curve.a.mul(x)).add(curve.b).mod(field_modulus); + var beta = alpha.powermodMontgomery(FIELD_MODULUS_PLUS_ONE_DIVIDED_BY_FOUR, field_modulus); + + // If beta is even but y isn't or + // if beta is odd and y is even + // then subtract beta from the field_modulus + var y; + var beta_is_even = beta.mod(2).equals(0); + if (beta_is_even && !compressed_point_y_coord_is_even || + !beta_is_even && compressed_point_y_coord_is_even) { + y = beta; + } else { + y = field_modulus.sub(beta); + } + + // generated_point_R is the point generated from x and y + var generated_point_R = new sjcl.ecc.point(curve, x, y); + + // step 1.4 check that R is valid and R x field_order !== infinity + // TODO: add check for R x field_order === infinity + if (!generated_point_R.isValidPoint()) { + throw new sjcl.exception.corrupt('point R. Not a valid point on the curve. Cannot recover public key'); + } + + // step 1.5 Compute e from M + var message_e = sjcl.bn.fromBits(hash_bits); + var message_e_neg = new sjcl.bn(0).sub(message_e).mod(field_order); + + // step 1.6 Compute Q = r^-1 (sR - eG) + // console.log('r: ', signature_r); + var signature_r_inv = signature_r.inverseMod(field_order); + var public_key_point = generated_point_R.mult2(signature_s, message_e_neg, curve.G).mult(signature_r_inv); + + // Validate public key point + if (!public_key_point.isValidPoint()) { + throw new sjcl.exception.corrupt('public_key_point. Not a valid point on the curve. Cannot recover public key'); + } + + // Verify that this public key matches the signature + if (!verify_raw(curve, message_e, signature_r, signature_s, public_key_point)) { + throw new sjcl.exception.corrupt('cannot recover public key'); + } + + return public_key_point; + +}; + + +/** + * Verify a signature given the raw components + * using method defined in section 4.1.5: + * "Alternative Verifying Operation" + * + * @param {sjcl.ecc.curve} curve + * @param {sjcl.bn} e + * @param {sjcl.bn} r + * @param {sjcl.bn} s + * @param {sjcl.ecc.point} public_key_point + * @returns {Boolean} + */ +function verify_raw(curve, e, r, s, public_key_point) { + + var field_order = curve.r; + + // Return false if r is out of bounds + if ((new sjcl.bn(1)).greaterEquals(r) || r.greaterEquals(new sjcl.bn(field_order))) { + return false; + } + + // Return false if s is out of bounds + if ((new sjcl.bn(1)).greaterEquals(s) || s.greaterEquals(new sjcl.bn(field_order))) { + return false; + } + + // Check that r = (u1 + u2)G + // u1 = e x s^-1 (mod field_order) + // u2 = r x s^-1 (mod field_order) + var s_mod_inverse_field_order = s.inverseMod(field_order); + var u1 = e.mul(s_mod_inverse_field_order).mod(field_order); + var u2 = r.mul(s_mod_inverse_field_order).mod(field_order); + + var point_computed = curve.G.mult2(u1, u2, public_key_point); + + return r.equals(point_computed.x.mod(field_order)); + +}; + + sjcl.bn.prototype.jacobi = function (that) { var a = this; that = new sjcl.bn(that); From a2b07d5edd5e96623a31685ab613b6a76ebec255 Mon Sep 17 00:00:00 2001 From: Evan Schwartz Date: Thu, 1 May 2014 17:22:20 -0700 Subject: [PATCH 34/40] [FIX] Handling public key validation for unfunded accounts --- src/js/ripple/message.js | 7 +- src/js/ripple/pubkeyvalidator.js | 83 +++++++++++++------ .../sjcl-ecdsa-recoverablepublickey.js | 2 - test/pubkeyvalidator-test.js | 44 ++++++++++ 4 files changed, 109 insertions(+), 27 deletions(-) diff --git a/src/js/ripple/message.js b/src/js/ripple/message.js index 5614eea3..33b5918d 100644 --- a/src/js/ripple/message.js +++ b/src/js/ripple/message.js @@ -170,7 +170,12 @@ Message.verifyHashSignature = function(data, remote, callback) { } catch (err) { return async_callback(err); } - async_callback(null, public_key); + + if (public_key) { + async_callback(null, public_key); + } else { + async_callback(new Error('Could not recover public key from signature')); + } }; diff --git a/src/js/ripple/pubkeyvalidator.js b/src/js/ripple/pubkeyvalidator.js index d26e5857..5ac3d501 100644 --- a/src/js/ripple/pubkeyvalidator.js +++ b/src/js/ripple/pubkeyvalidator.js @@ -19,27 +19,6 @@ function PubKeyValidator(remote) { throw(new Error('Must instantiate the PubKeyValidator with a ripple-lib Remote')); } - // Convert hex string to UInt160 - self._parsePublicKey = function(public_key) { - - // Based on functions in /src/js/ripple/keypair.js - function hexToUInt160(public_key) { - var bits = sjcl.codec.hex.toBits(public_key); - var hash = sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits)); - var address = UInt160.from_bits(hash); - address.set_version(Base.VER_ACCOUNT_ID); - return address.to_json(); - } - - if (UInt160.is_valid(public_key)) { - return public_key; - } else if (/^[0-9a-fA-F]+$/.test(public_key)) { - return hexToUInt160(public_key); - } else { - throw(new Error('Public key is invalid. Must be a UInt160 or a hex string')); - } - }; - } /** @@ -60,16 +39,37 @@ PubKeyValidator.prototype.validate = function(address, public_key, callback) { var public_key_as_uint160; try { public_key_as_uint160 = self._parsePublicKey(public_key); - } catch (e) { - return callback(e); + } catch (err) { + return callback(err); } function getAccountInfo(async_callback) { - self._remote.account(address).getInfo(async_callback); + self._remote.account(address).getInfo(function(err, account_info_res){ + + // If the remote responds with an Account Not Found error then the account + // is unfunded and thus we can assume that the master key is active + if (err && err.remote && err.remote.error === 'actNotFound') { + async_callback(null, null); + } else { + async_callback(err, account_info_res); + } + }); }; function publicKeyIsValid(account_info_res, async_callback) { + // Catch the case of unfunded accounts + if (!account_info_res) { + + if (public_key_as_uint160 === address) { + async_callback(null, true); + } else { + async_callback(null, false); + } + + return; + } + var account_info = account_info_res.account_data; // Respond with true if the RegularKey is set and matches the given public key or @@ -101,4 +101,39 @@ PubKeyValidator.prototype.validate = function(address, public_key, callback) { }; +/** + * Convert a hex-encoded public key to a Ripple Address + * + * @param {Hex-encoded string|RippleAddress} public_key + * @returns {RippleAddress} + */ +PubKeyValidator.prototype._parsePublicKey = function(public_key) { + + // Based on functions in /src/js/ripple/keypair.js + function hexToUInt160(public_key) { + + var bits = sjcl.codec.hex.toBits(public_key); + var hash = sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits)); + var address = UInt160.from_bits(hash); + address.set_version(Base.VER_ACCOUNT_ID); + + return address.to_json(); + + } + + if (UInt160.is_valid(public_key)) { + + return public_key; + + } else if (/^[0-9a-fA-F]+$/.test(public_key)) { + + return hexToUInt160(public_key); + + } else { + + throw(new Error('Public key is invalid. Must be a UInt160 or a hex string')); + + } +}; + module.exports = PubKeyValidator; diff --git a/src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js b/src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js index f8f1326e..3365342c 100644 --- a/src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js +++ b/src/js/sjcl-custom/sjcl-ecdsa-recoverablepublickey.js @@ -70,8 +70,6 @@ sjcl.ecc.ecdsa.secretKey.prototype.signWithRecoverablePublicKey = function(hash, */ sjcl.ecc.ecdsa.publicKey.recoverFromSignature = function(hash, signature, curve) { - var self = this; - if (!signature || signature instanceof sjcl.ecc.curve) { throw new sjcl.exception.invalid('must supply hash and signature to recover public key'); } diff --git a/test/pubkeyvalidator-test.js b/test/pubkeyvalidator-test.js index 16a50251..b0ead7be 100644 --- a/test/pubkeyvalidator-test.js +++ b/test/pubkeyvalidator-test.js @@ -25,6 +25,8 @@ describe('PubKeyValidator', function(){ assert('rLpq5RcRzA8FU1yUqEPW4xfsdwon7casuM' === pkv._parsePublicKey('03BFA879C00D58CF55F2B5975FF9B5293008FF49BEFB3EE6BEE2814247BF561A23')); assert('rP4yWwjoDGF2iZSBdAQAgpC449YDezEbT1' === pkv._parsePublicKey('02DF0AB18930B6410CA9F55CB37541F1FED891B8EDF8AB1D01D8F23018A4B204A7')); + + assert('rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ' === pkv._parsePublicKey('0310C451A40CAFFD39D6B8A3BD61BF65BCA55246E9DABC3170EBE431D30655B61F')); }); }); @@ -153,5 +155,47 @@ describe('PubKeyValidator', function(){ }); + it('should assume the master key is valid for unfunded accounts', function(){ + + var pkv = new PubKeyValidator({ + account: function(address){ + return { + getInfo: function(callback) { + if (address === 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ') { + callback({ error: 'remoteError', + error_message: 'Remote reported an error.', + remote: + { account: 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ', + error: 'actNotFound', + error_code: 15, + error_message: 'Account not found.', + id: 3, + ledger_current_index: 6391106, + request: + { account: 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ', + command: 'account_info', + id: 3, + ident: 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ' }, + status: 'error', + type: 'response' }, + result: 'remoteError', + engine_result: 'remoteError', + result_message: 'Remote reported an error.', + engine_result_message: 'Remote reported an error.', + message: 'Remote reported an error.' + }); + } + } + } + } + }); + pkv.validate('rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ', '0310C451A40CAFFD39D6B8A3BD61BF65BCA55246E9DABC3170EBE431D30655B61F', function(err, is_valid){ + assert(!err); + assert(is_valid); + }); + + }); + }); + }); \ No newline at end of file From d8504a300159cba0ef8ac305dc540323fa1e86d0 Mon Sep 17 00:00:00 2001 From: Evan Schwartz Date: Thu, 1 May 2014 19:37:59 -0700 Subject: [PATCH 35/40] [CHORE] Changed variable name to make Stefan happier --- src/js/sjcl-custom/sjcl-ecc-pointextras.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/js/sjcl-custom/sjcl-ecc-pointextras.js b/src/js/sjcl-custom/sjcl-ecc-pointextras.js index e840ce0b..057e9069 100644 --- a/src/js/sjcl-custom/sjcl-ecc-pointextras.js +++ b/src/js/sjcl-custom/sjcl-ecc-pointextras.js @@ -59,10 +59,10 @@ sjcl.ecc.point.prototype.isOnCurve = function() { var component_b = self.curve.b; var field_modulus = self.curve.field.modulus; - var y_squared_mod_field_order = self.y.mul(self.y).mod(field_modulus); - var x_cubed_plus_ax_plus_b = self.x.mul(self.x).mul(self.x).add(component_a.mul(self.x)).add(component_b).mod(field_modulus); + var left_hand_side = self.y.mul(self.y).mod(field_modulus); + var right_hand_side = self.x.mul(self.x).mul(self.x).add(component_a.mul(self.x)).add(component_b).mod(field_modulus); - return y_squared_mod_field_order.equals(x_cubed_plus_ax_plus_b); + return left_hand_side.equals(right_hand_side); }; From cf3a21a712e01e832026afe6b4212407a7befe46 Mon Sep 17 00:00:00 2001 From: Evan Schwartz Date: Thu, 1 May 2014 20:23:20 -0700 Subject: [PATCH 36/40] [CHORE] Merged PubKeyValidator into Account class --- src/js/ripple/account.js | 122 ++++++- src/js/ripple/message.js | 6 +- src/js/ripple/pubkeyvalidator.js | 139 -------- ...ubkeyvalidator-test.js => account-test.js} | 100 +++--- test/message-test.js | 327 ++++++++++++++++++ 5 files changed, 491 insertions(+), 203 deletions(-) delete mode 100644 src/js/ripple/pubkeyvalidator.js rename test/{pubkeyvalidator-test.js => account-test.js} (61%) create mode 100644 test/message-test.js diff --git a/src/js/ripple/account.js b/src/js/ripple/account.js index 4f221068..b1a0f144 100644 --- a/src/js/ripple/account.js +++ b/src/js/ripple/account.js @@ -10,13 +10,15 @@ // // var network = require("./network.js"); - +var async = require('async'); var EventEmitter = require('events').EventEmitter; var util = require('util'); var extend = require('extend'); var Amount = require('./amount').Amount; var UInt160 = require('./uint160').UInt160; var TransactionManager = require('./transactionmanager').TransactionManager; +var sjcl = require('./utils').sjcl; +var Base = require('./base').Base; /** * @constructor Account @@ -278,6 +280,124 @@ Account.prototype.submit = function(transaction) { this._transactionManager.submit(transaction); }; + +/** + * Check whether the given public key is valid for this account + * + * @param {Hex-encoded String|RippleAddress} public_key + * @param {Function} callback + * + * @callback + * @param {Error} err + * @param {Boolean} true if the public key is valid and active, false otherwise + */ +Account.prototype.publicKeyIsActive = function(public_key, callback) { + + var self = this; + + var public_key_as_uint160; + try { + public_key_as_uint160 = Account._publicKeyToAddress(public_key); + } catch (err) { + return callback(err); + } + + function getAccountInfo(async_callback) { + self.getInfo(function(err, account_info_res){ + + // If the remote responds with an Account Not Found error then the account + // is unfunded and thus we can assume that the master key is active + if (err && err.remote && err.remote.error === 'actNotFound') { + async_callback(null, null); + } else { + async_callback(err, account_info_res); + } + }); + }; + + function publicKeyIsValid(account_info_res, async_callback) { + // Catch the case of unfunded accounts + if (!account_info_res) { + + if (public_key_as_uint160 === self._account_id) { + async_callback(null, true); + } else { + async_callback(null, false); + } + + return; + } + + var account_info = account_info_res.account_data; + + // Respond with true if the RegularKey is set and matches the given public key or + // if the public key matches the account address and the lsfDisableMaster is not set + if (account_info.RegularKey && + account_info.RegularKey === public_key_as_uint160) { + + async_callback(null, true); + + } else if (account_info.Account === public_key_as_uint160 && + ((account_info.Flags & 0x00100000) === 0)) { + + async_callback(null, true); + + } else { + + async_callback(null, false); + + } + + }; + + var steps = [ + getAccountInfo, + publicKeyIsValid + ]; + + async.waterfall(steps, callback); + +}; + +/** + * Convert a hex-encoded public key to a Ripple Address + * + * @static + * + * @param {Hex-encoded string|RippleAddress} public_key + * @returns {RippleAddress} + */ +Account._publicKeyToAddress = function(public_key) { + + // Based on functions in /src/js/ripple/keypair.js + function hexToUInt160(public_key) { + + var bits = sjcl.codec.hex.toBits(public_key); + var hash = sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits)); + var address = UInt160.from_bits(hash); + address.set_version(Base.VER_ACCOUNT_ID); + + return address.to_json(); + + } + + if (UInt160.is_valid(public_key)) { + + return public_key; + + } else if (/^[0-9a-fA-F]+$/.test(public_key)) { + + return hexToUInt160(public_key); + + } else { + + throw(new Error('Public key is invalid. Must be a UInt160 or a hex string')); + + } +}; + + + exports.Account = Account; // vim:sw=2:sts=2:ts=8:et diff --git a/src/js/ripple/message.js b/src/js/ripple/message.js index 33b5918d..a6043740 100644 --- a/src/js/ripple/message.js +++ b/src/js/ripple/message.js @@ -4,7 +4,7 @@ var sjcl = require('./utils').sjcl; var Remote = require('./remote').Remote; var Seed = require('./seed').Seed; var KeyPair = require('./keypair').KeyPair; -var PublicKeyValidator = require('./pubkeyvalidator'); +var Account = require('./account').Account; var UInt160 = require('./uint160').UInt160; // Message class (static) @@ -186,8 +186,8 @@ Message.verifyHashSignature = function(data, remote, callback) { key_pair._pubkey = public_key; var public_key_hex = key_pair.to_hex_pub(); - var public_key_validator = new PublicKeyValidator(remote); - public_key_validator.validate(account, public_key_hex, async_callback); + var account_class_instance = new Account(remote, account); + account_class_instance.publicKeyIsActive(public_key_hex, async_callback); }; diff --git a/src/js/ripple/pubkeyvalidator.js b/src/js/ripple/pubkeyvalidator.js deleted file mode 100644 index 5ac3d501..00000000 --- a/src/js/ripple/pubkeyvalidator.js +++ /dev/null @@ -1,139 +0,0 @@ -var async = require('async'); -var UInt160 = require('./uint160').UInt160; -var sjcl = require('./utils').sjcl; -var Base = require('./base').Base; - - -/** - * @constructor PubKeyValidator - * @param {Remote} remote - */ - -function PubKeyValidator(remote) { - - var self = this; - - if (remote) { - self._remote = remote; - } else { - throw(new Error('Must instantiate the PubKeyValidator with a ripple-lib Remote')); - } - -} - -/** - * Check whether the public key is valid for the specified address. - * - * @param {String} address - * @param {String} public_key - * @param {Function} callback - * - * callback function is called with (err, is_valid), where is_valid - * is a boolean indicating whether the public_key supplied is active - */ - -PubKeyValidator.prototype.validate = function(address, public_key, callback) { - - var self = this; - - var public_key_as_uint160; - try { - public_key_as_uint160 = self._parsePublicKey(public_key); - } catch (err) { - return callback(err); - } - - - function getAccountInfo(async_callback) { - self._remote.account(address).getInfo(function(err, account_info_res){ - - // If the remote responds with an Account Not Found error then the account - // is unfunded and thus we can assume that the master key is active - if (err && err.remote && err.remote.error === 'actNotFound') { - async_callback(null, null); - } else { - async_callback(err, account_info_res); - } - }); - }; - - function publicKeyIsValid(account_info_res, async_callback) { - // Catch the case of unfunded accounts - if (!account_info_res) { - - if (public_key_as_uint160 === address) { - async_callback(null, true); - } else { - async_callback(null, false); - } - - return; - } - - var account_info = account_info_res.account_data; - - // Respond with true if the RegularKey is set and matches the given public key or - // if the public key matches the account address and the lsfDisableMaster is not set - if (account_info.RegularKey && - account_info.RegularKey === public_key_as_uint160) { - - async_callback(null, true); - - } else if (account_info.Account === public_key_as_uint160 && - ((account_info.Flags & 0x00100000) === 0)) { - - async_callback(null, true); - - } else { - - async_callback(null, false); - - } - - }; - - var steps = [ - getAccountInfo, - publicKeyIsValid - ]; - - async.waterfall(steps, callback); - -}; - -/** - * Convert a hex-encoded public key to a Ripple Address - * - * @param {Hex-encoded string|RippleAddress} public_key - * @returns {RippleAddress} - */ -PubKeyValidator.prototype._parsePublicKey = function(public_key) { - - // Based on functions in /src/js/ripple/keypair.js - function hexToUInt160(public_key) { - - var bits = sjcl.codec.hex.toBits(public_key); - var hash = sjcl.hash.ripemd160.hash(sjcl.hash.sha256.hash(bits)); - var address = UInt160.from_bits(hash); - address.set_version(Base.VER_ACCOUNT_ID); - - return address.to_json(); - - } - - if (UInt160.is_valid(public_key)) { - - return public_key; - - } else if (/^[0-9a-fA-F]+$/.test(public_key)) { - - return hexToUInt160(public_key); - - } else { - - throw(new Error('Public key is invalid. Must be a UInt160 or a hex string')); - - } -}; - -module.exports = PubKeyValidator; diff --git a/test/pubkeyvalidator-test.js b/test/account-test.js similarity index 61% rename from test/pubkeyvalidator-test.js rename to test/account-test.js index b0ead7be..78e4fb48 100644 --- a/test/pubkeyvalidator-test.js +++ b/test/account-test.js @@ -1,44 +1,41 @@ var assert = require('assert'); -var PubKeyValidator = require('../src/js/ripple/pubkeyvalidator'); +var Account = require('../src/js/ripple/account').Account; -describe('PubKeyValidator', function(){ +describe('Account', function(){ - describe('._parsePublicKey()', function(){ - - var pkv = new PubKeyValidator({}); + describe('._publicKeyToAddress()', function(){ it('should throw an error if the key is invalid', function(){ try { - pkv._parsePublicKey('not a real key'); + Account._publicKeyToAddress('not a real key'); } catch (e) { assert(e); } }); it('should return unchanged a valid UINT160', function(){ - assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === pkv._parsePublicKey('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz')); + assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === Account._publicKeyToAddress('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz')); }); it('should parse a hex-encoded public key as a UINT160', function(){ - assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === pkv._parsePublicKey('025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332')); + assert('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' === Account._publicKeyToAddress('025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332')); - assert('rLpq5RcRzA8FU1yUqEPW4xfsdwon7casuM' === pkv._parsePublicKey('03BFA879C00D58CF55F2B5975FF9B5293008FF49BEFB3EE6BEE2814247BF561A23')); + assert('rLpq5RcRzA8FU1yUqEPW4xfsdwon7casuM' === Account._publicKeyToAddress('03BFA879C00D58CF55F2B5975FF9B5293008FF49BEFB3EE6BEE2814247BF561A23')); - assert('rP4yWwjoDGF2iZSBdAQAgpC449YDezEbT1' === pkv._parsePublicKey('02DF0AB18930B6410CA9F55CB37541F1FED891B8EDF8AB1D01D8F23018A4B204A7')); + assert('rP4yWwjoDGF2iZSBdAQAgpC449YDezEbT1' === Account._publicKeyToAddress('02DF0AB18930B6410CA9F55CB37541F1FED891B8EDF8AB1D01D8F23018A4B204A7')); - assert('rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ' === pkv._parsePublicKey('0310C451A40CAFFD39D6B8A3BD61BF65BCA55246E9DABC3170EBE431D30655B61F')); + assert('rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ' === Account._publicKeyToAddress('0310C451A40CAFFD39D6B8A3BD61BF65BCA55246E9DABC3170EBE431D30655B61F')); }); }); - describe('.validate()', function(){ + describe('.publicKeyIsActive()', function(){ it('should respond true if the public key corresponds to the account address and the master key IS NOT disabled', function(){ - var pkv = new PubKeyValidator({ - account: function(address){ - return { - getInfo: function(callback) { + var account = new Account({ + on: function(){}, + request_account_info: function(address, callback) { if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') { callback(null, { account_data: { Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', @@ -47,10 +44,8 @@ describe('PubKeyValidator', function(){ }}); } } - } - } - }); - pkv.validate('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', '025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332', function(err, is_valid){ + }, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'); + account.publicKeyIsActive('025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332', function(err, is_valid){ assert(err === null); assert(is_valid === true); }); @@ -59,10 +54,9 @@ describe('PubKeyValidator', function(){ it('should respond false if the public key corresponds to the account address and the master key IS disabled', function(){ - var pkv = new PubKeyValidator({ - account: function(address){ - return { - getInfo: function(callback) { + var account = new Account({ + on: function(){}, + request_account_info: function(address, callback) { if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') { callback(null, { account_data: { Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', @@ -71,10 +65,8 @@ describe('PubKeyValidator', function(){ }}); } } - } - } - }); - pkv.validate('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', '025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332', function(err, is_valid){ + }, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'); + account.publicKeyIsActive('025B32A54BFA33FB781581F49B235C0E2820C929FF41E677ADA5D3E53CFBA46332', function(err, is_valid){ assert(err === null); assert(is_valid === false); }); @@ -83,10 +75,9 @@ describe('PubKeyValidator', function(){ it('should respond true if the public key corresponds to the regular key', function(){ - var pkv = new PubKeyValidator({ - account: function(address){ - return { - getInfo: function(callback) { + var account = new Account({ + on: function(){}, + request_account_info: function(address, callback) { if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') { callback(null, { account_data: { Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', @@ -96,10 +87,8 @@ describe('PubKeyValidator', function(){ }}); } } - } - } - }); - pkv.validate('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', '02BE53B7ACBB0900E0BB7729C9CAC1033A0137993B17800BD1191BBD1B29D96A8C', function(err, is_valid){ + }, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'); + account.publicKeyIsActive('02BE53B7ACBB0900E0BB7729C9CAC1033A0137993B17800BD1191BBD1B29D96A8C', function(err, is_valid){ assert(err === null); assert(is_valid === true); }); @@ -108,10 +97,9 @@ describe('PubKeyValidator', function(){ it('should respond false if the public key does not correspond to an active public key for the account', function(){ - var pkv = new PubKeyValidator({ - account: function(address){ - return { - getInfo: function(callback) { + var account = new Account({ + on: function(){}, + request_account_info: function(address, callback) { if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') { callback(null, { account_data: { Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', @@ -121,10 +109,8 @@ describe('PubKeyValidator', function(){ }}); } } - } - } - }); - pkv.validate('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', '032ECDA93970BC7E8872EF6582CB52A5557F117244A949EB4FA8AC7688CF24FBC8', function(err, is_valid){ + }, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'); + account.publicKeyIsActive('032ECDA93970BC7E8872EF6582CB52A5557F117244A949EB4FA8AC7688CF24FBC8', function(err, is_valid){ assert(err === null); assert(is_valid === false); }); @@ -133,10 +119,9 @@ describe('PubKeyValidator', function(){ it('should respond false if the public key is invalid', function(){ - var pkv = new PubKeyValidator({ - account: function(address){ - return { - getInfo: function(callback) { + var account = new Account({ + on: function(){}, + request_account_info: function(address, callback) { if (address === 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz') { callback(null, { account_data: { Account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', @@ -146,10 +131,8 @@ describe('PubKeyValidator', function(){ }}); } } - } - } - }); - pkv.validate('rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', 'not a real public key', function(err, is_valid){ + }, 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz'); + account.publicKeyIsActive('not a real public key', function(err, is_valid){ assert(err); }); @@ -157,10 +140,9 @@ describe('PubKeyValidator', function(){ it('should assume the master key is valid for unfunded accounts', function(){ - var pkv = new PubKeyValidator({ - account: function(address){ - return { - getInfo: function(callback) { + var account = new Account({ + on: function(){}, + request_account_info: function(address, callback) { if (address === 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ') { callback({ error: 'remoteError', error_message: 'Remote reported an error.', @@ -186,10 +168,8 @@ describe('PubKeyValidator', function(){ }); } } - } - } - }); - pkv.validate('rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ', '0310C451A40CAFFD39D6B8A3BD61BF65BCA55246E9DABC3170EBE431D30655B61F', function(err, is_valid){ + }, 'rLdfp6eoR948KVxfn6EpaaNTKwfwXhzSeQ'); + account.publicKeyIsActive('0310C451A40CAFFD39D6B8A3BD61BF65BCA55246E9DABC3170EBE431D30655B61F', function(err, is_valid){ assert(!err); assert(is_valid); }); diff --git a/test/message-test.js b/test/message-test.js new file mode 100644 index 00000000..c6e8d29d --- /dev/null +++ b/test/message-test.js @@ -0,0 +1,327 @@ +var assert = require('assert'); +var sjcl = require('../build/sjcl'); +var Message = require('../src/js/ripple/message'); +var Seed = require('../src/js/ripple/seed').Seed; +var Remote = require('../src/js/ripple/remote').Remote; + +describe('Message', function(){ + + describe('signMessage', function(){ + + it('should prepend the MAGIC_BYTES, call the HASH_FUNCTION, and then call signHash', function(){ + + var normal_signHash = Message.signHash; + + var message_text = 'Hello World!'; + + var signHash_called = false; + Message.signHash = function(hash) { + signHash_called = true; + assert.deepEqual(hash, Message.HASH_FUNCTION(Message.MAGIC_BYTES + message_text)); + }; + + Message.signMessage(message_text); + assert(signHash_called); + + Message.signHash = normal_signHash; + + }); + + }); + + describe('signHash', function(){ + + it('should accept the hash as either a hex string or a bitArray', function(){ + + var normal_random = sjcl.random.randomWords; + + sjcl.random.randomWords = function(num_words){ + var words = []; + for (var w = 0; w < num_words; w++) { + words.push(sjcl.codec.hex.toBits('00000000')); + } + return words; + }; + + var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC'; + // var address = 'rLLzaq61D633b5hhbNXKM9CkrYHboobVv3'; + var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'; + + var signature1 = Message.signHash(hash, secret_string); + var signature2 = Message.signHash(sjcl.codec.hex.toBits(hash), secret_string); + + assert.strictEqual(signature1, signature2); + + sjcl.random.randomWords = normal_random; + + }); + + it('should accept the secret as a string or scjl.ecc.ecdsa.secretKey object', function(){ + + var normal_random = sjcl.random.randomWords; + + sjcl.random.randomWords = function(num_words){ + var words = []; + for (var w = 0; w < num_words; w++) { + words.push(sjcl.codec.hex.toBits('00000000')); + } + return words; + }; + + var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC'; + // var address = 'rLLzaq61D633b5hhbNXKM9CkrYHboobVv3'; + var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'; + + var signature1 = Message.signHash(hash, secret_string); + var signature2 = Message.signHash(hash, Seed.from_json(secret_string).get_key()._secret); + + assert.strictEqual(signature1, signature2); + + sjcl.random.randomWords = normal_random; + + }); + + it('should throw an error if given an invalid secret key', function(){ + + var secret_string = 'badsafRpB5euNL52PZPTSqrE9gvuFwTC'; + var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'; + + assert.throws(function(){ + Message.signHash(hash, secret_string); + }, /Cannot\ generate\ keys\ from\ invalid\ seed/); + + }); + + it('should throw an error if the parameters are reversed', function(){ + + var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC'; + var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'; + + assert.throws(function(){ + Message.signHash(secret_string, hash); + }, Error); + + assert.throws(function(){ + Message.signHash(secret_string, sjcl.codec.hex.toBits(hash)); + }, Error); + + assert.throws(function(){ + Message.signHash(Seed.from_json(secret_string).get_key()._secret, hash); + }, Error); + + assert.throws(function(){ + Message.signHash(Seed.from_json(secret_string).get_key()._secret, sjcl.codec.hex.toBits(hash)); + }, Error); + + }); + + it('should produce a base64-encoded signature', function(){ + var REGEX_BASE64 = /^([A-Za-z0-9\+]{4})*([A-Za-z0-9\+]{2}==)|([A-Za-z0-9\+]{3}=)?$/; + + var normal_random = sjcl.random.randomWords; + + sjcl.random.randomWords = function(num_words){ + var words = []; + for (var w = 0; w < num_words; w++) { + words.push(sjcl.codec.hex.toBits('00000000')); + } + return words; + }; + + var secret_string = 'safRpB5euNL52PZPTSqrE9gvuFwTC'; + // var address = 'rLLzaq61D633b5hhbNXKM9CkrYHboobVv3'; + var hash = 'e865bcc63a86ef21585ac8340a7cc8590ed85175a2a718c6fb2bfb2715d13778'; + + var signature = Message.signHash(hash, secret_string); + + assert(REGEX_BASE64.test(signature)); + + sjcl.random.randomWords = normal_random; + }); + + }); + + describe('verifyMessageSignature', function(){ + + it('should prepend the MAGIC_BYTES, call the HASH_FUNCTION, and then call verifyHashSignature', function(){ + + var normal_verifyHashSignature = Message.verifyHashSignature; + + var data = { + message: 'Hello world!', + signature: 'AAAAGzFa1pYjhssCpDFZgFSnYQ8qCnMkLaZrg0mXZyNQ2NxgMQ8z9U3ngYerxSZCEt3Q4raMIpt03db7jDNGbfmHy8I=' + }; + + var verifyHashSignature_called = false; + Message.verifyHashSignature = function(vhs_data, remote, callback) { + verifyHashSignature_called = true; + + assert.deepEqual(vhs_data.hash, Message.HASH_FUNCTION(Message.MAGIC_BYTES + data.message)); + assert.strictEqual(vhs_data.signature, data.signature); + callback(); + + }; + + Message.verifyMessageSignature(data, {}, function(err){ + assert(!err); + }); + assert(verifyHashSignature_called); + + Message.verifyHashSignature = normal_verifyHashSignature; + + }); + + }); + + describe('verifyHashSignature', function(){ + + it('should throw an error if a callback function is not supplied', function(){ + + var data = { + message: 'Hello world!', + hash: '861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8', + signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk=', + account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' + }; + + Remote.prototype.addServer = function(){}; + var test_remote = new Remote({}); + + assert.throws(function(){ + Message.verifyHashSignature(data); + }, /(?=.*callback\ function).*/); + + + }); + + it('should respond with an error if the hash is missing or invalid', function(done){ + + var data = { + message: 'Hello world!', + signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk=', + account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' + }; + + Remote.prototype.addServer = function(){}; + var test_remote = new Remote({}); + test_remote.state = 'online'; + + Message.verifyHashSignature(data, test_remote, function(err, valid){ + assert(/hash/i.test(err.message)); + done(); + }); + + }); + + it('should respond with an error if the account is missing or invalid', function(done){ + + var data = { + message: 'Hello world!', + hash: '861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8', + signature: 'AAAAHOUJQzG/7BO82fGNt1TNE+GGVXKuQQ0N2nTO+iJETE69PiHnaAkkOzovM177OosxbKjpt3KvwuJflgUB2YGvgjk=' + }; + + Remote.prototype.addServer = function(){}; + var test_remote = new Remote({}); + test_remote.state = 'online'; + + Message.verifyHashSignature(data, test_remote, function(err, valid){ + assert(/account|address/i.test(err.message)); + done(); + }); + + }); + + it('should respond with an error if the signature is missing or invalid', function(done){ + + var data = { + message: 'Hello world!', + hash: '861844d6704e8573fec34d967e20bcfef3d424cf48be04e6dc08f2bd58c729743371015ead891cc3cf1c9d34b49264b510751b1ff9e537937bc46b5d6ff4ecc8', + account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz' + }; + + Remote.prototype.addServer = function(){}; + var test_remote = new Remote({}); + test_remote.state = 'online'; + + Message.verifyHashSignature(data, test_remote, function(err, valid){ + assert(/signature/i.test(err.message)); + done(); + }); + + }); + + it('should respond true if the signature is valid and corresponds to an active public key for the account', function(done){ + + var data = { + message: 'Hello world!', + hash: 'e9a82ea40514787918959b1100481500a5d384030f8770575c6a587675025fe212e6623e25643f251666a7b8b23af476c2850a8ea92153de5724db432892c752', + account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', + signature: 'AAAAHMIPCQGLgdnpX1Ccv1wHb56H4NggxIM6U08Qkb9mUjN2Vn9pZ3CHvq1yWLBi6NqpW+7kedLnmfu4VG2+y43p4Xs=' + }; + + Remote.prototype.addServer = function(){}; + var test_remote = new Remote({}); + test_remote.state = 'online'; + test_remote.request_account_info = function(account, callback) { + if (account === data.account) { + callback(null, { + "account_data": { + "Account": "rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz", + "Flags": 1114112, + "LedgerEntryType": "AccountRoot", + "RegularKey": "rHq2wyUtLkAad3vURUk33q9gozd97skhSf" + } + }); + } else { + callback(new Error('wrong account')); + } + }; + + Message.verifyHashSignature(data, test_remote, function(err, valid){ + assert(!err); + assert(valid); + done(); + }); + + }); + + it('should respond false if a key can be recovered from the signature but it does not correspond to an active public key', function(done){ + + // Signature created by disabled master key + var data = { + message: 'Hello world!', + hash: 'e9a82ea40514787918959b1100481500a5d384030f8770575c6a587675025fe212e6623e25643f251666a7b8b23af476c2850a8ea92153de5724db432892c752', + account: 'rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz', + signature: 'AAAAG+dB/rAjZ5m8eQ/opcqQOJsFbKxOu9jq9KrOAlNO4OdcBDXyCBlkZqS9Xr8oZI2uh0boVsgYOS3pOLJz+Dh3Otk=' + }; + + Remote.prototype.addServer = function(){}; + var test_remote = new Remote({}); + test_remote.state = 'online'; + test_remote.request_account_info = function(account, callback) { + if (account === data.account) { + callback(null, { + "account_data": { + "Account": "rKXCummUHnenhYudNb9UoJ4mGBR75vFcgz", + "Flags": 1114112, + "LedgerEntryType": "AccountRoot", + "RegularKey": "rHq2wyUtLkAad3vURUk33q9gozd97skhSf" + } + }); + } else { + callback(new Error('wrong account')); + } + }; + + Message.verifyHashSignature(data, test_remote, function(err, valid){ + assert(!err); + assert(!valid); + done(); + }); + + }); + + }); + +}); \ No newline at end of file From 3199aa438ad2dbee99a13da5cb6d1e694c252671 Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Fri, 2 May 2014 12:41:52 -0700 Subject: [PATCH 37/40] Allow remote signing - broken options --- src/js/ripple/remote.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/ripple/remote.js b/src/js/ripple/remote.js index a842b219..9a197be2 100644 --- a/src/js/ripple/remote.js +++ b/src/js/ripple/remote.js @@ -78,8 +78,8 @@ function Remote(opts, trace) { var self = this; - this.trusted = opts.trusted; - this.local_sequence = opts.local_sequence; // Locally track sequence numbers + this.trusted = Boolean(opts.trusted); + this.local_sequence = Boolean(opts.local_sequence); // Locally track sequence numbers this.local_fee = (typeof opts.local_fee === 'undefined') ? true : opts.local_fee; // Locally set fees this.local_signing = (typeof opts.local_signing === 'undefined') ? true : opts.local_signing; this.fee_cushion = (typeof opts.fee_cushion === 'undefined') ? 1.2 : opts.fee_cushion; From 0558ad689aaf86c5236188d80c38b14cbb5285cf Mon Sep 17 00:00:00 2001 From: wltsmrz Date: Fri, 2 May 2014 12:45:26 -0700 Subject: [PATCH 38/40] Async transaction sign --- src/js/ripple/transaction.js | 6 +- src/js/ripple/transactionmanager.js | 97 ++++++++++++++++------------- 2 files changed, 57 insertions(+), 46 deletions(-) diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index 7a8284eb..f275f9cc 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -353,7 +353,8 @@ Transaction.prototype.hash = function(prefix, as_uint256) { return as_uint256 ? hash : hash.to_hex(); }; -Transaction.prototype.sign = function() { +Transaction.prototype.sign = function(callback) { + var callback = typeof callback === 'function' ? callback : function(){}; var seed = Seed.from_json(this._secret); var prev_sig = this.tx_json.TxnSignature; @@ -364,6 +365,7 @@ Transaction.prototype.sign = function() { // If the hash is the same, we can re-use the previous signature if (prev_sig && hash === this.previousSigningHash) { this.tx_json.TxnSignature = prev_sig; + callback(); return this; } @@ -374,6 +376,8 @@ Transaction.prototype.sign = function() { this.tx_json.TxnSignature = hex; this.previousSigningHash = hash; + callback(); + return this; }; diff --git a/src/js/ripple/transactionmanager.js b/src/js/ripple/transactionmanager.js index 3616f5cb..56f9bd4a 100644 --- a/src/js/ripple/transactionmanager.js +++ b/src/js/ripple/transactionmanager.js @@ -328,43 +328,6 @@ TransactionManager.prototype._request = function(tx) { if (tx.finalized) return; - tx.submitIndex = this._remote._ledger_current_index; - - if (tx.attempts === 0) { - tx.initialSubmitIndex = tx.submitIndex; - } - - if (!tx._setLastLedger) { - // Honor LastLedgerSequence set by user of API. If - // left unset by API, bump LastLedgerSequence - tx.tx_json.LastLedgerSequence = tx.submitIndex + 8; - } - - tx.lastLedgerSequence = tx.tx_json.LastLedgerSequence; - - var submitRequest = remote.requestSubmit(); - - if (remote.local_signing) { - tx.sign(); - // TODO: We are serializing twice, when we could/should be feeding the - // tx_blob to `tx.hash()` which rebuilds it to sign it. - submitRequest.tx_blob(tx.serialize().to_hex()); - - // ND: ecdsa produces a random `TxnSignature` field value, a component of - // the hash. Attempting to identify a transaction via a hash synthesized - // locally while using remote signing is inherently flawed. - tx.addId(tx.hash()); - } else { - // ND: `build_path` is completely ignored when doing local signing as - // `Paths` is a component of the signed blob, the `tx_blob` is signed, - // sealed and delivered, and the txn unmodified. - // TODO: perhaps an exception should be raised if build_path is attempted - // while local signing - submitRequest.build_path(tx._build_path); - submitRequest.secret(tx._secret); - submitRequest.tx_json(tx.tx_json); - } - remote._trace('transactionmanager: submit:', tx.tx_json); function transactionProposed(message) { @@ -463,24 +426,48 @@ TransactionManager.prototype._request = function(tx) { } }; + var submitRequest = remote.requestSubmit(); + submitRequest.once('error', submitted); submitRequest.once('success', submitted); - if (tx._server) { - submitRequest.server = tx._server; - } + function prepareSubmit() { + if (remote.local_signing) { + // TODO: We are serializing twice, when we could/should be feeding the + // tx_blob to `tx.hash()` which rebuilds it to sign it. + submitRequest.tx_blob(tx.serialize().to_hex()); - if (typeof tx._iff !== 'function') { - submitTransaction(); - } else { - return tx._iff(tx.summary(), function(err, proceed) { + // ND: ecdsa produces a random `TxnSignature` field value, a component of + // the hash. Attempting to identify a transaction via a hash synthesized + // locally while using remote signing is inherently flawed. + tx.addId(tx.hash()); + } else { + // ND: `build_path` is completely ignored when doing local signing as + // `Paths` is a component of the signed blob, the `tx_blob` is signed, + // sealed and delivered, and the txn unmodified. + // TODO: perhaps an exception should be raised if build_path is attempted + // while local signing + submitRequest.build_path(tx._build_path); + submitRequest.secret(tx._secret); + submitRequest.tx_json(tx.tx_json); + } + + if (tx._server) { + submitRequest.server = tx._server; + } + + if (typeof tx._iff !== 'function') { + return submitTransaction(); + } + + tx._iff(tx.summary(), function(err, proceed) { if (err || !proceed) { tx.emit('abort'); } else { submitTransaction(); } }); - } + }; function requestTimeout() { // ND: What if the response is just slow and we get a response that @@ -513,6 +500,26 @@ TransactionManager.prototype._request = function(tx) { tx.emit('postsubmit'); }; + tx.submitIndex = this._remote._ledger_current_index; + + if (tx.attempts === 0) { + tx.initialSubmitIndex = tx.submitIndex; + } + + if (!tx._setLastLedger) { + // Honor LastLedgerSequence set by user of API. If + // left unset by API, bump LastLedgerSequence + tx.tx_json.LastLedgerSequence = tx.submitIndex + 8; + } + + tx.lastLedgerSequence = tx.tx_json.LastLedgerSequence; + + if (remote.local_signing) { + tx.sign(prepareSubmit); + } else { + prepareSubmit(); + } + return submitRequest; }; From 41ea820ae0f35384a377a620cbe83759e5500658 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 2 May 2014 13:44:04 -0700 Subject: [PATCH 39/40] [FEATURE] Transaction: Allow canonical signing to be disabled via config. --- src/js/ripple/remote.js | 2 ++ src/js/ripple/transaction.js | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/js/ripple/remote.js b/src/js/ripple/remote.js index 9a197be2..85c715b9 100644 --- a/src/js/ripple/remote.js +++ b/src/js/ripple/remote.js @@ -46,6 +46,7 @@ var log = require('./log').internal.sub('remote'); max_fee : Maximum acceptable transaction fee fee_cushion : Extra fee multiplier to account for async fee changes. servers : Array of server objects with the following form + canonical_signing : Signatures should be canonicalized and the "canonical" flag set { host: @@ -82,6 +83,7 @@ function Remote(opts, trace) { this.local_sequence = Boolean(opts.local_sequence); // Locally track sequence numbers this.local_fee = (typeof opts.local_fee === 'undefined') ? true : opts.local_fee; // Locally set fees this.local_signing = (typeof opts.local_signing === 'undefined') ? true : opts.local_signing; + this.canonical_signing = (typeof opts.canonical_signing === 'undefined') ? true : opts.canonical_signing; this.fee_cushion = (typeof opts.fee_cushion === 'undefined') ? 1.2 : opts.fee_cushion; this.max_fee = (typeof opts.max_fee === 'undefined') ? Infinity : opts.max_fee; this.id = 0; diff --git a/src/js/ripple/transaction.js b/src/js/ripple/transaction.js index f275f9cc..6ea4055f 100644 --- a/src/js/ripple/transaction.js +++ b/src/js/ripple/transaction.js @@ -74,7 +74,8 @@ function Transaction(remote) { // Index at which transaction was submitted this.submitIndex = void(0); - this.canonical = true; + // Canonical signing setting defaults to the Remote's configuration + this.canonical = "object" === typeof remote ? !!remote.canonical_signing : true; // We aren't clever enough to eschew preventative measures so we keep an array // of all submitted transactionIDs (which can change due to load_factor From 473d8a8d8c3b0fe604c96e43bedf9aec481230ec Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 2 May 2014 14:06:22 -0700 Subject: [PATCH 40/40] [CHORE] Bump version to 0.7.36 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 88bea9b5..732bb086 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ripple-lib", - "version": "0.7.35", + "version": "0.7.36", "description": "Ripple JavaScript client library", "files": [ "src/js/*",